contract_cli/
typst_assets.rs1use 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#[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; };
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}