weave_content/
writeback.rs1use std::path::Path;
2
3#[derive(Debug)]
5pub struct PendingId {
6 pub line: usize,
8 pub id: String,
10 pub kind: WriteBackKind,
12}
13
14#[derive(Debug)]
16pub enum WriteBackKind {
17 EntityFrontMatter,
20 InlineEvent,
23 Relationship,
26}
27
28pub fn apply_writebacks(content: &str, pending: &mut [PendingId]) -> Option<String> {
35 if pending.is_empty() {
36 return None;
37 }
38
39 pending.sort_by_key(|p| std::cmp::Reverse(p.line));
41
42 let mut lines: Vec<String> = content.lines().map(String::from).collect();
43 let trailing_newline = content.ends_with('\n');
45
46 for p in pending.iter() {
47 let insert_text = match p.kind {
48 WriteBackKind::EntityFrontMatter => {
49 format!("id: {}", p.id)
50 }
51 WriteBackKind::InlineEvent => {
52 format!("- id: {}", p.id)
53 }
54 WriteBackKind::Relationship => {
55 format!(" - id: {}", p.id)
56 }
57 };
58
59 let insert_after = match p.kind {
60 WriteBackKind::EntityFrontMatter => {
61 let idx = p.line.saturating_sub(1); if idx <= lines.len() {
66 lines.insert(idx, insert_text);
67 }
68 continue;
69 }
70 WriteBackKind::InlineEvent | WriteBackKind::Relationship => {
71 p.line }
74 };
75
76 if insert_after <= lines.len() {
77 lines.insert(insert_after, insert_text);
78 }
79 }
80
81 let mut result = lines.join("\n");
82 if trailing_newline {
83 result.push('\n');
84 }
85 Some(result)
86}
87
88pub fn write_file(path: &Path, content: &str) -> Result<(), String> {
90 std::fs::write(path, content)
91 .map_err(|e| format!("{}: error writing file: {e}", path.display()))
92}
93
94pub fn find_front_matter_end(content: &str) -> Option<usize> {
98 let mut in_front_matter = false;
99 for (i, line) in content.lines().enumerate() {
100 let trimmed = line.trim();
101 if trimmed == "---" && !in_front_matter {
102 in_front_matter = true;
103 } else if trimmed == "---" && in_front_matter {
104 return Some(i + 1); }
106 }
107 None
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn writeback_entity_front_matter() {
116 let content = "---\n---\n\n# Mark Bonnick\n\n- nationality: British\n";
117 let end_line = find_front_matter_end(content).unwrap();
118 assert_eq!(end_line, 2);
119
120 let mut pending = vec![PendingId {
121 line: end_line,
122 id: "01JXYZ123456789ABCDEFGHIJK".to_string(),
123 kind: WriteBackKind::EntityFrontMatter,
124 }];
125
126 let result = apply_writebacks(content, &mut pending).unwrap();
127 assert!(result.contains("id: 01JXYZ123456789ABCDEFGHIJK"));
128 let lines: Vec<&str> = result.lines().collect();
130 assert_eq!(lines[0], "---");
131 assert_eq!(lines[1], "id: 01JXYZ123456789ABCDEFGHIJK");
132 assert_eq!(lines[2], "---");
133 }
134
135 #[test]
136 fn writeback_entity_front_matter_with_existing_fields() {
137 let content = "---\nother: value\n---\n\n# Test\n";
138 let end_line = find_front_matter_end(content).unwrap();
139 assert_eq!(end_line, 3);
140
141 let mut pending = vec![PendingId {
142 line: end_line,
143 id: "01JABC000000000000000000AA".to_string(),
144 kind: WriteBackKind::EntityFrontMatter,
145 }];
146
147 let result = apply_writebacks(content, &mut pending).unwrap();
148 let lines: Vec<&str> = result.lines().collect();
149 assert_eq!(lines[0], "---");
150 assert_eq!(lines[1], "other: value");
151 assert_eq!(lines[2], "id: 01JABC000000000000000000AA");
152 assert_eq!(lines[3], "---");
153 }
154
155 #[test]
156 fn writeback_inline_event() {
157 let content = "\
158## Events
159
160### Dismissal
161- occurred_at: 2024-12-24
162- event_type: termination
163";
164 let mut pending = vec![PendingId {
166 line: 3,
167 id: "01JXYZ123456789ABCDEFGHIJK".to_string(),
168 kind: WriteBackKind::InlineEvent,
169 }];
170
171 let result = apply_writebacks(content, &mut pending).unwrap();
172 let lines: Vec<&str> = result.lines().collect();
173 assert_eq!(lines[2], "### Dismissal");
174 assert_eq!(lines[3], "- id: 01JXYZ123456789ABCDEFGHIJK");
175 assert_eq!(lines[4], "- occurred_at: 2024-12-24");
176 }
177
178 #[test]
179 fn writeback_relationship() {
180 let content = "\
181## Relationships
182
183- Alice -> Bob: employed_by
184 - source: https://example.com
185";
186 let mut pending = vec![PendingId {
188 line: 3,
189 id: "01JXYZ123456789ABCDEFGHIJK".to_string(),
190 kind: WriteBackKind::Relationship,
191 }];
192
193 let result = apply_writebacks(content, &mut pending).unwrap();
194 let lines: Vec<&str> = result.lines().collect();
195 assert_eq!(lines[2], "- Alice -> Bob: employed_by");
196 assert_eq!(lines[3], " - id: 01JXYZ123456789ABCDEFGHIJK");
197 assert_eq!(lines[4], " - source: https://example.com");
198 }
199
200 #[test]
201 fn writeback_multiple_insertions() {
202 let content = "\
203## Events
204
205### Event A
206- occurred_at: 2024-01-01
207
208### Event B
209- occurred_at: 2024-06-01
210
211## Relationships
212
213- Event A -> Event B: associate_of
214";
215 let mut pending = vec![
216 PendingId {
217 line: 3,
218 id: "01JAAA000000000000000000AA".to_string(),
219 kind: WriteBackKind::InlineEvent,
220 },
221 PendingId {
222 line: 6,
223 id: "01JBBB000000000000000000BB".to_string(),
224 kind: WriteBackKind::InlineEvent,
225 },
226 PendingId {
227 line: 10,
228 id: "01JCCC000000000000000000CC".to_string(),
229 kind: WriteBackKind::Relationship,
230 },
231 ];
232
233 let result = apply_writebacks(content, &mut pending).unwrap();
234 assert!(result.contains("- id: 01JAAA000000000000000000AA"));
235 assert!(result.contains("- id: 01JBBB000000000000000000BB"));
236 assert!(result.contains(" - id: 01JCCC000000000000000000CC"));
237 }
238
239 #[test]
240 fn writeback_empty_pending() {
241 let content = "some content\n";
242 let mut pending = Vec::new();
243 assert!(apply_writebacks(content, &mut pending).is_none());
244 }
245
246 #[test]
247 fn writeback_preserves_trailing_newline() {
248 let content = "---\n---\n\n# Test\n";
249 let mut pending = vec![PendingId {
250 line: 2,
251 id: "01JABC000000000000000000AA".to_string(),
252 kind: WriteBackKind::EntityFrontMatter,
253 }];
254 let result = apply_writebacks(content, &mut pending).unwrap();
255 assert!(result.ends_with('\n'));
256 }
257
258 #[test]
259 fn find_front_matter_end_basic() {
260 assert_eq!(find_front_matter_end("---\nid: foo\n---\n"), Some(3));
261 assert_eq!(find_front_matter_end("---\n---\n"), Some(2));
262 assert_eq!(find_front_matter_end("no front matter"), None);
263 assert_eq!(find_front_matter_end("---\nunclosed"), None);
264 }
265}