opys_engine/retired.rs
1//! The retired-id ledger: `_retired.md`, a frontmatter map of reserved
2//! `id -> title`. Closing, retiring, renumbering, or deleting a document
3//! records its id here so the global id sequence never hands the number out
4//! again; the title is kept for human reference (git records the when and why).
5//!
6//! This supersedes the pre-0.12 plaintext `_retired.txt` (`ID # retired …`).
7//! A legacy ledger is read on load and migrated to `_retired.md` on the next
8//! write. Reusing frontmatter + the `refs` id->title map machinery means one
9//! less bespoke format to parse.
10
11use std::path::{Path, PathBuf};
12
13use crate::frontmatter::{self, Frontmatter};
14use crate::refs;
15
16/// The ledger filename.
17pub const FILE: &str = "_retired.md";
18/// The pre-0.12 plaintext ledger, migrated to [`FILE`] on the next write.
19pub const LEGACY_FILE: &str = "_retired.txt";
20/// The frontmatter key holding the reserved `id -> title` map.
21pub const FIELD: &str = "retired";
22
23const BODY: &str = "# Retired ids\n\nReserved ids that must never be reused. \
24Managed by opys — the value is the document's last title; git records when and \
25why each id was retired.\n";
26
27/// Path to the markdown ledger under `base`.
28pub fn path(base: &Path) -> PathBuf {
29 base.join(FILE)
30}
31
32/// Path to the legacy plaintext ledger under `base`.
33pub fn legacy_path(base: &Path) -> PathBuf {
34 base.join(LEGACY_FILE)
35}
36
37/// The ledger as `(id, title)` pairs sorted by number. Prefers `_retired.md`;
38/// falls back to the legacy plaintext ledger (ids only, empty titles). A missing
39/// ledger reads as empty.
40pub fn read(base: &Path) -> Vec<(String, String)> {
41 if let Ok(text) = std::fs::read_to_string(path(base)) {
42 if let Ok((fm, _)) = frontmatter::parse(&text, FILE) {
43 return refs::parse_in(&fm, FIELD);
44 }
45 }
46 read_legacy(base)
47}
48
49/// Parse the legacy plaintext ledger (`ID # retired DATE: reason`): the id is
50/// the first token of each non-comment line; titles are unknown (empty).
51fn read_legacy(base: &Path) -> Vec<(String, String)> {
52 let Ok(text) = std::fs::read_to_string(legacy_path(base)) else {
53 return Vec::new();
54 };
55 let mut out = Vec::new();
56 for line in text.lines() {
57 let head = line.split('#').next().unwrap_or("");
58 if let Some(id) = head.split_whitespace().next() {
59 if !id.is_empty() {
60 out.push((id.to_string(), String::new()));
61 }
62 }
63 }
64 out
65}
66
67/// Render `(id, title)` entries (sorted by number) to the ledger file's text.
68pub fn serialize(entries: &[(String, String)]) -> String {
69 let mut fm = Frontmatter::new();
70 refs::set_in(&mut fm, FIELD, entries);
71 frontmatter::serialize(&fm, BODY)
72}