Skip to main content

markdown_that/plugins/cmark/block/
heading.rs

1//! ATX heading
2//!
3//! `# h1`, `## h2`, etc.
4//!
5//! <https://spec.commonmark.org/0.30/#atx-heading>
6use crate::parser::block::{BlockRule, BlockState};
7use crate::parser::inline::InlineRoot;
8use crate::{MarkdownThat, Node, NodeValue, Renderer};
9
10#[derive(Debug)]
11pub struct ATXHeading {
12    pub level: u8,
13}
14
15impl NodeValue for ATXHeading {
16    fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
17        static TAG: [&str; 6] = ["h1", "h2", "h3", "h4", "h5", "h6"];
18        debug_assert!(self.level >= 1 && self.level <= 6);
19
20        fmt.cr();
21        fmt.open(TAG[self.level as usize - 1], &node.attrs);
22        fmt.contents(&node.children);
23        fmt.close(TAG[self.level as usize - 1]);
24        fmt.cr();
25    }
26}
27
28pub fn add(md: &mut MarkdownThat) {
29    md.block.add_rule::<HeadingScanner>();
30}
31
32#[doc(hidden)]
33pub struct HeadingScanner;
34impl BlockRule for HeadingScanner {
35    fn run(state: &mut BlockState) -> Option<(Node, usize)> {
36        if state.line_indent(state.line) >= state.md.max_indent {
37            return None;
38        }
39
40        let line = state.get_line(state.line);
41        let Some('#') = line.chars().next() else {
42            return None;
43        };
44
45        let text_pos;
46
47        // count heading level
48        let mut level = 0u8;
49        let mut chars = line.char_indices();
50        loop {
51            match chars.next() {
52                Some((_, '#')) => {
53                    level += 1;
54                    if level > 6 {
55                        return None;
56                    }
57                }
58                Some((x, ' ' | '\t')) => {
59                    text_pos = x;
60                    break;
61                }
62                None => {
63                    text_pos = level as usize;
64                    break;
65                }
66                Some(_) => return None,
67            }
68        }
69
70        // Let's cut tails like '    ###  ' from the end of string
71
72        let mut chars_back = chars.rev().peekable();
73        while let Some((_, ' ' | '\t')) = chars_back.peek() {
74            chars_back.next();
75        }
76        while let Some((_, '#')) = chars_back.peek() {
77            chars_back.next();
78        }
79
80        let text_max = match chars_back.next() {
81            // ## foo ##
82            Some((last_pos, ' ' | '\t')) => last_pos + 1,
83            // ## foo##
84            Some(_) => line.len(),
85            // ## ## (already consumed the space)
86            None => text_pos,
87        };
88
89        let content = line[text_pos..text_max].to_owned();
90        let mapping = vec![(0, state.line_offsets[state.line].first_nonspace + text_pos)];
91
92        let mut node = Node::new(ATXHeading { level });
93        node.children
94            .push(Node::new(InlineRoot::new(content, mapping)));
95        Some((node, 1))
96    }
97}