1use std::path::{Path, PathBuf};
2
3use anyhow::{Result, bail};
4use serde::Serialize;
5use tantivy::{
6 Searcher, Term,
7 query::TermQuery,
8 schema::{IndexRecordOption, Value},
9};
10
11use crate::config;
12use crate::engine::EngineState;
13use crate::git;
14use crate::index_schema::IndexSchema;
15use crate::markdown;
16use crate::slug::{ReadTarget, Slug, WikiUri, resolve_read_target};
17
18#[derive(Debug, Clone, Serialize)]
20pub struct BacklinkRef {
21 pub slug: String,
23 pub title: String,
25}
26
27pub fn backlinks_query(
29 searcher: &Searcher,
30 is: &IndexSchema,
31 target_slug: &str,
32) -> Result<Vec<BacklinkRef>> {
33 let f_body_links = is.field("body_links");
34 let f_slug = is.field("slug");
35 let f_title = is.field("title");
36
37 let term = Term::from_field_text(f_body_links, target_slug);
38 let query = TermQuery::new(term, IndexRecordOption::Basic);
39
40 let doc_addrs = searcher.search(&query, &tantivy::collector::DocSetCollector)?;
41
42 let mut refs: Vec<BacklinkRef> = doc_addrs
43 .into_iter()
44 .filter_map(|addr| {
45 let doc: tantivy::TantivyDocument = searcher.doc(addr).ok()?;
46 let slug = doc
47 .get_first(f_slug)
48 .and_then(|v| v.as_str())
49 .unwrap_or("")
50 .to_string();
51 let title = doc
52 .get_first(f_title)
53 .and_then(|v| v.as_str())
54 .unwrap_or("")
55 .to_string();
56 if slug.is_empty() {
57 None
58 } else {
59 Some(BacklinkRef { slug, title })
60 }
61 })
62 .collect();
63
64 refs.sort_by(|a, b| a.slug.cmp(&b.slug));
65 Ok(refs)
66}
67
68pub fn backlinks_for(
70 engine: &EngineState,
71 wiki_name: &str,
72 target_slug: &str,
73) -> Result<Vec<BacklinkRef>> {
74 let space = engine.space(wiki_name)?;
75 let searcher = space.index_manager.searcher()?;
76 backlinks_query(&searcher, &space.index_schema, target_slug)
77}
78
79pub enum ContentReadResult {
81 Page(String),
83 Assets(Vec<String>),
85 Binary,
87}
88
89pub fn content_read(
91 engine: &EngineState,
92 uri: &str,
93 wiki_flag: Option<&str>,
94 no_frontmatter: bool,
95 list_assets: bool,
96) -> Result<ContentReadResult> {
97 let (entry, slug) = WikiUri::resolve(uri, wiki_flag, &engine.config)?;
98 let wiki_root = engine.space(&entry.name)?.wiki_root.clone();
99
100 if list_assets {
101 let assets = markdown::list_assets(&slug, &wiki_root)?;
102 return Ok(ContentReadResult::Assets(assets));
103 }
104
105 match resolve_read_target(slug.as_str(), &wiki_root)? {
106 ReadTarget::Page(_) => {
107 let wiki_cfg = config::load_wiki(&PathBuf::from(&entry.path)).unwrap_or_default();
108 let resolved = config::resolve(&engine.config, &wiki_cfg);
109 let strip = no_frontmatter || resolved.read.no_frontmatter;
110 let content = markdown::read_page(&slug, &wiki_root, strip)?;
111 Ok(ContentReadResult::Page(content))
112 }
113 ReadTarget::Asset(parent_slug, filename) => {
114 let parent = Slug::try_from(parent_slug.as_str())?;
115 let bytes = markdown::read_asset(&parent, &filename, &wiki_root)?;
116 match String::from_utf8(bytes) {
117 Ok(text) => Ok(ContentReadResult::Page(text)),
118 Err(_) => Ok(ContentReadResult::Binary),
119 }
120 }
121 }
122}
123
124pub struct WriteResult {
126 pub bytes_written: usize,
128 pub path: PathBuf,
130}
131
132pub fn content_write(
134 engine: &EngineState,
135 uri: &str,
136 wiki_flag: Option<&str>,
137 content: &str,
138) -> Result<WriteResult> {
139 let (_entry, slug) = WikiUri::resolve(uri, wiki_flag, &engine.config)?;
140 let wiki_root = engine.space(&_entry.name)?.wiki_root.clone();
141 let path = markdown::write_page(slug.as_str(), content, &wiki_root)?;
142 Ok(WriteResult {
143 bytes_written: content.len(),
144 path,
145 })
146}
147
148pub struct ContentNewResult {
150 pub uri: String,
152 pub slug: String,
154 pub path: PathBuf,
156 pub wiki_root: PathBuf,
158 pub bundle: bool,
160}
161
162pub fn content_new(
164 engine: &EngineState,
165 uri: &str,
166 wiki_flag: Option<&str>,
167 section: bool,
168 bundle: bool,
169 name: Option<&str>,
170 type_: Option<&str>,
171) -> Result<ContentNewResult> {
172 let (entry, slug) = WikiUri::resolve(uri, wiki_flag, &engine.config)?;
173 let repo_root = PathBuf::from(&entry.path);
174 let wiki_root = engine.space(&entry.name)?.wiki_root.clone();
175
176 let type_name = if section {
177 "section"
178 } else {
179 type_.unwrap_or("page")
180 };
181 let body_template = resolve_body_template(&repo_root, type_name);
182
183 let path = if section {
184 markdown::create_section(&slug, &wiki_root, body_template.as_deref())?
185 } else {
186 markdown::create_page(
187 &slug,
188 bundle,
189 &wiki_root,
190 name,
191 type_,
192 body_template.as_deref(),
193 )?
194 };
195
196 Ok(ContentNewResult {
197 uri: format!("wiki://{}/{slug}", entry.name),
198 slug: slug.as_str().to_string(),
199 path,
200 wiki_root,
201 bundle,
202 })
203}
204
205fn resolve_body_template(repo_root: &Path, type_name: &str) -> Option<String> {
210 let template_path = repo_root.join("schemas").join(format!("{type_name}.md"));
211 if template_path.is_file() {
212 return std::fs::read_to_string(&template_path).ok();
213 }
214 crate::default_schemas::embedded_body_template(type_name).map(|s| s.to_string())
215}
216
217pub fn content_commit(
219 engine: &EngineState,
220 wiki_name: &str,
221 slugs: &[String],
222 all: bool,
223 message: Option<&str>,
224) -> Result<String> {
225 let space = engine.space(wiki_name)?;
226
227 if slugs.is_empty() && !all {
228 bail!("specify slugs or --all");
229 }
230
231 if all {
232 let msg = message.unwrap_or("commit: all");
233 return git::commit(&space.repo_root, msg);
234 }
235
236 let mut paths = Vec::new();
237 for s in slugs {
238 let slug = Slug::try_from(s.as_str())?;
239 let resolved = slug.resolve(&space.wiki_root)?;
240 if resolved.file_name() == Some(std::ffi::OsStr::new("index.md")) {
241 let bundle_dir = resolved.parent().unwrap();
242 for entry in walkdir::WalkDir::new(bundle_dir)
243 .into_iter()
244 .filter_map(|e| e.ok())
245 {
246 if entry.path().is_file() {
247 paths.push(entry.path().to_path_buf());
248 }
249 }
250 } else {
251 paths.push(resolved);
252 }
253 }
254 let path_refs: Vec<&Path> = paths.iter().map(|p| p.as_path()).collect();
255 let default_msg = format!("commit: {}", slugs.join(", "));
256 let msg = message.unwrap_or(&default_msg);
257 git::commit_paths(&space.repo_root, &path_refs, msg)
258}