use indexmap::IndexMap;
use crate::ast::{Location, ValidationError, Value};
use crate::grammar::Attribute;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub enum NodeType {
Blockquote,
Code,
Comment,
Document,
Em,
Error,
Fence,
Hardbreak,
Heading,
Hr,
Image,
Inline,
Item,
Link,
List,
#[default]
Node,
Paragraph,
S,
Softbreak,
Strong,
Table,
Tag,
Tbody,
Td,
Text,
Th,
Thead,
Tr,
}
impl NodeType {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
NodeType::Blockquote => "blockquote",
NodeType::Code => "code",
NodeType::Comment => "comment",
NodeType::Document => "document",
NodeType::Em => "em",
NodeType::Error => "error",
NodeType::Fence => "fence",
NodeType::Hardbreak => "hardbreak",
NodeType::Heading => "heading",
NodeType::Hr => "hr",
NodeType::Image => "image",
NodeType::Inline => "inline",
NodeType::Item => "item",
NodeType::Link => "link",
NodeType::List => "list",
NodeType::Node => "node",
NodeType::Paragraph => "paragraph",
NodeType::S => "s",
NodeType::Softbreak => "softbreak",
NodeType::Strong => "strong",
NodeType::Table => "table",
NodeType::Tag => "tag",
NodeType::Tbody => "tbody",
NodeType::Td => "td",
NodeType::Text => "text",
NodeType::Th => "th",
NodeType::Thead => "thead",
NodeType::Tr => "tr",
}
}
pub const ALL: [NodeType; 28] = [
NodeType::Blockquote,
NodeType::Code,
NodeType::Comment,
NodeType::Document,
NodeType::Em,
NodeType::Error,
NodeType::Fence,
NodeType::Hardbreak,
NodeType::Heading,
NodeType::Hr,
NodeType::Image,
NodeType::Inline,
NodeType::Item,
NodeType::Link,
NodeType::List,
NodeType::Node,
NodeType::Paragraph,
NodeType::S,
NodeType::Softbreak,
NodeType::Strong,
NodeType::Table,
NodeType::Tag,
NodeType::Tbody,
NodeType::Td,
NodeType::Text,
NodeType::Th,
NodeType::Thead,
NodeType::Tr,
];
#[must_use]
pub fn from_name(name: &str) -> Option<NodeType> {
Some(match name {
"blockquote" => NodeType::Blockquote,
"code" => NodeType::Code,
"comment" => NodeType::Comment,
"document" => NodeType::Document,
"em" => NodeType::Em,
"error" => NodeType::Error,
"fence" => NodeType::Fence,
"hardbreak" => NodeType::Hardbreak,
"heading" => NodeType::Heading,
"hr" => NodeType::Hr,
"image" => NodeType::Image,
"inline" => NodeType::Inline,
"item" => NodeType::Item,
"link" => NodeType::Link,
"list" => NodeType::List,
"node" => NodeType::Node,
"paragraph" => NodeType::Paragraph,
"s" => NodeType::S,
"softbreak" => NodeType::Softbreak,
"strong" => NodeType::Strong,
"table" => NodeType::Table,
"tag" => NodeType::Tag,
"tbody" => NodeType::Tbody,
"td" => NodeType::Td,
"text" => NodeType::Text,
"th" => NodeType::Th,
"thead" => NodeType::Thead,
"tr" => NodeType::Tr,
_ => return None,
})
}
}
impl std::fmt::Display for NodeType {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Default)]
pub struct Node<'a> {
pub node_type: NodeType,
pub tag: Option<String>,
pub attributes: IndexMap<String, Value>,
pub children: Vec<Node<'a>>,
pub slots: IndexMap<String, Node<'a>>,
pub errors: Vec<ValidationError<'a>>,
pub lines: Vec<usize>,
pub annotations: Vec<Attribute>,
pub inline: bool,
pub location: Option<Location<'a>>,
}
impl Clone for Node<'_> {
fn clone(&self) -> Self {
enum Step<'s, 'a> {
Open(&'s Node<'a>),
Close(&'s Node<'a>),
}
let mut plan = vec![Step::Open(self)];
let mut done: Vec<Node<'_>> = Vec::new();
while let Some(step) = plan.pop() {
match step {
Step::Open(node) => {
plan.push(Step::Close(node));
for child in node.children.iter().rev() {
plan.push(Step::Open(child));
}
for (_, slot) in node.slots.iter().rev() {
plan.push(Step::Open(slot));
}
}
Step::Close(node) => {
let total = node.slots.len() + node.children.len();
let start = done.len().saturating_sub(total);
let mut finished = done.split_off(start).into_iter();
let slots: IndexMap<String, Node<'_>> = node
.slots
.keys()
.cloned()
.zip(finished.by_ref().take(node.slots.len()))
.collect();
let children: Vec<Node<'_>> = finished.collect();
done.push(Node {
node_type: node.node_type,
tag: node.tag.clone(),
attributes: node.attributes.clone(),
children,
slots,
errors: node.errors.clone(),
lines: node.lines.clone(),
annotations: node.annotations.clone(),
inline: node.inline,
location: node.location,
});
}
}
}
done.pop().unwrap_or_default()
}
}
impl PartialEq for Node<'_> {
fn eq(&self, other: &Self) -> bool {
let mut work: Vec<(&Node<'_>, &Node<'_>)> = vec![(self, other)];
while let Some((left, right)) = work.pop() {
if left.node_type != right.node_type
|| left.tag != right.tag
|| left.attributes != right.attributes
|| left.errors != right.errors
|| left.lines != right.lines
|| left.annotations != right.annotations
|| left.inline != right.inline
|| left.location != right.location
|| left.children.len() != right.children.len()
|| left.slots.len() != right.slots.len()
{
return false;
}
work.extend(left.children.iter().zip(right.children.iter()));
for (key, slot) in &left.slots {
match right.slots.get(key) {
Some(other_slot) => work.push((slot, other_slot)),
None => return false,
}
}
}
true
}
}
impl std::fmt::Debug for Node<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let alternate = f.alternate();
let mut stack: Vec<NodeTok<'_, '_>> = vec![NodeTok::Node(self, 0)];
while let Some(token) = stack.pop() {
match token {
NodeTok::Text(text) => f.write_str(text)?,
NodeTok::Owned(text) => f.write_str(&text)?,
NodeTok::Line(depth) => {
f.write_str("\n")?;
for _ in 0..depth {
f.write_str(" ")?;
}
}
NodeTok::Node(node, depth) => expand_node(f, &mut stack, node, depth, alternate)?,
}
}
Ok(())
}
}
enum NodeTok<'n, 'a> {
Node(&'n Node<'a>, usize),
Text(&'static str),
Owned(String),
Line(usize),
}
fn indent_block(body: &str, depth: usize) -> String {
let pad = " ".repeat(depth);
body.replace('\n', &format!("\n{pad}"))
}
fn expand_node<'n, 'a>(
f: &mut std::fmt::Formatter<'_>,
stack: &mut Vec<NodeTok<'n, 'a>>,
node: &'n Node<'a>,
depth: usize,
alternate: bool,
) -> std::fmt::Result {
fn flat(value: &dyn std::fmt::Debug, depth: usize, alternate: bool) -> String {
if alternate {
indent_block(&format!("{value:#?}"), depth)
} else {
format!("{value:?}")
}
}
let mut queued: Vec<NodeTok<'n, 'a>> = Vec::new();
if alternate {
let inner = depth + 1;
f.write_str("Node {")?;
for (name, rendered) in [
("node_type", flat(&node.node_type, inner, true)),
("tag", flat(&node.tag, inner, true)),
("attributes", flat(&node.attributes, inner, true)),
] {
queued.push(NodeTok::Line(inner));
queued.push(NodeTok::Owned(format!("{name}: {rendered},")));
}
queued.push(NodeTok::Line(inner));
if node.children.is_empty() {
queued.push(NodeTok::Text("children: [],"));
} else {
queued.push(NodeTok::Text("children: ["));
for child in &node.children {
queued.push(NodeTok::Line(inner + 1));
queued.push(NodeTok::Node(child, inner + 1));
queued.push(NodeTok::Text(","));
}
queued.push(NodeTok::Line(inner));
queued.push(NodeTok::Text("],"));
}
queued.push(NodeTok::Line(inner));
if node.slots.is_empty() {
queued.push(NodeTok::Text("slots: {},"));
} else {
queued.push(NodeTok::Text("slots: {"));
for (key, slot) in &node.slots {
queued.push(NodeTok::Line(inner + 1));
queued.push(NodeTok::Owned(format!("{key:?}: ")));
queued.push(NodeTok::Node(slot, inner + 1));
queued.push(NodeTok::Text(","));
}
queued.push(NodeTok::Line(inner));
queued.push(NodeTok::Text("},"));
}
for (name, rendered) in [
("errors", flat(&node.errors, inner, true)),
("lines", flat(&node.lines, inner, true)),
("annotations", flat(&node.annotations, inner, true)),
("inline", flat(&node.inline, inner, true)),
("location", flat(&node.location, inner, true)),
] {
queued.push(NodeTok::Line(inner));
queued.push(NodeTok::Owned(format!("{name}: {rendered},")));
}
queued.push(NodeTok::Line(depth));
queued.push(NodeTok::Text("}"));
} else {
write!(
f,
"Node {{ node_type: {}, tag: {}, attributes: {}, children: [",
flat(&node.node_type, depth, false),
flat(&node.tag, depth, false),
flat(&node.attributes, depth, false),
)?;
for (index, child) in node.children.iter().enumerate() {
if index > 0 {
queued.push(NodeTok::Text(", "));
}
queued.push(NodeTok::Node(child, depth));
}
queued.push(NodeTok::Text("], slots: {"));
for (index, (key, slot)) in node.slots.iter().enumerate() {
if index > 0 {
queued.push(NodeTok::Text(", "));
}
queued.push(NodeTok::Owned(format!("{key:?}: ")));
queued.push(NodeTok::Node(slot, depth));
}
queued.push(NodeTok::Owned(format!(
"}}, errors: {}, lines: {}, annotations: {}, inline: {}, location: {} }}",
flat(&node.errors, depth, false),
flat(&node.lines, depth, false),
flat(&node.annotations, depth, false),
flat(&node.inline, depth, false),
flat(&node.location, depth, false),
)));
}
stack.extend(queued.into_iter().rev());
Ok(())
}
impl<'a> Node<'a> {
#[must_use]
pub fn new(node_type: NodeType) -> Node<'a> {
Node {
node_type,
tag: None,
attributes: IndexMap::new(),
children: Vec::new(),
slots: IndexMap::new(),
errors: Vec::new(),
lines: Vec::new(),
annotations: Vec::new(),
inline: false,
location: None,
}
}
#[must_use]
pub fn with(
node_type: NodeType,
attributes: IndexMap<String, Value>,
children: Vec<Node<'a>>,
tag: Option<String>,
) -> Node<'a> {
Node {
node_type,
tag,
attributes,
children,
slots: IndexMap::new(),
errors: Vec::new(),
lines: Vec::new(),
annotations: Vec::new(),
inline: false,
location: None,
}
}
pub fn push(&mut self, node: Node<'a>) {
self.children.push(node);
}
pub fn set(&mut self, name: impl Into<String>, value: Value) {
self.attributes.insert(name.into(), value);
}
#[must_use]
pub fn get(&self, name: &str) -> Option<&Value> {
self.attributes.get(name)
}
#[must_use]
pub fn name(&self) -> &str {
self.tag
.as_deref()
.unwrap_or_else(|| self.node_type.as_str())
}
#[must_use]
pub fn walk(&self) -> Walk<'_, 'a> {
Walk {
stack: self.descendants_in_order(),
}
}
fn descendants_in_order(&self) -> Vec<&Node<'a>> {
let mut out: Vec<&Node<'a>> = self.slots.values().chain(self.children.iter()).collect();
out.reverse();
out
}
}
impl Drop for Node<'_> {
fn drop(&mut self) {
let mut pending: Vec<Node<'_>> = std::mem::take(&mut self.children);
pending.extend(self.slots.drain(..).map(|(_, node)| node));
while let Some(mut node) = pending.pop() {
pending.append(&mut node.children);
pending.extend(node.slots.drain(..).map(|(_, child)| child));
}
}
}
pub struct Walk<'n, 'a> {
stack: Vec<&'n Node<'a>>,
}
impl<'n, 'a> Iterator for Walk<'n, 'a> {
type Item = &'n Node<'a>;
fn next(&mut self) -> Option<&'n Node<'a>> {
let node = self.stack.pop()?;
self.stack.extend(node.descendants_in_order());
Some(node)
}
}
#[cfg(test)]
mod debug_parity {
use super::*;
mod mirror {
#![allow(dead_code, clippy::struct_field_names)]
use super::{Attribute, Location, NodeType, ValidationError, Value};
use indexmap::IndexMap;
#[derive(Debug)]
pub struct Node<'a> {
pub node_type: NodeType,
pub tag: Option<String>,
pub attributes: IndexMap<String, Value>,
pub children: Vec<Node<'a>>,
pub slots: IndexMap<String, Node<'a>>,
pub errors: Vec<ValidationError<'a>>,
pub lines: Vec<usize>,
pub annotations: Vec<Attribute>,
pub inline: bool,
pub location: Option<Location<'a>>,
}
}
fn to_mirror<'a>(node: &Node<'a>) -> mirror::Node<'a> {
mirror::Node {
node_type: node.node_type,
tag: node.tag.clone(),
attributes: node.attributes.clone(),
children: node.children.iter().map(to_mirror).collect(),
slots: node
.slots
.iter()
.map(|(key, slot)| (key.clone(), to_mirror(slot)))
.collect(),
errors: node.errors.clone(),
lines: node.lines.clone(),
annotations: node.annotations.clone(),
inline: node.inline,
location: node.location,
}
}
fn assert_parity(node: &Node<'_>) {
let reference = to_mirror(node);
assert_eq!(format!("{node:?}"), format!("{reference:?}"), "plain Debug");
assert_eq!(
format!("{node:#?}"),
format!("{reference:#?}"),
"alternate Debug"
);
}
#[test]
fn every_node_shape_formats_as_the_derive_would() {
let mut bare = Node::new(NodeType::Paragraph);
bare.lines = vec![1, 2];
let mut attributed = Node::new(NodeType::Tag);
attributed.tag = Some("callout".to_owned());
attributed.set("level", Value::Number(2.0));
attributed.set("title", Value::String("hi".to_owned()));
attributed.inline = true;
let nested = Node::with(
NodeType::Document,
IndexMap::new(),
vec![Node::with(
NodeType::Paragraph,
IndexMap::new(),
vec![Node::new(NodeType::Text)],
None,
)],
None,
);
let mut slotted = Node::new(NodeType::Tag);
slotted.tag = Some("card".to_owned());
slotted
.slots
.insert("header".to_owned(), Node::new(NodeType::Paragraph));
let mut deep_attribute = Node::new(NodeType::Tag);
deep_attribute.set(
"data",
Value::Array(vec![Value::Hash(
[("k".to_owned(), Value::Null)].into_iter().collect(),
)]),
);
for shape in &[bare, attributed, nested, slotted, deep_attribute] {
assert_parity(shape);
}
}
#[test]
fn a_deep_node_survives_all_three_traversals() {
let mut node = Node::new(NodeType::Paragraph);
for _ in 0..100_000 {
node = Node::with(NodeType::Tag, IndexMap::new(), vec![node], Some("a".into()));
}
let copy = node.clone();
assert!(copy == node, "an iterative clone must equal its source");
assert!(format!("{node:?}").starts_with("Node { node_type: Tag"));
}
#[test]
fn a_node_deep_through_slots_survives_all_three() {
let mut node = Node::new(NodeType::Paragraph);
for _ in 0..100_000 {
let mut outer = Node::new(NodeType::Tag);
outer.slots.insert("s".to_owned(), node);
node = outer;
}
let copy = node.clone();
assert_eq!(copy, node);
}
#[test]
fn cloning_preserves_child_and_slot_order() {
let mut node = Node::with(
NodeType::Document,
IndexMap::new(),
vec![Node::new(NodeType::Heading), Node::new(NodeType::Paragraph)],
None,
);
node.slots.insert("z".to_owned(), Node::new(NodeType::Text));
node.slots
.insert("a".to_owned(), Node::new(NodeType::Fence));
let copy = node.clone();
assert_eq!(copy.children.len(), 2);
assert_eq!(copy.children[0].node_type, NodeType::Heading);
assert_eq!(copy.children[1].node_type, NodeType::Paragraph);
assert_eq!(copy.slots.keys().collect::<Vec<_>>(), ["z", "a"]);
assert_eq!(copy.slots["a"].node_type, NodeType::Fence);
assert_eq!(copy, node);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn text(content: &str) -> Node<'static> {
let mut node = Node::new(NodeType::Text);
node.set("content", Value::String(content.to_string()));
node
}
fn block(node_type: NodeType, children: Vec<Node<'static>>) -> Node<'static> {
Node::with(node_type, IndexMap::new(), children, None)
}
#[test]
fn walking_a_simple_document_visits_every_descendant() {
let example = block(
NodeType::Document,
vec![
block(
NodeType::Heading,
vec![block(NodeType::Inline, vec![text("This is a heading")])],
),
block(
NodeType::Paragraph,
vec![block(NodeType::Inline, vec![text("This is a paragraph")])],
),
],
);
assert_eq!(example.walk().count(), 6);
}
#[test]
fn walking_visits_slots_before_children() {
let mut tag = Node::with(
NodeType::Tag,
IndexMap::new(),
Vec::new(),
Some("example".into()),
);
tag.slots.insert(
"foo".to_string(),
block(
NodeType::Paragraph,
vec![block(NodeType::Inline, vec![text("baz")])],
),
);
tag.push(block(
NodeType::Heading,
vec![block(NodeType::Inline, vec![text("bar")])],
));
let document = block(NodeType::Document, vec![tag]);
let visited: Vec<String> = document
.walk()
.map(|node| node.name().to_string())
.collect();
assert_eq!(
visited,
[
"example",
"paragraph",
"inline",
"text",
"heading",
"inline",
"text"
]
);
}
#[test]
fn walking_is_iterative_and_survives_deep_nesting() {
let mut node = Node::new(NodeType::Document);
for _ in 0..50_000 {
node = block(NodeType::Tag, vec![node]);
}
assert_eq!(node.walk().count(), 50_000);
}
#[test]
fn attribute_order_is_authored_order() {
let mut node = Node::new(NodeType::Tag);
node.set("z", Value::Number(1.0));
node.set("a", Value::Number(2.0));
node.set("z", Value::Number(3.0));
let keys: Vec<&str> = node.attributes.keys().map(String::as_str).collect();
assert_eq!(keys, ["z", "a"]);
assert_eq!(node.get("z"), Some(&Value::Number(3.0)));
}
#[test]
fn a_node_names_itself_by_tag_then_type() {
assert_eq!(Node::new(NodeType::Paragraph).name(), "paragraph");
let mut tagged = Node::new(NodeType::Tag);
tagged.tag = Some("callout".to_string());
assert_eq!(tagged.name(), "callout");
}
#[test]
fn node_types_spell_themselves_as_upstream_does() {
assert_eq!(NodeType::Fence.as_str(), "fence");
assert_eq!(NodeType::Hardbreak.to_string(), "hardbreak");
assert_eq!(NodeType::default(), NodeType::Node);
}
}
#[cfg(test)]
mod node_type_list {
use super::NodeType;
#[test]
fn all_round_trips_through_its_names_and_repeats_none() {
let mut seen = std::collections::HashSet::new();
for node_type in NodeType::ALL {
assert_eq!(NodeType::from_name(node_type.as_str()), Some(node_type));
assert!(seen.insert(node_type), "{node_type} is listed twice");
}
}
}