marco_core/parser/inlines/
text_parser.rs1use super::shared::{opt_span, GrammarSpan};
7use crate::parser::ast::{Node, NodeKind};
8use nom::bytes::complete::take;
9use nom::IResult;
10use nom::Input;
11use nom::Parser;
12
13pub fn parse_text(input: GrammarSpan) -> IResult<GrammarSpan, Node> {
35 let text_fragment = input.fragment();
36
37 let line_end = text_fragment.find('\n').unwrap_or(text_fragment.len());
45 let line = &text_fragment[..line_end];
46
47 let next_autolink_literal =
51 super::gfm_autolink_literal_parser::find_next_autolink_literal_start(line)
52 .unwrap_or(text_fragment.len());
53
54 let next_emoji_shortcode =
57 super::marco_emoji_shortcode_parser::find_next_emoji_shortcode_start(line)
58 .unwrap_or(text_fragment.len());
59
60 let next_platform_mention =
62 super::marco_platform_mentions_parser::find_next_platform_mention_start(line)
63 .unwrap_or(text_fragment.len());
64
65 let next_special = text_fragment
71 .char_indices()
72 .find_map(|(idx, ch)| match ch {
73 '*' | '_' | '`' | '[' | '<' | '!' | '&' | '\n' | '\\' | '$' => Some(idx),
74 '^' | '~' | '˅' => Some(idx),
75 '=' => {
76 if text_fragment[idx..].starts_with("==") {
77 Some(idx)
78 } else {
79 None
80 }
81 }
82 '-' => {
83 if text_fragment[idx..].starts_with("--") {
84 Some(idx)
85 } else {
86 None
87 }
88 }
89 _ => None,
90 })
91 .unwrap_or(text_fragment.len());
92
93 if next_autolink_literal == 0 {
95 return Err(nom::Err::Error(nom::error::Error::new(
96 input,
97 nom::error::ErrorKind::Verify,
98 )));
99 }
100
101 if next_emoji_shortcode == 0 {
103 return Err(nom::Err::Error(nom::error::Error::new(
104 input,
105 nom::error::ErrorKind::Verify,
106 )));
107 }
108
109 if next_platform_mention == 0 {
111 return Err(nom::Err::Error(nom::error::Error::new(
112 input,
113 nom::error::ErrorKind::Verify,
114 )));
115 }
116
117 let next_special = next_special
118 .min(next_autolink_literal)
119 .min(next_emoji_shortcode)
120 .min(next_platform_mention);
121
122 if next_special == 0 {
123 return Err(nom::Err::Error(nom::error::Error::new(
125 input,
126 nom::error::ErrorKind::Verify,
127 )));
128 }
129
130 let mut text_len = next_special;
133 if next_special < text_fragment.len() && text_fragment[next_special..].starts_with('\n') {
134 let mut trailing_spaces = 0;
136 for ch in text_fragment[..next_special].chars().rev() {
137 if ch == ' ' {
138 trailing_spaces += 1;
139 } else {
140 break;
141 }
142 }
143
144 if trailing_spaces >= 2 {
147 text_len = next_special - trailing_spaces;
148 }
149 }
150
151 if text_len == 0 {
152 return Err(nom::Err::Error(nom::error::Error::new(
154 input,
155 nom::error::ErrorKind::Verify,
156 )));
157 }
158
159 let text_content = input.take(text_len);
161 let rest = input.take_from(text_len);
162
163 let span = opt_span(text_content);
164
165 let node = Node {
166 kind: NodeKind::Text(text_content.fragment().to_string()),
167 span,
168 children: Vec::new(),
169 };
170
171 Ok((rest, node))
172}
173
174pub fn parse_special_as_text(input: GrammarSpan) -> IResult<GrammarSpan, Node> {
187 let text_fragment = input.fragment();
188
189 if text_fragment.is_empty() {
190 return Err(nom::Err::Error(nom::error::Error::new(
191 input,
192 nom::error::ErrorKind::Eof,
193 )));
194 }
195
196 let char_len = if text_fragment.starts_with('`') {
199 text_fragment.chars().take_while(|&c| c == '`').count()
201 } else {
202 text_fragment
203 .chars()
204 .next()
205 .map(|c| c.len_utf8())
206 .unwrap_or(1)
207 };
208
209 let (rest, text_content) = take(char_len).parse(input)?;
210
211 let span = opt_span(text_content);
212
213 let node = Node {
214 kind: NodeKind::Text(text_content.fragment().to_string()),
215 span,
216 children: Vec::new(),
217 };
218
219 Ok((rest, node))
220}
221
222#[cfg(test)]
223mod tests {
224 use super::*;
225
226 #[test]
227 fn smoke_test_parse_text_basic() {
228 let input = GrammarSpan::new("Hello World*");
229 let result = parse_text(input);
230
231 assert!(result.is_ok(), "Failed to parse plain text");
232 let (rest, node) = result.unwrap();
233
234 assert_eq!(rest.fragment(), &"*");
235
236 if let NodeKind::Text(text) = &node.kind {
237 assert_eq!(text, "Hello World");
238 } else {
239 panic!("Expected Text node");
240 }
241 }
242
243 #[test]
244 fn smoke_test_parse_text_up_to_special() {
245 let input = GrammarSpan::new("text with `code`");
246 let result = parse_text(input);
247
248 assert!(result.is_ok());
249 let (rest, node) = result.unwrap();
250
251 assert_eq!(rest.fragment(), &"`code`");
252
253 if let NodeKind::Text(text) = &node.kind {
254 assert_eq!(text, "text with ");
255 }
256 }
257
258 #[test]
259 fn smoke_test_parse_text_trailing_spaces() {
260 let input = GrammarSpan::new("text \n");
261 let result = parse_text(input);
262
263 assert!(result.is_ok());
264 let (rest, node) = result.unwrap();
265
266 assert_eq!(rest.fragment(), &" \n");
268
269 if let NodeKind::Text(text) = &node.kind {
270 assert_eq!(text, "text");
271 }
272 }
273
274 #[test]
275 fn smoke_test_parse_text_starts_with_special() {
276 let input = GrammarSpan::new("*emphasis*");
277 let result = parse_text(input);
278
279 assert!(
280 result.is_err(),
281 "Should not parse text starting with special char"
282 );
283 }
284
285 #[test]
286 fn smoke_test_parse_special_as_text_asterisk() {
287 let input = GrammarSpan::new("* not emphasis");
288 let result = parse_special_as_text(input);
289
290 assert!(result.is_ok(), "Failed to parse special as text");
291 let (rest, node) = result.unwrap();
292
293 assert_eq!(rest.fragment(), &" not emphasis");
294
295 if let NodeKind::Text(text) = &node.kind {
296 assert_eq!(text, "*");
297 }
298 }
299
300 #[test]
301 fn smoke_test_parse_special_as_text_backticks() {
302 let input = GrammarSpan::new("```not code");
303 let result = parse_special_as_text(input);
304
305 assert!(result.is_ok());
306 let (rest, node) = result.unwrap();
307
308 assert_eq!(rest.fragment(), &"not code");
309
310 if let NodeKind::Text(text) = &node.kind {
311 assert_eq!(text, "```");
312 }
313 }
314
315 #[test]
316 fn smoke_test_parse_text_position() {
317 let input = GrammarSpan::new("Hello*");
318 let result = parse_text(input);
319
320 assert!(result.is_ok());
321 let (_, node) = result.unwrap();
322
323 assert!(node.span.is_some(), "Text should have position info");
324
325 let span = node.span.unwrap();
326 assert_eq!(span.start.offset, 0);
327 assert_eq!(span.end.offset, 5); }
329}