use omgbase_format::hash::{hex, sha256};
use serde_json::{Map, Value};
#[derive(Clone, Debug, PartialEq)]
pub struct MutBlock {
pub id: String,
pub kind: String,
pub raw: String,
pub trivia: String,
pub attrs: Map<String, Value>,
pub children: Vec<MutBlock>,
pub dirty: bool,
}
impl MutBlock {
#[must_use]
pub fn new(id: &str, kind: &str, raw: &str, trivia: &str) -> Self {
Self {
id: id.to_owned(),
kind: kind.to_owned(),
raw: raw.to_owned(),
trivia: trivia.to_owned(),
attrs: Map::new(),
children: Vec::new(),
dirty: false,
}
}
pub fn iter(&self) -> impl Iterator<Item = &MutBlock> {
Walk { stack: vec![self] }
}
#[must_use]
pub fn has_dirty_descendant(&self) -> bool {
self.dirty || self.children.iter().any(MutBlock::has_dirty_descendant)
}
pub fn mark_subtree_clean(&mut self) {
self.dirty = false;
for c in &mut self.children {
c.mark_subtree_clean();
}
}
#[must_use]
pub fn subtree_ids(&self) -> Vec<String> {
self.iter().map(|b| b.id.clone()).collect()
}
}
struct Walk<'a> {
stack: Vec<&'a MutBlock>,
}
impl<'a> Iterator for Walk<'a> {
type Item = &'a MutBlock;
fn next(&mut self) -> Option<Self::Item> {
let b = self.stack.pop()?;
self.stack.extend(b.children.iter().rev());
Some(b)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct MutDoc {
pub doc_id: String,
pub path: String,
pub format: String,
pub leading_trivia: String,
pub frontmatter_raw: Option<String>,
pub children: Vec<MutBlock>,
}
pub type BlockPath = Vec<usize>;
impl MutDoc {
#[must_use]
pub fn new(doc_id: &str, path: &str, children: Vec<MutBlock>) -> Self {
Self {
doc_id: doc_id.to_owned(),
path: path.to_owned(),
format: "markdown".to_owned(),
leading_trivia: String::new(),
frontmatter_raw: None,
children,
}
}
pub fn iter(&self) -> impl Iterator<Item = &MutBlock> {
Walk {
stack: self.children.iter().rev().collect(),
}
}
#[must_use]
pub fn locate(&self, block_id: &str) -> Option<BlockPath> {
fn search(list: &[MutBlock], id: &str, path: &mut BlockPath) -> bool {
for (i, b) in list.iter().enumerate() {
path.push(i);
if b.id == id || search(&b.children, id, path) {
return true;
}
path.pop();
}
false
}
let mut path = Vec::new();
search(&self.children, block_id, &mut path).then_some(path)
}
#[must_use]
pub fn contains(&self, block_id: &str) -> bool {
self.locate(block_id).is_some()
}
#[must_use]
pub fn siblings(&self, parent: &[usize]) -> &Vec<MutBlock> {
let mut list = &self.children;
for &i in parent {
list = &list[i].children;
}
list
}
pub fn siblings_mut(&mut self, parent: &[usize]) -> &mut Vec<MutBlock> {
let mut list = &mut self.children;
for &i in parent {
list = &mut list[i].children;
}
list
}
#[must_use]
pub fn block(&self, path: &[usize]) -> &MutBlock {
let (last, parent) = path.split_last().expect("a block path is non-empty");
&self.siblings(parent)[*last]
}
pub fn block_mut(&mut self, path: &[usize]) -> &mut MutBlock {
let (last, parent) = path.split_last().expect("a block path is non-empty");
&mut self.siblings_mut(parent)[*last]
}
#[must_use]
pub fn owner_of(&self, parent: &[usize]) -> Option<&MutBlock> {
if parent.is_empty() {
None
} else {
Some(self.block(parent))
}
}
pub fn mark_container_dirty(&mut self, parent: &[usize]) {
if !parent.is_empty() {
self.block_mut(parent).dirty = true;
}
}
#[must_use]
pub fn all_ids(&self) -> Vec<String> {
self.iter().map(|b| b.id.clone()).collect()
}
}
#[must_use]
pub fn raw_hash_hex(raw: &str) -> String {
hex(&sha256(raw.as_bytes()))
}
#[must_use]
pub fn child_ids(list: &[MutBlock]) -> Vec<String> {
list.iter().map(|b| b.id.clone()).collect()
}
#[must_use]
pub fn parent_children_hash(list: &[MutBlock]) -> String {
hex(&sha256(child_ids(list).join(",").as_bytes()))
}
#[cfg(test)]
mod tests {
use super::*;
fn nested() -> MutDoc {
let mut list = MutBlock::new("l", "list", "- a\n- b", "\n");
let mut a = MutBlock::new("a", "list_item", "- a", "");
a.children.push(MutBlock::new("p", "paragraph", "a", ""));
list.children.push(a);
list.children
.push(MutBlock::new("b", "list_item", "- b", ""));
MutDoc::new(
"d_0",
"a.md",
vec![MutBlock::new("h", "heading", "# H", "\n\n"), list],
)
}
#[test]
fn locate_returns_paths_and_accessors_resolve_them() {
let doc = nested();
assert_eq!(doc.locate("h"), Some(vec![0]));
assert_eq!(doc.locate("l"), Some(vec![1]));
assert_eq!(doc.locate("b"), Some(vec![1, 1]));
assert_eq!(doc.locate("p"), Some(vec![1, 0, 0]));
assert_eq!(doc.locate("zz"), None);
assert_eq!(doc.block(&[1, 0, 0]).raw, "a");
assert_eq!(doc.siblings(&[1]).len(), 2);
assert_eq!(doc.owner_of(&[1]).map(|b| b.id.as_str()), Some("l"));
assert!(doc.owner_of(&[]).is_none());
assert_eq!(doc.all_ids(), ["h", "l", "a", "p", "b"]);
}
#[test]
fn dirty_marks_propagate_to_ancestors_only_by_query() {
let mut doc = nested();
assert!(!doc.block(&[1]).has_dirty_descendant());
doc.mark_container_dirty(&[1, 0]);
assert!(doc.block(&[1, 0]).dirty);
assert!(doc.block(&[1]).has_dirty_descendant());
assert!(!doc.block(&[1]).dirty);
doc.mark_container_dirty(&[]);
doc.block_mut(&[1]).mark_subtree_clean();
assert!(!doc.block(&[1]).has_dirty_descendant());
}
#[test]
fn hashes() {
assert_eq!(
raw_hash_hex("abc"),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
let doc = nested();
assert_eq!(child_ids(&doc.children), ["h", "l"]);
assert_eq!(parent_children_hash(&doc.children), raw_hash_hex("h,l"));
assert_eq!(parent_children_hash(&[]), raw_hash_hex(""));
}
}