Skip to content

Instantly share code, notes, and snippets.

@lucperkins
Last active December 26, 2022 19:16
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lucperkins/de0fad1261a41f6cbcd5d34719a29aa0 to your computer and use it in GitHub Desktop.
Save lucperkins/de0fad1261a41f6cbcd5d34719a29aa0 to your computer and use it in GitHub Desktop.
Rust TOC example

Here's a level 2 header

Some text.

Here's a level 3 header

Some more text.

Back to level 2

Some more text.

Going deeper

Some more text.

Going yet deeper

And fin.

// The Rust logic that's supposed to build `toc_result.rs` out of `example.md` but doesn't (yet)
use markdown_it::{plugins::cmark::block::heading::ATXHeading, Node};
use serde::Serialize;
use slug::slugify;
// Recursive TOC struct
#[derive(Debug, PartialEq, Serialize)]
pub struct TableOfContents(Vec<(Heading, TableOfContents)>);
// Initializer. Pass in an AST and a heading level to start (level 2 is standard).
impl TableOfContents {
pub fn new(document: &Node) -> Self {
toc_for_level(&document.children, 2)
}
}
// Create a table of contents for a specific heading level
fn toc_for_level(nodes: &[Node], level: u8) -> TableOfContents {
let mut toc: Vec<(Heading, TableOfContents)> = Vec::new();
// Iterate through each node in the provided portion of the AST
for (idx, node) in nodes.iter().enumerate() {
// Stop at each heading
if let Some(h) = node.cast::<ATXHeading>() {
// Stop at headings that match the desired level
if h.level == level {
// Convert the content of the heading to a string
let text = &node_to_string(node);
// Create a new heading to add to the TOC
let heading = Heading::new(level, text);
// Add the new heading to the mutable vector, and pass in the TOC
// for the next heading level down (h2 to h3, h3 to h4, etc.) by
// passing in the remaining nodes in the AST. This is the part that
// doesn't work, because passing &nodes[idx..] here means that the
// whole rest of the document is iterated over, not just the portion
// that's underneath this heading. But I can't figure out a good way
// to tell the function where to stop! Any help would be greatly
// appreciated! 💯❤️
toc.push((heading, toc_for_level(&nodes[idx..], level + 1)));
}
}
}
TableOfContents(toc)
}
// Heading struct (for use in the TOC)
#[derive(Debug, PartialEq, Serialize)]
struct Heading {
level: u8,
text: String,
slug: String,
}
// Heading initializer
impl Heading {
fn new(level: u8, text: &str) -> Self {
let slug = slugify(text);
Self {
level,
text: String::from(text),
slug,
}
}
}
// Desired result struct from parsing `example.md`
TableOfContents(vec![
(
Heading {
level: 2,
text: String::from("Here's a level 2 header"),
slug: String::from("here-s-a-level-2-header"),
},
TableOfContents(vec![
(
Heading: {
level: 3,
text: String::from("Here's a level 3 header"),
slug: String::from("here-s-a-level-3-header"),
},
TableOfContents(vec![])
)
]),
),
(
Heading {
level: 2,
text: String::from("Back to level 2"),
slug: String::from("back-to-level-2"),
},
TableOfContents(vec![
(
Heading {
level: 3,
text: String::from("Going deeper"),
slug: String::from("going-deeper"),
},
TableOfContents(vec![
(
Heading {
level: 4,
text: String::from("Going yet deeper"),
slug: String::from("going-yet-deeper"),
},
TableOfContents(vec![]),
)
]),
),
]),
),
])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment