Skip to main content

dmc_transform/builtin/
autolink_headings.rs

1use crate::pipeline::Transformer;
2use crate::visit::{NodeAction, Visitor, walk_root};
3use dmc_diagnostic::Code;
4use dmc_diagnostic::metadata::SourceMeta;
5use dmc_parser::ast::*;
6
7/// Wrap each `Heading`'s children in a `Link` to its own `#id` anchor.
8/// `aria_label` flows through as the link's `title`.
9#[derive(Default)]
10pub struct AutolinkHeadings {
11  pub aria_label: Option<String>,
12}
13
14impl AutolinkHeadings {
15  /// Construct with the conventional shadcn-style aria label. Use
16  /// `Default::default()` for an unset label.
17  pub fn new() -> Self {
18    Self { aria_label: Some("Link to section".to_string()) }
19  }
20}
21
22impl Transformer for AutolinkHeadings {
23  fn name(&self) -> &str {
24    "autolink-headings"
25  }
26
27  fn transform(
28    &self,
29    doc: &mut Document,
30    _meta: &SourceMeta,
31    _diag_engine: &mut duck_diagnostic::DiagnosticEngine<Code>,
32  ) {
33    let mut v = Apply { aria_label: self.aria_label.clone() };
34    walk_root(&mut doc.children, &mut v);
35  }
36}
37
38struct Apply {
39  aria_label: Option<String>,
40}
41
42impl Visitor for Apply {
43  fn visit_node(&mut self, node: &mut Node) -> NodeAction {
44    if let Node::Heading(h) = node {
45      let slug = h.slug();
46      let span = h.span.clone();
47      let original = std::mem::take(&mut h.children);
48      let already = matches!(original.as_slice(), [Node::Link(l)] if l.href == format!("#{}", slug));
49      if already {
50        h.children = original;
51        return NodeAction::KeepSkipChildren;
52      }
53      let link =
54        Node::Link(Link { href: format!("#{}", slug), title: self.aria_label.clone(), children: original, span });
55      h.children = vec![link];
56      return NodeAction::KeepSkipChildren;
57    }
58    NodeAction::Keep
59  }
60}