markdown_it/plugins/cmark/block/
lheading.rs

1//! Setext headings
2//!
3//! Paragraph underlined with `===` or `---`.
4//!
5//! <https://spec.commonmark.org/0.30/#setext-headings>
6use crate::parser::block::{BlockRule, BlockState};
7use crate::parser::inline::InlineRoot;
8use crate::plugins::cmark::block::paragraph::ParagraphScanner;
9use crate::{MarkdownIt, Node, NodeValue, Renderer};
10
11#[derive(Debug)]
12pub struct SetextHeader {
13    pub level: u8,
14    pub marker: char,
15}
16
17impl NodeValue for SetextHeader {
18    fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
19        static TAG : [&str; 2] = [ "h1", "h2" ];
20        debug_assert!(self.level >= 1 && self.level <= 2);
21
22        fmt.cr();
23        fmt.open(TAG[self.level as usize - 1], &node.attrs);
24        fmt.contents(&node.children);
25        fmt.close(TAG[self.level as usize - 1]);
26        fmt.cr();
27    }
28}
29
30pub fn add(md: &mut MarkdownIt) {
31    md.block.add_rule::<LHeadingScanner>()
32        .before::<ParagraphScanner>()
33        .after_all();
34}
35
36#[doc(hidden)]
37pub struct LHeadingScanner;
38impl BlockRule for LHeadingScanner {
39    fn check(_: &mut BlockState) -> Option<()> {
40        None // can't interrupt any tags
41    }
42
43    fn run(state: &mut BlockState) -> Option<(Node, usize)> {
44
45        if state.line_indent(state.line) >= state.md.max_indent { return None; }
46
47        let start_line = state.line;
48        let mut next_line = start_line;
49        let mut level = 0;
50
51        'outer: loop {
52            next_line += 1;
53
54            if next_line >= state.line_max || state.is_empty(next_line) { break; }
55
56            // this may be a code block normally, but after paragraph
57            // it's considered a lazy continuation regardless of what's there
58            if state.line_indent(next_line) >= state.md.max_indent { continue; }
59
60            //
61            // Check for underline in setext header
62            //
63            if state.line_indent(next_line) >= 0 {
64                let mut chars = state.get_line(next_line).chars().peekable();
65                if let Some(marker @ ('-' | '=')) = chars.next() {
66                    while Some(&marker) == chars.peek() { chars.next(); }
67                    while let Some(' ' | '\t') = chars.peek() { chars.next(); }
68                    if chars.next().is_none() {
69                        level = if marker == '=' { 1 } else { 2 };
70                        break 'outer;
71                    }
72                }
73            }
74
75            // quirk for blockquotes, this line should already be checked by that rule
76            if state.line_offsets[next_line].indent_nonspace < 0 { continue; }
77
78            // Some tags can terminate paragraph without empty line.
79            let old_state_line = state.line;
80            state.line = next_line;
81            if state.test_rules_at_line() {
82                state.line = old_state_line;
83                break 'outer;
84            }
85            state.line = old_state_line;
86        }
87
88
89        if level == 0 {
90            // Didn't find valid underline
91            return None;
92        }
93
94        let (content, mapping) = state.get_lines(start_line, next_line, state.blk_indent, false);
95
96        let mut node = Node::new(SetextHeader {
97            level,
98            marker: if level == 2 { '-' } else { '=' }
99        });
100        node.children.push(Node::new(InlineRoot::new(content, mapping)));
101
102        Some((node, next_line + 1 - start_line))
103    }
104}