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(
72    builder: &mut GreenNodeBuilder,
73    inner_text: &str,
74    config: &ParserOptions,
75    suppress_footnote_refs: bool,
76) {
77    builder.start_node(SyntaxKind::STRIKEOUT.into());
78
79    // Opening marker
80    builder.start_node(SyntaxKind::STRIKEOUT_MARKER.into());
81    builder.token(SyntaxKind::STRIKEOUT_MARKER.into(), "~~");
82    builder.finish_node();
83
84    // Parse inner content recursively for nested inline elements
85    parse_inline_text(builder, inner_text, config, false, suppress_footnote_refs);
86
87    // Closing marker
88    builder.start_node(SyntaxKind::STRIKEOUT_MARKER.into());
89    builder.token(SyntaxKind::STRIKEOUT_MARKER.into(), "~~");
90    builder.finish_node();
91
92    builder.finish_node();
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    #[test]
100    fn test_simple_strikeout() {
101        assert_eq!(try_parse_strikeout("~~hello~~"), Some((9, "hello")));
102    }
103
104    #[test]
105    fn test_strikeout_with_spaces() {
106        assert_eq!(
107            try_parse_strikeout("~~hello world~~"),
108            Some((15, "hello world"))
109        );
110    }
111
112    #[test]
113    fn test_no_whitespace_inside_delimiters() {
114        // Content cannot start with whitespace
115        assert_eq!(try_parse_strikeout("~~ hello~~"), None);
116
117        // Content cannot end with whitespace
118        assert_eq!(try_parse_strikeout("~~hello ~~"), None);
119    }
120
121    #[test]
122    fn test_empty_content() {
123        assert_eq!(try_parse_strikeout("~~~~"), None);
124        assert_eq!(try_parse_strikeout("~~ ~~"), None);
125    }
126
127    #[test]
128    fn test_not_enough_tildes() {
129        assert_eq!(try_parse_strikeout("~hello~"), None);
130    }
131
132    #[test]
133    fn test_too_many_tildes() {
134        // Three tildes would be code fence
135        assert_eq!(try_parse_strikeout("~~~hello~~~"), None);
136    }
137
138    #[test]
139    fn test_no_closing() {
140        assert_eq!(try_parse_strikeout("~~hello"), None);
141        assert_eq!(try_parse_strikeout("~~hello world"), None);
142    }
143
144    #[test]
145    fn test_strikeout_with_other_content_after() {
146        assert_eq!(try_parse_strikeout("~~hello~~ world"), Some((9, "hello")));
147    }
148
149    #[test]
150    fn test_strikeout_in_middle() {
151        assert_eq!(try_parse_strikeout("~~text~~"), Some((8, "text")));
152    }
153}