dmc_transform/builtin/
autolink_headings.rs1use crate::pipeline::Transformer;
5use crate::visit::{NodeAction, Visitor, walk_root};
6use dmc_diagnostic::Code;
7use dmc_diagnostic::metadata::SourceMeta;
8use dmc_parser::ast::*;
9
10#[derive(Default, Debug)]
12pub struct AutolinkHeadings {
13 pub aria_label: Option<String>,
15 pub class_name: Option<String>,
17 pub icon_class_name: Option<String>,
19}
20
21impl AutolinkHeadings {
22 pub fn new() -> Self {
24 Self::default()
25 }
26
27 fn aria(&self) -> &str {
28 self.aria_label.as_deref().unwrap_or("Link to section")
29 }
30 fn class(&self) -> &str {
31 self.class_name.as_deref().unwrap_or("subheading-anchor")
32 }
33 fn icon_class(&self) -> &str {
34 self.icon_class_name.as_deref().unwrap_or("icon icon-link")
35 }
36}
37
38impl Transformer for AutolinkHeadings {
39 fn name(&self) -> &str {
40 "autolink-headings"
41 }
42
43 fn transform(
44 &self,
45 doc: &mut Document,
46 _meta: &SourceMeta,
47 _diag_engine: &mut duck_diagnostic::DiagnosticEngine<Code>,
48 ) {
49 let mut v = Apply {
50 aria_label: self.aria().to_string(),
51 class_name: self.class().to_string(),
52 icon_class_name: self.icon_class().to_string(),
53 };
54 walk_root(&mut doc.children, &mut v);
55 }
56}
57
58struct Apply {
59 aria_label: String,
60 class_name: String,
61 icon_class_name: String,
62}
63
64impl Visitor for Apply {
65 fn visit_node(&mut self, node: &mut Node) -> NodeAction {
66 if let Node::Heading(h) = node {
67 let id = h.slug();
68 let span = h.span.clone();
69 let already_prepended = matches!(
72 h.children.first(),
73 Some(Node::JsxElement(e))
74 if e.name == "a"
75 && e.attrs.iter().any(|a| a.name == "href" && matches!(&a.value, JsxAttrValue::String(s) if s == &format!("#{}", id)))
76 );
77 if already_prepended {
78 return NodeAction::KeepSkipChildren;
79 }
80 let anchor = build_anchor(&id, &self.aria_label, &self.class_name, &self.icon_class_name, span);
81 h.children.insert(0, anchor);
82 return NodeAction::KeepSkipChildren;
83 }
84 NodeAction::Keep
85 }
86}
87
88fn build_anchor(id: &str, aria: &str, class: &str, icon_class: &str, span: duck_diagnostic::Span) -> Node {
90 let attr = |name: &str, value: &str| JsxAttr {
91 name: name.into(),
92 value: JsxAttrValue::String(value.into()),
93 span: span.clone(),
94 };
95 let icon = Node::JsxSelfClosing(JsxSelfClosing {
96 name: "span".into(),
97 attrs: vec![attr("className", icon_class)],
98 span: span.clone(),
99 });
100 Node::JsxElement(JsxElement {
101 name: "a".into(),
102 attrs: vec![attr("aria-label", aria), attr("className", class), attr("href", &format!("#{}", id))],
103 children: vec![icon],
104 span,
105 })
106}