Repository: yshavit/mdq
Pull request #44 (2024-06-19 18:57:03)

Selector recursion with MdqNodeRefs
This is one variant I'm going to try.

In this variant, `MdqNode` stays as a relatively strictly typed enum (and in fact, I'll be able to type it even more strictly if I want, e.g. by removing the `Inline(Inline)` variant). Then, we have a new `MdqNodeRef<'a>` enum that can peer into sections of the `MdqNode` tree.

Because of lifetime stuff, this requires redoing the variants checker macro. We can't have it be generic on `E`, because for the `MdqNodeRef` that would be `static VARIANTS_CHECKER<MdqNodeRef<'static>>`, which means you can't pass a non-static `MdqNodeRef<'a>` into it. So instead, we need to lift that whole `see(...)` method into the macro.


Patch:
diff --git a/Cargo.lock b/Cargo.lock
index ab18e73..754410f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -74,6 +74,7 @@ dependencies = [
  "lazy_static",
  "log",
  "markdown",
+ "paste",
  "regex",
  "serde",
  "serde_json",
@@ -86,6 +87,12 @@ version = "2.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
 
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
 [[package]]
 name = "proc-macro2"
 version = "1.0.85"
diff --git a/Cargo.toml b/Cargo.toml
index bebc569..bc2e402 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,3 +14,4 @@ regex = "1.10.4"
 log = "0.4.21"
 indoc = "2"
 lazy_static = "1.4.0"
+paste = "1.0.15"
diff --git a/src/fmt_md.rs b/src/fmt_md.rs
index fde938f..6b40bc3 100644
--- a/src/fmt_md.rs
+++ b/src/fmt_md.rs
@@ -1,11 +1,12 @@
 use std::borrow::Borrow;
 use std::cmp::max;
 use std::collections::{HashMap, HashSet};
-use std::fmt::{Alignment, Write};
+use std::fmt::Alignment;
 
 use crate::output::{Block, Output, SimpleWrite};
 use crate::str_utils::{pad_to, standard_align, CountingWriter};
 use crate::tree::*;
+use crate::tree_ref::MdqNodeRef;
 
 #[derive(Default)]
 pub struct MdOptions {
@@ -60,9 +61,9 @@ impl Default for ReferencePlacement {
     }
 }
 
-pub fn write_md<N, W>(options: &MdOptions, out: &mut Output<W>, nodes: &[N])
+pub fn write_md<'a, I, W>(options: &'a MdOptions, out: &mut Output<W>, nodes: I)
 where
-    N: Borrow<MdqNode>,
+    I: Iterator<Item = MdqNodeRef<'a>>,
     W: SimpleWrite,
 {
     let pending_refs_capacity = 8; // arbitrary guess
@@ -112,22 +113,22 @@ enum ReifiedLabel<'a> {
 }
 
 impl<'a> MdWriterState<'a> {
-    fn write_md<N, W>(&mut self, out: &mut Output<W>, nodes: &'a [N])
+    fn write_md<I, W>(&mut self, out: &mut Output<W>, nodes: I)
     where
-        N: Borrow<MdqNode>,
+        I: Iterator<Item = MdqNodeRef<'a>>,
         W: SimpleWrite,
     {
         for node in nodes {
-            self.write_one_md(out, node.borrow());
+            self.write_one_md(out, node);
         }
     }
 
-    pub fn write_one_md<W>(&mut self, out: &mut Output<W>, node: &'a MdqNode)
+    pub fn write_one_md<W>(&mut self, out: &mut Output<W>, node: MdqNodeRef<'a>)
     where
         W: SimpleWrite,
     {
         match node {
-            MdqNode::Section(Section { depth, title, body }) => {
+            MdqNodeRef::Section(Section { depth, title, body }) => {
                 out.with_block(Block::Plain, |out| {
                     for _ in 0..*depth {
                         out.write_str("#");
@@ -137,7 +138,7 @@ impl<'a> MdWriterState<'a> {
                         self.write_line(out, title);
                     }
                 });
-                self.write_md(out, body);
+                self.write_md(out, MdqNodeRef::wrap_vec(body).into_iter());
                 let which_defs_to_write =
                     match (&self.opts.link_reference_options, &self.opts.footnote_reference_options) {
                         (ReferencePlacement::BottomOfSection, ReferencePlacement::BottomOfSection) => {
@@ -149,17 +150,17 @@ impl<'a> MdWriterState<'a> {
                     };
                 self.write_definitions(out, which_defs_to_write);
             }
-            MdqNode::Paragraph(Paragraph { body }) => {
+            MdqNodeRef::Paragraph(Paragraph { body }) => {
                 out.with_block(Block::Plain, |out| {
                     self.write_line(out, body);
                 });
             }
-            MdqNode::BlockQuote(BlockQuote { body }) => {
+            MdqNodeRef::BlockQuote(BlockQuote { body }) => {
                 out.with_block(Block::Quote, |out| {
-                    self.write_md(out, body);
+                    self.write_md(out, MdqNodeRef::wrap_vec(body).into_iter());
                 });
             }
-            MdqNode::List(List { starting_index, items }) => {
+            MdqNodeRef::List(List { starting_index, items }) => {
                 out.with_block(Block::Plain, |out| {
                     let mut index = starting_index.clone();
                     // let mut prefix = String::with_capacity(8); // enough for "12. [ ] "
@@ -171,10 +172,10 @@ impl<'a> MdWriterState<'a> {
                     }
                 });
             }
-            MdqNode::ListItem(idx, item) => {
-                self.write_list_item(out, idx, item);
+            MdqNodeRef::ListItem(idx, item) => {
+                self.write_list_item(out, &idx, item);
             }
-            MdqNode::Table(Table { alignments, rows }) => {
+            MdqNodeRef::Table(Table { alignments, rows }) => {
                 let mut row_strs = Vec::with_capacity(rows.len());
 
                 let mut column_widths = [0].repeat(alignments.len());
@@ -283,10 +284,10 @@ impl<'a> MdWriterState<'a> {
                     write_row(out, row, rows_iter.peek().is_some());
                 }
             }
-            MdqNode::ThematicBreak => {
+            MdqNodeRef::ThematicBreak => {
                 out.with_block(Block::Plain, |out| out.write_str("***"));
             }
-            MdqNode::CodeBlock(CodeBlock { variant, value }) => {
+            MdqNodeRef::CodeBlock(CodeBlock { variant, value }) => {
                 let (surround, meta) = match variant {
                     CodeVariant::Code(opts) => {
                         let meta = if let Some(opts) = opts {
@@ -327,7 +328,7 @@ impl<'a> MdWriterState<'a> {
                     out.write_str(surround);
                 });
             }
-            MdqNode::Inline(inline) => {
+            MdqNodeRef::Inline(inline) => {
                 self.write_inline_element(out, inline);
             }
         }
@@ -338,7 +339,7 @@ impl<'a> MdWriterState<'a> {
         match index {
             None => std::fmt::Write::write_str(&mut counting_writer, "- ").unwrap(),
             Some(i) => {
-                Write::write_fmt(&mut counting_writer, format_args!("{}. ", &i)).unwrap();
+                std::fmt::Write::write_fmt(&mut counting_writer, format_args!("{}. ", &i)).unwrap();
             }
         };
         if let Some(checked) = &item.checked {
@@ -348,7 +349,7 @@ impl<'a> MdWriterState<'a> {
         }
         let count = counting_writer.count();
         out.with_block(Block::Inlined(count), |out| {
-            self.write_md(out, &item.item);
+            self.write_md(out, MdqNodeRef::wrap_vec(&item.item).into_iter());
         });
     }
 
@@ -519,7 +520,7 @@ impl<'a> MdWriterState<'a> {
                     out.write_str(link_ref);
                     out.write_str("]: ");
                     out.with_block(Block::Inlined(2), |out| {
-                        self.write_md(out, text);
+                        self.write_md(out, MdqNodeRef::wrap_vec(text).into_iter());
                         newline(out);
                     });
                 }
@@ -565,64 +566,61 @@ enum DefinitionsToWrite {
 #[cfg(test)]
 pub mod tests {
     use indoc::indoc;
-    use lazy_static::lazy_static;
 
     use crate::fmt_md::MdOptions;
     use crate::mdq_inline;
     use crate::mdq_nodes;
     use crate::output::Output;
     use crate::tree::*;
-    use crate::utils_for_test::*;
+    use crate::tree_ref::MdqNodeRef;
 
     use super::write_md;
 
-    lazy_static! {
-        static ref VARIANTS_CHECKER: VariantsChecker<MdqNode> = crate::new_variants_checker! (MdqNode {
-            Section(_),
-            Paragraph(_),
-            BlockQuote(_),
-            List(_),
-            ListItem(..),
-            Table(_),
-            ThematicBreak,
-            CodeBlock(crate::tree::CodeBlock{variant: CodeVariant::Code(None), ..}),
-            CodeBlock(crate::tree::CodeBlock{variant: CodeVariant::Code(Some(CodeOpts{metadata: None, ..})), ..}),
-            CodeBlock(crate::tree::CodeBlock{variant: CodeVariant::Code(Some(CodeOpts{metadata: Some(_), ..})), ..}),
-            CodeBlock(crate::tree::CodeBlock{variant: CodeVariant::Math{metadata: None}, ..}),
-            CodeBlock(crate::tree::CodeBlock{variant: CodeVariant::Math{metadata: Some(_)}, ..}),
-            CodeBlock(crate::tree::CodeBlock{variant: CodeVariant::Toml, ..}),
-            CodeBlock(crate::tree::CodeBlock{variant: CodeVariant::Yaml, ..}),
-
-            Inline(crate::tree::Inline::Span{variant: SpanVariant::Delete, ..}),
-            Inline(crate::tree::Inline::Span{variant: SpanVariant::Emphasis, ..}),
-            Inline(crate::tree::Inline::Span{variant: SpanVariant::Strong, ..}),
-
-            Inline(crate::tree::Inline::Text{variant: TextVariant::Plain, ..}),
-            Inline(crate::tree::Inline::Text{variant: TextVariant::Code, ..}),
-            Inline(crate::tree::Inline::Text{variant: TextVariant::Math, ..}),
-            Inline(crate::tree::Inline::Text{variant: TextVariant::Html, ..}),
-
-            Inline(crate::tree::Inline::Link{link: Link{title: None, reference: LinkReference::Inline, ..}, ..}),
-            Inline(crate::tree::Inline::Link{link: Link{title: None, reference: LinkReference::Full(_), ..}, ..}),
-            Inline(crate::tree::Inline::Link{link: Link{title: None, reference: LinkReference::Collapsed, ..}, ..}),
-            Inline(crate::tree::Inline::Link{link: Link{title: None, reference: LinkReference::Shortcut, ..}, ..}),
-            Inline(crate::tree::Inline::Link{link: Link{title: Some(_), reference: LinkReference::Inline, ..}, ..}),
-            Inline(crate::tree::Inline::Link{link: Link{title: Some(_), reference: LinkReference::Full(_), ..}, ..}),
-            Inline(crate::tree::Inline::Link{link: Link{title: Some(_), reference: LinkReference::Collapsed, ..}, ..}),
-            Inline(crate::tree::Inline::Link{link: Link{title: Some(_), reference: LinkReference::Shortcut, ..}, ..}),
-
-            Inline(crate::tree::Inline::Image{link: Link{title: None, reference: LinkReference::Inline, ..}, ..}),
-            Inline(crate::tree::Inline::Image{link: Link{title: None, reference: LinkReference::Full(_), ..}, ..}),
-            Inline(crate::tree::Inline::Image{link: Link{title: None, reference: LinkReference::Collapsed, ..}, ..}),
-            Inline(crate::tree::Inline::Image{link: Link{title: None, reference: LinkReference::Shortcut, ..}, ..}),
-            Inline(crate::tree::Inline::Image{link: Link{title: Some(_), reference: LinkReference::Inline, ..}, ..}),
-            Inline(crate::tree::Inline::Image{link: Link{title: Some(_), reference: LinkReference::Full(_), ..}, ..}),
-            Inline(crate::tree::Inline::Image{link: Link{title: Some(_), reference: LinkReference::Collapsed, ..}, ..}),
-            Inline(crate::tree::Inline::Image{link: Link{title: Some(_), reference: LinkReference::Shortcut, ..}, ..}),
-
-            Inline(crate::tree::Inline::Footnote{..}),
-        });
-    }
+    crate::variants_checker!(VARIANTS_CHECKER = MdqNodeRef {
+        Section(_),
+        Paragraph(_),
+        BlockQuote(_),
+        List(_),
+        ListItem(..),
+        Table(_),
+        ThematicBreak,
+        CodeBlock(crate::tree::CodeBlock{variant: CodeVariant::Code(None), ..}),
+        CodeBlock(crate::tree::CodeBlock{variant: CodeVariant::Code(Some(CodeOpts{metadata: None, ..})), ..}),
+        CodeBlock(crate::tree::CodeBlock{variant: CodeVariant::Code(Some(CodeOpts{metadata: Some(_), ..})), ..}),
+        CodeBlock(crate::tree::CodeBlock{variant: CodeVariant::Math{metadata: None}, ..}),
+        CodeBlock(crate::tree::CodeBlock{variant: CodeVariant::Math{metadata: Some(_)}, ..}),
+        CodeBlock(crate::tree::CodeBlock{variant: CodeVariant::Toml, ..}),
+        CodeBlock(crate::tree::CodeBlock{variant: CodeVariant::Yaml, ..}),
+
+        Inline(crate::tree::Inline::Span{variant: SpanVariant::Delete, ..}),
+        Inline(crate::tree::Inline::Span{variant: SpanVariant::Emphasis, ..}),
+        Inline(crate::tree::Inline::Span{variant: SpanVariant::Strong, ..}),
+
+        Inline(crate::tree::Inline::Text{variant: TextVariant::Plain, ..}),
+        Inline(crate::tree::Inline::Text{variant: TextVariant::Code, ..}),
+        Inline(crate::tree::Inline::Text{variant: TextVariant::Math, ..}),
+        Inline(crate::tree::Inline::Text{variant: TextVariant::Html, ..}),
+
+        Inline(crate::tree::Inline::Link{link: Link{title: None, reference: LinkReference::Inline, ..}, ..}),
+        Inline(crate::tree::Inline::Link{link: Link{title: None, reference: LinkReference::Full(_), ..}, ..}),
+        Inline(crate::tree::Inline::Link{link: Link{title: None, reference: LinkReference::Collapsed, ..}, ..}),
+        Inline(crate::tree::Inline::Link{link: Link{title: None, reference: LinkReference::Shortcut, ..}, ..}),
+        Inline(crate::tree::Inline::Link{link: Link{title: Some(_), reference: LinkReference::Inline, ..}, ..}),
+        Inline(crate::tree::Inline::Link{link: Link{title: Some(_), reference: LinkReference::Full(_), ..}, ..}),
+        Inline(crate::tree::Inline::Link{link: Link{title: Some(_), reference: LinkReference::Collapsed, ..}, ..}),
+        Inline(crate::tree::Inline::Link{link: Link{title: Some(_), reference: LinkReference::Shortcut, ..}, ..}),
+
+        Inline(crate::tree::Inline::Image{link: Link{title: None, reference: LinkReference::Inline, ..}, ..}),
+        Inline(crate::tree::Inline::Image{link: Link{title: None, reference: LinkReference::Full(_), ..}, ..}),
+        Inline(crate::tree::Inline::Image{link: Link{title: None, reference: LinkReference::Collapsed, ..}, ..}),
+        Inline(crate::tree::Inline::Image{link: Link{title: None, reference: LinkReference::Shortcut, ..}, ..}),
+        Inline(crate::tree::Inline::Image{link: Link{title: Some(_), reference: LinkReference::Inline, ..}, ..}),
+        Inline(crate::tree::Inline::Image{link: Link{title: Some(_), reference: LinkReference::Full(_), ..}, ..}),
+        Inline(crate::tree::Inline::Image{link: Link{title: Some(_), reference: LinkReference::Collapsed, ..}, ..}),
+        Inline(crate::tree::Inline::Image{link: Link{title: Some(_), reference: LinkReference::Shortcut, ..}, ..}),
+
+        Inline(crate::tree::Inline::Footnote{..}),
+    });
 
     #[test]
     fn empty() {
@@ -923,57 +921,41 @@ pub mod tests {
 
     mod list_item {
         use super::*;
+        use crate::tree_ref::MdqNodeRef;
 
         #[test]
         fn unordered_no_checkbox() {
-            check_render(
-                create_li_singleton(None, None, mdq_nodes!("plain text")),
-                "- plain text",
-            );
+            create_li_singleton(None, None, mdq_nodes!("plain text"), "- plain text");
         }
 
         #[test]
         fn unordered_unchecked() {
-            check_render(
-                create_li_singleton(None, Some(false), mdq_nodes!("plain text")),
-                "- [ ] plain text",
-            );
+            create_li_singleton(None, Some(false), mdq_nodes!("plain text"), "- [ ] plain text");
         }
 
         #[test]
         fn unordered_checked() {
-            check_render(
-                create_li_singleton(None, Some(true), mdq_nodes!("plain text")),
-                "- [x] plain text",
-            );
+            create_li_singleton(None, Some(true), mdq_nodes!("plain text"), "- [x] plain text");
         }
 
         #[test]
         fn ordered_no_checkbox() {
-            check_render(
-                create_li_singleton(Some(3), None, mdq_nodes!("plain text")),
-                "3. plain text",
-            );
+            create_li_singleton(Some(3), None, mdq_nodes!("plain text"), "3. plain text");
         }
 
         #[test]
         fn ordered_unchecked() {
-            check_render(
-                create_li_singleton(Some(3), Some(false), mdq_nodes!("plain text")),
-                "3. [ ] plain text",
-            );
+            create_li_singleton(Some(3), Some(false), mdq_nodes!("plain text"), "3. [ ] plain text");
         }
 
         #[test]
         fn ordered_checked() {
-            check_render(
-                create_li_singleton(Some(3), Some(true), mdq_nodes!("plain text")),
-                "3. [x] plain text",
-            );
+            create_li_singleton(Some(3), Some(true), mdq_nodes!("plain text"), "3. [x] plain text");
         }
 
-        fn create_li_singleton(idx: Option<u32>, checked: Option<bool>, item: Vec<MdqNode>) -> Vec<MdqNode> {
-            vec![MdqNode::ListItem(idx, ListItem { checked, item })]
+        fn create_li_singleton<'a>(idx: Option<u32>, checked: Option<bool>, item: Vec<MdqNode>, expected: &str) {
+            let li = ListItem { checked, item };
+            check_render_with_refs(&MdOptions::default(), vec![MdqNodeRef::ListItem(idx, &li)], expected)
         }
     }
 
@@ -1877,20 +1859,19 @@ pub mod tests {
         }
     }
 
-    #[test]
-    fn all_variants_checked() {
-        VARIANTS_CHECKER.wait_for_all();
-    }
-
     fn check_render(nodes: Vec<MdqNode>, expect: &str) {
         check_render_with(&MdOptions::default(), nodes, expect);
     }
 
     fn check_render_with(options: &MdOptions, nodes: Vec<MdqNode>, expect: &str) {
+        check_render_with_refs(options, MdqNodeRef::wrap_vec(&nodes), expect)
+    }
+
+    fn check_render_with_refs<'a>(options: &MdOptions, nodes: Vec<MdqNodeRef<'a>>, expect: &str) {
         nodes.iter().for_each(|n| VARIANTS_CHECKER.see(n));
 
         let mut out = Output::new(String::default());
-        write_md(options, &mut out, &nodes);
+        write_md(options, &mut out, nodes.into_iter());
         let actual = out.take_underlying().unwrap();
         assert_eq!(&actual, expect);
     }
diff --git a/src/fmt_str.rs b/src/fmt_str.rs
index 6f29f18..5547575 100644
--- a/src/fmt_str.rs
+++ b/src/fmt_str.rs
@@ -39,24 +39,20 @@ mod tests {
 
     use crate::tree::{Inline, MdqNode, ReadOptions, SpanVariant, TextVariant};
     use crate::unwrap;
-    use crate::utils_for_test::VariantsChecker;
-    use lazy_static::lazy_static;
     use markdown::ParseOptions;
 
-    lazy_static! {
-        static ref VARIANTS_CHECKER: VariantsChecker<Inline> = crate::new_variants_checker!(Inline {
-            Span { variant: SpanVariant::Delete, .. },
-            Span { variant: SpanVariant::Emphasis, .. },
-            Span { variant: SpanVariant::Strong, .. },
-            Text { variant: TextVariant::Plain, .. },
-            Text { variant: TextVariant::Code, .. },
-            Text { variant: TextVariant::Math, .. },
-            Text { variant: TextVariant::Html, .. },
-            Link { .. },
-            Image { .. },
-            Footnote(_),
-        });
-    }
+    crate::variants_checker!(VARIANTS_CHECKER = Inline {
+        Span { variant: SpanVariant::Delete, .. },
+        Span { variant: SpanVariant::Emphasis, .. },
+        Span { variant: SpanVariant::Strong, .. },
+        Text { variant: TextVariant::Plain, .. },
+        Text { variant: TextVariant::Code, .. },
+        Text { variant: TextVariant::Math, .. },
+        Text { variant: TextVariant::Html, .. },
+        Link { .. },
+        Image { .. },
+        Footnote(_),
+    });
 
     #[test]
     fn spans() {
@@ -120,11 +116,6 @@ mod tests {
         )
     }
 
-    #[test]
-    fn all_variants_checked() {
-        VARIANTS_CHECKER.wait_for_all();
-    }
-
     /// Because this is such simple functionality, we're just going to do a simple end-to-end test from original
     /// markdown to plain text.
     fn check(md: &str, expect: &str) {
diff --git a/src/main.rs b/src/main.rs
index cbcaa1d..fc4a071 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -5,6 +5,7 @@ use crate::fmt_md::MdOptions;
 use crate::output::Stream;
 use crate::select::Selector;
 use crate::tree::{MdqNode, ReadOptions};
+use crate::tree_ref::MdqNodeRef;
 
 mod fmt_md;
 mod fmt_str;
@@ -15,6 +16,7 @@ mod parsing_iter;
 mod select;
 mod str_utils;
 mod tree;
+mod tree_ref;
 mod tree_test_utils;
 mod utils_for_test;
 
@@ -22,18 +24,18 @@ fn main() {
     let mut contents = String::new();
     stdin().read_to_string(&mut contents).expect("invalid input (not utf8)");
     let ast = markdown::to_mdast(&mut contents, &markdown::ParseOptions::gfm()).unwrap();
-    let mdq: MdqNode = MdqNode::read(ast, &ReadOptions::default()).unwrap().pop().unwrap();
+    let mdqs = MdqNode::read(ast, &ReadOptions::default()).unwrap();
     let mut out = output::Output::new(Stream(io::stdout()));
 
     let selectors_str = env::args().nth(1).unwrap_or("".to_string());
     let selectors = Selector::parse(&selectors_str).expect("failed to parse selector");
 
-    let mut pipeline_nodes = vec![&mdq];
+    let mut pipeline_nodes = MdqNodeRef::wrap_vec(&mdqs);
     for selector in selectors {
         let new_pipeline = selector.find_nodes(pipeline_nodes);
         pipeline_nodes = new_pipeline;
     }
 
-    fmt_md::write_md(&MdOptions::default(), &mut out, &pipeline_nodes);
+    fmt_md::write_md(&MdOptions::default(), &mut out, pipeline_nodes.into_iter());
     out.write_str("\n");
 }
diff --git a/src/select.rs b/src/select.rs
index 078b736..06eae29 100644
--- a/src/select.rs
+++ b/src/select.rs
@@ -2,7 +2,9 @@ use crate::fmt_str::inlines_to_plain_string;
 use crate::matcher::Matcher;
 use crate::parse_common::{ParseError, ParseErrorReason, ParseResult};
 use crate::parsing_iter::ParsingIterator;
-use crate::tree::MdqNode;
+use crate::tree::Inline;
+use crate::tree_ref::MdqNodeRef;
+use crate::wrap_mdq_refs;
 
 #[derive(Debug, PartialEq)]
 pub enum Selector {
@@ -17,7 +19,7 @@ pub enum Selector {
 }
 
 impl Selector {
-    pub fn find_nodes<'a>(&self, nodes: Vec<&'a MdqNode>) -> Vec<&'a MdqNode> {
+    pub fn find_nodes<'a>(&self, nodes: Vec<MdqNodeRef<'a>>) -> Vec<MdqNodeRef<'a>> {
         let mut result = Vec::with_capacity(8); // arbitrary guess
         for node in nodes {
             self.find_nodes_one(&mut result, node);
@@ -26,19 +28,74 @@ impl Selector {
     }
 
     // TODO need better name -- here but also in all the other methods
-    pub fn find_nodes_one<'a>(&self, out: &mut Vec<&'a MdqNode>, node: &'a MdqNode) {
-        match (self, node) {
-            (Selector::Heading(selector), MdqNode::Section(header)) => {
+    pub fn find_nodes_one<'a>(&self, out: &mut Vec<MdqNodeRef<'a>>, node: MdqNodeRef<'a>) {
+        let found = match (self, node.clone()) {
+            (Selector::Heading(selector), MdqNodeRef::Section(header)) => {
                 let header_text = inlines_to_plain_string(&header.title);
                 if selector.matcher.matches(&header_text) {
-                    header.body.iter().for_each(|child| out.push(child));
+                    header.body.iter().for_each(|child| out.push(child.into()));
+                    true
+                } else {
+                    false
                 }
             }
-            _ => {
-                // TODO better recursion
+            _ => false,
+        };
+        if !found {
+            for child in Self::find_children(node) {
+                self.find_nodes_one(out, child);
             }
         }
     }
+
+    /// Recurse from this node to its children.
+    ///
+    /// This makes sense to put here (as opposed to in the [tree] module) because the definition of a "child" is
+    /// selector-specific. For example, an [MdqNode::Section] has child nodes both in its title and in its body, but
+    /// only the body nodes are relevant for select recursion. `MdqNode` shouldn't need to know about that oddity; it
+    /// belongs here.
+    fn find_children<'a>(node: MdqNodeRef) -> Vec<MdqNodeRef> {
+        match node {
+            MdqNodeRef::Section(s) => MdqNodeRef::wrap_vec(&s.body),
+            MdqNodeRef::Paragraph(p) => wrap_mdq_refs!(Inline: &p.body),
+            MdqNodeRef::BlockQuote(b) => MdqNodeRef::wrap_vec(&b.body),
+            MdqNodeRef::List(list) => {
+                let mut idx = list.starting_index;
+                let mut result = Vec::with_capacity(list.items.len());
+                for item in &list.items {
+                    result.push(MdqNodeRef::ListItem(idx.clone(), item));
+                    if let Some(idx) = idx.as_mut() {
+                        *idx += 1;
+                    }
+                }
+                result
+            }
+            MdqNodeRef::Table(table) => {
+                let count_estimate = table.rows.len() * table.rows.first().map(|tr| tr.len()).unwrap_or(0);
+                let mut result = Vec::with_capacity(count_estimate);
+                for row in &table.rows {
+                    for col in row {
+                        for cell in col {
+                            result.push(MdqNodeRef::Inline(cell));
+                        }
+                    }
+                }
+                result
+            }
+            MdqNodeRef::ThematicBreak => Vec::new(),
+            MdqNodeRef::CodeBlock(_) => Vec::new(),
+            MdqNodeRef::ListItem(_, item) => MdqNodeRef::wrap_vec(&item.item),
+            MdqNodeRef::Inline(inline) => match inline {
+                Inline::Span { children, .. } => children.iter().map(|child| MdqNodeRef::Inline(child)).collect(),
+                Inline::Footnote(footnote) => MdqNodeRef::wrap_vec(&footnote.text),
+                Inline::Link { .. } => {
+                    // TODO need to return an MdqNodeRef::Link
+                    Vec::new()
+                }
+                Inline::Text { .. } | Inline::Image { .. } => Vec::new(),
+            },
+        }
+    }
 }
 
 #[derive(Debug, PartialEq)]
diff --git a/src/tree.rs b/src/tree.rs
index 7daa2be..f5bc917 100644
--- a/src/tree.rs
+++ b/src/tree.rs
@@ -20,9 +20,6 @@ pub enum MdqNode {
     // blocks that contain strings (as opposed to nodes)
     CodeBlock(CodeBlock),
 
-    #[allow(dead_code)] // selectors will create this later
-    ListItem(Option<u32>, ListItem),
-
     // inline spans
     Inline(Inline),
 }
@@ -677,9 +674,7 @@ mod tests {
     /// For example, footnote are `[^a]` in markdown; does that identifier get parsed as `"^a"` or `"a"`?
     mod all_nodes {
         use crate::unwrap;
-        use crate::utils_for_test::VariantsChecker;
         use indoc::indoc;
-        use lazy_static::lazy_static;
         use markdown::mdast::Node;
         use markdown::{mdast, ParseOptions};
 
@@ -1656,11 +1651,6 @@ mod tests {
             });
         }
 
-        #[test]
-        fn all_variants_tested() {
-            NODES_CHECKER.wait_for_all();
-        }
-
         fn parse(md: &str) -> (mdast::Root, Lookups) {
             parse_with(&ParseOptions::default(), md)
         }
@@ -1672,41 +1662,43 @@ mod tests {
             (root, lookups)
         }
 
-        lazy_static! {
-            static ref NODES_CHECKER: VariantsChecker<Node> = crate::new_variants_checker!(Node {
-                BlockQuote(_),
-                Break(_),
-                Code(_),
-                Definition(_),
-                Delete(_),
-                Emphasis(_),
-                FootnoteDefinition(_),
-                FootnoteReference(_),
-                Heading(_),
-                Html(_),
-                Image(_),
-                ImageReference(_),
-                InlineCode(_),
-                InlineMath(_),
-                Link(_),
-                LinkReference(_),
-                List(_),
-                ListItem(_),
-                Math(_),
-                Paragraph(_),
-                Root(_),
-                Strong(_),
-                Table(_),
-                TableCell(_),
-                TableRow(_),
-                Text(_),
-                ThematicBreak(_),
-                Toml(_),
-                Yaml(_),
-            } ignore {
-                mdx_nodes!(),
-            });
-        }
+        crate::variants_checker!(NODES_CHECKER = Node {
+            BlockQuote(_),
+            Break(_),
+            Code(_),
+            Definition(_),
+            Delete(_),
+            Emphasis(_),
+            FootnoteDefinition(_),
+            FootnoteReference(_),
+            Heading(_),
+            Html(_),
+            Image(_),
+            ImageReference(_),
+            InlineCode(_),
+            InlineMath(_),
+            Link(_),
+            LinkReference(_),
+            List(_),
+            ListItem(_),
+            Math(_),
+            Paragraph(_),
+            Root(_),
+            Strong(_),
+            Table(_),
+            TableCell(_),
+            TableRow(_),
+            Text(_),
+            ThematicBreak(_),
+            Toml(_),
+            Yaml(_),
+        } ignore {
+            MdxJsxFlowElement(_),
+            MdxjsEsm(_),
+            MdxTextExpression(_),
+            MdxJsxTextElement(_),
+            MdxFlowExpression(_),
+        });
     }
 
     mod lookups {
diff --git a/src/tree_ref.rs b/src/tree_ref.rs
new file mode 100644
index 0000000..00e71a1
--- /dev/null
+++ b/src/tree_ref.rs
@@ -0,0 +1,58 @@
+use crate::tree::{BlockQuote, CodeBlock, Inline, List, ListItem, MdqNode, Paragraph, Section, Table};
+
+#[derive(Debug, Clone)]
+pub enum MdqNodeRef<'a> {
+    // paragraphs with child nodes
+    Section(&'a Section),
+    Paragraph(&'a Paragraph),
+    BlockQuote(&'a BlockQuote),
+    List(&'a List),
+    Table(&'a Table),
+
+    ThematicBreak,
+
+    // blocks that contain strings (&'a as opposed to nodes)
+    CodeBlock(&'a CodeBlock),
+
+    ListItem(Option<u32>, &'a ListItem),
+
+    // inline spans
+    Inline(&'a Inline),
+}
+
+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::Inline(v) => Self::Inline(v),
+        }
+    }
+}
+
+#[macro_export]
+macro_rules! wrap_mdq_refs {
+    ($variant:ident: $source:expr) => {{
+        let source = $source;
+        let mut result: Vec<MdqNodeRef> = Vec::with_capacity(source.len());
+        for elem in source {
+            result.push(MdqNodeRef::$variant(elem));
+        }
+        result
+    }};
+}
+
+impl<'a> MdqNodeRef<'a> {
+    pub fn wrap_vec(source: &'a Vec<MdqNode>) -> Vec<Self> {
+        let mut result: Vec<Self> = Vec::with_capacity(source.len());
+        for elem in source {
+            result.push(elem.into());
+        }
+        result
+    }
+}


Test patch:
diff --git a/src/utils_for_test.rs b/src/utils_for_test.rs
index 8baf3ec..8c5905e 100644
--- a/src/utils_for_test.rs
+++ b/src/utils_for_test.rs
@@ -1,11 +1,7 @@
-#[cfg(test)]
-pub use test_utils::*;
-
 // We this file's contents from prod by putting them in a submodule guarded by cfg(test), but then "pub use" it to
 // export its contents.
 #[cfg(test)]
 mod test_utils {
-    use std::{thread, time};
 
     /// Turn a pattern match into an `if let ... { else panic! }`.
     #[macro_export]
@@ -20,64 +16,17 @@ mod test_utils {
         };
     }
 
-    pub struct VariantsChecker<E> {
-        require: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
-        resolver: fn(&E) -> &str,
-    }
-
-    impl<E> VariantsChecker<E> {
-        pub fn new<I>(require: I, resolver: fn(&E) -> &str) -> Self
-        where
-            I: IntoIterator<Item = String>,
-        {
-            Self {
-                require: std::sync::Arc::new(std::sync::Mutex::new(require.into_iter().collect())),
-                resolver,
-            }
-        }
-
-        pub fn see(&self, node: &E) {
-            let node_str = (self.resolver)(node);
-            self.require.lock().map(|mut set| set.remove(node_str)).unwrap();
-        }
-
-        pub fn wait_for_all(&self) {
-            let timeout = time::Duration::from_millis(500);
-            let retry_delay = time::Duration::from_millis(50);
-            let start = time::Instant::now();
-            loop {
-                if self.require.lock().map(|set| set.is_empty()).unwrap() {
-                    break;
-                }
-                if start.elapsed() >= timeout {
-                    let mut remaining: Vec<String> = self
-                        .require
-                        .lock()
-                        .map(|set| set.iter().map(|s| s.to_owned()).collect())
-                        .unwrap();
-                    remaining.sort();
-                    panic!(
-                        "Timed out, and missing {} variants:\n- {}",
-                        remaining.len(),
-                        remaining.join("\n- ")
-                    )
-                }
-                thread::sleep(retry_delay);
-            }
-        }
-    }
-
-    /// Creates a new `VariantsChecker` that looks for all the variants of enum `E`.
+    /// Creates a static object named `$name` that looks for all the variants of enum `E`.
     ///
     /// ```
-    /// new_variants_checker(MyEnum: { Variant1, Variant2(_), ... })
+    /// variants_checker(CHECKER_NAME = MyEnum: { Variant1, Variant2(_), ... })
     /// ```
     ///
     /// You can also mark some variants as ignored; these will be added to the pattern match, but not be required to
     /// be seen:
     ///
     /// ```
-    /// new_variants_checker(MyEnum: { Variant1, ... } ignore { Variant2, ... } )
+    /// variants_checker(CHECKER_NAME = MyEnum: { Variant1, ... } ignore { Variant2, ... } )
     /// ```
     ///
     /// If you see a compilation failure here, it means the call site is missing variants (or has an unknown
@@ -90,18 +39,67 @@ mod test_utils {
     /// This requires that each pattern matches exactly one shape of item; in other words, that there aren't any
     /// dead-code branches.
     #[macro_export]
-    macro_rules! new_variants_checker {
-        ($enum_type:ty { $($variant:pat),* $(,)? } $(ignore { $($ignore_variant:pat),* $(,)? })?) => {
-            {
-                use $enum_type::*;
+    macro_rules! variants_checker {
+        ($name:ident = $enum_type:ty { $($variant:pat),* $(,)? } $(ignore { $($ignore_variant:pat),* $(,)? })?) => {
+
+            paste::paste!{
+                pub struct [<VariantsChecker $name:lower:camel>] {
+                    require: std::sync::Arc<std::sync::Mutex<std::collections::HashSet<String>>>,
+                }
+
+                impl [<VariantsChecker $name:lower:camel>] {
+                    pub fn see(&self, node: &$enum_type) {
+                        let node_str = match node {
+                            $($enum_type::$variant => stringify!($variant),)*
+                            $($($enum_type::$ignore_variant => {
+                                panic!("unexpected variant: {}", stringify!($ignore_variant));
+                            },)*)?
+                        };
+                        self.require.lock().map(|mut set| set.remove(node_str)).unwrap();
+                    }
+
+                    pub fn wait_for_all(&self) {
+                        use std::{thread, time};
 
-                VariantsChecker::new(
-                    vec![$(stringify!($variant).to_string(),)*],
-                    {|elem| match elem {
-                        $($variant => stringify!($variant),)*
-                        $($($ignore_variant => {""},)*)?
-                    }}
-                )
+                        let timeout = time::Duration::from_millis(500);
+                        let retry_delay = time::Duration::from_millis(50);
+                        let start = time::Instant::now();
+                        loop {
+                            if self.require.lock().map(|set| set.is_empty()).unwrap() {
+                                break;
+                            }
+                            if start.elapsed() >= timeout {
+                                let mut remaining: Vec<String> = self
+                                    .require
+                                    .lock()
+                                    .map(|set| set.iter().map(|s| s.to_owned()).collect())
+                                    .unwrap();
+                                remaining.sort();
+                                panic!(
+                                    "Timed out, and missing {} variants:\n- {}",
+                                    remaining.len(),
+                                    remaining.join("\n- ")
+                                )
+                            }
+                            thread::sleep(retry_delay);
+                        }
+                    }
+                }
+
+                lazy_static::lazy_static! {
+                    static ref $name: [<VariantsChecker $name:lower:camel>] = [<VariantsChecker $name:lower:camel>] {
+                        require: std::sync::Arc::new(
+                            std::sync::Mutex::new(
+                                vec![$(stringify!($variant).to_string(),)*].into_iter().collect()
+                            )
+                        )
+                    };
+                }
+
+                #[test]
+                fn [<all_variants_checked_for_ $name:lower:snake>]() {
+                    $name.wait_for_all();
+                }
             }
         };
     }