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(
60    builder: &mut GreenNodeBuilder,
61    inner_text: &str,
62    config: &ParserOptions,
63    suppress_footnote_refs: bool,
64) {
65    builder.start_node(SyntaxKind::MARK.into());
66
67    builder.start_node(SyntaxKind::MARK_MARKER.into());
68    builder.token(SyntaxKind::MARK_MARKER.into(), "==");
69    builder.finish_node();
70
71    parse_inline_text(builder, inner_text, config, false, suppress_footnote_refs);
72
73    builder.start_node(SyntaxKind::MARK_MARKER.into());
74    builder.token(SyntaxKind::MARK_MARKER.into(), "==");
75    builder.finish_node();
76
77    builder.finish_node();
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn test_simple_mark() {
86        assert_eq!(try_parse_mark("==hi=="), Some((6, "hi")));
87    }
88
89    #[test]
90    fn test_mark_with_spaces_inside_content() {
91        assert_eq!(try_parse_mark("==hello world=="), Some((15, "hello world")));
92    }
93
94    #[test]
95    fn test_mark_requires_non_whitespace_content() {
96        assert_eq!(try_parse_mark("===="), None);
97        assert_eq!(try_parse_mark("==  =="), None);
98    }
99
100    #[test]
101    fn test_mark_disallows_whitespace_just_inside_delimiters() {
102        assert_eq!(try_parse_mark("== hi=="), None);
103        assert_eq!(try_parse_mark("==hi =="), None);
104        assert_eq!(try_parse_mark("== hi =="), None);
105    }
106
107    #[test]
108    fn test_mark_allows_neighboring_extra_equals_like_pandoc() {
109        assert_eq!(try_parse_mark("===a==="), Some((6, "=a")));
110        assert_eq!(try_parse_mark("==a==="), Some((5, "a")));
111        assert_eq!(try_parse_mark("====a=="), None);
112        assert_eq!(try_parse_mark("==a===="), Some((5, "a")));
113    }
114}