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    let trimmed = line.trim();
15
16    // Must have at least 3 characters
17    if trimmed.len() < 3 {
18        return None;
19    }
20
21    // Determine which character is being used
22    let rule_char = trimmed.chars().next()?;
23    if !matches!(rule_char, '*' | '-' | '_') {
24        return None;
25    }
26
27    // Check that the line only contains the rule character and spaces
28    let mut count = 0;
29    for ch in trimmed.chars() {
30        match ch {
31            c if c == rule_char => count += 1,
32            ' ' | '\t' => continue,
33            _ => return None,
34        }
35    }
36
37    // Must have at least 3 of the rule character
38    if count >= 3 { Some(rule_char) } else { None }
39}
40
41/// Emit a horizontal rule node to the builder.
42pub(crate) fn emit_horizontal_rule(builder: &mut GreenNodeBuilder<'static>, line: &str) {
43    builder.start_node(SyntaxKind::HORIZONTAL_RULE.into());
44
45    // Strip trailing newline and emit the rule content as-is for losslessness.
46    let (line_without_newline, newline_str) = strip_newline(line);
47    builder.token(SyntaxKind::HORIZONTAL_RULE.into(), line_without_newline);
48
49    // Emit newline separately if present
50    if !newline_str.is_empty() {
51        builder.token(SyntaxKind::NEWLINE.into(), newline_str);
52    }
53
54    builder.finish_node();
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn test_asterisk_rule() {
63        assert_eq!(try_parse_horizontal_rule("***"), Some('*'));
64        assert_eq!(try_parse_horizontal_rule("* * *"), Some('*'));
65        assert_eq!(try_parse_horizontal_rule("*  *  *"), Some('*'));
66        assert_eq!(try_parse_horizontal_rule("****"), Some('*'));
67    }
68
69    #[test]
70    fn test_dash_rule() {
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_underscore_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_with_leading_trailing_spaces() {
85        assert_eq!(try_parse_horizontal_rule("  ***  "), Some('*'));
86        assert_eq!(try_parse_horizontal_rule("\t---\t"), Some('-'));
87    }
88
89    #[test]
90    fn test_too_few_characters() {
91        assert_eq!(try_parse_horizontal_rule("**"), None);
92        assert_eq!(try_parse_horizontal_rule("--"), None);
93        assert_eq!(try_parse_horizontal_rule("__"), None);
94    }
95
96    #[test]
97    fn test_mixed_characters() {
98        assert_eq!(try_parse_horizontal_rule("*-*"), None);
99        assert_eq!(try_parse_horizontal_rule("*_*"), None);
100    }
101
102    #[test]
103    fn test_with_other_content() {
104        assert_eq!(try_parse_horizontal_rule("*** hello"), None);
105        assert_eq!(try_parse_horizontal_rule("---a"), None);
106    }
107
108    #[test]
109    fn test_empty_line() {
110        assert_eq!(try_parse_horizontal_rule(""), None);
111        assert_eq!(try_parse_horizontal_rule("   "), None);
112    }
113}