use std::path::{Path, PathBuf};
use escriba_lisp::{CatalogError, EscribaPluginSpec, emit_caixa_lisp};
use thiserror::Error;
#[derive(Debug, Clone)]
pub struct CaixaArtifacts {
pub caixa_lisp: String,
pub entry_lisp: String,
pub flake_nix: String,
pub spec_source: String,
}
#[derive(Debug, Error)]
pub enum ForgeError {
#[error("catalog source: {0}")]
Catalog(#[from] CatalogError),
#[error("io error writing {path}: {source}")]
Io {
path: String,
source: std::io::Error,
},
}
pub fn forge_plugin(source: &str) -> Result<(EscribaPluginSpec, CaixaArtifacts), ForgeError> {
let spec = escriba_lisp::read_catalog_meta(source)?;
let artifacts = CaixaArtifacts {
caixa_lisp: emit_caixa_lisp(&spec),
entry_lisp: emit_entry_lisp(&spec, source),
flake_nix: emit_flake_nix(&spec),
spec_source: source.to_string(),
};
Ok((spec, artifacts))
}
fn emit_entry_lisp(spec: &EscribaPluginSpec, source: &str) -> String {
let mut out = String::new();
out.push_str(";; GENERATED escriba entry — escriba LOADS + APPLIES this file.\n");
out.push_str(&format!(
";; plugin: {} (v{})\n",
spec.name,
spec.effective_version()
));
out.push_str(";; The (defescribaplugin …) manifest below is inert at apply time\n");
out.push_str(";; (escriba-lisp ignores it); it is kept so the entry round-trips.\n");
out.push_str(source.trim_start());
if !out.ends_with('\n') {
out.push('\n');
}
out
}
#[must_use]
pub fn emit_flake_nix(spec: &EscribaPluginSpec) -> String {
let name = &spec.name;
let desc = spec.description.replace('"', "'");
format!(
r#"# GENERATED by `escriba plugin forge` from {name}.escribaplugin.lisp.
# A pure-source escriba plugin caixa (:kind Biblioteca). Copied verbatim
# into escriba's plugins dir; escriba loads escriba/plugin.lisp.
{{
description = "escriba plugin caixa: {name} — {desc}";
inputs = {{
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
}};
outputs = {{ self, nixpkgs, flake-utils }}:
flake-utils.lib.eachDefaultSystem (system:
let pkgs = import nixpkgs {{ inherit system; }};
in {{
# The caixa, materialized: caixa.lisp + escriba/plugin.lisp.
packages.default = pkgs.runCommandLocal "{name}" {{ }} ''
mkdir -p "$out"
cp -r ${{self}}/caixa.lisp "$out/" 2>/dev/null || true
cp -r ${{self}}/escriba "$out/" 2>/dev/null || true
'';
}});
}}
"#
)
}
pub fn write_plugin_caixa(
spec: &EscribaPluginSpec,
artifacts: &CaixaArtifacts,
out: &Path,
) -> Result<PathBuf, ForgeError> {
let root = out.join(&spec.name);
let escriba_dir = root.join("escriba");
mkdirs(&escriba_dir)?;
write_file(&root.join("caixa.lisp"), &artifacts.caixa_lisp)?;
write_file(&escriba_dir.join("plugin.lisp"), &artifacts.entry_lisp)?;
write_file(&root.join("flake.nix"), &artifacts.flake_nix)?;
write_file(
&root.join(format!("{}.escribaplugin.lisp", spec.name)),
&artifacts.spec_source,
)?;
Ok(root)
}
fn mkdirs(p: &Path) -> Result<(), ForgeError> {
std::fs::create_dir_all(p).map_err(|e| ForgeError::Io {
path: p.display().to_string(),
source: e,
})
}
fn write_file(p: &Path, contents: &str) -> Result<(), ForgeError> {
std::fs::write(p, contents).map_err(|e| ForgeError::Io {
path: p.display().to_string(),
source: e,
})
}
#[cfg(test)]
mod tests {
use super::*;
const SRC: &str = r##"
(defescribaplugin
:name "escriba-gitsigns"
:version "0.1.0"
:category "git"
:description "Git gutter signs, blame, hunks"
:blnvim-origin "lewis6991/gitsigns.nvim"
:ativar-em ("Event: BufReadPost"))
(defkeybind :mode "normal" :key "<leader>gb" :action "git.blame"
:description "git blame")
(defcmd :name "GitBlame" :description "toggle git blame" :action "git.blame")
(defhighlight :group "GitSignsAdd" :fg "#a9bb8c")
"##;
#[test]
fn forge_produces_reparseable_artifacts() {
let (spec, art) = forge_plugin(SRC).expect("forge succeeds");
assert_eq!(spec.name, "escriba-gitsigns");
let caixa_forms = tatara_lisp::read(&art.caixa_lisp).expect("caixa.lisp re-parses");
assert_eq!(caixa_forms.len(), 1);
assert!(art.caixa_lisp.contains("Biblioteca"));
assert!(art.caixa_lisp.contains("lewis6991/gitsigns.nvim"));
let plan = escriba_lisp::apply_source(&art.entry_lisp).expect("entry applies");
assert_eq!(plan.keybinds.len(), 1);
assert_eq!(plan.commands.len(), 1);
assert_eq!(plan.highlights.len(), 1);
assert!(art.flake_nix.contains("escriba-gitsigns"));
}
#[test]
fn write_materializes_full_caixa_tree() {
let (spec, art) = forge_plugin(SRC).unwrap();
let out = std::env::temp_dir().join("escriba-forge-test-out");
let _ = std::fs::remove_dir_all(&out);
let root = write_plugin_caixa(&spec, &art, &out).expect("write succeeds");
assert!(root.join("caixa.lisp").exists());
assert!(root.join("escriba/plugin.lisp").exists());
assert!(root.join("flake.nix").exists());
assert!(root.join("escriba-gitsigns.escribaplugin.lisp").exists());
let loaded = crate::PluginCaixa::load("escriba-gitsigns", "0.1.0", &[], &root)
.expect("forged caixa loads via PluginCaixa");
assert!(loaded.entry_src.contains("git.blame"));
let _ = std::fs::remove_dir_all(&out);
}
#[test]
fn re_forge_is_idempotent() {
let (_, a) = forge_plugin(SRC).unwrap();
let (_, b) = forge_plugin(SRC).unwrap();
assert_eq!(a.caixa_lisp, b.caixa_lisp);
assert_eq!(a.entry_lisp, b.entry_lisp);
assert_eq!(a.flake_nix, b.flake_nix);
}
}