Skip to main content

contract_cli/
typst_assets.rs

1// Embed contract templates + shared helpers. Extract on first use into the
2// shared accounting-suite assets dir, namespaced under `contracts/`.
3
4use rust_embed::RustEmbed;
5
6use crate::config;
7use crate::error::Result;
8
9#[derive(RustEmbed)]
10#[folder = "typst/"]
11#[prefix = ""]
12pub struct Assets;
13
14fn root() -> Result<std::path::PathBuf> {
15    Ok(config::assets_path()?.join("contracts"))
16}
17
18pub fn ensure_extracted() -> Result<()> {
19    let root = root()?;
20    std::fs::create_dir_all(&root)?;
21    for path in Assets::iter() {
22        let file = Assets::get(&path).expect("embedded asset");
23        let dest = root.join(path.as_ref());
24        if let Some(parent) = dest.parent() {
25            std::fs::create_dir_all(parent)?;
26        }
27        let needs_write = match std::fs::read(&dest) {
28            Ok(existing) => existing != file.data.as_ref(),
29            Err(_) => true,
30        };
31        if needs_write {
32            std::fs::write(&dest, file.data.as_ref())?;
33        }
34    }
35    Ok(())
36}
37
38pub fn template_dir() -> Result<std::path::PathBuf> {
39    Ok(root()?.join("templates"))
40}
41
42pub fn template_path(name: &str) -> Result<std::path::PathBuf> {
43    Ok(template_dir()?.join(format!("{name}.typ")))
44}
45
46pub fn list_templates() -> Result<Vec<String>> {
47    ensure_extracted()?;
48    let dir = template_dir()?;
49    let mut names = Vec::new();
50    if dir.exists() {
51        for entry in std::fs::read_dir(&dir)? {
52            let entry = entry?;
53            let path = entry.path();
54            if path.extension().and_then(|s| s.to_str()) == Some("typ") {
55                if let Some(name) = path.file_stem().and_then(|s| s.to_str()) {
56                    names.push(name.to_string());
57                }
58            }
59        }
60    }
61    names.sort();
62    Ok(names)
63}
64
65pub fn has_template(name: &str) -> Result<bool> {
66    ensure_extracted()?;
67    Ok(template_path(name)?.exists())
68}
69
70/// Per-template metadata, parsed from the `//!` doc-comment header at the top
71/// of each .typ file. Metadata travels with the design, so user-dropped
72/// custom templates participate in discovery the moment they document
73/// themselves the same way.
74#[derive(Debug, Clone, serde::Serialize)]
75pub struct TemplateMeta {
76    pub name: String,
77    pub description: String,
78    pub mood: Vec<String>,
79    pub tags: Vec<String>,
80    pub fonts: String,
81    pub paper: String,
82}
83
84pub fn template_meta(name: &str) -> Result<TemplateMeta> {
85    let src = std::fs::read_to_string(template_path(name)?)?;
86    let mut meta = TemplateMeta {
87        name: name.to_string(),
88        description: String::new(),
89        mood: Vec::new(),
90        tags: Vec::new(),
91        fonts: String::new(),
92        paper: String::new(),
93    };
94    for line in src.lines() {
95        let Some(rest) = line.trim().strip_prefix("//!") else {
96            if line.trim().is_empty() || line.trim().starts_with("//") {
97                continue;
98            }
99            break; // first real code line ends the header block
100        };
101        if let Some((key, value)) = rest.split_once(':') {
102            let value = value.trim().to_string();
103            match key.trim() {
104                "description" => meta.description = value,
105                "mood" => meta.mood = split_list(&value),
106                "tags" => meta.tags = split_list(&value),
107                "fonts" => meta.fonts = value,
108                "paper" => meta.paper = value,
109                _ => {}
110            }
111        }
112    }
113    if meta.description.is_empty() {
114        meta.description = "(custom template — no //! metadata header)".into();
115    }
116    Ok(meta)
117}
118
119pub fn list_template_meta() -> Result<Vec<TemplateMeta>> {
120    list_templates()?
121        .iter()
122        .map(|n| template_meta(n))
123        .collect()
124}
125
126fn split_list(s: &str) -> Vec<String> {
127    s.split(',')
128        .map(|t| t.trim().to_lowercase())
129        .filter(|t| !t.is_empty())
130        .collect()
131}
132
133pub fn project_root() -> Result<std::path::PathBuf> {
134    root()
135}