panache_parser/parser/inlines/
subscript.rs1use super::core::parse_inline_text;
13use crate::options::ParserOptions;
14use crate::syntax::SyntaxKind;
15use rowan::GreenNodeBuilder;
16
17pub fn try_parse_subscript(text: &str) -> Option<(usize, &str)> {
20 let bytes = text.as_bytes();
21
22 if bytes.is_empty() || bytes[0] != b'~' {
24 return None;
25 }
26
27 if bytes.len() > 1 && bytes[1] == b'~' {
35 return Some((2, ""));
36 }
37
38 if bytes.len() > 1 && bytes[1].is_ascii_whitespace() {
40 return None;
41 }
42
43 let mut pos = 1;
45 let mut found_close = false;
46
47 while pos < bytes.len() {
48 if bytes[pos] == b'~' {
49 if pos + 1 < bytes.len() && bytes[pos + 1] == b'~' {
51 return None;
52 }
53 found_close = true;
54 break;
55 }
56 pos += 1;
57 }
58
59 if !found_close {
60 return None;
61 }
62
63 let content = &text[1..pos];
65
66 if content.trim().is_empty() {
68 return None;
69 }
70
71 if content.ends_with(char::is_whitespace) {
73 return None;
74 }
75
76 if contains_unescaped_whitespace(content) {
81 return None;
82 }
83
84 let total_len = pos + 1; Some((total_len, content))
86}
87
88fn contains_unescaped_whitespace(content: &str) -> bool {
89 let bytes = content.as_bytes();
90 let mut i = 0;
91 while i < bytes.len() {
92 let b = bytes[i];
93 if b == b'\\' && i + 1 < bytes.len() {
94 i += 2;
95 continue;
96 }
97 if (b as char).is_whitespace() {
98 return true;
99 }
100 i += 1;
101 }
102 false
103}
104
105pub fn emit_subscript(builder: &mut GreenNodeBuilder, inner_text: &str, config: &ParserOptions) {
107 builder.start_node(SyntaxKind::SUBSCRIPT.into());
108
109 builder.start_node(SyntaxKind::SUBSCRIPT_MARKER.into());
111 builder.token(SyntaxKind::SUBSCRIPT_MARKER.into(), "~");
112 builder.finish_node();
113
114 parse_inline_text(builder, inner_text, config, false);
116
117 builder.start_node(SyntaxKind::SUBSCRIPT_MARKER.into());
119 builder.token(SyntaxKind::SUBSCRIPT_MARKER.into(), "~");
120 builder.finish_node();
121
122 builder.finish_node();
123}
124
125#[cfg(test)]
126mod tests {
127 use super::*;
128
129 #[test]
130 fn test_simple_subscript() {
131 assert_eq!(try_parse_subscript("~2~"), Some((3, "2")));
132 assert_eq!(try_parse_subscript("~n~"), Some((3, "n")));
133 }
134
135 #[test]
136 fn test_subscript_with_multiple_chars() {
137 assert_eq!(try_parse_subscript("~text~"), Some((6, "text")));
138 assert_eq!(try_parse_subscript("~i+1~"), Some((5, "i+1")));
139 }
140
141 #[test]
142 fn test_no_whitespace_inside_delimiters() {
143 assert_eq!(try_parse_subscript("~ text~"), None);
145
146 assert_eq!(try_parse_subscript("~text ~"), None);
148 }
149
150 #[test]
151 fn test_empty_content() {
152 assert_eq!(try_parse_subscript("~~"), Some((2, "")));
156 assert_eq!(try_parse_subscript("~ ~"), None);
157 }
158
159 #[test]
160 fn test_no_closing() {
161 assert_eq!(try_parse_subscript("~text"), None);
162 assert_eq!(try_parse_subscript("~hello world"), None);
163 }
164
165 #[test]
166 fn test_double_tilde_unclosed_is_empty_subscript() {
167 assert_eq!(try_parse_subscript("~~text~~"), Some((2, "")));
176 assert_eq!(try_parse_subscript("~~unclosed"), Some((2, "")));
177 }
178
179 #[test]
180 fn test_subscript_with_other_content_after() {
181 assert_eq!(try_parse_subscript("~2~ text"), Some((3, "2")));
182 assert_eq!(try_parse_subscript("~n~ of sequence"), Some((3, "n")));
183 }
184
185 #[test]
186 fn test_internal_whitespace_rejected() {
187 assert_eq!(try_parse_subscript("~some text~"), None);
190 assert_eq!(
191 try_parse_subscript("~some\\ text~"),
192 Some((12, "some\\ text"))
193 );
194 }
195
196 #[test]
197 fn test_single_char() {
198 assert_eq!(try_parse_subscript("~a~"), Some((3, "a")));
199 }
200
201 #[test]
202 fn test_subscript_before_strikeout_marker() {
203 assert_eq!(try_parse_subscript("~x~ ~"), Some((3, "x")));
205 }
206}