markdown_it/plugins/extra/
heading_anchors.rs

1//! Add id attribute (slug) to headings.
2//!
3//! ```rust
4//! // it is recommended to use 3rd party slug implementation
5//! //let slugify_fn = |s: &str| slug::slugify(s);
6//! let slugify_fn = markdown_it::plugins::extra::heading_anchors::simple_slugify_fn;
7//!
8//! let md = &mut markdown_it::MarkdownIt::new();
9//! markdown_it::plugins::cmark::add(md);
10//! markdown_it::plugins::extra::heading_anchors::add(md, slugify_fn);
11//!
12//! assert_eq!(
13//!     md.parse("## An example heading").render(),
14//!     "<h2 id=\"an-example-heading\">An example heading</h2>\n",
15//! );
16//! ```
17use std::fmt::Debug;
18
19use crate::parser::core::CoreRule;
20use crate::parser::extset::MarkdownItExt;
21use crate::plugins::cmark::block::heading::ATXHeading;
22use crate::plugins::cmark::block::lheading::SetextHeader;
23use crate::{MarkdownIt, Node};
24
25pub fn add(md: &mut MarkdownIt, slugify: fn (&str) -> String) {
26    md.ext.insert(SlugifyFunction(slugify));
27    md.add_rule::<AddHeadingAnchors>();
28}
29
30/// Simple built-in slugify function. It is added for testing and demonstration
31/// purposes only, you should be using `slug`/`slugify` crate instead or your own impl.
32pub fn simple_slugify_fn(s: &str) -> String {
33    s.chars().map(|x| {
34        if x.is_alphanumeric() {
35            x.to_ascii_lowercase()
36        } else {
37            '-'
38        }
39    }).collect()
40}
41
42#[derive(Clone, Copy)]
43struct SlugifyFunction(fn (&str) -> String);
44impl MarkdownItExt for SlugifyFunction {}
45
46impl Default for SlugifyFunction {
47    fn default() -> Self {
48        Self(simple_slugify_fn)
49    }
50}
51
52impl Debug for SlugifyFunction {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        f.debug_struct("SlugifyFunction").finish()
55    }
56}
57
58pub struct AddHeadingAnchors;
59impl CoreRule for AddHeadingAnchors {
60    fn run(root: &mut Node, md: &MarkdownIt) {
61        let slugify = md.ext.get::<SlugifyFunction>().copied().unwrap_or_default().0;
62
63        root.walk_mut(|node, _| {
64            if node.is::<ATXHeading>() || node.is::<SetextHeader>() {
65                node.attrs.push(("id", slugify(&node.collect_text())));
66            }
67        });
68    }
69}