Skip to main content

normalize_languages/
markdown.rs

1//! Markdown language support.
2
3use crate::{ContainerBody, Language, LanguageSymbols};
4use tree_sitter::Node;
5
6/// Markdown language support.
7pub struct Markdown;
8
9impl Language for Markdown {
10    fn name(&self) -> &'static str {
11        "Markdown"
12    }
13    fn extensions(&self) -> &'static [&'static str] {
14        &["md", "markdown"]
15    }
16    fn grammar_name(&self) -> &'static str {
17        "markdown"
18    }
19
20    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
21        Some(self)
22    }
23
24    // Markdown sections are modeled as `section` nodes in the grammar,
25    // each containing an atx_heading as the first child followed by content blocks.
26
27    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
28        // The section node itself contains the heading + content as children.
29        // Returning it here lets collect_symbols recurse into child sections.
30        Some(*node)
31    }
32    fn analyze_container_body(
33        &self,
34        section_node: &Node,
35        _content: &str,
36        _inner_indent: &str,
37    ) -> Option<ContainerBody> {
38        // Skip the first child (atx_heading); body is everything after it.
39        let mut cursor = section_node.walk();
40        let mut children = section_node.children(&mut cursor);
41        children.next(); // skip heading
42        let content_start = children
43            .next()
44            .map(|n| n.start_byte())
45            .unwrap_or(section_node.end_byte());
46        let content_end = section_node.end_byte();
47        Some(ContainerBody {
48            content_start,
49            content_end,
50            inner_indent: String::new(),
51            is_empty: content_start >= content_end,
52        })
53    }
54
55    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
56        // section → atx_heading → inline
57        let heading = node.child(0).filter(|c| c.kind() == "atx_heading")?;
58        let mut cursor = heading.walk();
59        let inline = heading
60            .children(&mut cursor)
61            .find(|c| c.kind() == "inline")?;
62        Some(content[inline.byte_range()].trim())
63    }
64}
65
66impl LanguageSymbols for Markdown {}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use crate::validate_unused_kinds_audit;
72
73    #[test]
74    fn unused_node_kinds_audit() {
75        #[rustfmt::skip]
76        let documented_unused: &[&str] = &[
77            "block_continuation", "block_quote", "block_quote_marker",
78            "fenced_code_block", "fenced_code_block_delimiter",
79            "html_block", "indented_code_block", "link_reference_definition",
80        ];
81        // Note: section and atx_heading are captured via markdown.tags.scm (`@definition.heading`).
82
83        validate_unused_kinds_audit(&Markdown, documented_unused)
84            .expect("Markdown unused node kinds audit failed");
85    }
86}