cgx_engine/docs/
prompts_index.rs1use std::path::{Path, PathBuf};
8
9#[derive(Debug, Clone)]
10pub struct PromptEntry {
11 pub note_path: PathBuf,
12 pub source_path: Option<String>,
13 pub byte_offset_begin: usize,
14 pub byte_offset_end: usize,
15 pub packet: String,
16}
17
18pub fn list_unfilled(vault_root: &Path) -> std::io::Result<Vec<PromptEntry>> {
20 let mut out = Vec::new();
21 walk(vault_root, &mut out)?;
22 Ok(out)
23}
24
25fn walk(dir: &Path, out: &mut Vec<PromptEntry>) -> std::io::Result<()> {
26 for entry in std::fs::read_dir(dir)? {
27 let entry = entry?;
28 let path = entry.path();
29 if path.is_dir() {
30 if path
32 .file_name()
33 .and_then(|n| n.to_str())
34 .map(|n| n.starts_with('.'))
35 .unwrap_or(false)
36 {
37 continue;
38 }
39 walk(&path, out)?;
40 continue;
41 }
42 if path.extension().and_then(|e| e.to_str()) != Some("md") {
43 continue;
44 }
45 let content = match std::fs::read_to_string(&path) {
46 Ok(c) => c,
47 Err(_) => continue,
48 };
49 if let Some((b, e, packet, source)) = find_packet(&content) {
50 out.push(PromptEntry {
51 note_path: path.clone(),
52 source_path: source,
53 byte_offset_begin: b,
54 byte_offset_end: e,
55 packet,
56 });
57 }
58 }
59 Ok(())
60}
61
62const BEGIN: &str = "<!-- cgx-prompt:begin -->";
63const END: &str = "<!-- cgx-prompt:end -->";
64
65fn find_packet(content: &str) -> Option<(usize, usize, String, Option<String>)> {
66 let begin = content.find(BEGIN)?;
67 let end_marker = content[begin..].find(END)? + begin;
68 let end = end_marker + END.len();
69 let packet = content[begin..end].to_string();
70 let source_path = find_frontmatter_field(content, "path");
71 Some((begin, end, packet, source_path))
72}
73
74fn find_frontmatter_field(content: &str, field: &str) -> Option<String> {
75 if !content.starts_with("---") {
76 return None;
77 }
78 let after = &content[3..];
79 let end = after.find("\n---")?;
80 let fm = &after[..end];
81 let prefix = format!("{}: ", field);
82 for line in fm.lines() {
83 if let Some(rest) = line.trim_start().strip_prefix(&prefix) {
84 let rest = rest.trim();
85 let rest = rest.trim_matches('"').trim_matches('\'');
86 return Some(rest.to_string());
87 }
88 }
89 None
90}
91
92pub fn fill_packet(content: &str, prose: &str) -> Option<String> {
95 let (b, e, _, _) = find_packet(content)?;
96 let mut out = String::with_capacity(content.len() + prose.len());
97 out.push_str(&content[..b]);
98 out.push_str(prose.trim_end());
99 out.push('\n');
100 out.push_str(&content[e..]);
101 Some(out)
102}