Skip to main content

llm_wiki/
markdown.rs

1use std::path::{Path, PathBuf};
2
3use anyhow::{Result, bail};
4
5use crate::frontmatter;
6use crate::slug::Slug;
7
8/// Read a page by slug. Optionally strip frontmatter.
9/// Appends a supersession notice if `superseded_by` is set.
10pub fn read_page(slug: &Slug, wiki_root: &Path, no_frontmatter: bool) -> Result<String> {
11    let path = slug.resolve(wiki_root)?;
12    let content = std::fs::read_to_string(&path)?;
13
14    let page = frontmatter::parse(&content);
15    let notice = page
16        .superseded_by()
17        .map(|s| format!("\n> **Superseded** by [{s}](wiki://{s})\n"));
18
19    if no_frontmatter {
20        let body = &page.body;
21        let mut out = body.to_string();
22        if let Some(n) = notice {
23            out.push_str(&n);
24        }
25        Ok(out)
26    } else {
27        let mut out = content;
28        if let Some(n) = notice {
29            out.push_str(&n);
30        }
31        Ok(out)
32    }
33}
34
35/// Write content to a page path resolved from slug.
36/// Creates parent directories if needed. Does not validate or commit.
37pub fn write_page(slug: &str, content: &str, wiki_root: &Path) -> Result<PathBuf> {
38    // Try to resolve existing page first
39    if let Ok(s) = Slug::try_from(slug)
40        && let Ok(path) = s.resolve(wiki_root)
41    {
42        std::fs::write(&path, content)?;
43        return Ok(path);
44    }
45
46    // New file — write as flat page
47    let path = wiki_root.join(format!("{slug}.md"));
48    if let Some(parent) = path.parent() {
49        std::fs::create_dir_all(parent)?;
50    }
51    std::fs::write(&path, content)?;
52    Ok(path)
53}
54
55/// List co-located assets of a bundle page.
56pub fn list_assets(slug: &Slug, wiki_root: &Path) -> Result<Vec<String>> {
57    let bundle_dir = wiki_root.join(slug.as_str());
58    if !bundle_dir.is_dir() || !bundle_dir.join("index.md").is_file() {
59        return Ok(Vec::new());
60    }
61    let mut assets = Vec::new();
62    for entry in std::fs::read_dir(&bundle_dir)? {
63        let entry = entry?;
64        let name = entry.file_name().to_string_lossy().into_owned();
65        if name != "index.md" && entry.file_type()?.is_file() {
66            assets.push(format!("wiki://{slug}/{name}"));
67        }
68    }
69    assets.sort();
70    Ok(assets)
71}
72
73/// Read raw bytes of a co-located asset.
74pub fn read_asset(slug: &Slug, filename: &str, wiki_root: &Path) -> Result<Vec<u8>> {
75    let path = wiki_root.join(slug.as_str()).join(filename);
76    if !path.is_file() {
77        bail!("asset not found: {slug}/{filename}");
78    }
79    Ok(std::fs::read(&path)?)
80}
81
82/// Create a new page with scaffolded frontmatter.
83///
84/// - `name_override`: override the title (default: derived from slug)
85/// - `type_override`: override the type (default: "page")
86/// - `bundle`: create as folder + index.md instead of flat file
87///
88/// Auto-creates missing parent sections with `type: section`.
89pub fn create_page(
90    slug: &Slug,
91    bundle: bool,
92    wiki_root: &Path,
93    name_override: Option<&str>,
94    type_override: Option<&str>,
95    body_template: Option<&str>,
96) -> Result<PathBuf> {
97    let slug_str = slug.as_str();
98
99    // Auto-create parent sections
100    let parts: Vec<&str> = slug_str.split('/').collect();
101    if parts.len() > 1 {
102        for i in 1..parts.len() {
103            let parent_slug = parts[..i].join("/");
104            let parent_dir = wiki_root.join(&parent_slug);
105            if !parent_dir.exists() {
106                std::fs::create_dir_all(&parent_dir)?;
107                let parent_s = Slug::try_from(parent_slug.as_str())?;
108                let fm = frontmatter::scaffold(&parent_s, true);
109                let content = frontmatter::write(&fm, "");
110                std::fs::write(parent_dir.join("index.md"), content)?;
111            }
112        }
113    }
114
115    let mut fm = frontmatter::scaffold(slug, false);
116    if let Some(name) = name_override {
117        fm.insert("title".into(), serde_yaml::Value::String(name.to_string()));
118    }
119    if let Some(t) = type_override {
120        fm.insert("type".into(), serde_yaml::Value::String(t.to_string()));
121    }
122    let body = body_template.unwrap_or("");
123    let content = frontmatter::write(&fm, body);
124
125    let path = if bundle {
126        let dir = wiki_root.join(slug_str);
127        std::fs::create_dir_all(&dir)?;
128        let p = dir.join("index.md");
129        std::fs::write(&p, content)?;
130        p
131    } else {
132        if let Some(parent) = wiki_root.join(slug_str).parent() {
133            std::fs::create_dir_all(parent)?;
134        }
135        let p = wiki_root.join(format!("{slug_str}.md"));
136        std::fs::write(&p, content)?;
137        p
138    };
139
140    Ok(path)
141}
142
143/// Create a new section (directory + index.md with type: section).
144pub fn create_section(
145    slug: &Slug,
146    wiki_root: &Path,
147    body_template: Option<&str>,
148) -> Result<PathBuf> {
149    let dir = wiki_root.join(slug.as_str());
150    std::fs::create_dir_all(&dir)?;
151
152    let fm = frontmatter::scaffold(slug, true);
153    let body = body_template.unwrap_or("");
154    let content = frontmatter::write(&fm, body);
155    let path = dir.join("index.md");
156    std::fs::write(&path, content)?;
157    Ok(path)
158}
159
160/// Promote a flat page to a bundle (move .md into folder/index.md).
161pub fn promote_to_bundle(slug: &Slug, wiki_root: &Path) -> Result<()> {
162    let flat = wiki_root.join(format!("{}.md", slug.as_str()));
163    if !flat.is_file() {
164        bail!("flat page not found for slug: {slug}");
165    }
166    let bundle_dir = wiki_root.join(slug.as_str());
167    std::fs::create_dir_all(&bundle_dir)?;
168    let dest = bundle_dir.join("index.md");
169    std::fs::rename(&flat, &dest)?;
170    Ok(())
171}
172
173/// Delete a page from disk. Handles both flat (.md) and bundle (slug/index.md) formats.
174/// Returns true if a file was deleted, false if the page was not found.
175pub fn delete_page(slug: &str, wiki_root: &Path) -> Result<bool> {
176    // Try flat format: slug.md
177    let flat_path = wiki_root.join(format!("{slug}.md"));
178    if flat_path.exists() {
179        std::fs::remove_file(&flat_path)?;
180        return Ok(true);
181    }
182
183    // Try bundle format: slug/index.md
184    let bundle_path = wiki_root.join(slug).join("index.md");
185    if bundle_path.exists() {
186        // Remove the entire bundle directory
187        let bundle_dir = wiki_root.join(slug);
188        std::fs::remove_dir_all(&bundle_dir)?;
189        return Ok(true);
190    }
191
192    Ok(false)
193}