Skip to main content

panache_parser/parser/inlines/
strikeout.rs

1//! Parsing for strikeout (~~strikethrough~~)
2//!
3//! This is a Pandoc extension that's also part of GitHub Flavored Markdown.
4//! Syntax: ~~text~~ produces strikethrough text.
5//!
6//! Rules:
7//! - Must have exactly 2 tildes on each side
8//! - Content cannot be empty
9//! - Tildes cannot have whitespace immediately inside
10
11use super::core::parse_inline_text;
12use crate::options::ParserOptions;
13use crate::syntax::SyntaxKind;
14use rowan::GreenNodeBuilder;
15
16/// Try to parse strikeout (~~text~~)
17/// Returns: (total_len, inner_content)
18pub fn try_parse_strikeout(text: &str) -> Option<(usize, &str)> {
19    let bytes = text.as_bytes();
20
21    // Must start with ~~
22    if bytes.len() < 4 || bytes[0] != b'~' || bytes[1] != b'~' {
23        return None;
24    }
25
26    // Check that it's not more than 2 tildes at the start (~~~ would be a code fence)
27    if bytes.get(2) == Some(&b'~') {
28        return None;
29    }
30
31    // Find the closing ~~
32    let mut pos = 2;
33    let mut found_close = false;
34
35    while pos + 1 < bytes.len() {
36        if bytes[pos] == b'~' && bytes[pos + 1] == b'~' {
37            // Check that there's no third tilde (to avoid ~~text~~~)
38            if pos + 2 < bytes.len() && bytes[pos + 2] == b'~' {
39                pos += 1;
40                continue;
41            }
42
43            found_close = true;
44            break;
45        }
46        pos += 1;
47    }
48
49    if !found_close {
50        return None;
51    }
52
53    // Extract content between the delimiters
54    let content = &text[2..pos];
55
56    // Content cannot be empty or only whitespace
57    if content.trim().is_empty() {
58        return None;
59    }
60
61    // Content cannot start or end with whitespace (CommonMark-style rule)
62    if content.starts_with(char::is_whitespace) || content.ends_with(char::is_whitespace) {
63        return None;
64    }
65
66    let total_len = pos + 2; // Include closing ~~
67    Some((total_len, content))
68}
69
70/// Emit a strikeout node with its content
71pub fn emit_strikeout(builder: &mut GreenNodeBuilder, inner_text: &str, config: &ParserOptions) {
72    builder.start_node(SyntaxKind::STRIKEOUT.into());
73
74    // Opening marker
75    builder.start_node(SyntaxKind::STRIKEOUT_MARKER.into());
76    builder.token(SyntaxKind::STRIKEOUT_MARKER.into(), "~~");
77    builder.finish_node();
78
79    // Parse inner content recursively for nested inline elements
80    parse_inline_text(builder, inner_text, config, false);
81
82    // Closing marker
83    builder.start_node(SyntaxKind::STRIKEOUT_MARKER.into());
84    builder.token(SyntaxKind::STRIKEOUT_MARKER.into(), "~~");
85    builder.finish_node();
86
87    builder.finish_node();
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn test_simple_strikeout() {
96        assert_eq!(try_parse_strikeout("~~hello~~"), Some((9, "hello")));
97    }
98
99    #[test]
100    fn test_strikeout_with_spaces() {
101        assert_eq!(
102            try_parse_strikeout("~~hello world~~"),
103            Some((15, "hello world"))
104        );
105    }
106
107    #[test]
108    fn test_no_whitespace_inside_delimiters() {
109        // Content cannot start with whitespace
110        assert_eq!(try_parse_strikeout("~~ hello~~"), None);
111
112        // Content cannot end with whitespace
113        assert_eq!(try_parse_strikeout("~~hello ~~"), None);
114    }
115
116    #[test]
117    fn test_empty_content() {
118        assert_eq!(try_parse_strikeout("~~~~"), None);
119        assert_eq!(try_parse_strikeout("~~ ~~"), None);
120    }
121
122    #[test]
123    fn test_not_enough_tildes() {
124        assert_eq!(try_parse_strikeout("~hello~"), None);
125    }
126
127    #[test]
128    fn test_too_many_tildes() {
129        // Three tildes would be code fence
130        assert_eq!(try_parse_strikeout("~~~hello~~~"), None);
131    }
132
133    #[test]
134    fn test_no_closing() {
135        assert_eq!(try_parse_strikeout("~~hello"), None);
136        assert_eq!(try_parse_strikeout("~~hello world"), None);
137    }
138
139    #[test]
140    fn test_strikeout_with_other_content_after() {
141        assert_eq!(try_parse_strikeout("~~hello~~ world"), Some((9, "hello")));
142    }
143
144    #[test]
145    fn test_strikeout_in_middle() {
146        assert_eq!(try_parse_strikeout("~~text~~"), Some((8, "text")));
147    }
148}