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