use crate::pipeline::Transformer;
use crate::visit::{NodeAction, Visitor, walk_root};
use dmc_diagnostic::Code;
use dmc_diagnostic::metadata::SourceMeta;
use dmc_parser::ast::*;
#[derive(Default, Debug)]
pub struct AutolinkHeadings {
pub aria_label: Option<String>,
pub class_name: Option<String>,
pub icon_class_name: Option<String>,
}
impl AutolinkHeadings {
pub fn new() -> Self {
Self::default()
}
fn aria(&self) -> &str {
self.aria_label.as_deref().unwrap_or("Link to section")
}
fn class(&self) -> &str {
self.class_name.as_deref().unwrap_or("subheading-anchor")
}
fn icon_class(&self) -> &str {
self.icon_class_name.as_deref().unwrap_or("icon icon-link")
}
}
impl Transformer for AutolinkHeadings {
fn name(&self) -> &str {
"autolink-headings"
}
fn transform(
&self,
doc: &mut Document,
_meta: &SourceMeta,
_diag_engine: &mut duck_diagnostic::DiagnosticEngine<Code>,
) {
let mut v = Apply {
aria_label: self.aria().to_string(),
class_name: self.class().to_string(),
icon_class_name: self.icon_class().to_string(),
};
walk_root(&mut doc.children, &mut v);
}
}
struct Apply {
aria_label: String,
class_name: String,
icon_class_name: String,
}
impl Visitor for Apply {
fn visit_node(&mut self, node: &mut Node) -> NodeAction {
if let Node::Heading(h) = node {
let id = h.slug();
let span = h.span.clone();
let already_prepended = matches!(
h.children.first(),
Some(Node::JsxElement(e))
if e.name == "a"
&& e.attrs.iter().any(|a| a.name == "href" && matches!(&a.value, JsxAttrValue::String(s) if s == &format!("#{}", id)))
);
if already_prepended {
return NodeAction::KeepSkipChildren;
}
let anchor = build_anchor(&id, &self.aria_label, &self.class_name, &self.icon_class_name, span);
h.children.insert(0, anchor);
return NodeAction::KeepSkipChildren;
}
NodeAction::Keep
}
}
fn build_anchor(id: &str, aria: &str, class: &str, icon_class: &str, span: duck_diagnostic::Span) -> Node {
let attr = |name: &str, value: &str| JsxAttr {
name: name.into(),
value: JsxAttrValue::String(value.into()),
span: span.clone(),
};
let icon = Node::JsxSelfClosing(JsxSelfClosing {
name: "span".into(),
attrs: vec![attr("className", icon_class)],
span: span.clone(),
});
Node::JsxElement(JsxElement {
name: "a".into(),
attrs: vec![attr("aria-label", aria), attr("className", class), attr("href", &format!("#{}", id))],
children: vec![icon],
span,
})
}