diff --git a/crates/service-webpage/src/ast/mod.rs b/crates/service-webpage/src/ast/mod.rs new file mode 100644 index 0000000..ff912a6 --- /dev/null +++ b/crates/service-webpage/src/ast/mod.rs @@ -0,0 +1,5 @@ +mod walk; +pub use walk::*; + +mod walk_mut; +pub use walk_mut::*; diff --git a/crates/service-webpage/src/ast/walk.rs b/crates/service-webpage/src/ast/walk.rs new file mode 100644 index 0000000..b9cbe28 --- /dev/null +++ b/crates/service-webpage/src/ast/walk.rs @@ -0,0 +1,384 @@ +use markdown::mdast::Node; +use std::fmt::Debug; +use std::marker::PhantomPinned; + +pub enum AstWalkStep<'a, T> { + Enter(&'a T), + Exit(&'a T), +} + +impl Debug for AstWalkStep<'_, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Enter(x) => f.debug_tuple("AstWalkStep::Enter").field(x).finish(), + Self::Exit(x) => f.debug_tuple("AstWalkStep::Exit").field(x).finish(), + } + } +} + +pub struct AstWalk<'a> { + _pin: PhantomPinned, + child_stack: Vec, + node_stack: Vec<&'a Node>, +} + +impl<'a> AstWalk<'a> { + pub fn new(root: &'a Node) -> Self { + let mut res = Self { + _pin: PhantomPinned {}, + node_stack: Vec::with_capacity(32), + child_stack: Vec::with_capacity(32), + }; + + res.node_stack.push(root); + return res; + } + + fn _next_inner(&mut self) -> Option> { + if self.node_stack.is_empty() { + return None; + } + + let current_node = *self.node_stack.last().unwrap(); + + // The index of the next child we should look at. + // If `None`, we look at the parent. + let current_child = { + let n_nodes = self.node_stack.len(); + let n_childs = self.child_stack.len(); + match n_nodes - n_childs { + 2.. => unreachable!(), + 1 => None, + 0 => Some(self.child_stack.pop().unwrap()), + } + }; + + match current_child { + None => { + self.child_stack.push(0); + return Some(AstWalkStep::Enter(current_node)); + } + + Some(current_child) => { + let child = current_node + .children() + .map(|x| x.get(current_child)) + .flatten(); + + match child { + None => { + self.node_stack.pop(); + return Some(AstWalkStep::Exit(current_node)); + } + + Some(x) => { + self.child_stack.push(current_child + 1); + self.node_stack.push(&x); + self.child_stack.push(0); + return Some(AstWalkStep::Enter(x)); + } + } + } + } + } +} + +impl<'a> Iterator for AstWalk<'a> { + type Item = AstWalkStep<'a, Node>; + + fn next(self: &mut Self) -> Option { + return self._next_inner(); + } +} + +// +// MARK: tests +// + +#[cfg(test)] +mod tests { + use super::*; + use markdown::mdast::{Emphasis, Paragraph, Root, Strong, Text}; + + #[test] + fn single_leaf() { + let node = Node::Text(Text { + value: "Hello".to_string(), + position: None, + }); + + let walker = AstWalk::new(&node); + let steps: Vec<_> = walker.collect(); + + assert_eq!(steps.len(), 2); + assert!(matches!(steps[0], AstWalkStep::Enter(_))); + assert!(matches!(steps[1], AstWalkStep::Exit(_))); + } + + #[test] + fn single_child() { + let node = Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "Hello".to_string(), + position: None, + })], + position: None, + }); + + let walker = AstWalk::new(&node); + let steps: Vec<_> = walker.collect(); + + // Should be: Enter(Paragraph), Leaf(Text), Exit(Paragraph) + assert_eq!(steps.len(), 4); + assert!(matches!(steps[0], AstWalkStep::Enter(_))); + assert!(matches!(steps[1], AstWalkStep::Enter(_))); + assert!(matches!(steps[2], AstWalkStep::Exit(_))); + assert!(matches!(steps[3], AstWalkStep::Exit(_))); + } + + #[test] + fn multiple_children() { + let node = Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "Hello".to_string(), + position: None, + }), + Node::Text(Text { + value: " ".to_string(), + position: None, + }), + Node::Text(Text { + value: "World".to_string(), + position: None, + }), + ], + position: None, + }); + + let walker = AstWalk::new(&node); + let steps: Vec<_> = walker.collect(); + + // Should be: Enter(Paragraph), Leaf(Text1), Leaf(Text2), Leaf(Text3), Exit(Paragraph) + assert_eq!(steps.len(), 8); + assert!(matches!(steps[0], AstWalkStep::Enter(_))); + assert!(matches!(steps[1], AstWalkStep::Enter(_))); + assert!(matches!(steps[2], AstWalkStep::Exit(_))); + assert!(matches!(steps[3], AstWalkStep::Enter(_))); + assert!(matches!(steps[4], AstWalkStep::Exit(_))); + assert!(matches!(steps[5], AstWalkStep::Enter(_))); + assert!(matches!(steps[6], AstWalkStep::Exit(_))); + assert!(matches!(steps[7], AstWalkStep::Exit(_))); + } + + #[test] + fn nested_1() { + let node = Node::Paragraph(Paragraph { + children: vec![Node::Emphasis(Emphasis { + children: vec![Node::Text(Text { + value: "emphasized".to_string(), + position: None, + })], + position: None, + })], + position: None, + }); + + let walker = AstWalk::new(&node); + let steps: Vec<_> = walker.collect(); + + // Should be: Enter(Paragraph), Enter(Emphasis), Leaf(Text), Exit(Emphasis), Exit(Paragraph) + assert_eq!(steps.len(), 6); + assert!(matches!(steps[0], AstWalkStep::Enter(_))); + assert!(matches!(steps[1], AstWalkStep::Enter(_))); + assert!(matches!(steps[2], AstWalkStep::Enter(_))); + assert!(matches!(steps[3], AstWalkStep::Exit(_))); + assert!(matches!(steps[4], AstWalkStep::Exit(_))); + assert!(matches!(steps[5], AstWalkStep::Exit(_))); + } + + #[test] + fn nested_2() { + // Create: Paragraph -> [Text, Strong -> Text, Text] + let node = Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "Before ".to_string(), + position: None, + }), + Node::Strong(Strong { + children: vec![Node::Text(Text { + value: "bold".to_string(), + position: None, + })], + position: None, + }), + Node::Text(Text { + value: " after".to_string(), + position: None, + }), + ], + position: None, + }); + + let walker = AstWalk::new(&node); + let steps: Vec<_> = walker.collect(); + + // Expected order: + // 0: Enter(Paragraph) + // 1: Leaf(Text "Before ") + // 2: Enter(Strong) + // 3: Leaf(Text "bold") + // 4: Exit(Strong) + // 5: Leaf(Text " after") + // 6: Exit(Paragraph) + assert_eq!(steps.len(), 10); + assert!(matches!(steps[0], AstWalkStep::Enter(_))); + assert!(matches!(steps[1], AstWalkStep::Enter(_))); + assert!(matches!(steps[2], AstWalkStep::Exit(_))); + assert!(matches!(steps[3], AstWalkStep::Enter(_))); + assert!(matches!(steps[4], AstWalkStep::Enter(_))); + assert!(matches!(steps[5], AstWalkStep::Exit(_))); + assert!(matches!(steps[6], AstWalkStep::Exit(_))); + assert!(matches!(steps[7], AstWalkStep::Enter(_))); + assert!(matches!(steps[8], AstWalkStep::Exit(_))); + assert!(matches!(steps[9], AstWalkStep::Exit(_))); + } + + #[test] + fn nested_3() { + let node = Node::Paragraph(Paragraph { + children: vec![Node::Emphasis(Emphasis { + children: vec![Node::Strong(Strong { + children: vec![Node::Text(Text { + value: "deeply nested".to_string(), + position: None, + })], + position: None, + })], + position: None, + })], + position: None, + }); + + let walker = AstWalk::new(&node); + let steps: Vec<_> = walker.collect(); + + // Should be: Enter(Paragraph), Enter(Emphasis), Enter(Strong), Leaf(Text), Exit(Strong), Exit(Emphasis), Exit(Paragraph) + assert_eq!(steps.len(), 8); + assert!(matches!(steps[0], AstWalkStep::Enter(_))); + assert!(matches!(steps[1], AstWalkStep::Enter(_))); + assert!(matches!(steps[2], AstWalkStep::Enter(_))); + assert!(matches!(steps[3], AstWalkStep::Enter(_))); + assert!(matches!(steps[4], AstWalkStep::Exit(_))); + assert!(matches!(steps[5], AstWalkStep::Exit(_))); + assert!(matches!(steps[6], AstWalkStep::Exit(_))); + assert!(matches!(steps[7], AstWalkStep::Exit(_))); + } + + #[test] + fn empty_parent() { + let node = Node::Paragraph(Paragraph { + children: vec![], + position: None, + }); + + let walker = AstWalk::new(&node); + let steps: Vec<_> = walker.collect(); + + // Should be: Enter(Paragraph), Exit(Paragraph) + assert_eq!(steps.len(), 2); + assert!(matches!(steps[0], AstWalkStep::Enter(_))); + assert!(matches!(steps[1], AstWalkStep::Exit(_))); + } + + #[test] + fn multiple_paragraphs() { + let node = Node::Root(Root { + children: vec![ + Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "First".to_string(), + position: None, + })], + position: None, + }), + Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "Second".to_string(), + position: None, + })], + position: None, + }), + ], + position: None, + }); + + let walker = AstWalk::new(&node); + let steps: Vec<_> = walker.collect(); + + // Expected order: + // 0: Enter(Root) + // 1: Enter(Paragraph) + // 2: Enter(Text "First") + // 2: Exit(Text "First") + // 3: Exit(Paragraph) + // 4: Enter(Paragraph) + // 5: Enter(Text "Second") + // 5: Exit(Text "Second") + // 6: Exit(Paragraph) + // 7: Exit(Root) + assert_eq!(steps.len(), 10); + assert!(matches!(steps[0], AstWalkStep::Enter(_))); + assert!(matches!(steps[1], AstWalkStep::Enter(_))); + assert!(matches!(steps[2], AstWalkStep::Enter(_))); + assert!(matches!(steps[3], AstWalkStep::Exit(_))); + assert!(matches!(steps[4], AstWalkStep::Exit(_))); + assert!(matches!(steps[5], AstWalkStep::Enter(_))); + assert!(matches!(steps[6], AstWalkStep::Enter(_))); + assert!(matches!(steps[7], AstWalkStep::Exit(_))); + assert!(matches!(steps[8], AstWalkStep::Exit(_))); + assert!(matches!(steps[9], AstWalkStep::Exit(_))); + } + + #[test] + fn enter_exit() { + let node = Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::Emphasis(Emphasis { + children: vec![Node::Text(Text { + value: "a".to_string(), + position: None, + })], + position: None, + }), + Node::Strong(Strong { + children: vec![Node::Text(Text { + value: "b".to_string(), + position: None, + })], + position: None, + }), + ], + position: None, + })], + position: None, + }); + + let walker = AstWalk::new(&node); + let steps: Vec<_> = walker.collect(); + + let enter_count = steps + .iter() + .filter(|s| matches!(s, AstWalkStep::Enter(_))) + .count(); + let exit_count = steps + .iter() + .filter(|s| matches!(s, AstWalkStep::Exit(_))) + .count(); + + assert_eq!(enter_count, exit_count); + assert_eq!(enter_count, 6); + } +} diff --git a/crates/service-webpage/src/ast/walk_mut.rs b/crates/service-webpage/src/ast/walk_mut.rs new file mode 100644 index 0000000..0740902 --- /dev/null +++ b/crates/service-webpage/src/ast/walk_mut.rs @@ -0,0 +1,378 @@ +use markdown::mdast::Node; +use std::{fmt::Debug, marker::PhantomData}; + +pub enum AstWalkMutStep<'a, T> { + Enter(&'a T), + Exit(&'a mut T), +} + +impl Debug for AstWalkMutStep<'_, T> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Enter(x) => f.debug_tuple("AstWalkMutStep::Enter").field(x).finish(), + Self::Exit(x) => f.debug_tuple("AstWalkMutStep::Exit").field(x).finish(), + } + } +} + +pub struct AstWalkMut<'a> { + _life: PhantomData<&'a mut Node>, + child_stack: Vec, + node_stack: Vec<*mut Node>, +} + +impl<'a> AstWalkMut<'a> { + pub fn new(root: &'a mut Node) -> Self { + let mut res = Self { + _life: PhantomData, + node_stack: Vec::with_capacity(32), + child_stack: Vec::with_capacity(32), + }; + + res.node_stack.push(root); + return res; + } + + fn _next_inner(&mut self) -> Option> { + if self.node_stack.is_empty() { + return None; + } + + let current_node = unsafe { &mut **self.node_stack.last().unwrap_unchecked() }; + + // The index of the next child we should look at. + // If `None`, we look at the parent. + let current_child = { + let n_nodes = self.node_stack.len(); + let n_childs = self.child_stack.len(); + match n_nodes - n_childs { + 2.. => unreachable!(), + 1 => None, + 0 => Some(self.child_stack.pop().unwrap()), + } + }; + + match current_child { + None => { + self.child_stack.push(0); + return Some(AstWalkMutStep::Enter(current_node)); + } + + Some(current_child) => { + if current_node.children().is_none() + || current_child >= current_node.children().unwrap().len() + { + self.node_stack.pop(); + return Some(AstWalkMutStep::Exit(current_node)); + } + + let child = &mut current_node.children_mut().unwrap()[current_child]; + + self.child_stack.push(current_child + 1); + self.node_stack.push(child); + self.child_stack.push(0); + return Some(AstWalkMutStep::Enter(child)); + } + } + } +} + +impl<'a> Iterator for AstWalkMut<'a> { + type Item = AstWalkMutStep<'a, Node>; + + fn next(self: &mut Self) -> Option { + return self._next_inner(); + } +} + +// +// MARK: tests +// + +#[cfg(test)] +mod tests { + use super::*; + use markdown::mdast::{Emphasis, Paragraph, Root, Strong, Text}; + + #[test] + fn single_leaf() { + let mut node = Node::Text(Text { + value: "Hello".to_string(), + position: None, + }); + + let walker = AstWalkMut::new(&mut node); + let steps: Vec<_> = walker.collect(); + + assert_eq!(steps.len(), 2); + assert!(matches!(steps[0], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[1], AstWalkMutStep::Exit(_))); + } + + #[test] + fn single_child() { + let mut node = Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "Hello".to_string(), + position: None, + })], + position: None, + }); + + let walker = AstWalkMut::new(&mut node); + let steps: Vec<_> = walker.collect(); + + // Should be: Enter(Paragraph), Leaf(Text), Exit(Paragraph) + assert_eq!(steps.len(), 4); + assert!(matches!(steps[0], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[1], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[2], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[3], AstWalkMutStep::Exit(_))); + } + + #[test] + fn multiple_children() { + let mut node = Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "Hello".to_string(), + position: None, + }), + Node::Text(Text { + value: " ".to_string(), + position: None, + }), + Node::Text(Text { + value: "World".to_string(), + position: None, + }), + ], + position: None, + }); + + let walker = AstWalkMut::new(&mut node); + let steps: Vec<_> = walker.collect(); + + // Should be: Enter(Paragraph), Leaf(Text1), Leaf(Text2), Leaf(Text3), Exit(Paragraph) + assert_eq!(steps.len(), 8); + assert!(matches!(steps[0], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[1], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[2], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[3], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[4], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[5], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[6], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[7], AstWalkMutStep::Exit(_))); + } + + #[test] + fn nested_1() { + let mut node = Node::Paragraph(Paragraph { + children: vec![Node::Emphasis(Emphasis { + children: vec![Node::Text(Text { + value: "emphasized".to_string(), + position: None, + })], + position: None, + })], + position: None, + }); + + let walker = AstWalkMut::new(&mut node); + let steps: Vec<_> = walker.collect(); + + // Should be: Enter(Paragraph), Enter(Emphasis), Leaf(Text), Exit(Emphasis), Exit(Paragraph) + assert_eq!(steps.len(), 6); + assert!(matches!(steps[0], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[1], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[2], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[3], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[4], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[5], AstWalkMutStep::Exit(_))); + } + + #[test] + fn nested_2() { + // Create: Paragraph -> [Text, Strong -> Text, Text] + let mut node = Node::Paragraph(Paragraph { + children: vec![ + Node::Text(Text { + value: "Before ".to_string(), + position: None, + }), + Node::Strong(Strong { + children: vec![Node::Text(Text { + value: "bold".to_string(), + position: None, + })], + position: None, + }), + Node::Text(Text { + value: " after".to_string(), + position: None, + }), + ], + position: None, + }); + + let walker = AstWalkMut::new(&mut node); + let steps: Vec<_> = walker.collect(); + + // Expected order: + // 0: Enter(Paragraph) + // 1: Leaf(Text "Before ") + // 2: Enter(Strong) + // 3: Leaf(Text "bold") + // 4: Exit(Strong) + // 5: Leaf(Text " after") + // 6: Exit(Paragraph) + assert_eq!(steps.len(), 10); + assert!(matches!(steps[0], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[1], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[2], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[3], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[4], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[5], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[6], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[7], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[8], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[9], AstWalkMutStep::Exit(_))); + } + + #[test] + fn nested_3() { + let mut node = Node::Paragraph(Paragraph { + children: vec![Node::Emphasis(Emphasis { + children: vec![Node::Strong(Strong { + children: vec![Node::Text(Text { + value: "deeply nested".to_string(), + position: None, + })], + position: None, + })], + position: None, + })], + position: None, + }); + + let walker = AstWalkMut::new(&mut node); + let steps: Vec<_> = walker.collect(); + + // Should be: Enter(Paragraph), Enter(Emphasis), Enter(Strong), Leaf(Text), Exit(Strong), Exit(Emphasis), Exit(Paragraph) + assert_eq!(steps.len(), 8); + assert!(matches!(steps[0], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[1], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[2], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[3], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[4], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[5], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[6], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[7], AstWalkMutStep::Exit(_))); + } + + #[test] + fn empty_parent() { + let mut node = Node::Paragraph(Paragraph { + children: vec![], + position: None, + }); + + let walker = AstWalkMut::new(&mut node); + let steps: Vec<_> = walker.collect(); + + // Should be: Enter(Paragraph), Exit(Paragraph) + assert_eq!(steps.len(), 2); + assert!(matches!(steps[0], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[1], AstWalkMutStep::Exit(_))); + } + + #[test] + fn multiple_paragraphs() { + let mut node = Node::Root(Root { + children: vec![ + Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "First".to_string(), + position: None, + })], + position: None, + }), + Node::Paragraph(Paragraph { + children: vec![Node::Text(Text { + value: "Second".to_string(), + position: None, + })], + position: None, + }), + ], + position: None, + }); + + let walker = AstWalkMut::new(&mut node); + let steps: Vec<_> = walker.collect(); + + // Expected order: + // 0: Enter(Root) + // 1: Enter(Paragraph) + // 2: Enter(Text "First") + // 2: Exit(Text "First") + // 3: Exit(Paragraph) + // 4: Enter(Paragraph) + // 5: Enter(Text "Second") + // 5: Exit(Text "Second") + // 6: Exit(Paragraph) + // 7: Exit(Root) + assert_eq!(steps.len(), 10); + assert!(matches!(steps[0], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[1], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[2], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[3], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[4], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[5], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[6], AstWalkMutStep::Enter(_))); + assert!(matches!(steps[7], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[8], AstWalkMutStep::Exit(_))); + assert!(matches!(steps[9], AstWalkMutStep::Exit(_))); + } + + #[test] + fn enter_exit() { + let mut node = Node::Root(Root { + children: vec![Node::Paragraph(Paragraph { + children: vec![ + Node::Emphasis(Emphasis { + children: vec![Node::Text(Text { + value: "a".to_string(), + position: None, + })], + position: None, + }), + Node::Strong(Strong { + children: vec![Node::Text(Text { + value: "b".to_string(), + position: None, + })], + position: None, + }), + ], + position: None, + })], + position: None, + }); + + let walker = AstWalkMut::new(&mut node); + let steps: Vec<_> = walker.collect(); + + let enter_count = steps + .iter() + .filter(|s| matches!(s, AstWalkMutStep::Enter(_))) + .count(); + let exit_count = steps + .iter() + .filter(|s| matches!(s, AstWalkMutStep::Exit(_))) + .count(); + + assert_eq!(enter_count, exit_count); + assert_eq!(enter_count, 6); + } +} diff --git a/crates/service-webpage/src/lib.rs b/crates/service-webpage/src/lib.rs index 7368703..ad419a2 100644 --- a/crates/service-webpage/src/lib.rs +++ b/crates/service-webpage/src/lib.rs @@ -2,6 +2,7 @@ use axum::Router; use libservice::ToService; use utoipa::OpenApi; +mod ast; mod components; mod routes;