Repository: yshavit/mdq
Pull request #57 (2024-06-23 00:35:55)

split MdqNode up into more precise enums
- MdqNode is either Block or Inline
- Blocks are either Leafs Blocks or Containers
- sort the various node types into those leafs and containers

In service of #53.


Patch:
diff --git a/src/fmt_md.rs b/src/fmt_md.rs
index 5f75e2f..3a331df 100644
--- a/src/fmt_md.rs
+++ b/src/fmt_md.rs
@@ -7,7 +7,7 @@ use crate::fmt_str::inlines_to_plain_string;
 use crate::output::{Block, Output, SimpleWrite};
 use crate::str_utils::{pad_to, standard_align, CountingWriter};
 use crate::tree::*;
-use crate::tree_ref::{ListItemRef, MdqNodeRef};
+use crate::tree_ref::{ListItemRef, MdqNodeRef, NonSelectable};
 
 #[derive(Default)]
 pub struct MdOptions {
@@ -294,54 +294,61 @@ impl<'a> MdWriterState<'a> {
                     write_row(out, row, rows_iter.peek().is_some());
                 }
             }
-            MdqNodeRef::ThematicBreak => {
-                out.with_block(Block::Plain, |out| out.write_str("***"));
+            MdqNodeRef::Inline(inline) => {
+                self.write_inline_element(out, inline);
             }
-            MdqNodeRef::CodeBlock(CodeBlock { variant, value }) => {
-                let (surround, meta) = match variant {
-                    CodeVariant::Code(opts) => {
-                        let meta = if let Some(opts) = opts {
-                            let mut extras = opts.language.to_string();
-                            if let Some(meta) = &opts.metadata {
-                                extras.push(' ');
-                                extras.push_str(meta);
-                            }
-                            Some(extras)
-                        } else {
-                            None
-                        };
-                        ("```", meta)
-                    }
-                    CodeVariant::Math { metadata } => {
-                        let meta = if let Some(meta) = metadata {
-                            let mut meta_with_space = String::with_capacity(meta.len() + 1);
-                            meta_with_space.push(' ');
-                            meta_with_space.push_str(meta);
-                            Some(meta_with_space)
-                        } else {
-                            None
-                        };
-                        ("$$", meta)
-                    }
-                    CodeVariant::Toml => ("+++", None),
-                    CodeVariant::Yaml => ("---", None),
-                };
+            MdqNodeRef::NonSelectable(node) => match node {
+                NonSelectable::ThematicBreak => {
+                    out.with_block(Block::Plain, |out| out.write_str("***"));
+                }
+                NonSelectable::CodeBlock(block) => {
+                    self.write_code_block(out, block);
+                }
+            },
+        }
+    }
 
-                out.with_pre_block(|out| {
-                    out.write_str(surround);
-                    if let Some(meta) = meta {
-                        out.write_str(&meta);
+    fn write_code_block<W: SimpleWrite>(&mut self, out: &mut Output<W>, block: &CodeBlock) {
+        let CodeBlock { variant, value } = block;
+        let (surround, meta) = match variant {
+            CodeVariant::Code(opts) => {
+                let meta = if let Some(opts) = opts {
+                    let mut extras = opts.language.to_string();
+                    if let Some(meta) = &opts.metadata {
+                        extras.push(' ');
+                        extras.push_str(meta);
                     }
-                    out.write_char('\n');
-                    out.write_str(value);
-                    out.write_char('\n');
-                    out.write_str(surround);
-                });
+                    Some(extras)
+                } else {
+                    None
+                };
+                ("```", meta)
             }
-            MdqNodeRef::Inline(inline) => {
-                self.write_inline_element(out, inline);
+            CodeVariant::Math { metadata } => {
+                let meta = if let Some(meta) = metadata {
+                    let mut meta_with_space = String::with_capacity(meta.len() + 1);
+                    meta_with_space.push(' ');
+                    meta_with_space.push_str(meta);
+                    Some(meta_with_space)
+                } else {
+                    None
+                };
+                ("$$", meta)
             }
-        }
+            CodeVariant::Toml => ("+++", None),
+            CodeVariant::Yaml => ("---", None),
+        };
+
+        out.with_pre_block(|out| {
+            out.write_str(surround);
+            if let Some(meta) = meta {
+                out.write_str(&meta);
+            }
+            out.write_char('\n');
+            out.write_str(value);
+            out.write_char('\n');
+            out.write_str(surround);
+        });
     }
 
     fn write_list_item<W: SimpleWrite>(&mut self, out: &mut Output<W>, index: &Option<u32>, item: &'a ListItem) {
@@ -583,11 +590,13 @@ pub mod tests {
     use indoc::indoc;
 
     use crate::fmt_md::MdOptions;
+    use crate::m_node;
     use crate::mdq_inline;
+    use crate::mdq_node;
     use crate::mdq_nodes;
     use crate::output::Output;
     use crate::tree::*;
-    use crate::tree_ref::MdqNodeRef;
+    use crate::tree_ref::{MdqNodeRef, NonSelectable};
 
     use super::write_md;
 
@@ -598,14 +607,6 @@ pub mod tests {
         List(_),
         ListItem(..),
         Table(_),
-        ThematicBreak,
-        CodeBlock(CodeBlock{variant: CodeVariant::Code(None), ..}),
-        CodeBlock(CodeBlock{variant: CodeVariant::Code(Some(CodeOpts{metadata: None, ..})), ..}),
-        CodeBlock(CodeBlock{variant: CodeVariant::Code(Some(CodeOpts{metadata: Some(_), ..})), ..}),
-        CodeBlock(CodeBlock{variant: CodeVariant::Math{metadata: None}, ..}),
-        CodeBlock(CodeBlock{variant: CodeVariant::Math{metadata: Some(_)}, ..}),
-        CodeBlock(CodeBlock{variant: CodeVariant::Toml, ..}),
-        CodeBlock(CodeBlock{variant: CodeVariant::Yaml, ..}),
 
         Inline(Inline::Span{variant: SpanVariant::Delete, ..}),
         Inline(Inline::Span{variant: SpanVariant::Emphasis, ..}),
@@ -635,6 +636,15 @@ pub mod tests {
         Inline(Inline::Image{link: LinkDefinition{title: Some(_), reference: LinkReference::Shortcut, ..}, ..}),
 
         Inline(Inline::Footnote{..}),
+
+        NonSelectable(NonSelectable::ThematicBreak),
+        NonSelectable(NonSelectable::CodeBlock(CodeBlock{variant: CodeVariant::Code(None), ..})),
+        NonSelectable(NonSelectable::CodeBlock(CodeBlock{variant: CodeVariant::Code(Some(CodeOpts{metadata: None, ..})), ..})),
+        NonSelectable(NonSelectable::CodeBlock(CodeBlock{variant: CodeVariant::Code(Some(CodeOpts{metadata: Some(_), ..})), ..})),
+        NonSelectable(NonSelectable::CodeBlock(CodeBlock{variant: CodeVariant::Math{metadata: None}, ..})),
+        NonSelectable(NonSelectable::CodeBlock(CodeBlock{variant: CodeVariant::Math{metadata: Some(_)}, ..})),
+        NonSelectable(NonSelectable::CodeBlock(CodeBlock{variant: CodeVariant::Toml, ..})),
+        NonSelectable(NonSelectable::CodeBlock(CodeBlock{variant: CodeVariant::Yaml, ..})),
     });
 
     #[test]
@@ -648,12 +658,7 @@ pub mod tests {
         #[test]
         fn one_paragraph() {
             check_render(
-                mdq_nodes![Paragraph {
-                    body: vec![Inline::Text {
-                        variant: TextVariant::Plain,
-                        value: "Hello, world".to_string()
-                    }]
-                }],
+                mdq_nodes!["Hello, world"],
                 indoc! {r#"
                 Hello, world"#},
             );
@@ -662,20 +667,7 @@ pub mod tests {
         #[test]
         fn two_paragraphs() {
             check_render(
-                mdq_nodes![
-                    Paragraph {
-                        body: vec![Inline::Text {
-                            variant: TextVariant::Plain,
-                            value: "First".to_string()
-                        }]
-                    },
-                    Paragraph {
-                        body: vec![Inline::Text {
-                            variant: TextVariant::Plain,
-                            value: "Second".to_string()
-                        }]
-                    },
-                ],
+                mdq_nodes!["First", "Second",],
                 indoc! {r#"
                 First
 
@@ -692,7 +684,7 @@ pub mod tests {
         #[test]
         fn totally_empty() {
             check_render(
-                mdq_nodes![Section {
+                mdq_nodes![Block::Container::Section {
                     depth: 3,
                     title: vec![],
                     body: vec![],
@@ -705,7 +697,7 @@ pub mod tests {
         #[test]
         fn only_title() {
             check_render(
-                mdq_nodes![Section {
+                mdq_nodes![Block::Container::Section {
                     depth: 3,
                     title: vec![mdq_inline!("My header")],
                     body: vec![],
@@ -718,12 +710,10 @@ pub mod tests {
         #[test]
         fn only_body() {
             check_render(
-                mdq_nodes![Section {
+                mdq_nodes![Block::Container::Section {
                     depth: 3,
                     title: vec![],
-                    body: mdq_nodes![Paragraph {
-                        body: vec![mdq_inline!("Hello, world.")]
-                    },],
+                    body: mdq_nodes!["Hello, world."],
                 }],
                 indoc! {r#"
                     ###
@@ -735,13 +725,11 @@ pub mod tests {
         #[test]
         fn title_and_body() {
             check_render(
-                mdq_nodes![Section {
+                mdq_nodes![Block::Container::Section {
                     depth: 1,
                     title: vec![mdq_inline!("My title")],
-                    body: mdq_nodes![BlockQuote {
-                        body: mdq_nodes![Paragraph {
-                            body: vec![mdq_inline!("Hello, world.")]
-                        },],
+                    body: mdq_nodes![Block::Container::BlockQuote {
+                        body: mdq_nodes!["Hello, world."],
                     },],
                 }],
                 indoc! {r#"
@@ -758,12 +746,7 @@ pub mod tests {
         #[test]
         fn simple() {
             check_render(
-                mdq_nodes![Paragraph {
-                    body: vec![Inline::Text {
-                        variant: TextVariant::Plain,
-                        value: "Hello, world".to_string()
-                    }]
-                }],
+                mdq_nodes!["Hello, world"],
                 indoc! {r#"
                 Hello, world"#},
             );
@@ -771,20 +754,14 @@ pub mod tests {
     }
 
     mod block_quote {
-        use crate::mdq_inline;
 
         use super::*;
 
         #[test]
         fn single_level() {
             check_render(
-                mdq_nodes![BlockQuote {
-                    body: mdq_nodes![Paragraph {
-                        body: vec![Inline::Text {
-                            variant: TextVariant::Plain,
-                            value: "Hello, world".to_string()
-                        }]
-                    }]
+                mdq_nodes![Block::Container::BlockQuote {
+                    body: mdq_nodes!["Hello, world"]
                 }],
                 indoc! {
                     r#"> Hello, world"#
@@ -795,15 +772,11 @@ pub mod tests {
         #[test]
         fn two_levels() {
             check_render(
-                mdq_nodes![BlockQuote {
+                mdq_nodes![Block::Container::BlockQuote {
                     body: mdq_nodes![
-                        Paragraph {
-                            body: vec![mdq_inline!("Outer")],
-                        },
-                        BlockQuote {
-                            body: mdq_nodes![Paragraph {
-                                body: vec![mdq_inline!("Inner")],
-                            },]
+                        "Outer",
+                        Block::Container::BlockQuote {
+                            body: mdq_nodes!["Inner"],
                         },
                     ]
                 }],
@@ -822,7 +795,7 @@ pub mod tests {
         #[test]
         fn ordered() {
             check_render(
-                mdq_nodes![List {
+                mdq_nodes![Block::Container::List {
                     starting_index: Some(3),
                     items: vec![
                         ListItem {
@@ -849,7 +822,7 @@ pub mod tests {
         #[test]
         fn unordered() {
             check_render(
-                mdq_nodes![List {
+                mdq_nodes![Block::Container::List {
                     starting_index: None,
                     items: vec![
                         ListItem {
@@ -876,7 +849,7 @@ pub mod tests {
         #[test]
         fn block_alignments() {
             check_render(
-                mdq_nodes![List {
+                mdq_nodes![Block::Container::List {
                     starting_index: None,
                     items: vec![
                         ListItem {
@@ -885,13 +858,13 @@ pub mod tests {
                         },
                         ListItem {
                             checked: None,
-                            item: mdq_nodes!(BlockQuote {
+                            item: mdq_nodes!(Block::Container::BlockQuote {
                                 body: mdq_nodes!["quoted block one", "quoted block two"]
                             })
                         },
                         ListItem {
                             checked: None,
-                            item: mdq_nodes!(CodeBlock {
+                            item: mdq_nodes!(Block::LeafBlock::CodeBlock {
                                 variant: CodeVariant::Code(None),
                                 value: "line 1\nline 2".to_string(),
                             })
@@ -899,13 +872,11 @@ pub mod tests {
                         ListItem {
                             checked: Some(false),
                             item: mdq_nodes![
-                                Paragraph {
-                                    body: vec![mdq_inline!("closing argument")]
-                                },
-                                BlockQuote {
+                                "closing argument",
+                                Block::Container::BlockQuote {
                                     body: mdq_nodes!["supporting evidence"]
                                 },
-                                CodeBlock {
+                                Block::LeafBlock::CodeBlock {
                                     variant: CodeVariant::Code(None),
                                     value: "line a\nline b".to_string(),
                                 },
@@ -989,7 +960,7 @@ pub mod tests {
         #[test]
         fn simple() {
             check_render(
-                mdq_nodes![Table {
+                mdq_nodes![Block::LeafBlock::Table {
                     alignments: vec![
                         mdast::AlignKind::Left,
                         mdast::AlignKind::Right,
@@ -1026,7 +997,7 @@ pub mod tests {
         fn single_char_cells() {
             // This checks the minimum padding aspects
             check_render(
-                mdq_nodes![Table {
+                mdq_nodes![Block::LeafBlock::Table {
                     alignments: vec![
                         mdast::AlignKind::Left,
                         mdast::AlignKind::Right,
@@ -1063,7 +1034,7 @@ pub mod tests {
         fn empty_cells() {
             // This checks the minimum padding aspects
             check_render(
-                mdq_nodes![Table {
+                mdq_nodes![Block::LeafBlock::Table {
                     alignments: vec![
                         mdast::AlignKind::Left,
                         mdast::AlignKind::Right,
@@ -1100,7 +1071,7 @@ pub mod tests {
         fn row_counts_inconsistent() {
             // This is an invalid table, but we should still support it
             check_render(
-                mdq_nodes![Table {
+                mdq_nodes![Block::LeafBlock::Table {
                     alignments: vec![mdast::AlignKind::None, mdast::AlignKind::None,],
                     rows: vec![
                         // Header row: two values
@@ -1133,14 +1104,12 @@ pub mod tests {
     }
 
     mod thematic_break {
-        use crate::mdq_node;
-
         use super::*;
 
         #[test]
         fn by_itself() {
             check_render(
-                vec![MdqNode::ThematicBreak],
+                vec![m_node!(MdqNode::Block::LeafBlock::ThematicBreak)],
                 indoc! {r#"
                 ***"#},
             );
@@ -1149,7 +1118,11 @@ pub mod tests {
         #[test]
         fn with_paragraphs() {
             check_render(
-                vec![mdq_node!("before"), MdqNode::ThematicBreak, mdq_node!("after")],
+                vec![
+                    mdq_node!("before"),
+                    m_node!(MdqNode::Block::LeafBlock::ThematicBreak),
+                    mdq_node!("after"),
+                ],
                 indoc! {r#"
                 before
 
@@ -1166,7 +1139,7 @@ pub mod tests {
         #[test]
         fn code_no_lang() {
             check_render(
-                mdq_nodes![CodeBlock {
+                mdq_nodes![Block::LeafBlock::CodeBlock {
                     variant: CodeVariant::Code(None),
                     value: "one\ntwo".to_string(),
                 }],
@@ -1181,7 +1154,7 @@ pub mod tests {
         #[test]
         fn code_with_lang() {
             check_render(
-                mdq_nodes![CodeBlock {
+                mdq_nodes![Block::LeafBlock::CodeBlock {
                     variant: CodeVariant::Code(Some(CodeOpts {
                         language: "rust".to_string(),
                         metadata: None
@@ -1199,7 +1172,7 @@ pub mod tests {
         #[test]
         fn code_with_lang_and_title() {
             check_render(
-                mdq_nodes![CodeBlock {
+                mdq_nodes![Block::LeafBlock::CodeBlock {
                     variant: CodeVariant::Code(Some(CodeOpts {
                         language: "rust".to_string(),
                         metadata: Some(r#"title="my code""#.to_string())
@@ -1217,7 +1190,7 @@ pub mod tests {
         #[test]
         fn math_no_metadata() {
             check_render(
-                mdq_nodes![CodeBlock {
+                mdq_nodes![Block::LeafBlock::CodeBlock {
                     variant: CodeVariant::Math { metadata: None },
                     value: "one\ntwo".to_string(),
                 }],
@@ -1232,7 +1205,7 @@ pub mod tests {
         #[test]
         fn math_with_metadata() {
             check_render(
-                mdq_nodes![CodeBlock {
+                mdq_nodes![Block::LeafBlock::CodeBlock {
                     variant: CodeVariant::Math {
                         metadata: Some("metadata".to_string())
                     },
@@ -1249,7 +1222,7 @@ pub mod tests {
         #[test]
         fn toml() {
             check_render(
-                mdq_nodes![CodeBlock {
+                mdq_nodes![Block::LeafBlock::CodeBlock {
                     variant: CodeVariant::Toml,
                     value: "one\ntwo".to_string(),
                 }],
@@ -1264,7 +1237,7 @@ pub mod tests {
         #[test]
         fn yaml() {
             check_render(
-                mdq_nodes![CodeBlock {
+                mdq_nodes![Block::LeafBlock::CodeBlock {
                     variant: CodeVariant::Yaml,
                     value: "one\ntwo".to_string(),
                 }],
@@ -1519,7 +1492,7 @@ pub mod tests {
                         ],
                         link_definition: link,
                     }),
-                    MdqNode::ThematicBreak,
+                    m_node!(MdqNode::Block::LeafBlock::ThematicBreak),
                 ];
                 check_render(nodes, expect);
             }
@@ -1668,7 +1641,7 @@ pub mod tests {
                         alt: "hello _world_!".to_string(),
                         link,
                     }),
-                    MdqNode::ThematicBreak,
+                    m_node!(MdqNode::Block::LeafBlock::ThematicBreak),
                 ];
                 check_render(nodes, expect);
             }
@@ -1684,11 +1657,9 @@ pub mod tests {
                 vec![
                     MdqNode::Inline(Inline::Footnote(Footnote {
                         label: "a".to_string(),
-                        text: mdq_nodes![Paragraph {
-                            body: vec![mdq_inline!("Hello, world.")]
-                        }],
+                        text: mdq_nodes!["Hello, world."],
                     })),
-                    MdqNode::ThematicBreak,
+                    m_node!(MdqNode::Block::LeafBlock::ThematicBreak),
                 ],
                 indoc! {r#"
                     [^a]
@@ -1705,11 +1676,9 @@ pub mod tests {
                 vec![
                     MdqNode::Inline(Inline::Footnote(Footnote {
                         label: "a".to_string(),
-                        text: mdq_nodes![Paragraph {
-                            body: vec![mdq_inline!("Hello,\nworld.")]
-                        }],
+                        text: mdq_nodes!["Hello,\nworld."],
                     })),
-                    MdqNode::ThematicBreak,
+                    m_node!(MdqNode::Block::LeafBlock::ThematicBreak),
                 ],
                 indoc! {r#"
                     [^a]
@@ -1729,7 +1698,7 @@ pub mod tests {
         #[test]
         fn link_and_footnote() {
             check_render(
-                mdq_nodes![Paragraph {
+                mdq_nodes![Block::LeafBlock::Paragraph {
                     body: vec![
                         mdq_inline!("Hello, "),
                         Inline::Link {
@@ -1743,9 +1712,7 @@ pub mod tests {
                         mdq_inline!("! This is interesting"),
                         Inline::Footnote(Footnote {
                             label: "a".to_string(),
-                            text: mdq_nodes![Paragraph {
-                                body: vec![mdq_inline!("this is my note")]
-                            }],
+                            text: mdq_nodes!["this is my note"],
                         }),
                         mdq_inline!("."),
                     ],
@@ -1856,7 +1823,7 @@ pub mod tests {
                     footnote_reference_options: ReferencePlacement::BottomOfDoc,
                 },
                 // Define them in the opposite order that we'd expect them
-                mdq_nodes![Paragraph {
+                mdq_nodes![Block::LeafBlock::Paragraph {
                     body: vec![
                         Inline::Footnote(Footnote {
                             label: "d".to_string(),
@@ -1896,10 +1863,10 @@ pub mod tests {
 
         fn link_and_footnote_markdown() -> Vec<MdqNode> {
             mdq_nodes![
-                Section {
+                Block::Container::Section {
                     depth: 1,
                     title: vec![mdq_inline!("First section")],
-                    body: mdq_nodes![Paragraph {
+                    body: mdq_nodes![Block::LeafBlock::Paragraph {
                         body: vec![
                             Inline::Link {
                                 text: vec![mdq_inline!("link description")],
@@ -1912,20 +1879,16 @@ pub mod tests {
                             mdq_inline!(" and then a thought"),
                             Inline::Footnote(Footnote {
                                 label: "a".to_string(),
-                                text: mdq_nodes![Paragraph {
-                                    body: vec![mdq_inline!("the footnote")]
-                                }],
+                                text: mdq_nodes!["the footnote"],
                             }),
                             mdq_inline!("."),
                         ],
                     }],
                 },
-                Section {
+                Block::Container::Section {
                     depth: 1,
                     title: vec![mdq_inline!("Second section")],
-                    body: mdq_nodes![Paragraph {
-                        body: vec![mdq_inline!("Second section contents.")]
-                    }],
+                    body: mdq_nodes!["Second section contents."],
                 },
             ]
         }
diff --git a/src/fmt_str.rs b/src/fmt_str.rs
index 53a2b53..60fefc7 100644
--- a/src/fmt_str.rs
+++ b/src/fmt_str.rs
@@ -36,7 +36,7 @@ mod tests {
     use super::*;
     use indoc::indoc;
 
-    use crate::tree::{Inline, MdqNode, ReadOptions, SpanVariant, TextVariant};
+    use crate::tree::{Block, Inline, LeafBlock, MdqNode, ReadOptions, SpanVariant, TextVariant};
     use crate::unwrap;
     use markdown::ParseOptions;
 
@@ -122,7 +122,7 @@ mod tests {
         options.constructs.math_text = true;
         let node = markdown::to_mdast(md, &options).unwrap();
         let mdq_nodes = MdqNode::read(node, &ReadOptions::default()).unwrap();
-        unwrap!(&mdq_nodes[0], MdqNode::Paragraph(p));
+        unwrap!(&mdq_nodes[0], MdqNode::Block(Block::LeafBlock(LeafBlock::Paragraph(p)))); // TODO can I use m_node here?
         p.body.iter().for_each(|inline| VARIANTS_CHECKER.see(inline));
         let actual = inlines_to_plain_string(&p.body);
         assert_eq!(&actual, expect);
diff --git a/src/matcher.rs b/src/matcher.rs
index f0444a9..9e13647 100644
--- a/src/matcher.rs
+++ b/src/matcher.rs
@@ -1,7 +1,7 @@
 use crate::fmt_str::inlines_to_plain_string;
 use crate::parsing_iter::ParsingIterator;
 use crate::select::{ParseErrorReason, ParseResult, SELECTOR_SEPARATOR};
-use crate::tree::{Inline, MdqNode};
+use crate::tree::{Block, Container, Inline, LeafBlock, MdqNode};
 use regex::Regex;
 use std::borrow::Borrow;
 
@@ -43,18 +43,11 @@ impl StringMatcher {
         haystacks.iter().any(|node| self.matches_node(node.borrow()))
     }
 
-    fn matches_node(&self, node: &MdqNode) -> bool {
-        match node {
-            MdqNode::Section(section) => {
-                if self.matches_inlines(&section.title) {
-                    return true;
-                }
-                self.matches_any(&section.body)
-            }
-            MdqNode::Paragraph(paragraph) => self.matches_inlines(&paragraph.body),
-            MdqNode::BlockQuote(block) => self.matches_any(&block.body),
-            MdqNode::List(list) => list.items.iter().any(|li| self.matches_any(&li.item)),
-            MdqNode::Table(table) => {
+    fn matches_block(&self, block: &Block) -> bool {
+        match block {
+            Block::LeafBlock(LeafBlock::Paragraph(p)) => self.matches_inlines(&p.body),
+            Block::LeafBlock(LeafBlock::ThematicBreak | LeafBlock::CodeBlock(_)) => false,
+            Block::LeafBlock(LeafBlock::Table(table)) => {
                 for row in &table.rows {
                     for cell in row {
                         if self.matches_inlines(cell) {
@@ -64,8 +57,20 @@ impl StringMatcher {
                 }
                 false
             }
-            MdqNode::ThematicBreak => false,
-            MdqNode::CodeBlock(_) => false,
+            Block::Container(Container::List(list)) => list.items.iter().any(|li| self.matches_any(&li.item)),
+            Block::Container(Container::BlockQuote(block)) => self.matches_any(&block.body),
+            Block::Container(Container::Section(section)) => {
+                if self.matches_inlines(&section.title) {
+                    return true;
+                }
+                self.matches_any(&section.body)
+            }
+        }
+    }
+
+    fn matches_node(&self, node: &MdqNode) -> bool {
+        match node {
+            MdqNode::Block(block) => self.matches_block(block),
             MdqNode::Inline(inline) => self.matches_inlines(&[inline]),
         }
     }
diff --git a/src/select/selector.rs b/src/select/selector.rs
index bf752fa..b302402 100644
--- a/src/select/selector.rs
+++ b/src/select/selector.rs
@@ -115,8 +115,6 @@ impl MdqRefSelector {
                 }
                 result
             }
-            MdqNodeRef::ThematicBreak => Vec::new(),
-            MdqNodeRef::CodeBlock(_) => Vec::new(),
             MdqNodeRef::ListItem(ListItemRef(_, item)) => MdqNodeRef::wrap_vec(&item.item),
             MdqNodeRef::Inline(inline) => match inline {
                 Inline::Span { children, .. } => children.iter().map(|child| MdqNodeRef::Inline(child)).collect(),
@@ -127,6 +125,7 @@ impl MdqRefSelector {
                 }
                 Inline::Text { .. } | Inline::Image { .. } => Vec::new(),
             },
+            MdqNodeRef::NonSelectable(_) => Vec::new(),
         }
     }
 
diff --git a/src/tree.rs b/src/tree.rs
index 95a08d5..23b78eb 100644
--- a/src/tree.rs
+++ b/src/tree.rs
@@ -8,25 +8,29 @@ use markdown::mdast;
 
 #[derive(Debug, PartialEq)]
 pub enum MdqNode {
-    // paragraphs with child nodes
-    Section(Section),
-    Paragraph(Paragraph),
-    BlockQuote(BlockQuote),
-    List(List),
-    Table(Table),
+    Block(Block),
+    Inline(Inline),
+}
 
-    ThematicBreak,
+#[derive(Debug, PartialEq)]
+pub enum Block {
+    Container(Container),
+    LeafBlock(LeafBlock),
+}
 
-    // blocks that contain strings (as opposed to nodes)
+#[derive(Debug, PartialEq)]
+pub enum LeafBlock {
     CodeBlock(CodeBlock),
-
-    // inline spans
-    Inline(Inline),
+    Paragraph(Paragraph),
+    Table(Table),
+    ThematicBreak,
 }
 
 #[derive(Debug, PartialEq)]
-pub struct Root {
-    pub body: Vec<MdqNode>,
+pub enum Container {
+    BlockQuote(BlockQuote),
+    List(List),
+    Section(Section),
 }
 
 #[derive(Debug, PartialEq)]
@@ -219,6 +223,38 @@ macro_rules! mdx_nodes {
     };
 }
 
+/// Creates a nested enum.
+///
+/// This macro translates:
+/// ```
+/// node!(A::B::C { foo: 123 }
+/// ```
+///
+/// into:
+///
+/// ```
+/// A::B(B::C(C { foo: 123 }))
+/// ```
+#[macro_export]
+macro_rules! m_node {
+    // Terminal cases for Foo{ bar: bazz } in its various configurations
+    ($last:ident { $($args:tt)* }) => {
+        $last {
+            $($args)*
+        }
+    };
+
+    // Terminal empty struct: Foo::Bar
+    ($last:ident :: $next:ident) => {
+        $last::$next
+    };
+
+    // Recursive case: A::B<tail> -> A::B(B<tail>)
+    ($head:ident :: $next:ident $(:: $($tail:ident)::*)? $({ $($args:tt)* })? ) => {
+        $head::$next( m_node!($next $(:: $($tail)::*)? $({ $($args)* })?) )
+    };
+}
+
 impl MdqNode {
     pub fn read(node: mdast::Node, opts: &ReadOptions) -> Result<Vec<Self>, InvalidMd> {
         let lookups = Lookups::new(&node, opts)?;
@@ -228,7 +264,7 @@ impl MdqNode {
     fn from_mdast_0(node: mdast::Node, lookups: &Lookups) -> Result<Vec<Self>, InvalidMd> {
         let result = match node {
             mdast::Node::Root(node) => return MdqNode::all(node.children, lookups),
-            mdast::Node::BlockQuote(node) => MdqNode::BlockQuote(BlockQuote {
+            mdast::Node::BlockQuote(node) => m_node!(MdqNode::Block::Container::BlockQuote {
                 body: MdqNode::all(node.children, lookups)?,
             }),
             mdast::Node::FootnoteDefinition(_) => return Ok(Vec::new()),
@@ -244,7 +280,7 @@ impl MdqNode {
                     };
                     li_nodes.push(li_mdq);
                 }
-                MdqNode::List(List {
+                m_node!(MdqNode::Block::Container::List {
                     starting_index: node.start,
                     items: li_nodes,
                 })
@@ -310,7 +346,7 @@ impl MdqNode {
             }),
             mdast::Node::Code(node) => {
                 let mdast::Code { value, lang, meta, .. } = node;
-                MdqNode::CodeBlock(CodeBlock {
+                m_node!(MdqNode::Block::LeafBlock::CodeBlock {
                     value,
                     variant: CodeVariant::Code(match lang {
                         None => None,
@@ -323,12 +359,12 @@ impl MdqNode {
             }
             mdast::Node::Math(node) => {
                 let mdast::Math { value, meta, .. } = node;
-                MdqNode::CodeBlock(CodeBlock {
+                m_node!(MdqNode::Block::LeafBlock::CodeBlock {
                     value,
                     variant: CodeVariant::Math { metadata: meta },
                 })
             }
-            mdast::Node::Heading(node) => MdqNode::Section(Section {
+            mdast::Node::Heading(node) => m_node!(MdqNode::Block::Container::Section {
                 depth: node.depth,
                 title: Self::inlines(node.children, lookups)?,
                 body: Vec::new(),
@@ -353,24 +389,24 @@ impl MdqNode {
                     }
                     rows.push(column);
                 }
-                MdqNode::Table(Table {
+                m_node!(MdqNode::Block::LeafBlock::Table {
                     alignments: align,
                     rows,
                 })
             }
-            mdast::Node::ThematicBreak(_) => MdqNode::ThematicBreak,
+            mdast::Node::ThematicBreak(_) => m_node!(MdqNode::Block::LeafBlock::ThematicBreak),
             mdast::Node::TableRow(_) | mdast::Node::TableCell(_) | mdast::Node::ListItem(_) => {
                 return Err(InvalidMd::InternalError); // should have been handled by Node::Table
             }
             mdast::Node::Definition(_) => return Ok(Vec::new()),
-            mdast::Node::Paragraph(node) => MdqNode::Paragraph(Paragraph {
+            mdast::Node::Paragraph(node) => m_node!(MdqNode::Block::LeafBlock::Paragraph {
                 body: Self::inlines(node.children, lookups)?,
             }),
-            mdast::Node::Toml(node) => MdqNode::CodeBlock(CodeBlock {
+            mdast::Node::Toml(node) => m_node!(MdqNode::Block::LeafBlock::CodeBlock {
                 variant: CodeVariant::Toml,
                 value: node.value,
             }),
-            mdast::Node::Yaml(node) => MdqNode::CodeBlock(CodeBlock {
+            mdast::Node::Yaml(node) => m_node!(MdqNode::Block::LeafBlock::CodeBlock {
                 variant: CodeVariant::Yaml,
                 value: node.value,
             }),
@@ -413,7 +449,7 @@ impl MdqNode {
         let mut headers: Vec<HContainer> = Vec::with_capacity(result.capacity());
         for child_mdq in iter {
             let child_mdq = child_mdq?;
-            if let MdqNode::Section(Section {
+            if let m_node!(MdqNode::Block::Container::Section {
                 depth,
                 title,
                 body: children,
@@ -437,7 +473,7 @@ impl MdqNode {
                         // to the new previous, or else to the top-level results if there is no new
                         // previous. Then, we'll just loop back around.
                         let HContainer { depth, title, children } = headers.pop().unwrap(); // "let Some(prev)" above guarantees that this works
-                        let prev = MdqNode::Section(Section {
+                        let prev = m_node!(MdqNode::Block::Container::Section {
                             depth,
                             title,
                             body: children,
@@ -463,7 +499,7 @@ impl MdqNode {
 
         // At this point, we still have our last tree branch of headers. Fold it up into the results.
         while let Some(HContainer { depth, title, children }) = headers.pop() {
-            let mdq_header = MdqNode::Section(Section {
+            let mdq_header = m_node!(MdqNode::Block::Container::Section {
                 depth,
                 title,
                 body: children,
@@ -478,7 +514,7 @@ impl MdqNode {
         headers
             .drain(..)
             .map(|HContainer { depth, title, children }| {
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth,
                     title,
                     body: children,
@@ -724,8 +760,8 @@ mod tests {
         fn block_quote() {
             let (root, lookups) = parse("> hello");
             let child = &root.children[0];
-            check!(child, Node::BlockQuote(_), lookups => MdqNode::BlockQuote(bq)= {
-                assert_eq!(bq.body, mdq_nodes!["hello"]);
+            check!(child, Node::BlockQuote(_), lookups => m_node!(MdqNode::Block::Container::BlockQuote{body}) = {
+                assert_eq!(body, mdq_nodes!["hello"]);
             });
         }
 
@@ -763,7 +799,7 @@ mod tests {
                     assert_eq!(footnote, Inline::Footnote(Footnote{
                         label: "a".to_string(),
                         text: vec![
-                            MdqNode::List(List{
+                            m_node!(MdqNode::Block::Container::List{
                                 starting_index: None,
                                 items: vec![
                                     ListItem{
@@ -797,7 +833,7 @@ mod tests {
             );
             assert_eq!(root.children.len(), 2); // unordered list, then ordered
 
-            check!(&root.children[0], Node::List(ul), lookups => MdqNode::List(List{starting_index, items}) = {
+            check!(&root.children[0], Node::List(ul), lookups => m_node!(MdqNode::Block::Container::List{starting_index, items}) = {
                 for child in &ul.children {
                     check!(error: child, Node::ListItem(_), lookups => InvalidMd::InternalError);
                 }
@@ -817,7 +853,7 @@ mod tests {
                     },
                 ]);
             });
-            check!(&root.children[1], Node::List(ol), lookups => MdqNode::List(List{starting_index, items}) = {
+            check!(&root.children[1], Node::List(ol), lookups => m_node!(MdqNode::Block::Container::List{starting_index, items}) = {
                 for child in &ol.children {
                     check!(error: child, Node::ListItem(_), lookups => InvalidMd::InternalError);
                 }
@@ -852,7 +888,7 @@ mod tests {
                 "#},
             );
 
-            check!(&root.children[0], Node::Paragraph(p), lookups => MdqNode::Paragraph(Paragraph{body}) = {
+            check!(&root.children[0], Node::Paragraph(p), lookups => m_node!(MdqNode::Block::LeafBlock::Paragraph{body}) = {
                 assert_eq!(p.children.len(), 3);
                 check!(&p.children[0], Node::Text(_), lookups => MdqNode::Inline(text) = {
                     assert_eq!(text, Inline::Text {variant: TextVariant::Plain, value: "hello ".to_string()});
@@ -954,7 +990,7 @@ mod tests {
                 let (root, lookups) = parse(indoc! {r#"
                 In <em>a paragraph.</em>
                 "#});
-                check!(&root.children[0], Node::Paragraph(_), lookups => MdqNode::Paragraph(Paragraph{body}) = {
+                check!(&root.children[0], Node::Paragraph(_), lookups => m_node!(MdqNode::Block::LeafBlock::Paragraph{body}) = {
                     assert_eq!(body.len(), 4);
                     assert_eq!(body, vec![
                         mdq_inline!("In "),
@@ -974,7 +1010,7 @@ mod tests {
                 In <em
                 newline  >a paragraph.</em>
                 "#});
-                check!(&root.children[0], Node::Paragraph(_), lookups => MdqNode::Paragraph(Paragraph{body}) = {
+                check!(&root.children[0], Node::Paragraph(_), lookups => m_node!(MdqNode::Block::LeafBlock::Paragraph{body}) = {
                     assert_eq!(body.len(), 4);
                     assert_eq!(body, vec![
                         mdq_inline!("In "),
@@ -1051,7 +1087,7 @@ mod tests {
             {
                 // This isn't an image, though it almost looks like one
                 let (root, lookups) = parse(r#"![]("only a tooltip")"#);
-                check!(&root.children[0], Node::Paragraph(_), lookups => p @ MdqNode::Paragraph{ .. } = {
+                check!(&root.children[0], Node::Paragraph(_), lookups => p @ m_node!(MdqNode::Block::LeafBlock::Paragraph{ .. }) = {
                     assert_eq!(p, mdq_node!(r#"![]("only a tooltip")"#));
                 });
             }
@@ -1432,7 +1468,7 @@ mod tests {
                     plain code block
                     ```"#},
                 );
-                check!(&root.children[0], Node::Code(_), lookups => MdqNode::CodeBlock(CodeBlock{variant, value}) = {
+                check!(&root.children[0], Node::Code(_), lookups => m_node!(MdqNode::Block::LeafBlock::CodeBlock{variant, value}) = {
                     assert_eq!(variant, CodeVariant::Code(None));
                     assert_eq!(value, "plain code block");
                 })
@@ -1445,7 +1481,7 @@ mod tests {
                     code block with language
                     ```"#},
                 );
-                check!(&root.children[0], Node::Code(_), lookups => MdqNode::CodeBlock(CodeBlock{variant, value}) = {
+                check!(&root.children[0], Node::Code(_), lookups => m_node!(MdqNode::Block::LeafBlock::CodeBlock{variant, value}) = {
                     assert_eq!(variant, CodeVariant::Code(Some(CodeOpts{
                         language: "rust".to_string(),
                         metadata: None})));
@@ -1460,7 +1496,7 @@ mod tests {
                     code block with language and title
                     ```"#},
                 );
-                check!(&root.children[0], Node::Code(_), lookups => MdqNode::CodeBlock(CodeBlock{variant, value}) = {
+                check!(&root.children[0], Node::Code(_), lookups => m_node!(MdqNode::Block::LeafBlock::CodeBlock{variant, value}) = {
                     assert_eq!(variant, CodeVariant::Code(Some(CodeOpts{
                         language: "rust".to_string(),
                         metadata: Some(r#"title="example.rs""#.to_string())})));
@@ -1475,7 +1511,7 @@ mod tests {
                     code block with only title
                     ```"#},
                 );
-                check!(&root.children[0], Node::Code(_), lookups => MdqNode::CodeBlock(CodeBlock{variant, value}) = {
+                check!(&root.children[0], Node::Code(_), lookups => m_node!(MdqNode::Block::LeafBlock::CodeBlock{variant, value}) = {
                     // It's actually just a bogus language!
                     assert_eq!(variant, CodeVariant::Code(Some(CodeOpts{
                         language: r#"title="example.rs""#.to_string(),
@@ -1497,7 +1533,7 @@ mod tests {
                     x = {-b \pm \sqrt{b^2-4ac} \over 2a}
                     $$"#},
                 );
-                check!(&root.children[0], Node::Math(_), lookups => MdqNode::CodeBlock(CodeBlock{variant, value}) = {
+                check!(&root.children[0], Node::Math(_), lookups => m_node!(MdqNode::Block::LeafBlock::CodeBlock{variant, value}) = {
                     assert_eq!(variant, CodeVariant::Math{metadata: None});
                     assert_eq!(value, r#"x = {-b \pm \sqrt{b^2-4ac} \over 2a}"#);
                 })
@@ -1510,7 +1546,7 @@ mod tests {
                     x = {-b \pm \sqrt{b^2-4ac} \over 2a}
                     $$"#},
                 );
-                check!(&root.children[0], Node::Math(_), lookups => MdqNode::CodeBlock(CodeBlock{variant, value}) = {
+                check!(&root.children[0], Node::Math(_), lookups => m_node!(MdqNode::Block::LeafBlock::CodeBlock{variant, value}) = {
                     assert_eq!(variant, CodeVariant::Math{metadata: Some("my metadata".to_string())});
                     assert_eq!(value, r#"x = {-b \pm \sqrt{b^2-4ac} \over 2a}"#);
                 })
@@ -1528,7 +1564,7 @@ mod tests {
                 my: toml
                 +++"#},
             );
-            check!(&root.children[0], Node::Toml(_), lookups => MdqNode::CodeBlock(CodeBlock{variant, value}) = {
+            check!(&root.children[0], Node::Toml(_), lookups => m_node!(MdqNode::Block::LeafBlock::CodeBlock{variant, value}) = {
                 assert_eq!(variant, CodeVariant::Toml);
                 assert_eq!(value, r#"my: toml"#);
             })
@@ -1545,7 +1581,7 @@ mod tests {
                 my: toml
                 ---"#},
             );
-            check!(&root.children[0], Node::Yaml(_), lookups => MdqNode::CodeBlock(CodeBlock{variant, value}) = {
+            check!(&root.children[0], Node::Yaml(_), lookups => m_node!(MdqNode::Block::LeafBlock::CodeBlock{variant, value}) = {
                 assert_eq!(variant, CodeVariant::Yaml);
                 assert_eq!(value, r#"my: toml"#);
             })
@@ -1560,7 +1596,7 @@ mod tests {
                     And some text below it."#},
             );
 
-            let (header_depth, header_title) = check!(&root.children[0], Node::Heading(_), lookups => MdqNode::Section(Section{depth, title, body}) = {
+            let (header_depth, header_title) = check!(&root.children[0], Node::Heading(_), lookups => m_node!(MdqNode::Block::Container::Section{depth, title, body}) = {
                 assert_eq!(depth, 2);
                 assert_eq!(title, vec![
                     Inline::Text { variant: TextVariant::Plain, value: "Header with ".to_string()},
@@ -1583,7 +1619,7 @@ mod tests {
 
             assert_eq!(
                 mdqs,
-                vec![MdqNode::Section(Section {
+                vec![m_node!(MdqNode::Block::Container::Section {
                     depth: header_depth,
                     title: header_title,
                     body: mdq_nodes!["And some text below it."],
@@ -1605,7 +1641,7 @@ mod tests {
             );
 
             assert_eq!(root.children.len(), 3);
-            check!(&root.children[1], Node::ThematicBreak(_), lookups => MdqNode::ThematicBreak = {
+            check!(&root.children[1], Node::ThematicBreak(_), lookups => m_node!(MdqNode::Block::LeafBlock::ThematicBreak) = {
                 // nothing to check
             });
         }
@@ -1624,7 +1660,7 @@ mod tests {
                     "#},
             );
             assert_eq!(root.children.len(), 1);
-            check!(&root.children[0], Node::Table(table_node), lookups => MdqNode::Table(Table{alignments, rows}) = {
+            check!(&root.children[0], Node::Table(table_node), lookups => m_node!(MdqNode::Block::LeafBlock::Table{alignments, rows}) = {
                 assert_eq!(alignments, vec![mdast::AlignKind::Left, mdast::AlignKind::Center, mdast::AlignKind::Right, mdast::AlignKind::None]);
                 assert_eq!(rows,
                     vec![ // rows
@@ -1873,26 +1909,26 @@ mod tests {
         #[test]
         fn h1_with_two_paragraphs() -> Result<(), InvalidMd> {
             let linear = vec![
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 1,
                     title: vec![mdq_inline!("first")],
                     body: vec![],
                 }),
-                MdqNode::Paragraph(Paragraph {
+                m_node!(MdqNode::Block::LeafBlock::Paragraph {
                     body: vec![mdq_inline!("aaa")],
                 }),
-                MdqNode::Paragraph(Paragraph {
+                m_node!(MdqNode::Block::LeafBlock::Paragraph {
                     body: vec![mdq_inline!("bbb")],
                 }),
             ];
-            let expect = vec![MdqNode::Section(Section {
+            let expect = vec![m_node!(MdqNode::Block::Container::Section {
                 depth: 1,
                 title: vec![mdq_inline!("first")],
                 body: vec![
-                    MdqNode::Paragraph(Paragraph {
+                    m_node!(MdqNode::Block::LeafBlock::Paragraph {
                         body: vec![mdq_inline!("aaa")],
                     }),
-                    MdqNode::Paragraph(Paragraph {
+                    m_node!(MdqNode::Block::LeafBlock::Paragraph {
                         body: vec![mdq_inline!("bbb")],
                     }),
                 ],
@@ -1905,27 +1941,27 @@ mod tests {
         #[test]
         fn simple_nesting() -> Result<(), InvalidMd> {
             let linear = vec![
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 1,
                     title: vec![mdq_inline!("first")],
                     body: vec![],
                 }),
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 2,
                     title: vec![mdq_inline!("aaa")],
                     body: vec![],
                 }),
-                MdqNode::Paragraph(Paragraph {
+                m_node!(MdqNode::Block::LeafBlock::Paragraph {
                     body: vec![mdq_inline!("bbb")],
                 }),
             ];
-            let expect = vec![MdqNode::Section(Section {
+            let expect = vec![m_node!(MdqNode::Block::Container::Section {
                 depth: 1,
                 title: vec![mdq_inline!("first")],
-                body: vec![MdqNode::Section(Section {
+                body: vec![m_node!(MdqNode::Block::Container::Section {
                     depth: 2,
                     title: vec![mdq_inline!("aaa")],
-                    body: vec![MdqNode::Paragraph(Paragraph {
+                    body: vec![m_node!(MdqNode::Block::LeafBlock::Paragraph {
                         body: vec![mdq_inline!("bbb")],
                     })],
                 })],
@@ -1938,53 +1974,53 @@ mod tests {
         #[test]
         fn only_headers() -> Result<(), InvalidMd> {
             let linear = vec![
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 1,
                     title: vec![mdq_inline!("first")],
                     body: vec![],
                 }),
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 2,
                     title: vec![mdq_inline!("second")],
                     body: vec![],
                 }),
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 3,
                     title: vec![mdq_inline!("third")],
                     body: vec![],
                 }),
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 3,
                     title: vec![mdq_inline!("fourth")],
                     body: vec![],
                 }),
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 2,
                     title: vec![mdq_inline!("fifth")],
                     body: vec![],
                 }),
             ];
-            let expect = vec![MdqNode::Section(Section {
+            let expect = vec![m_node!(MdqNode::Block::Container::Section {
                 depth: 1,
                 title: vec![mdq_inline!("first")],
                 body: vec![
-                    MdqNode::Section(Section {
+                    m_node!(MdqNode::Block::Container::Section {
                         depth: 2,
                         title: vec![mdq_inline!("second")],
                         body: vec![
-                            MdqNode::Section(Section {
+                            m_node!(MdqNode::Block::Container::Section {
                                 depth: 3,
                                 title: vec![mdq_inline!("third")],
                                 body: vec![],
                             }),
-                            MdqNode::Section(Section {
+                            m_node!(MdqNode::Block::Container::Section {
                                 depth: 3,
                                 title: vec![mdq_inline!("fourth")],
                                 body: vec![],
                             }),
                         ],
                     }),
-                    MdqNode::Section(Section {
+                    m_node!(MdqNode::Block::Container::Section {
                         depth: 2,
                         title: vec![mdq_inline!("fifth")],
                         body: vec![],
@@ -1999,18 +2035,18 @@ mod tests {
         #[test]
         fn no_headers() -> Result<(), InvalidMd> {
             let linear = vec![
-                MdqNode::Paragraph(Paragraph {
+                m_node!(MdqNode::Block::LeafBlock::Paragraph {
                     body: vec![mdq_inline!("one")],
                 }),
-                MdqNode::Paragraph(Paragraph {
+                m_node!(MdqNode::Block::LeafBlock::Paragraph {
                     body: vec![mdq_inline!("two")],
                 }),
             ];
             let expect = vec![
-                MdqNode::Paragraph(Paragraph {
+                m_node!(MdqNode::Block::LeafBlock::Paragraph {
                     body: vec![mdq_inline!("one")],
                 }),
-                MdqNode::Paragraph(Paragraph {
+                m_node!(MdqNode::Block::LeafBlock::Paragraph {
                     body: vec![mdq_inline!("two")],
                 }),
             ];
@@ -2022,40 +2058,40 @@ mod tests {
         #[test]
         fn header_skips() -> Result<(), InvalidMd> {
             let linear = vec![
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 1,
                     title: vec![mdq_inline!("one")],
                     body: vec![],
                 }),
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 5,
                     title: vec![mdq_inline!("five")],
                     body: vec![],
                 }),
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 2,
                     title: vec![mdq_inline!("two")],
                     body: vec![],
                 }),
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 3,
                     title: vec![mdq_inline!("three")],
                     body: vec![],
                 }),
             ];
-            let expect = vec![MdqNode::Section(Section {
+            let expect = vec![m_node!(MdqNode::Block::Container::Section {
                 depth: 1,
                 title: vec![mdq_inline!("one")],
                 body: vec![
-                    MdqNode::Section(Section {
+                    m_node!(MdqNode::Block::Container::Section {
                         depth: 5,
                         title: vec![mdq_inline!("five")],
                         body: vec![],
                     }),
-                    MdqNode::Section(Section {
+                    m_node!(MdqNode::Block::Container::Section {
                         depth: 2,
                         title: vec![mdq_inline!("two")],
-                        body: vec![MdqNode::Section(Section {
+                        body: vec![m_node!(MdqNode::Block::Container::Section {
                             depth: 3,
                             title: vec![mdq_inline!("three")],
                             body: vec![],
@@ -2071,34 +2107,34 @@ mod tests {
         #[test]
         fn backwards_order() -> Result<(), InvalidMd> {
             let linear = vec![
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 3,
                     title: vec![mdq_inline!("three")],
                     body: vec![],
                 }),
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 2,
                     title: vec![mdq_inline!("two")],
                     body: vec![],
                 }),
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 1,
                     title: vec![mdq_inline!("one")],
                     body: vec![],
                 }),
             ];
             let expect = vec![
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 3,
                     title: vec![mdq_inline!("three")],
                     body: vec![],
                 }),
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 2,
                     title: vec![mdq_inline!("two")],
                     body: vec![],
                 }),
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 1,
                     title: vec![mdq_inline!("one")],
                     body: vec![],
@@ -2112,26 +2148,26 @@ mod tests {
         #[test]
         fn paragraph_before_and_after_header() -> Result<(), InvalidMd> {
             let linear = vec![
-                MdqNode::Paragraph(Paragraph {
+                m_node!(MdqNode::Block::LeafBlock::Paragraph {
                     body: vec![mdq_inline!("before")],
                 }),
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 3,
                     title: vec![mdq_inline!("the header")],
                     body: vec![],
                 }),
-                MdqNode::Paragraph(Paragraph {
+                m_node!(MdqNode::Block::LeafBlock::Paragraph {
                     body: vec![mdq_inline!("after")],
                 }),
             ];
             let expect = vec![
-                MdqNode::Paragraph(Paragraph {
+                m_node!(MdqNode::Block::LeafBlock::Paragraph {
                     body: vec![mdq_inline!("before")],
                 }),
-                MdqNode::Section(Section {
+                m_node!(MdqNode::Block::Container::Section {
                     depth: 3,
                     title: vec![mdq_inline!("the header")],
-                    body: vec![MdqNode::Paragraph(Paragraph {
+                    body: vec![m_node!(MdqNode::Block::LeafBlock::Paragraph {
                         body: vec![mdq_inline!("after")],
                     })],
                 }),
diff --git a/src/tree_ref.rs b/src/tree_ref.rs
index 76c5f0d..922b674 100644
--- a/src/tree_ref.rs
+++ b/src/tree_ref.rs
@@ -1,5 +1,9 @@
-use crate::tree::{BlockQuote, CodeBlock, Inline, List, ListItem, MdqNode, Paragraph, Section, Table};
+use crate::tree::{
+    Block, BlockQuote, CodeBlock, Container, Inline, LeafBlock, List, ListItem, MdqNode, Paragraph, Section, Table,
+};
 
+/// An MdqNodeRef is a slice into an MdqNode tree, where each element can be outputted, and certain elements can be
+/// selected.
 #[derive(Debug, Clone)]
 pub enum MdqNodeRef<'a> {
     // paragraphs with child nodes
@@ -9,15 +13,18 @@ pub enum MdqNodeRef<'a> {
     List(&'a List),
     Table(&'a Table),
 
-    ThematicBreak,
-
-    // blocks that contain strings (&'a as opposed to nodes)
-    CodeBlock(&'a CodeBlock),
-
     ListItem(ListItemRef<'a>),
 
     // inline spans
     Inline(&'a Inline),
+
+    NonSelectable(NonSelectable<'a>),
+}
+
+#[derive(Debug, Clone)]
+pub enum NonSelectable<'a> {
+    ThematicBreak,
+    CodeBlock(&'a CodeBlock),
 }
 
 #[derive(Debug, Clone, Copy)]
@@ -26,13 +33,19 @@ pub struct ListItemRef<'a>(pub Option<u32>, pub &'a ListItem);
 impl<'a> From<&'a MdqNode> for MdqNodeRef<'a> {
     fn from(value: &'a MdqNode) -> Self {
         match value {
-            MdqNode::Section(v) => Self::Section(v),
-            MdqNode::Paragraph(v) => Self::Paragraph(v),
-            MdqNode::BlockQuote(v) => Self::BlockQuote(v),
-            MdqNode::List(v) => Self::List(v),
-            MdqNode::Table(v) => Self::Table(v),
-            MdqNode::ThematicBreak => Self::ThematicBreak,
-            MdqNode::CodeBlock(v) => Self::CodeBlock(v),
+            MdqNode::Block(block) => match block {
+                Block::LeafBlock(leaf) => match leaf {
+                    LeafBlock::ThematicBreak => Self::NonSelectable(NonSelectable::ThematicBreak),
+                    LeafBlock::Paragraph(p) => Self::Paragraph(p),
+                    LeafBlock::CodeBlock(c) => Self::NonSelectable(NonSelectable::CodeBlock(c)),
+                    LeafBlock::Table(t) => Self::Table(t),
+                },
+                Block::Container(container) => match container {
+                    Container::List(list) => Self::List(list),
+                    Container::BlockQuote(block) => Self::BlockQuote(block),
+                    Container::Section(section) => Self::Section(section),
+                },
+            },
             MdqNode::Inline(v) => Self::Inline(v),
         }
     }


Test patch:
diff --git a/src/tree_test_utils.rs b/src/tree_test_utils.rs
index 293b18e..07301ef 100644
--- a/src/tree_test_utils.rs
+++ b/src/tree_test_utils.rs
@@ -3,30 +3,28 @@ mod test_utils {
 
     #[macro_export]
     macro_rules! mdq_node {
-        ($node_type:tt {$($attr:ident: $val:expr),*}) => {
-            MdqNode::$node_type($node_type{$($attr: $val),*})
+        ( $($node_names:ident)::* {$($attr:ident: $val:expr),*}) => {
+            crate::m_node!(MdqNode::$($node_names)::* {$($attr: $val),*})
         };
         ($paragraph_text:literal) => {
-            crate::mdq_node!(Paragraph{body: vec![crate::mdq_inline!($paragraph_text)]})
+            crate::m_node!(MdqNode::Block::LeafBlock::Paragraph{body: vec![crate::mdq_inline!($paragraph_text)]})
         };
     }
 
     #[macro_export]
     macro_rules! mdq_nodes {
-        [$($node_type:tt {$($attr:ident: $val:expr),*$(,)?}),*$(,)?] => {
+        [$($first:tt $( $(:: $($rest:ident)::* )? {$($attr:ident: $val:expr),*$(,)?})? ),*$(,)?] => {
             vec![$(
-                crate::mdq_node!($node_type {
-                    $($attr: $val),*
-                })
+                crate::mdq_node!($first$( $(:: $($rest)::*)? { $($attr: $val),* })?)
                 ),*
             ]
         };
-        [$($paragraph_text:literal),*$(,)?] => {
-            vec![$(
-                    crate::mdq_node!($paragraph_text)
-                ),*
-            ]
-        }
+        // [$($paragraph_text:literal),*$(,)?] => {
+        //     vec![$(
+        //             crate::mdq_node!($paragraph_text)
+        //         ),*
+        //     ]
+        // } TODO rm if I don't need this
     }
 
     #[macro_export]