Skip to main content

str_format/
meta_edit.rs

1//! `._meta` 的**保注释写回**与新建模板渲染。
2//!
3//! 写回直接基于 `toml_edit::DocumentMut`,因此已有的 `#` 注释与键的书写顺序都会被保留
4//! (规范硬性约束 6、DoD 16)。
5
6use std::path::Path;
7
8use toml_edit::{DocumentMut, Item, Table, value};
9
10use crate::error::{Error, Result};
11use crate::meta::{Author, Entry, Kind, Meta, RefItem, extract};
12use crate::util;
13
14/// TOML 基本字符串转义。
15pub fn toml_str(s: &str) -> String {
16    let mut out = String::with_capacity(s.len() + 2);
17    out.push('"');
18    for c in s.chars() {
19        match c {
20            '"' => out.push_str("\\\""),
21            '\\' => out.push_str("\\\\"),
22            '\n' => out.push_str("\\n"),
23            '\r' => out.push_str("\\r"),
24            '\t' => out.push_str("\\t"),
25            c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04X}", c as u32)),
26            c => out.push(c),
27        }
28    }
29    out.push('"');
30    out
31}
32
33/// 从 TOML 文本构建 `Meta`(模板渲染后使用)。
34pub fn meta_from_text(text: &str) -> Result<Meta> {
35    let doc: DocumentMut = text
36        .parse()
37        .map_err(|e| Error::Other(format!("内部模板 TOML 解析失败:{e}")))?;
38    let (meta, _issues) = extract(doc, "<template>");
39    Ok(meta)
40}
41
42/// 渲染 root `._meta` 模板。
43pub fn render_root_meta(
44    name: &str,
45    title: Option<&str>,
46    summary: Option<&str>,
47    root_id: &str,
48    created: &str,
49    schema_count: usize,
50    id_version: usize,
51) -> String {
52    let mut s = String::new();
53    s.push_str("# ── STR bundle 根元数据 ──────────────────────────────────────────\n");
54    s.push_str("# ROOT 的 [[entries]] 中 role = \"node\" 的条目即一级分支结构。\n");
55    s.push_str(&format!("str = {}\n", crate::STR_MAJOR));
56    s.push_str(&format!("spec = {}\n", toml_str(crate::SPEC_VERSION)));
57    s.push_str("kind = \"root\"\n");
58    s.push_str(&format!("id = {}\n", toml_str(root_id)));
59    s.push_str(&format!("name = {}\n", toml_str(name)));
60    if let Some(t) = title {
61        s.push_str(&format!("title = {}\n", toml_str(t)));
62    }
63    if let Some(t) = summary {
64        s.push_str(&format!("summary = {}\n", toml_str(t)));
65    }
66    s.push_str("tags = []\n");
67    s.push_str("revision = 1\n");
68    s.push_str(&format!("created_at = {created}\n"));
69    s.push_str(&format!("updated_at = {created}\n"));
70    s.push('\n');
71    s.push_str("[policies]\n");
72    s.push_str(&format!("id_version = {id_version}\n"));
73    s.push_str("max_depth = 32\n");
74    s.push_str("manifest = \"strict\"\n");
75    s.push_str("sha256 = \"required\"\n");
76    s.push_str("large_asset_bytes = 10485760\n");
77    s.push_str("deep_tree_warn = 16\n");
78    s.push('\n');
79    if schema_count > 0 {
80        s.push_str("[[entries]]\n");
81        s.push_str("path = \"._schema\"\n");
82        s.push_str("role = \"schema\"\n");
83        s.push_str(&format!("count = {schema_count}\n"));
84        s.push_str("note = \"bundle 级校验 Schema 存放处\"\n");
85        s.push('\n');
86    }
87    s.push_str("[ext]\n");
88    s
89}
90
91/// 渲染 node / branch `._meta` 模板。
92pub fn render_branch_meta(
93    kind: Kind,
94    id: &str,
95    type_: Option<&str>,
96    title: Option<&str>,
97    summary: Option<&str>,
98    created: &str,
99) -> String {
100    let mut s = String::new();
101    s.push_str(&format!("str = {}\n", crate::STR_MAJOR));
102    s.push_str(&format!("spec = {}\n", toml_str(crate::SPEC_VERSION)));
103    s.push_str(&format!("kind = {}\n", toml_str(kind.as_str())));
104    s.push_str(&format!("id = {}\n", toml_str(id)));
105    if let Some(t) = type_ {
106        s.push_str(&format!("type = {}\n", toml_str(t)));
107    }
108    if let Some(t) = title {
109        s.push_str(&format!("title = {}\n", toml_str(t)));
110    }
111    if let Some(t) = summary {
112        s.push_str(&format!("summary = {}\n", toml_str(t)));
113    }
114    s.push_str("tags = []\n");
115    s.push_str("revision = 1\n");
116    s.push_str(&format!("created_at = {created}\n"));
117    s.push_str(&format!("updated_at = {created}\n"));
118    s.push('\n');
119    s.push_str("[ext]\n");
120    s
121}
122
123/// `[[entries]]` 元素 → TOML 表(按规范键序)。
124pub fn entry_to_table(e: &Entry) -> Table {
125    let mut t = Table::new();
126    t.insert("path", value(e.path.clone()));
127    t.insert("role", value(e.role.clone()));
128    if let Some(v) = &e.id {
129        t.insert("id", value(v.clone()));
130    }
131    if let Some(v) = &e.r#type {
132        t.insert("type", value(v.clone()));
133    }
134    if let Some(v) = &e.title {
135        t.insert("title", value(v.clone()));
136    }
137    if let Some(v) = &e.summary {
138        t.insert("summary", value(v.clone()));
139    }
140    if let Some(v) = e.order {
141        t.insert("order", value(v));
142    }
143    if let Some(v) = &e.media_type {
144        t.insert("media_type", value(v.clone()));
145    }
146    if let Some(v) = e.size {
147        t.insert("size", value(v));
148    }
149    if let Some(v) = &e.sha256 {
150        t.insert("sha256", value(v.clone()));
151    }
152    if let Some(v) = e.count {
153        t.insert("count", value(v));
154    }
155    if let Some(v) = &e.schema {
156        t.insert("schema", value(v.clone()));
157    }
158    if e.optional {
159        t.insert("optional", value(true));
160    }
161    if let Some(v) = &e.note {
162        t.insert("note", value(v.clone()));
163    }
164    t
165}
166
167/// `[[refs]]` 元素 → TOML 表(按规范键序)。
168pub fn ref_to_table(r: &RefItem) -> Table {
169    let mut t = Table::new();
170    t.insert("id", value(r.id.clone()));
171    t.insert("target", value(r.target.clone()));
172    t.insert("rel", value(r.rel.clone()));
173    if let Some(v) = &r.title {
174        t.insert("title", value(v.clone()));
175    }
176    if let Some(v) = r.order {
177        t.insert("order", value(v));
178    }
179    if let Some(v) = &r.note {
180        t.insert("note", value(v.clone()));
181    }
182    t
183}
184
185/// `[[authors]]` 元素 → TOML 表(按规范键序)。
186pub fn author_to_table(a: &Author) -> Table {
187    let mut t = Table::new();
188    t.insert("id", value(a.id.clone()));
189    if let Some(v) = &a.name {
190        t.insert("name", value(v.clone()));
191    }
192    t.insert("role", value(a.role.clone()));
193    if let Some(at) = &a.at
194        && let Ok(dt) = at.parse::<toml_edit::Datetime>()
195    {
196        t.insert("at", value(dt));
197    }
198    t
199}
200
201/// 去掉整行 `#` 注释(不处理字符串内的 `#`);`str fmt --strip-comments` 的唯一入口。
202pub fn strip_line_comments(text: &str) -> String {
203    let mut out = String::with_capacity(text.len());
204    for line in text.lines() {
205        if line.trim_start().starts_with('#') {
206            continue;
207        }
208        out.push_str(line);
209        out.push('\n');
210    }
211    out
212}
213
214impl Meta {
215    /// 写操作后重新同步类型化视图(`doc` 为准)。
216    pub fn resync(&mut self) {
217        let doc = std::mem::replace(&mut self.doc, DocumentMut::new());
218        let (meta, _issues) = extract(doc, "<resync>");
219        *self = meta;
220    }
221
222    /// 按规范表序(`policies → authors → refs → entries → ext`)安置一张表 / 数组表。
223    fn place_table(&mut self, key: &'static str, item: Item) {
224        let order = crate::meta::TABLE_ORDER;
225        let pos = order.iter().position(|k| *k == key).unwrap_or(order.len());
226        let table = self.doc.as_table_mut();
227        let mut moved: Vec<(String, Item)> = Vec::new();
228        for k in order.iter().skip(pos + 1) {
229            if let Some(it) = table.remove(k) {
230                moved.push(((*k).to_string(), it));
231            }
232        }
233        table.insert(key, item);
234        for (k, it) in moved {
235            table.insert(&k, it);
236        }
237    }
238
239    /// `revision + 1` 并刷新 `updated_at`。
240    ///
241    /// `updated_at` 一律写成 TOML **原生 offset date-time**(规范 4.1),不得是字符串。
242    pub fn touch(&mut self) {
243        let next = self.revision.unwrap_or(0) + 1;
244        let now = util::now_rfc3339();
245        let table = self.doc.as_table_mut();
246        table.insert("revision", value(next));
247        if let Ok(dt) = now.parse::<toml_edit::Datetime>() {
248            table.insert("updated_at", value(dt));
249        }
250        self.resync();
251    }
252
253    /// 设置顶层字符串字段(保注释)。
254    pub fn set_str(&mut self, key: &str, v: &str) {
255        self.doc.as_table_mut().insert(key, value(v.to_string()));
256        self.resync();
257    }
258
259    /// 设置顶层字符串字段;`v` 为空串则**移除**该字段。
260    pub fn set_str_or_remove(&mut self, key: &str, v: &str) {
261        if v.is_empty() {
262            self.doc.as_table_mut().remove(key);
263        } else {
264            self.doc.as_table_mut().insert(key, value(v.to_string()));
265        }
266        self.resync();
267    }
268
269    /// 设置顶层字符串数组字段(如 `tags`)。
270    pub fn set_str_array(&mut self, key: &str, values: &[String]) {
271        let arr: toml_edit::Array = values
272            .iter()
273            .map(|s| toml_edit::Value::from(s.clone()))
274            .collect();
275        self.doc.as_table_mut().insert(key, value(arr));
276        self.resync();
277    }
278
279    /// 按 `id` 插入或替换一条 `[[authors]]`,返回 `true` 表示新增。
280    pub fn upsert_author(&mut self, a: &Author) -> bool {
281        let new_table = author_to_table(a);
282        let mut aot = self
283            .doc
284            .as_table()
285            .get("authors")
286            .and_then(|i| i.as_array_of_tables())
287            .cloned()
288            .unwrap_or_default();
289        let mut replaced = false;
290        for t in aot.iter_mut() {
291            if t.get("id").and_then(|v| v.as_str()) == Some(a.id.as_str()) {
292                *t = new_table.clone();
293                replaced = true;
294                break;
295            }
296        }
297        if !replaced {
298            aot.push(new_table);
299        }
300        self.place_table("authors", Item::ArrayOfTables(aot));
301        self.resync();
302        !replaced
303    }
304
305    /// 按 `id` 删除 `[[authors]]`。
306    pub fn remove_author(&mut self, id: &str) -> bool {
307        let Some(aot) = self
308            .doc
309            .as_table_mut()
310            .get_mut("authors")
311            .and_then(|i| i.as_array_of_tables_mut())
312        else {
313            return false;
314        };
315        let before = aot.len();
316        aot.retain(|t| t.get("id").and_then(|v| v.as_str()) != Some(id));
317        let changed = aot.len() != before;
318        if changed {
319            self.resync();
320        }
321        changed
322    }
323
324    /// 在**指定分支**的 `entries[path]` 上设置字符串字段(保注释,键序由规范化收口)。
325    ///
326    /// `key` 限 `ENTRY_KEYS` 中的字符串字段;`v` 为空串则移除该键。
327    /// 返回 `false` 表示该分支没有 `path` 对应的条目。
328    pub fn set_entry_str(&mut self, path: &str, key: &str, v: &str) -> bool {
329        self.with_entry(path, |t| {
330            if v.is_empty() {
331                t.remove(key);
332            } else {
333                t.insert(key, value(v.to_string()));
334            }
335        })
336    }
337
338    /// 在 `entries[path]` 上设置整数字段(如 `order`)。`None` 表示移除。
339    pub fn set_entry_int(&mut self, path: &str, key: &str, v: Option<i64>) -> bool {
340        self.with_entry(path, |t| match v {
341            Some(n) => {
342                t.insert(key, value(n));
343            }
344            None => {
345                t.remove(key);
346            }
347        })
348    }
349
350    /// 对 `entries[path]` 施加一次就地编辑。
351    fn with_entry(&mut self, path: &str, edit: impl FnOnce(&mut Table)) -> bool {
352        let Some(aot) = self
353            .doc
354            .as_table_mut()
355            .get_mut("entries")
356            .and_then(|i| i.as_array_of_tables_mut())
357        else {
358            return false;
359        };
360        let mut hit = false;
361        for t in aot.iter_mut() {
362            if t.get("path").and_then(|v| v.as_str()) == Some(path) {
363                edit(t);
364                hit = true;
365                break;
366            }
367        }
368        if hit {
369            self.resync();
370        }
371        hit
372    }
373
374    /// 插入或替换一条 `[[entries]]`,返回 `true` 表示新增。
375    pub fn upsert_entry(&mut self, e: &Entry) -> bool {
376        let new_table = entry_to_table(e);
377        let mut aot = self
378            .doc
379            .as_table()
380            .get("entries")
381            .and_then(|i| i.as_array_of_tables())
382            .cloned()
383            .unwrap_or_default();
384
385        let mut replaced = false;
386        for t in aot.iter_mut() {
387            if t.get("path").and_then(|v| v.as_str()) == Some(e.path.as_str()) {
388                *t = new_table.clone();
389                replaced = true;
390                break;
391            }
392        }
393        if !replaced {
394            aot.push(new_table);
395        }
396        self.place_table("entries", Item::ArrayOfTables(aot));
397        self.resync();
398        !replaced
399    }
400
401    /// 删除指定 `path` 的 `[[entries]]` 元素。
402    pub fn remove_entry_path(&mut self, path: &str) -> bool {
403        let Some(aot) = self
404            .doc
405            .as_table_mut()
406            .get_mut("entries")
407            .and_then(|i| i.as_array_of_tables_mut())
408        else {
409            return false;
410        };
411        let before = aot.len();
412        aot.retain(|t| t.get("path").and_then(|v| v.as_str()) != Some(path));
413        let changed = aot.len() != before;
414        if changed {
415            self.resync();
416        }
417        changed
418    }
419
420    /// 追加一条 `[[refs]]`。
421    pub fn push_ref(&mut self, r: &RefItem) {
422        let mut aot = self
423            .doc
424            .as_table()
425            .get("refs")
426            .and_then(|i| i.as_array_of_tables())
427            .cloned()
428            .unwrap_or_default();
429        aot.push(ref_to_table(r));
430        self.place_table("refs", Item::ArrayOfTables(aot));
431        self.resync();
432    }
433
434    /// 按 id 删除 `[[refs]]`。
435    pub fn remove_ref(&mut self, id: &str) -> bool {
436        let Some(aot) = self
437            .doc
438            .as_table_mut()
439            .get_mut("refs")
440            .and_then(|i| i.as_array_of_tables_mut())
441        else {
442            return false;
443        };
444        let before = aot.len();
445        aot.retain(|t| t.get("id").and_then(|v| v.as_str()) != Some(id));
446        let changed = aot.len() != before;
447        if changed {
448            self.resync();
449        }
450        changed
451    }
452
453    /// 规范 §4.9 归一化后的 TOML 文本(键序 / 表序固定,注释保留)。
454    ///
455    /// 属性是只读的派生视图:这里在**副本**上做规范化,不改动 `self.doc`。
456    pub fn canonical_text(&self, strip_comments: bool) -> String {
457        let mut doc = self.doc.clone();
458        crate::meta::canonicalize_doc(&mut doc);
459        let mut text = doc.to_string();
460        if !text.ends_with('\n') {
461            text.push('\n');
462        }
463        if strip_comments {
464            text = strip_line_comments(&text);
465        }
466        text
467    }
468
469    /// 保注释写回磁盘(写出的字节一律是 §4.9 规范形式)。
470    pub fn save(&self, path: &Path) -> Result<()> {
471        let text = self.canonical_text(false);
472        std::fs::write(path, text).map_err(|e| Error::io(path, e))
473    }
474}