1use omgbase_format::hash::{hex, sha256};
9use serde_json::{Map, Value};
10
11#[derive(Clone, Debug, PartialEq)]
13pub struct MutBlock {
14 pub id: String,
15 pub kind: String,
17 pub raw: String,
19 pub trivia: String,
21 pub attrs: Map<String, Value>,
23 pub children: Vec<MutBlock>,
24 pub dirty: bool,
27}
28
29impl MutBlock {
30 #[must_use]
32 pub fn new(id: &str, kind: &str, raw: &str, trivia: &str) -> Self {
33 Self {
34 id: id.to_owned(),
35 kind: kind.to_owned(),
36 raw: raw.to_owned(),
37 trivia: trivia.to_owned(),
38 attrs: Map::new(),
39 children: Vec::new(),
40 dirty: false,
41 }
42 }
43
44 pub fn iter(&self) -> impl Iterator<Item = &MutBlock> {
46 Walk { stack: vec![self] }
47 }
48
49 #[must_use]
51 pub fn has_dirty_descendant(&self) -> bool {
52 self.dirty || self.children.iter().any(MutBlock::has_dirty_descendant)
53 }
54
55 pub fn mark_subtree_clean(&mut self) {
57 self.dirty = false;
58 for c in &mut self.children {
59 c.mark_subtree_clean();
60 }
61 }
62
63 #[must_use]
65 pub fn subtree_ids(&self) -> Vec<String> {
66 self.iter().map(|b| b.id.clone()).collect()
67 }
68}
69
70struct Walk<'a> {
71 stack: Vec<&'a MutBlock>,
72}
73
74impl<'a> Iterator for Walk<'a> {
75 type Item = &'a MutBlock;
76
77 fn next(&mut self) -> Option<Self::Item> {
78 let b = self.stack.pop()?;
79 self.stack.extend(b.children.iter().rev());
80 Some(b)
81 }
82}
83
84#[derive(Clone, Debug, PartialEq)]
86pub struct MutDoc {
87 pub doc_id: String,
88 pub path: String,
89 pub format: String,
91 pub leading_trivia: String,
93 pub frontmatter_raw: Option<String>,
96 pub children: Vec<MutBlock>,
98}
99
100pub type BlockPath = Vec<usize>;
104
105impl MutDoc {
106 #[must_use]
108 pub fn new(doc_id: &str, path: &str, children: Vec<MutBlock>) -> Self {
109 Self {
110 doc_id: doc_id.to_owned(),
111 path: path.to_owned(),
112 format: "markdown".to_owned(),
113 leading_trivia: String::new(),
114 frontmatter_raw: None,
115 children,
116 }
117 }
118
119 pub fn iter(&self) -> impl Iterator<Item = &MutBlock> {
121 Walk {
122 stack: self.children.iter().rev().collect(),
123 }
124 }
125
126 #[must_use]
128 pub fn locate(&self, block_id: &str) -> Option<BlockPath> {
129 fn search(list: &[MutBlock], id: &str, path: &mut BlockPath) -> bool {
130 for (i, b) in list.iter().enumerate() {
131 path.push(i);
132 if b.id == id || search(&b.children, id, path) {
133 return true;
134 }
135 path.pop();
136 }
137 false
138 }
139 let mut path = Vec::new();
140 search(&self.children, block_id, &mut path).then_some(path)
141 }
142
143 #[must_use]
145 pub fn contains(&self, block_id: &str) -> bool {
146 self.locate(block_id).is_some()
147 }
148
149 #[must_use]
151 pub fn siblings(&self, parent: &[usize]) -> &Vec<MutBlock> {
152 let mut list = &self.children;
153 for &i in parent {
154 list = &list[i].children;
155 }
156 list
157 }
158
159 pub fn siblings_mut(&mut self, parent: &[usize]) -> &mut Vec<MutBlock> {
161 let mut list = &mut self.children;
162 for &i in parent {
163 list = &mut list[i].children;
164 }
165 list
166 }
167
168 #[must_use]
174 pub fn block(&self, path: &[usize]) -> &MutBlock {
175 let (last, parent) = path.split_last().expect("a block path is non-empty");
176 &self.siblings(parent)[*last]
177 }
178
179 pub fn block_mut(&mut self, path: &[usize]) -> &mut MutBlock {
185 let (last, parent) = path.split_last().expect("a block path is non-empty");
186 &mut self.siblings_mut(parent)[*last]
187 }
188
189 #[must_use]
192 pub fn owner_of(&self, parent: &[usize]) -> Option<&MutBlock> {
193 if parent.is_empty() {
194 None
195 } else {
196 Some(self.block(parent))
197 }
198 }
199
200 pub fn mark_container_dirty(&mut self, parent: &[usize]) {
203 if !parent.is_empty() {
204 self.block_mut(parent).dirty = true;
205 }
206 }
207
208 #[must_use]
210 pub fn all_ids(&self) -> Vec<String> {
211 self.iter().map(|b| b.id.clone()).collect()
212 }
213}
214
215#[must_use]
217pub fn raw_hash_hex(raw: &str) -> String {
218 hex(&sha256(raw.as_bytes()))
219}
220
221#[must_use]
223pub fn child_ids(list: &[MutBlock]) -> Vec<String> {
224 list.iter().map(|b| b.id.clone()).collect()
225}
226
227#[must_use]
229pub fn parent_children_hash(list: &[MutBlock]) -> String {
230 hex(&sha256(child_ids(list).join(",").as_bytes()))
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236
237 fn nested() -> MutDoc {
238 let mut list = MutBlock::new("l", "list", "- a\n- b", "\n");
239 let mut a = MutBlock::new("a", "list_item", "- a", "");
240 a.children.push(MutBlock::new("p", "paragraph", "a", ""));
241 list.children.push(a);
242 list.children
243 .push(MutBlock::new("b", "list_item", "- b", ""));
244 MutDoc::new(
245 "d_0",
246 "a.md",
247 vec![MutBlock::new("h", "heading", "# H", "\n\n"), list],
248 )
249 }
250
251 #[test]
252 fn locate_returns_paths_and_accessors_resolve_them() {
253 let doc = nested();
254 assert_eq!(doc.locate("h"), Some(vec![0]));
255 assert_eq!(doc.locate("l"), Some(vec![1]));
256 assert_eq!(doc.locate("b"), Some(vec![1, 1]));
257 assert_eq!(doc.locate("p"), Some(vec![1, 0, 0]));
258 assert_eq!(doc.locate("zz"), None);
259 assert_eq!(doc.block(&[1, 0, 0]).raw, "a");
260 assert_eq!(doc.siblings(&[1]).len(), 2);
261 assert_eq!(doc.owner_of(&[1]).map(|b| b.id.as_str()), Some("l"));
262 assert!(doc.owner_of(&[]).is_none());
263 assert_eq!(doc.all_ids(), ["h", "l", "a", "p", "b"]);
264 }
265
266 #[test]
267 fn dirty_marks_propagate_to_ancestors_only_by_query() {
268 let mut doc = nested();
269 assert!(!doc.block(&[1]).has_dirty_descendant());
270 doc.mark_container_dirty(&[1, 0]);
271 assert!(doc.block(&[1, 0]).dirty);
272 assert!(doc.block(&[1]).has_dirty_descendant());
273 assert!(!doc.block(&[1]).dirty);
274 doc.mark_container_dirty(&[]);
275 doc.block_mut(&[1]).mark_subtree_clean();
276 assert!(!doc.block(&[1]).has_dirty_descendant());
277 }
278
279 #[test]
280 fn hashes() {
281 assert_eq!(
282 raw_hash_hex("abc"),
283 "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
284 );
285 let doc = nested();
286 assert_eq!(child_ids(&doc.children), ["h", "l"]);
287 assert_eq!(parent_children_hash(&doc.children), raw_hash_hex("h,l"));
288 assert_eq!(parent_children_hash(&[]), raw_hash_hex(""));
289 }
290}