Skip to main content

panache_parser/parser/inlines/
mark.rs

1//! Parsing for mark/highlight (==text==).
2//!
3//! This is a Pandoc non-default extension.
4//! Syntax: ==text== produces highlighted text.
5//!
6//! Rules (Pandoc parity):
7//! - Must start with exactly two `=` delimiters
8//! - Content cannot be empty or all whitespace
9//! - Content cannot start or end with whitespace
10//! - Closers are matched greedily at the first valid `==`
11
12use super::core::parse_inline_text;
13use crate::options::ParserOptions;
14use crate::syntax::SyntaxKind;
15use rowan::GreenNodeBuilder;
16
17/// Try to parse mark/highlight (==text==).
18/// Returns: (total_len, inner_content)
19pub fn try_parse_mark(text: &str) -> Option<(usize, &str)> {
20    let bytes = text.as_bytes();
21
22    // Must start with ==
23    if bytes.len() < 4 || bytes[0] != b'=' || bytes[1] != b'=' {
24        return None;
25    }
26
27    // Find the closing ==
28    let mut pos = 2;
29    let mut found_close = false;
30    while pos + 1 < bytes.len() {
31        if bytes[pos] == b'=' && bytes[pos + 1] == b'=' {
32            found_close = true;
33            break;
34        }
35        pos += 1;
36    }
37
38    if !found_close {
39        return None;
40    }
41
42    let content = &text[2..pos];
43
44    // Content cannot be empty or only whitespace
45    if content.trim().is_empty() {
46        return None;
47    }
48
49    // Pandoc parity: no whitespace immediately inside delimiters.
50    if content.starts_with(char::is_whitespace) || content.ends_with(char::is_whitespace) {
51        return None;
52    }
53
54    let total_len = pos + 2; // include closing ==
55    Some((total_len, content))
56}
57
58/// Emit a mark node with its content.
59pub fn emit_mark(builder: &mut GreenNodeBuilder, inner_text: &str, config: &ParserOptions) {
60    builder.start_node(SyntaxKind::MARK.into());
61
62    builder.start_node(SyntaxKind::MARK_MARKER.into());
63    builder.token(SyntaxKind::MARK_MARKER.into(), "==");
64    builder.finish_node();
65
66    parse_inline_text(builder, inner_text, config, false);
67
68    builder.start_node(SyntaxKind::MARK_MARKER.into());
69    builder.token(SyntaxKind::MARK_MARKER.into(), "==");
70    builder.finish_node();
71
72    builder.finish_node();
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn test_simple_mark() {
81        assert_eq!(try_parse_mark("==hi=="), Some((6, "hi")));
82    }
83
84    #[test]
85    fn test_mark_with_spaces_inside_content() {
86        assert_eq!(try_parse_mark("==hello world=="), Some((15, "hello world")));
87    }
88
89    #[test]
90    fn test_mark_requires_non_whitespace_content() {
91        assert_eq!(try_parse_mark("===="), None);
92        assert_eq!(try_parse_mark("==  =="), None);
93    }
94
95    #[test]
96    fn test_mark_disallows_whitespace_just_inside_delimiters() {
97        assert_eq!(try_parse_mark("== hi=="), None);
98        assert_eq!(try_parse_mark("==hi =="), None);
99        assert_eq!(try_parse_mark("== hi =="), None);
100    }
101
102    #[test]
103    fn test_mark_allows_neighboring_extra_equals_like_pandoc() {
104        assert_eq!(try_parse_mark("===a==="), Some((6, "=a")));
105        assert_eq!(try_parse_mark("==a==="), Some((5, "a")));
106        assert_eq!(try_parse_mark("====a=="), None);
107        assert_eq!(try_parse_mark("==a===="), Some((5, "a")));
108    }
109}