Skip to main content

panache_parser/parser/blocks/
horizontal_rules.rs

1//! Horizontal rule parsing utilities.
2
3use crate::syntax::SyntaxKind;
4use rowan::GreenNodeBuilder;
5
6use crate::parser::utils::helpers::strip_newline;
7
8/// Try to parse a horizontal rule from a line.
9/// Returns true if this line is a valid horizontal rule.
10///
11/// A horizontal rule is 3 or more `*`, `-`, or `_` characters,
12/// optionally separated by spaces.
13pub(crate) fn try_parse_horizontal_rule(line: &str) -> Option<char> {
14    // Per CommonMark ยง4.1, a thematic break may be indented up to three spaces.
15    // Four or more spaces of indentation makes the line an indented code block
16    // (or, inside a paragraph, plain text continuation), not a thematic break.
17    let leading = line.bytes().take_while(|b| *b == b' ').count();
18    if leading >= 4 {
19        return None;
20    }
21    let trimmed = line.trim();
22
23    // Must have at least 3 characters
24    if trimmed.len() < 3 {
25        return None;
26    }
27
28    // Determine which character is being used
29    let rule_char = trimmed.chars().next()?;
30    if !matches!(rule_char, '*' | '-' | '_') {
31        return None;
32    }
33
34    // Check that the line only contains the rule character and spaces
35    let mut count = 0;
36    for ch in trimmed.chars() {
37        match ch {
38            c if c == rule_char => count += 1,
39            ' ' | '\t' => continue,
40            _ => return None,
41        }
42    }
43
44    // Must have at least 3 of the rule character
45    if count >= 3 { Some(rule_char) } else { None }
46}
47
48/// Emit a horizontal rule node to the builder.
49pub(crate) fn emit_horizontal_rule(builder: &mut GreenNodeBuilder<'static>, line: &str) {
50    builder.start_node(SyntaxKind::HORIZONTAL_RULE.into());
51
52    // Strip trailing newline and emit the rule content as-is for losslessness.
53    let (line_without_newline, newline_str) = strip_newline(line);
54    builder.token(SyntaxKind::HORIZONTAL_RULE.into(), line_without_newline);
55
56    // Emit newline separately if present
57    if !newline_str.is_empty() {
58        builder.token(SyntaxKind::NEWLINE.into(), newline_str);
59    }
60
61    builder.finish_node();
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn test_asterisk_rule() {
70        assert_eq!(try_parse_horizontal_rule("***"), Some('*'));
71        assert_eq!(try_parse_horizontal_rule("* * *"), Some('*'));
72        assert_eq!(try_parse_horizontal_rule("*  *  *"), Some('*'));
73        assert_eq!(try_parse_horizontal_rule("****"), Some('*'));
74    }
75
76    #[test]
77    fn test_dash_rule() {
78        assert_eq!(try_parse_horizontal_rule("---"), Some('-'));
79        assert_eq!(try_parse_horizontal_rule("- - -"), Some('-'));
80        assert_eq!(try_parse_horizontal_rule("---------------"), Some('-'));
81    }
82
83    #[test]
84    fn test_underscore_rule() {
85        assert_eq!(try_parse_horizontal_rule("___"), Some('_'));
86        assert_eq!(try_parse_horizontal_rule("_ _ _"), Some('_'));
87        assert_eq!(try_parse_horizontal_rule("_____"), Some('_'));
88    }
89
90    #[test]
91    fn test_with_leading_trailing_spaces() {
92        assert_eq!(try_parse_horizontal_rule("  ***  "), Some('*'));
93        assert_eq!(try_parse_horizontal_rule("\t---\t"), Some('-'));
94    }
95
96    #[test]
97    fn test_too_few_characters() {
98        assert_eq!(try_parse_horizontal_rule("**"), None);
99        assert_eq!(try_parse_horizontal_rule("--"), None);
100        assert_eq!(try_parse_horizontal_rule("__"), None);
101    }
102
103    #[test]
104    fn test_mixed_characters() {
105        assert_eq!(try_parse_horizontal_rule("*-*"), None);
106        assert_eq!(try_parse_horizontal_rule("*_*"), None);
107    }
108
109    #[test]
110    fn test_with_other_content() {
111        assert_eq!(try_parse_horizontal_rule("*** hello"), None);
112        assert_eq!(try_parse_horizontal_rule("---a"), None);
113    }
114
115    #[test]
116    fn test_empty_line() {
117        assert_eq!(try_parse_horizontal_rule(""), None);
118        assert_eq!(try_parse_horizontal_rule("   "), None);
119    }
120}