1use crate::options::ParserOptions;
24use crate::syntax::SyntaxKind;
25use rowan::GreenNodeBuilder;
26
27use super::bookdown::{
29 try_parse_bookdown_definition, try_parse_bookdown_reference, try_parse_bookdown_text_reference,
30};
31use super::bracketed_spans::{emit_bracketed_span, try_parse_bracketed_span};
32use super::citations::{
33 emit_bare_citation, emit_bracketed_citation, try_parse_bare_citation,
34 try_parse_bracketed_citation,
35};
36use super::code_spans::{emit_code_span, try_parse_code_span};
37use super::emoji::{emit_emoji, try_parse_emoji};
38use super::escapes::{EscapeType, emit_escape, try_parse_escape};
39use super::inline_executable::{emit_inline_executable, try_parse_inline_executable};
40use super::inline_footnotes::{
41 emit_footnote_reference, emit_inline_footnote, try_parse_footnote_reference,
42 try_parse_inline_footnote,
43};
44use super::latex::{parse_latex_command, try_parse_latex_command};
45use super::links::{
46 emit_autolink, emit_bare_uri_link, emit_inline_image, emit_inline_link, emit_reference_image,
47 emit_reference_link, try_parse_autolink, try_parse_bare_uri, try_parse_inline_image,
48 try_parse_inline_link, try_parse_reference_image, try_parse_reference_link,
49};
50use super::mark::{emit_mark, try_parse_mark};
51use super::math::{
52 emit_display_math, emit_display_math_environment, emit_double_backslash_display_math,
53 emit_double_backslash_inline_math, emit_gfm_inline_math, emit_inline_math,
54 emit_single_backslash_display_math, emit_single_backslash_inline_math, try_parse_display_math,
55 try_parse_double_backslash_display_math, try_parse_double_backslash_inline_math,
56 try_parse_gfm_inline_math, try_parse_inline_math, try_parse_math_environment,
57 try_parse_single_backslash_display_math, try_parse_single_backslash_inline_math,
58};
59use super::native_spans::{emit_native_span, try_parse_native_span};
60use super::raw_inline::is_raw_inline;
61use super::shortcodes::{emit_shortcode, try_parse_shortcode};
62use super::strikeout::{emit_strikeout, try_parse_strikeout};
63use super::subscript::{emit_subscript, try_parse_subscript};
64use super::superscript::{emit_superscript, try_parse_superscript};
65
66pub fn parse_inline_text_recursive(
83 builder: &mut GreenNodeBuilder,
84 text: &str,
85 config: &ParserOptions,
86) {
87 log::trace!(
88 "Recursive inline parsing: {:?} ({} bytes)",
89 &text[..text.len().min(40)],
90 text.len()
91 );
92
93 parse_inline_range(text, 0, text.len(), config, builder);
94
95 log::trace!("Recursive inline parsing complete");
96}
97
98pub fn parse_inline_text(
104 builder: &mut GreenNodeBuilder,
105 text: &str,
106 config: &ParserOptions,
107 _allow_reference_links: bool,
108) {
109 log::trace!(
110 "Parsing inline text (recursive): {:?} ({} bytes)",
111 &text[..text.len().min(40)],
112 text.len()
113 );
114
115 parse_inline_text_recursive(builder, text, config);
117}
118
119pub fn try_parse_emphasis(
144 text: &str,
145 pos: usize,
146 end: usize,
147 config: &ParserOptions,
148 builder: &mut GreenNodeBuilder,
149) -> Option<(usize, usize)> {
150 let bytes = text.as_bytes();
151
152 if pos >= bytes.len() {
153 return None;
154 }
155
156 let delim_char = bytes[pos] as char;
157 if delim_char != '*' && delim_char != '_' {
158 return None;
159 }
160
161 let mut count = 0;
163 while pos + count < bytes.len() && bytes[pos + count] == bytes[pos] {
164 count += 1;
165 }
166
167 let after_pos = pos + count;
168
169 log::trace!(
170 "try_parse_emphasis: '{}' x {} at pos {}",
171 delim_char,
172 count,
173 pos
174 );
175
176 if after_pos < text.len()
178 && let Some(next_char) = text[after_pos..].chars().next()
179 && next_char.is_whitespace()
180 {
181 log::trace!("Delimiter followed by whitespace, treating as literal");
182 return None;
183 }
184
185 if delim_char == '_'
188 && pos > 0
189 && let Some(prev_char) = text[..pos].chars().last()
190 && prev_char.is_alphanumeric()
191 {
192 log::trace!("Underscore preceded by alphanumeric, can't open (intraword)");
193 return None;
194 }
195
196 let result = match count {
198 1 => try_parse_one(text, pos, delim_char, end, config, builder),
199 2 => try_parse_two(text, pos, delim_char, end, config, builder),
200 3 => try_parse_three(text, pos, delim_char, end, config, builder),
201 _ => {
202 log::trace!("{} delimiters (4+), treating as literal", count);
204 None
205 }
206 };
207
208 result.map(|consumed| (consumed, count))
211}
212
213fn try_parse_emphasis_nested(
222 text: &str,
223 pos: usize,
224 end: usize,
225 config: &ParserOptions,
226 builder: &mut GreenNodeBuilder,
227) -> Option<(usize, usize)> {
228 let bytes = text.as_bytes();
229
230 if pos >= bytes.len() {
231 return None;
232 }
233
234 let delim_char = bytes[pos] as char;
235 if delim_char != '*' && delim_char != '_' {
236 return None;
237 }
238
239 let mut count = 0;
241 while pos + count < bytes.len() && bytes[pos + count] == bytes[pos] {
242 count += 1;
243 }
244
245 log::trace!(
246 "try_parse_emphasis_nested: '{}' x {} at pos {}",
247 delim_char,
248 count,
249 pos
250 );
251
252 if delim_char == '_'
255 && pos > 0
256 && let Some(prev_char) = text[..pos].chars().last()
257 && prev_char.is_alphanumeric()
258 {
259 log::trace!("Underscore preceded by alphanumeric, can't open (intraword)");
260 return None;
261 }
262
263 let result = match count {
269 1 => try_parse_one(text, pos, delim_char, end, config, builder),
270 2 => try_parse_two(text, pos, delim_char, end, config, builder),
271 3 => try_parse_three(text, pos, delim_char, end, config, builder),
272 _ => {
273 log::trace!("{} delimiters (4+), treating as literal", count);
275 None
276 }
277 };
278
279 result.map(|consumed| (consumed, count))
280}
281
282fn try_parse_three(
287 text: &str,
288 pos: usize,
289 delim_char: char,
290 end: usize,
291 config: &ParserOptions,
292 builder: &mut GreenNodeBuilder,
293) -> Option<usize> {
294 let content_start = pos + 3;
295 let one = delim_char.to_string();
296 let two = one.repeat(2);
297
298 log::trace!("try_parse_three: '{}' x 3 at pos {}", delim_char, pos);
299
300 let mut search_pos = content_start;
304
305 loop {
306 let closer_start = match find_first_potential_ender(text, search_pos, delim_char, end) {
308 Some(p) => p,
309 None => {
310 log::trace!("No potential ender found for ***");
311 return None;
312 }
313 };
314
315 log::trace!("Potential ender at pos {}", closer_start);
316
317 let bytes = text.as_bytes();
319 let mut closer_count = 0;
320 let mut check_pos = closer_start;
321 while check_pos < bytes.len() && bytes[check_pos] == delim_char as u8 {
322 closer_count += 1;
323 check_pos += 1;
324 }
325
326 log::trace!(
327 "Found {} x {} at pos {}",
328 delim_char,
329 closer_count,
330 closer_start
331 );
332
333 if closer_count >= 3 && is_valid_ender(text, closer_start, delim_char, 3) {
337 log::trace!("Matched *** closer, emitting Strong[Emph[content]]");
338
339 builder.start_node(SyntaxKind::STRONG.into());
340 builder.token(SyntaxKind::STRONG_MARKER.into(), &two);
341
342 builder.start_node(SyntaxKind::EMPHASIS.into());
343 builder.token(SyntaxKind::EMPHASIS_MARKER.into(), &one);
344 parse_inline_range_nested(text, content_start, closer_start, config, builder);
345 builder.token(SyntaxKind::EMPHASIS_MARKER.into(), &one);
346 builder.finish_node(); builder.token(SyntaxKind::STRONG_MARKER.into(), &two);
349 builder.finish_node(); return Some(closer_start + 3 - pos);
352 }
353
354 if closer_count >= 2 && is_valid_ender(text, closer_start, delim_char, 2) {
356 log::trace!("Matched ** closer, wrapping as Strong and continuing with one");
357
358 let continue_pos = closer_start + 2;
359
360 if let Some(final_closer_pos) =
361 parse_until_closer_with_nested_two(text, continue_pos, delim_char, 1, end, config)
362 {
363 log::trace!(
364 "Found * closer at pos {}, emitting Emph[Strong[...], ...]",
365 final_closer_pos
366 );
367
368 builder.start_node(SyntaxKind::EMPHASIS.into());
369 builder.token(SyntaxKind::EMPHASIS_MARKER.into(), &one);
370
371 builder.start_node(SyntaxKind::STRONG.into());
372 builder.token(SyntaxKind::STRONG_MARKER.into(), &two);
373 parse_inline_range_nested(text, content_start, closer_start, config, builder);
374 builder.token(SyntaxKind::STRONG_MARKER.into(), &two);
375 builder.finish_node(); parse_inline_range_nested(text, continue_pos, final_closer_pos, config, builder);
379
380 builder.token(SyntaxKind::EMPHASIS_MARKER.into(), &one);
381 builder.finish_node(); return Some(final_closer_pos + 1 - pos);
384 }
385
386 log::trace!("No * closer found after **, emitting * + STRONG");
388 builder.token(SyntaxKind::TEXT.into(), &one);
389
390 builder.start_node(SyntaxKind::STRONG.into());
391 builder.token(SyntaxKind::STRONG_MARKER.into(), &two);
392 parse_inline_range_nested(text, content_start, closer_start, config, builder);
393 builder.token(SyntaxKind::STRONG_MARKER.into(), &two);
394 builder.finish_node(); return Some(closer_start + 2 - pos);
397 }
398
399 if closer_count >= 1 && is_valid_ender(text, closer_start, delim_char, 1) {
401 log::trace!("Matched * closer, wrapping as Emph and continuing with two");
402
403 let continue_pos = closer_start + 1;
404
405 if let Some(final_closer_pos) =
406 parse_until_closer_with_nested_one(text, continue_pos, delim_char, 2, end, config)
407 {
408 log::trace!(
409 "Found ** closer at pos {}, emitting Strong[Emph[...], ...]",
410 final_closer_pos
411 );
412
413 builder.start_node(SyntaxKind::STRONG.into());
414 builder.token(SyntaxKind::STRONG_MARKER.into(), &two);
415
416 builder.start_node(SyntaxKind::EMPHASIS.into());
417 builder.token(SyntaxKind::EMPHASIS_MARKER.into(), &one);
418 parse_inline_range_nested(text, content_start, closer_start, config, builder);
419 builder.token(SyntaxKind::EMPHASIS_MARKER.into(), &one);
420 builder.finish_node(); parse_inline_range_nested(text, continue_pos, final_closer_pos, config, builder);
423
424 builder.token(SyntaxKind::STRONG_MARKER.into(), &two);
425 builder.finish_node(); return Some(final_closer_pos + 2 - pos);
428 }
429
430 log::trace!("No ** closer found after *, emitting ** + EMPH");
432 builder.token(SyntaxKind::TEXT.into(), &two);
433
434 builder.start_node(SyntaxKind::EMPHASIS.into());
435 builder.token(SyntaxKind::EMPHASIS_MARKER.into(), &one);
436 parse_inline_range_nested(text, content_start, closer_start, config, builder);
437 builder.token(SyntaxKind::EMPHASIS_MARKER.into(), &one);
438 builder.finish_node(); return Some(closer_start + 1 - pos);
441 }
442
443 log::trace!(
445 "No valid ender at pos {}, continuing search from {}",
446 closer_start,
447 closer_start + closer_count
448 );
449 search_pos = closer_start + closer_count;
450 }
451}
452
453fn find_first_potential_ender(
457 text: &str,
458 start: usize,
459 delim_char: char,
460 end: usize,
461) -> Option<usize> {
462 let bytes = text.as_bytes();
463 let mut pos = start;
464
465 while pos < end.min(text.len()) {
466 if bytes[pos] == delim_char as u8 {
468 let is_escaped = {
470 let mut backslash_count = 0;
471 let mut check_pos = pos;
472 while check_pos > 0 && bytes[check_pos - 1] == b'\\' {
473 backslash_count += 1;
474 check_pos -= 1;
475 }
476 backslash_count % 2 == 1
477 };
478
479 if !is_escaped {
480 return Some(pos);
482 }
483 }
484
485 pos += 1;
486 }
487
488 None
489}
490
491fn is_valid_ender(text: &str, pos: usize, delim_char: char, delim_count: usize) -> bool {
494 let bytes = text.as_bytes();
495
496 if pos + delim_count > text.len() {
498 return false;
499 }
500
501 for i in 0..delim_count {
502 if bytes[pos + i] != delim_char as u8 {
503 return false;
504 }
505 }
506
507 if pos > 0 && bytes[pos - 1] == delim_char as u8 {
509 return false;
510 }
511
512 let after_pos = pos + delim_count;
514 if after_pos < bytes.len() && bytes[after_pos] == delim_char as u8 {
515 return false;
516 }
517
518 if delim_char == '_' {
521 if pos > 0
522 && let Some(prev_char) = text[..pos].chars().last()
523 && prev_char.is_whitespace()
524 {
525 return false;
526 }
527
528 if after_pos < text.len()
530 && let Some(next_char) = text[after_pos..].chars().next()
531 && next_char.is_alphanumeric()
532 {
533 return false;
534 }
535 }
536
537 true
538}
539
540fn try_parse_two(
545 text: &str,
546 pos: usize,
547 delim_char: char,
548 end: usize,
549 config: &ParserOptions,
550 builder: &mut GreenNodeBuilder,
551) -> Option<usize> {
552 let content_start = pos + 2;
553
554 log::trace!("try_parse_two: '{}' x 2 at pos {}", delim_char, pos);
555
556 if let Some(closer_pos) =
558 parse_until_closer_with_nested_one(text, content_start, delim_char, 2, end, config)
559 {
560 log::trace!("Found ** closer at pos {}", closer_pos);
561
562 builder.start_node(SyntaxKind::STRONG.into());
564 builder.token(SyntaxKind::STRONG_MARKER.into(), &text[pos..pos + 2]);
565 parse_inline_range_nested(text, content_start, closer_pos, config, builder);
566 builder.token(
567 SyntaxKind::STRONG_MARKER.into(),
568 &text[closer_pos..closer_pos + 2],
569 );
570 builder.finish_node(); return Some(closer_pos + 2 - pos);
573 }
574
575 log::trace!("No closer found for **");
577 None
578}
579
580fn try_parse_one(
591 text: &str,
592 pos: usize,
593 delim_char: char,
594 end: usize,
595 config: &ParserOptions,
596 builder: &mut GreenNodeBuilder,
597) -> Option<usize> {
598 let content_start = pos + 1;
599
600 log::trace!("try_parse_one: '{}' x 1 at pos {}", delim_char, pos);
601
602 if let Some(closer_pos) =
604 parse_until_closer_with_nested_two(text, content_start, delim_char, 1, end, config)
605 {
606 log::trace!("Found * closer at pos {}", closer_pos);
607
608 builder.start_node(SyntaxKind::EMPHASIS.into());
610 builder.token(SyntaxKind::EMPHASIS_MARKER.into(), &text[pos..pos + 1]);
611 parse_inline_range_nested(text, content_start, closer_pos, config, builder);
612 builder.token(
613 SyntaxKind::EMPHASIS_MARKER.into(),
614 &text[closer_pos..closer_pos + 1],
615 );
616 builder.finish_node(); return Some(closer_pos + 1 - pos);
619 }
620
621 log::trace!("No closer found for *");
623 None
624}
625
626fn parse_until_closer_with_nested_two(
645 text: &str,
646 start: usize,
647 delim_char: char,
648 delim_count: usize,
649 end: usize,
650 config: &ParserOptions,
651) -> Option<usize> {
652 let bytes = text.as_bytes();
653 let mut pos = start;
654
655 while pos < end.min(text.len()) {
656 if bytes[pos] == b'`'
657 && let Some(m) = try_parse_inline_executable(
658 &text[pos..],
659 config.extensions.rmarkdown_inline_code,
660 config.extensions.quarto_inline_code,
661 )
662 {
663 log::trace!(
664 "Skipping inline executable span of {} bytes at pos {}",
665 m.total_len,
666 pos
667 );
668 pos += m.total_len;
669 continue;
670 }
671
672 if bytes[pos] == b'`'
674 && let Some((len, _, _, _)) = try_parse_code_span(&text[pos..])
675 {
676 log::trace!("Skipping code span of {} bytes at pos {}", len, pos);
677 pos += len;
678 continue;
679 }
680
681 if bytes[pos] == b'$'
683 && let Some((len, _)) = try_parse_inline_math(&text[pos..])
684 {
685 log::trace!("Skipping inline math of {} bytes at pos {}", len, pos);
686 pos += len;
687 continue;
688 }
689
690 if bytes[pos] == b'['
692 && let Some((len, _, _, _)) = try_parse_inline_link(&text[pos..])
693 {
694 log::trace!("Skipping inline link of {} bytes at pos {}", len, pos);
695 pos += len;
696 continue;
697 }
698
699 if delim_count == 1
703 && pos + 2 <= text.len()
704 && bytes[pos] == delim_char as u8
705 && bytes[pos + 1] == delim_char as u8
706 {
707 let first_is_escaped = {
709 let mut backslash_count = 0;
710 let mut check_pos = pos;
711 while check_pos > 0 && bytes[check_pos - 1] == b'\\' {
712 backslash_count += 1;
713 check_pos -= 1;
714 }
715 backslash_count % 2 == 1
716 };
717
718 if first_is_escaped {
719 log::trace!(
722 "First * at pos {} is escaped, skipping to check second *",
723 pos
724 );
725 pos = advance_char_boundary(text, pos, end);
726 continue;
727 }
728
729 let no_third_delim = pos + 2 >= bytes.len() || bytes[pos + 2] != delim_char as u8;
732
733 if no_third_delim {
734 log::trace!(
735 "try_parse_one: found ** at pos {}, attempting nested two",
736 pos
737 );
738
739 let mut temp_builder = GreenNodeBuilder::new();
742 if let Some(two_consumed) =
743 try_parse_two(text, pos, delim_char, end, config, &mut temp_builder)
744 {
745 log::trace!(
748 "Nested two succeeded, consumed {} bytes, continuing search",
749 two_consumed
750 );
751 pos += two_consumed;
752 continue;
753 }
754 log::trace!("Nested two failed at pos {}, entire one() should fail", pos);
760 return None;
761 }
762 }
763
764 if pos + delim_count <= text.len() {
766 let mut matches = true;
767 for i in 0..delim_count {
768 if bytes[pos + i] != delim_char as u8 {
769 matches = false;
770 break;
771 }
772 }
773
774 if matches {
775 let is_escaped = {
781 let mut backslash_count = 0;
782 let mut check_pos = pos;
783 while check_pos > 0 && bytes[check_pos - 1] == b'\\' {
784 backslash_count += 1;
785 check_pos -= 1;
786 }
787 backslash_count % 2 == 1 };
789
790 let at_run_start = pos == 0 || bytes[pos - 1] != delim_char as u8;
794 let after_pos = pos + delim_count;
795 let at_run_end = after_pos >= bytes.len() || bytes[after_pos] != delim_char as u8;
796
797 if (at_run_start || at_run_end) && !is_escaped {
798 if delim_char == '_'
802 && pos > start
803 && let Some(prev_char) = text[..pos].chars().last()
804 && prev_char.is_whitespace()
805 {
806 log::trace!(
807 "Underscore closer preceded by whitespace at pos {}, not right-flanking",
808 pos
809 );
810 pos = advance_char_boundary(text, pos, end);
812 continue;
813 }
814
815 log::trace!(
816 "Found exact {} x {} closer at pos {}",
817 delim_char,
818 delim_count,
819 pos
820 );
821 return Some(pos);
822 }
823 }
824 }
825
826 pos = advance_char_boundary(text, pos, end);
828 }
829
830 None
831}
832
833fn parse_until_closer_with_nested_one(
853 text: &str,
854 start: usize,
855 delim_char: char,
856 delim_count: usize,
857 end: usize,
858 config: &ParserOptions,
859) -> Option<usize> {
860 let bytes = text.as_bytes();
861 let mut pos = start;
862
863 while pos < end.min(text.len()) {
864 if bytes[pos] == b'`'
865 && let Some(m) = try_parse_inline_executable(
866 &text[pos..],
867 config.extensions.rmarkdown_inline_code,
868 config.extensions.quarto_inline_code,
869 )
870 {
871 log::trace!(
872 "Skipping inline executable span of {} bytes at pos {}",
873 m.total_len,
874 pos
875 );
876 pos += m.total_len;
877 continue;
878 }
879
880 if bytes[pos] == b'`'
882 && let Some((len, _, _, _)) = try_parse_code_span(&text[pos..])
883 {
884 log::trace!("Skipping code span of {} bytes at pos {}", len, pos);
885 pos += len;
886 continue;
887 }
888
889 if bytes[pos] == b'$'
891 && let Some((len, _)) = try_parse_inline_math(&text[pos..])
892 {
893 log::trace!("Skipping inline math of {} bytes at pos {}", len, pos);
894 pos += len;
895 continue;
896 }
897
898 if bytes[pos] == b'['
900 && let Some((len, _, _, _)) = try_parse_inline_link(&text[pos..])
901 {
902 log::trace!("Skipping inline link of {} bytes at pos {}", len, pos);
903 pos += len;
904 continue;
905 }
906
907 if delim_count == 2 && pos < text.len() && bytes[pos] == delim_char as u8 {
914 let no_second_delim = pos + 1 >= bytes.len() || bytes[pos + 1] != delim_char as u8;
917
918 if no_second_delim {
919 let is_escaped = {
921 let mut backslash_count = 0;
922 let mut check_pos = pos;
923 while check_pos > 0 && bytes[check_pos - 1] == b'\\' {
924 backslash_count += 1;
925 check_pos -= 1;
926 }
927 backslash_count % 2 == 1
928 };
929
930 if is_escaped {
931 log::trace!("* at pos {} is escaped, skipping", pos);
933 pos = advance_char_boundary(text, pos, end);
934 continue;
935 }
936
937 let after_delim = pos + 1;
940 let followed_by_whitespace = after_delim < text.len()
941 && text[after_delim..]
942 .chars()
943 .next()
944 .is_some_and(|c| c.is_whitespace());
945
946 if followed_by_whitespace {
947 log::trace!(
949 "* at pos {} followed by whitespace, not an opener, skipping",
950 pos
951 );
952 pos = advance_char_boundary(text, pos, end);
953 continue;
954 }
955
956 log::trace!(
957 "try_parse_two: found * at pos {}, attempting nested one",
958 pos
959 );
960
961 let mut temp_builder = GreenNodeBuilder::new();
964 if let Some(one_consumed) =
965 try_parse_one(text, pos, delim_char, end, config, &mut temp_builder)
966 {
967 log::trace!(
970 "Nested one succeeded, consumed {} bytes, continuing search",
971 one_consumed
972 );
973 pos += one_consumed;
974 continue;
975 }
976
977 log::trace!(
983 "Nested one failed at pos {}, poisoning outer two (no closer found)",
984 pos
985 );
986 return None;
987 }
988 }
989
990 if pos + delim_count <= text.len() {
992 let mut matches = true;
993 for i in 0..delim_count {
994 if bytes[pos + i] != delim_char as u8 {
995 matches = false;
996 break;
997 }
998 }
999
1000 if matches {
1001 let is_escaped = {
1003 let mut backslash_count = 0;
1004 let mut check_pos = pos;
1005 while check_pos > 0 && bytes[check_pos - 1] == b'\\' {
1006 backslash_count += 1;
1007 check_pos -= 1;
1008 }
1009 backslash_count % 2 == 1 };
1011
1012 let at_run_start = pos == 0 || bytes[pos - 1] != delim_char as u8;
1016 let after_pos = pos + delim_count;
1017 let at_run_end = after_pos >= bytes.len() || bytes[after_pos] != delim_char as u8;
1018
1019 if (at_run_start || at_run_end) && !is_escaped {
1020 if delim_char == '_'
1024 && pos > start
1025 && let Some(prev_char) = text[..pos].chars().last()
1026 && prev_char.is_whitespace()
1027 {
1028 log::trace!(
1029 "Underscore closer preceded by whitespace at pos {}, not right-flanking",
1030 pos
1031 );
1032 pos = advance_char_boundary(text, pos, end);
1034 continue;
1035 }
1036
1037 log::trace!(
1038 "Found exact {} x {} closer at pos {}",
1039 delim_char,
1040 delim_count,
1041 pos
1042 );
1043 return Some(pos);
1044 }
1045 }
1046 }
1047
1048 pos = advance_char_boundary(text, pos, end);
1050 }
1051
1052 None
1053}
1054
1055fn parse_inline_range(
1072 text: &str,
1073 start: usize,
1074 end: usize,
1075 config: &ParserOptions,
1076 builder: &mut GreenNodeBuilder,
1077) {
1078 parse_inline_range_impl(text, start, end, config, builder, false)
1079}
1080
1081fn parse_inline_range_nested(
1084 text: &str,
1085 start: usize,
1086 end: usize,
1087 config: &ParserOptions,
1088 builder: &mut GreenNodeBuilder,
1089) {
1090 parse_inline_range_impl(text, start, end, config, builder, true)
1091}
1092
1093fn is_emoji_boundary(text: &str, pos: usize) -> bool {
1094 if pos > 0 {
1095 let prev = text.as_bytes()[pos - 1] as char;
1096 if prev.is_ascii_alphanumeric() || prev == '_' {
1097 return false;
1098 }
1099 }
1100 true
1101}
1102
1103#[inline]
1104fn advance_char_boundary(text: &str, pos: usize, end: usize) -> usize {
1105 if pos >= end || pos >= text.len() {
1106 return pos;
1107 }
1108 let ch_len = text[pos..]
1109 .chars()
1110 .next()
1111 .map_or(1, std::primitive::char::len_utf8);
1112 (pos + ch_len).min(end)
1113}
1114
1115fn parse_inline_range_impl(
1116 text: &str,
1117 start: usize,
1118 end: usize,
1119 config: &ParserOptions,
1120 builder: &mut GreenNodeBuilder,
1121 nested_emphasis: bool,
1122) {
1123 log::trace!(
1124 "parse_inline_range: start={}, end={}, text={:?}",
1125 start,
1126 end,
1127 &text[start..end]
1128 );
1129 let mut pos = start;
1130 let mut text_start = start;
1131
1132 while pos < end {
1133 let byte = text.as_bytes()[pos];
1134
1135 if byte == b'\\' {
1137 if config.extensions.tex_math_double_backslash {
1139 if let Some((len, content)) = try_parse_double_backslash_display_math(&text[pos..])
1140 {
1141 if pos > text_start {
1142 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1143 }
1144 log::trace!("Matched double backslash display math at pos {}", pos);
1145 emit_double_backslash_display_math(builder, content);
1146 pos += len;
1147 text_start = pos;
1148 continue;
1149 }
1150
1151 if let Some((len, content)) = try_parse_double_backslash_inline_math(&text[pos..]) {
1153 if pos > text_start {
1154 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1155 }
1156 log::trace!("Matched double backslash inline math at pos {}", pos);
1157 emit_double_backslash_inline_math(builder, content);
1158 pos += len;
1159 text_start = pos;
1160 continue;
1161 }
1162 }
1163
1164 if config.extensions.tex_math_single_backslash {
1166 if let Some((len, content)) = try_parse_single_backslash_display_math(&text[pos..])
1167 {
1168 if pos > text_start {
1169 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1170 }
1171 log::trace!("Matched single backslash display math at pos {}", pos);
1172 emit_single_backslash_display_math(builder, content);
1173 pos += len;
1174 text_start = pos;
1175 continue;
1176 }
1177
1178 if let Some((len, content)) = try_parse_single_backslash_inline_math(&text[pos..]) {
1180 if pos > text_start {
1181 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1182 }
1183 log::trace!("Matched single backslash inline math at pos {}", pos);
1184 emit_single_backslash_inline_math(builder, content);
1185 pos += len;
1186 text_start = pos;
1187 continue;
1188 }
1189 }
1190
1191 if config.extensions.raw_tex
1193 && let Some((len, begin_marker, content, end_marker)) =
1194 try_parse_math_environment(&text[pos..])
1195 {
1196 if pos > text_start {
1197 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1198 }
1199 log::trace!("Matched math environment at pos {}", pos);
1200 emit_display_math_environment(builder, begin_marker, content, end_marker);
1201 pos += len;
1202 text_start = pos;
1203 continue;
1204 }
1205
1206 if config.extensions.bookdown_references
1208 && let Some((len, label)) = try_parse_bookdown_reference(&text[pos..])
1209 {
1210 if pos > text_start {
1211 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1212 }
1213 log::trace!("Matched bookdown reference at pos {}: {}", pos, label);
1214 super::citations::emit_bookdown_crossref(builder, label);
1215 pos += len;
1216 text_start = pos;
1217 continue;
1218 }
1219
1220 if let Some((len, ch, escape_type)) = try_parse_escape(&text[pos..]) {
1222 let escape_enabled = match escape_type {
1223 EscapeType::HardLineBreak => config.extensions.escaped_line_breaks,
1224 EscapeType::NonbreakingSpace => config.extensions.all_symbols_escapable,
1225 EscapeType::Literal => {
1226 const BASE_ESCAPABLE: &str = "\\`*_{}[]()>#+-.!";
1227 BASE_ESCAPABLE.contains(ch) || config.extensions.all_symbols_escapable
1228 }
1229 };
1230 if !escape_enabled {
1231 pos = advance_char_boundary(text, pos, end);
1234 continue;
1235 }
1236
1237 if pos > text_start {
1239 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1240 }
1241
1242 log::trace!("Matched escape at pos {}: \\{}", pos, ch);
1243 emit_escape(builder, ch, escape_type);
1244 pos += len;
1245 text_start = pos;
1246 continue;
1247 }
1248
1249 if config.extensions.raw_tex
1251 && let Some(len) = try_parse_latex_command(&text[pos..])
1252 {
1253 if pos > text_start {
1254 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1255 }
1256 log::trace!("Matched LaTeX command at pos {}", pos);
1257 parse_latex_command(builder, &text[pos..], len);
1258 pos += len;
1259 text_start = pos;
1260 continue;
1261 }
1262 }
1263
1264 if byte == b'{'
1266 && pos + 1 < text.len()
1267 && text.as_bytes()[pos + 1] == b'{'
1268 && let Some((len, name, attrs)) = try_parse_shortcode(&text[pos..])
1269 {
1270 if pos > text_start {
1271 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1272 }
1273 log::trace!("Matched shortcode at pos {}: {}", pos, &name);
1274 emit_shortcode(builder, &name, attrs);
1275 pos += len;
1276 text_start = pos;
1277 continue;
1278 }
1279
1280 if byte == b'`'
1282 && let Some(m) = try_parse_inline_executable(
1283 &text[pos..],
1284 config.extensions.rmarkdown_inline_code,
1285 config.extensions.quarto_inline_code,
1286 )
1287 {
1288 if pos > text_start {
1289 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1290 }
1291 log::trace!("Matched inline executable code at pos {}", pos);
1292 emit_inline_executable(builder, &m);
1293 pos += m.total_len;
1294 text_start = pos;
1295 continue;
1296 }
1297
1298 if byte == b'`'
1300 && let Some((len, content, backtick_count, attributes)) =
1301 try_parse_code_span(&text[pos..])
1302 {
1303 if pos > text_start {
1305 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1306 }
1307
1308 log::trace!(
1309 "Matched code span at pos {}: {} backticks",
1310 pos,
1311 backtick_count
1312 );
1313
1314 if let Some(ref attrs) = attributes
1316 && config.extensions.raw_attribute
1317 && let Some(format) = is_raw_inline(attrs)
1318 {
1319 use super::raw_inline::emit_raw_inline;
1320 log::trace!("Matched raw inline span at pos {}: format={}", pos, format);
1321 emit_raw_inline(builder, content, backtick_count, format);
1322 } else if !config.extensions.inline_code_attributes && attributes.is_some() {
1323 let code_span_len = backtick_count * 2 + content.len();
1324 emit_code_span(builder, content, backtick_count, None);
1325 pos += code_span_len;
1326 text_start = pos;
1327 continue;
1328 } else {
1329 emit_code_span(builder, content, backtick_count, attributes);
1330 }
1331
1332 pos += len;
1333 text_start = pos;
1334 continue;
1335 }
1336
1337 if byte == b':'
1339 && config.extensions.emoji
1340 && is_emoji_boundary(text, pos)
1341 && let Some((len, _alias)) = try_parse_emoji(&text[pos..])
1342 {
1343 if pos > text_start {
1344 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1345 }
1346 log::trace!("Matched emoji at pos {}", pos);
1347 emit_emoji(builder, &text[pos..pos + len]);
1348 pos += len;
1349 text_start = pos;
1350 continue;
1351 }
1352
1353 if byte == b'^'
1355 && pos + 1 < text.len()
1356 && text.as_bytes()[pos + 1] == b'['
1357 && config.extensions.inline_footnotes
1358 && let Some((len, content)) = try_parse_inline_footnote(&text[pos..])
1359 {
1360 if pos > text_start {
1361 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1362 }
1363 log::trace!("Matched inline footnote at pos {}", pos);
1364 emit_inline_footnote(builder, content, config);
1365 pos += len;
1366 text_start = pos;
1367 continue;
1368 }
1369
1370 if byte == b'^'
1372 && config.extensions.superscript
1373 && let Some((len, content)) = try_parse_superscript(&text[pos..])
1374 {
1375 if pos > text_start {
1376 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1377 }
1378 log::trace!("Matched superscript at pos {}", pos);
1379 emit_superscript(builder, content, config);
1380 pos += len;
1381 text_start = pos;
1382 continue;
1383 }
1384
1385 if byte == b'(' && config.extensions.bookdown_references {
1387 if let Some((len, label)) = try_parse_bookdown_definition(&text[pos..]) {
1388 if pos > text_start {
1389 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1390 }
1391 log::trace!("Matched bookdown definition at pos {}: {}", pos, label);
1392 builder.token(SyntaxKind::TEXT.into(), &text[pos..pos + len]);
1393 pos += len;
1394 text_start = pos;
1395 continue;
1396 }
1397 if let Some((len, label)) = try_parse_bookdown_text_reference(&text[pos..]) {
1398 if pos > text_start {
1399 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1400 }
1401 log::trace!("Matched bookdown text reference at pos {}: {}", pos, label);
1402 builder.token(SyntaxKind::TEXT.into(), &text[pos..pos + len]);
1403 pos += len;
1404 text_start = pos;
1405 continue;
1406 }
1407 }
1408
1409 if byte == b'~'
1411 && config.extensions.subscript
1412 && let Some((len, content)) = try_parse_subscript(&text[pos..])
1413 {
1414 if pos > text_start {
1415 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1416 }
1417 log::trace!("Matched subscript at pos {}", pos);
1418 emit_subscript(builder, content, config);
1419 pos += len;
1420 text_start = pos;
1421 continue;
1422 }
1423
1424 if byte == b'~'
1426 && config.extensions.strikeout
1427 && let Some((len, content)) = try_parse_strikeout(&text[pos..])
1428 {
1429 if pos > text_start {
1430 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1431 }
1432 log::trace!("Matched strikeout at pos {}", pos);
1433 emit_strikeout(builder, content, config);
1434 pos += len;
1435 text_start = pos;
1436 continue;
1437 }
1438
1439 if byte == b'='
1441 && config.extensions.mark
1442 && let Some((len, content)) = try_parse_mark(&text[pos..])
1443 {
1444 if pos > text_start {
1445 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1446 }
1447 log::trace!("Matched mark at pos {}", pos);
1448 emit_mark(builder, content, config);
1449 pos += len;
1450 text_start = pos;
1451 continue;
1452 }
1453
1454 if byte == b'$'
1456 && config.extensions.tex_math_gfm
1457 && let Some((len, content)) = try_parse_gfm_inline_math(&text[pos..])
1458 {
1459 if pos > text_start {
1460 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1461 }
1462 log::trace!("Matched GFM inline math at pos {}", pos);
1463 emit_gfm_inline_math(builder, content);
1464 pos += len;
1465 text_start = pos;
1466 continue;
1467 }
1468
1469 if byte == b'$' && config.extensions.tex_math_dollars {
1471 if let Some((len, content)) = try_parse_display_math(&text[pos..]) {
1473 if pos > text_start {
1475 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1476 }
1477
1478 let dollar_count = text[pos..].chars().take_while(|&c| c == '$').count();
1479 log::trace!(
1480 "Matched display math at pos {}: {} dollars",
1481 pos,
1482 dollar_count
1483 );
1484
1485 let after_math = &text[pos + len..];
1487 let attr_len = if config.extensions.quarto_crossrefs {
1488 use crate::parser::utils::attributes::try_parse_trailing_attributes;
1489 if let Some((_attr_block, _)) = try_parse_trailing_attributes(after_math) {
1490 let trimmed_after = after_math.trim_start();
1491 if let Some(open_brace_pos) = trimmed_after.find('{') {
1492 let ws_before_brace = after_math.len() - trimmed_after.len();
1493 let attr_text_len = trimmed_after[open_brace_pos..]
1494 .find('}')
1495 .map(|close| close + 1)
1496 .unwrap_or(0);
1497 ws_before_brace + open_brace_pos + attr_text_len
1498 } else {
1499 0
1500 }
1501 } else {
1502 0
1503 }
1504 } else {
1505 0
1506 };
1507
1508 let total_len = len + attr_len;
1509 emit_display_math(builder, content, dollar_count);
1510
1511 if attr_len > 0 {
1513 use crate::parser::utils::attributes::{
1514 emit_attributes, try_parse_trailing_attributes,
1515 };
1516 let attr_text = &text[pos + len..pos + total_len];
1517 if let Some((attr_block, _text_before)) =
1518 try_parse_trailing_attributes(attr_text)
1519 {
1520 let trimmed_after = attr_text.trim_start();
1521 let ws_len = attr_text.len() - trimmed_after.len();
1522 if ws_len > 0 {
1523 builder.token(SyntaxKind::WHITESPACE.into(), &attr_text[..ws_len]);
1524 }
1525 emit_attributes(builder, &attr_block);
1526 }
1527 }
1528
1529 pos += total_len;
1530 text_start = pos;
1531 continue;
1532 }
1533
1534 if let Some((len, content)) = try_parse_inline_math(&text[pos..]) {
1536 if pos > text_start {
1538 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1539 }
1540
1541 log::trace!("Matched inline math at pos {}", pos);
1542 emit_inline_math(builder, content);
1543 pos += len;
1544 text_start = pos;
1545 continue;
1546 }
1547
1548 if pos > text_start {
1551 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1552 }
1553 builder.token(SyntaxKind::TEXT.into(), "$");
1554 pos = advance_char_boundary(text, pos, end);
1555 text_start = pos;
1556 continue;
1557 }
1558
1559 if byte == b'<'
1561 && config.extensions.autolinks
1562 && let Some((len, url)) = try_parse_autolink(&text[pos..])
1563 {
1564 if pos > text_start {
1565 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1566 }
1567 log::trace!("Matched autolink at pos {}", pos);
1568 emit_autolink(builder, &text[pos..pos + len], url);
1569 pos += len;
1570 text_start = pos;
1571 continue;
1572 }
1573
1574 if config.extensions.autolink_bare_uris
1575 && let Some((len, url)) = try_parse_bare_uri(&text[pos..])
1576 {
1577 if pos > text_start {
1578 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1579 }
1580 log::trace!("Matched bare URI at pos {}", pos);
1581 emit_bare_uri_link(builder, url, config);
1582 pos += len;
1583 text_start = pos;
1584 continue;
1585 }
1586
1587 if byte == b'<'
1589 && config.extensions.native_spans
1590 && let Some((len, content, attributes)) = try_parse_native_span(&text[pos..])
1591 {
1592 if pos > text_start {
1593 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1594 }
1595 log::trace!("Matched native span at pos {}", pos);
1596 emit_native_span(builder, content, &attributes, config);
1597 pos += len;
1598 text_start = pos;
1599 continue;
1600 }
1601
1602 if byte == b'!' && pos + 1 < text.len() && text.as_bytes()[pos + 1] == b'[' {
1604 if let Some((len, alt_text, dest, attributes)) = try_parse_inline_image(&text[pos..]) {
1606 if pos > text_start {
1607 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1608 }
1609 log::trace!("Matched inline image at pos {}", pos);
1610 emit_inline_image(
1611 builder,
1612 &text[pos..pos + len],
1613 alt_text,
1614 dest,
1615 attributes,
1616 config,
1617 );
1618 pos += len;
1619 text_start = pos;
1620 continue;
1621 }
1622
1623 if config.extensions.reference_links {
1625 let allow_shortcut = config.extensions.shortcut_reference_links;
1626 if let Some((len, alt_text, reference, is_implicit)) =
1627 try_parse_reference_image(&text[pos..], allow_shortcut)
1628 {
1629 if pos > text_start {
1630 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1631 }
1632 log::trace!("Matched reference image at pos {}", pos);
1633 emit_reference_image(builder, alt_text, &reference, is_implicit, config);
1634 pos += len;
1635 text_start = pos;
1636 continue;
1637 }
1638 }
1639 }
1640
1641 if byte == b'[' {
1643 if config.extensions.footnotes
1645 && let Some((len, id)) = try_parse_footnote_reference(&text[pos..])
1646 {
1647 if pos > text_start {
1648 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1649 }
1650 log::trace!("Matched footnote reference at pos {}", pos);
1651 emit_footnote_reference(builder, &id);
1652 pos += len;
1653 text_start = pos;
1654 continue;
1655 }
1656
1657 if config.extensions.inline_links
1659 && let Some((len, link_text, dest, attributes)) =
1660 try_parse_inline_link(&text[pos..])
1661 {
1662 if pos > text_start {
1663 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1664 }
1665 log::trace!("Matched inline link at pos {}", pos);
1666 emit_inline_link(
1667 builder,
1668 &text[pos..pos + len],
1669 link_text,
1670 dest,
1671 attributes,
1672 config,
1673 );
1674 pos += len;
1675 text_start = pos;
1676 continue;
1677 }
1678
1679 if config.extensions.reference_links {
1681 let allow_shortcut = config.extensions.shortcut_reference_links;
1682 if let Some((len, link_text, reference, is_implicit)) =
1683 try_parse_reference_link(&text[pos..], allow_shortcut)
1684 {
1685 if pos > text_start {
1686 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1687 }
1688 log::trace!("Matched reference link at pos {}", pos);
1689 emit_reference_link(builder, link_text, &reference, is_implicit, config);
1690 pos += len;
1691 text_start = pos;
1692 continue;
1693 }
1694 }
1695
1696 if config.extensions.citations
1698 && let Some((len, content)) = try_parse_bracketed_citation(&text[pos..])
1699 {
1700 if pos > text_start {
1701 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1702 }
1703 log::trace!("Matched bracketed citation at pos {}", pos);
1704 emit_bracketed_citation(builder, content);
1705 pos += len;
1706 text_start = pos;
1707 continue;
1708 }
1709 }
1710
1711 if byte == b'['
1714 && config.extensions.bracketed_spans
1715 && let Some((len, text_content, attrs)) = try_parse_bracketed_span(&text[pos..])
1716 {
1717 if pos > text_start {
1718 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1719 }
1720 log::trace!("Matched bracketed span at pos {}", pos);
1721 emit_bracketed_span(builder, &text_content, &attrs, config);
1722 pos += len;
1723 text_start = pos;
1724 continue;
1725 }
1726
1727 if byte == b'@'
1729 && (config.extensions.citations || config.extensions.quarto_crossrefs)
1730 && let Some((len, key, has_suppress)) = try_parse_bare_citation(&text[pos..])
1731 {
1732 let is_crossref =
1733 config.extensions.quarto_crossrefs && super::citations::is_quarto_crossref_key(key);
1734 if is_crossref || config.extensions.citations {
1735 if pos > text_start {
1736 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1737 }
1738 if is_crossref {
1739 log::trace!("Matched Quarto crossref at pos {}: {}", pos, &key);
1740 super::citations::emit_crossref(builder, key, has_suppress);
1741 } else {
1742 log::trace!("Matched bare citation at pos {}: {}", pos, &key);
1743 emit_bare_citation(builder, key, has_suppress);
1744 }
1745 pos += len;
1746 text_start = pos;
1747 continue;
1748 }
1749 }
1750
1751 if byte == b'-'
1753 && pos + 1 < text.len()
1754 && text.as_bytes()[pos + 1] == b'@'
1755 && (config.extensions.citations || config.extensions.quarto_crossrefs)
1756 && let Some((len, key, has_suppress)) = try_parse_bare_citation(&text[pos..])
1757 {
1758 let is_crossref =
1759 config.extensions.quarto_crossrefs && super::citations::is_quarto_crossref_key(key);
1760 if is_crossref || config.extensions.citations {
1761 if pos > text_start {
1762 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1763 }
1764 if is_crossref {
1765 log::trace!("Matched Quarto crossref at pos {}: {}", pos, &key);
1766 super::citations::emit_crossref(builder, key, has_suppress);
1767 } else {
1768 log::trace!("Matched suppress-author citation at pos {}: {}", pos, &key);
1769 emit_bare_citation(builder, key, has_suppress);
1770 }
1771 pos += len;
1772 text_start = pos;
1773 continue;
1774 }
1775 }
1776
1777 if byte == b'*' || byte == b'_' {
1779 let bytes = text.as_bytes();
1781 let mut delim_count = 0;
1782 while pos + delim_count < bytes.len() && bytes[pos + delim_count] == byte {
1783 delim_count += 1;
1784 }
1785
1786 if pos > text_start {
1788 log::trace!(
1789 "Emitting TEXT before delimiter: {:?}",
1790 &text[text_start..pos]
1791 );
1792 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1793 text_start = pos; }
1795
1796 let emphasis_result = if nested_emphasis {
1799 try_parse_emphasis_nested(text, pos, end, config, builder)
1800 } else {
1801 try_parse_emphasis(text, pos, end, config, builder)
1802 };
1803
1804 if let Some((consumed, _)) = emphasis_result {
1805 log::trace!(
1807 "Parsed emphasis, consumed {} bytes from pos {}",
1808 consumed,
1809 pos
1810 );
1811 pos += consumed;
1812 text_start = pos;
1813 } else {
1814 log::trace!(
1817 "Failed to parse emphasis at pos {}, skipping {} delimiters as literal",
1818 pos,
1819 delim_count
1820 );
1821 pos += delim_count;
1822 }
1824 continue;
1825 }
1826
1827 if byte == b'\r' && pos + 1 < end && text.as_bytes()[pos + 1] == b'\n' {
1829 let text_before = &text[text_start..pos];
1830
1831 let trailing_spaces = text_before.chars().rev().take_while(|&c| c == ' ').count();
1833 if trailing_spaces >= 2 {
1834 let text_content = &text_before[..text_before.len() - trailing_spaces];
1836 if !text_content.is_empty() {
1837 builder.token(SyntaxKind::TEXT.into(), text_content);
1838 }
1839 let spaces = " ".repeat(trailing_spaces);
1840 builder.token(
1841 SyntaxKind::HARD_LINE_BREAK.into(),
1842 &format!("{}\r\n", spaces),
1843 );
1844 pos += 2;
1845 text_start = pos;
1846 continue;
1847 }
1848
1849 if config.extensions.hard_line_breaks {
1851 if !text_before.is_empty() {
1852 builder.token(SyntaxKind::TEXT.into(), text_before);
1853 }
1854 builder.token(SyntaxKind::HARD_LINE_BREAK.into(), "\r\n");
1855 pos += 2;
1856 text_start = pos;
1857 continue;
1858 }
1859
1860 if !text_before.is_empty() {
1862 builder.token(SyntaxKind::TEXT.into(), text_before);
1863 }
1864 builder.token(SyntaxKind::NEWLINE.into(), "\r\n");
1865 pos += 2;
1866 text_start = pos;
1867 continue;
1868 }
1869
1870 if byte == b'\n' {
1871 let text_before = &text[text_start..pos];
1872
1873 let trailing_spaces = text_before.chars().rev().take_while(|&c| c == ' ').count();
1875 if trailing_spaces >= 2 {
1876 let text_content = &text_before[..text_before.len() - trailing_spaces];
1878 if !text_content.is_empty() {
1879 builder.token(SyntaxKind::TEXT.into(), text_content);
1880 }
1881 let spaces = " ".repeat(trailing_spaces);
1882 builder.token(SyntaxKind::HARD_LINE_BREAK.into(), &format!("{}\n", spaces));
1883 pos += 1;
1884 text_start = pos;
1885 continue;
1886 }
1887
1888 if config.extensions.hard_line_breaks {
1890 if !text_before.is_empty() {
1891 builder.token(SyntaxKind::TEXT.into(), text_before);
1892 }
1893 builder.token(SyntaxKind::HARD_LINE_BREAK.into(), "\n");
1894 pos += 1;
1895 text_start = pos;
1896 continue;
1897 }
1898
1899 if !text_before.is_empty() {
1901 builder.token(SyntaxKind::TEXT.into(), text_before);
1902 }
1903 builder.token(SyntaxKind::NEWLINE.into(), "\n");
1904 pos += 1;
1905 text_start = pos;
1906 continue;
1907 }
1908
1909 pos = advance_char_boundary(text, pos, end);
1911 }
1912
1913 if pos > text_start && text_start < end {
1915 log::trace!("Emitting remaining TEXT: {:?}", &text[text_start..end]);
1916 builder.token(SyntaxKind::TEXT.into(), &text[text_start..end]);
1917 }
1918
1919 log::trace!("parse_inline_range complete: start={}, end={}", start, end);
1920}
1921
1922#[cfg(test)]
1923mod tests {
1924 use super::*;
1925 use crate::syntax::{SyntaxKind, SyntaxNode};
1926 use rowan::GreenNode;
1927
1928 #[test]
1929 fn test_recursive_simple_emphasis() {
1930 let text = "*test*";
1931 let config = ParserOptions::default();
1932 let mut builder = GreenNodeBuilder::new();
1933
1934 parse_inline_text_recursive(&mut builder, text, &config);
1935
1936 let green: GreenNode = builder.finish();
1937 let node = SyntaxNode::new_root(green);
1938
1939 assert_eq!(node.text().to_string(), text);
1941
1942 let has_emph = node.descendants().any(|n| n.kind() == SyntaxKind::EMPHASIS);
1944 assert!(has_emph, "Should have EMPHASIS node");
1945 }
1946
1947 #[test]
1948 fn test_recursive_nested() {
1949 let text = "*foo **bar** baz*";
1950 let config = ParserOptions::default();
1951 let mut builder = GreenNodeBuilder::new();
1952
1953 builder.start_node(SyntaxKind::PARAGRAPH.into());
1955 parse_inline_text_recursive(&mut builder, text, &config);
1956 builder.finish_node();
1957
1958 let green: GreenNode = builder.finish();
1959 let node = SyntaxNode::new_root(green);
1960
1961 assert_eq!(node.text().to_string(), text);
1963
1964 let has_emph = node.descendants().any(|n| n.kind() == SyntaxKind::EMPHASIS);
1966 let has_strong = node.descendants().any(|n| n.kind() == SyntaxKind::STRONG);
1967
1968 assert!(has_emph, "Should have EMPHASIS node");
1969 assert!(has_strong, "Should have STRONG node");
1970 }
1971
1972 #[test]
1974 fn test_parse_simple_emphasis() {
1975 use crate::options::ParserOptions;
1976 use crate::syntax::SyntaxNode;
1977 use rowan::GreenNode;
1978
1979 let text = "*test*";
1980 let config = ParserOptions::default();
1981 let mut builder = GreenNodeBuilder::new();
1982
1983 let result = try_parse_emphasis(text, 0, text.len(), &config, &mut builder);
1985
1986 assert_eq!(result, Some((6, 1))); let green: GreenNode = builder.finish();
1991 let node = SyntaxNode::new_root(green);
1992
1993 assert_eq!(node.kind(), SyntaxKind::EMPHASIS);
1995
1996 assert_eq!(node.text().to_string(), text);
1998 }
1999
2000 #[test]
2002 fn test_parse_nested_emphasis_strong() {
2003 use crate::options::ParserOptions;
2004
2005 let text = "*foo **bar** baz*";
2006 let config = ParserOptions::default();
2007 let mut builder = GreenNodeBuilder::new();
2008
2009 parse_inline_range(text, 0, text.len(), &config, &mut builder);
2011
2012 let green = builder.finish();
2013 let node = crate::syntax::SyntaxNode::new_root(green);
2014
2015 assert_eq!(node.text().to_string(), text);
2017
2018 let has_emph = node.descendants().any(|n| n.kind() == SyntaxKind::EMPHASIS);
2020 let has_strong = node.descendants().any(|n| n.kind() == SyntaxKind::STRONG);
2021
2022 assert!(has_emph, "Should have EMPHASIS node");
2023 assert!(has_strong, "Should have STRONG node");
2024 }
2025
2026 #[test]
2030 fn test_triple_emphasis_star_then_double_star() {
2031 use crate::options::ParserOptions;
2032 use crate::syntax::SyntaxNode;
2033 use rowan::GreenNode;
2034
2035 let text = "***foo* bar**";
2036 let config = ParserOptions::default();
2037 let mut builder = GreenNodeBuilder::new();
2038
2039 builder.start_node(SyntaxKind::DOCUMENT.into());
2040 parse_inline_range(text, 0, text.len(), &config, &mut builder);
2041 builder.finish_node();
2042
2043 let green: GreenNode = builder.finish();
2044 let node = SyntaxNode::new_root(green);
2045
2046 assert_eq!(node.text().to_string(), text);
2048
2049 let structure = format!("{:#?}", node);
2052
2053 assert!(structure.contains("STRONG"), "Should have STRONG node");
2055 assert!(structure.contains("EMPHASIS"), "Should have EMPHASIS node");
2056
2057 let mut found_strong = false;
2060 let mut found_emph_after_strong = false;
2061 for descendant in node.descendants() {
2062 if descendant.kind() == SyntaxKind::STRONG {
2063 found_strong = true;
2064 }
2065 if found_strong && descendant.kind() == SyntaxKind::EMPHASIS {
2066 found_emph_after_strong = true;
2067 break;
2068 }
2069 }
2070
2071 assert!(
2072 found_emph_after_strong,
2073 "EMPH should be inside STRONG, not before it. Current structure:\n{}",
2074 structure
2075 );
2076 }
2077
2078 #[test]
2081 fn test_triple_emphasis_double_star_then_star() {
2082 use crate::options::ParserOptions;
2083 use crate::syntax::SyntaxNode;
2084 use rowan::GreenNode;
2085
2086 let text = "***foo** bar*";
2087 let config = ParserOptions::default();
2088 let mut builder = GreenNodeBuilder::new();
2089
2090 builder.start_node(SyntaxKind::DOCUMENT.into());
2091 parse_inline_range(text, 0, text.len(), &config, &mut builder);
2092 builder.finish_node();
2093
2094 let green: GreenNode = builder.finish();
2095 let node = SyntaxNode::new_root(green);
2096
2097 assert_eq!(node.text().to_string(), text);
2099
2100 let structure = format!("{:#?}", node);
2102
2103 assert!(structure.contains("EMPHASIS"), "Should have EMPHASIS node");
2105 assert!(structure.contains("STRONG"), "Should have STRONG node");
2106
2107 let mut found_emph = false;
2109 let mut found_strong_after_emph = false;
2110 for descendant in node.descendants() {
2111 if descendant.kind() == SyntaxKind::EMPHASIS {
2112 found_emph = true;
2113 }
2114 if found_emph && descendant.kind() == SyntaxKind::STRONG {
2115 found_strong_after_emph = true;
2116 break;
2117 }
2118 }
2119
2120 assert!(
2121 found_strong_after_emph,
2122 "STRONG should be inside EMPH. Current structure:\n{}",
2123 structure
2124 );
2125 }
2126
2127 #[test]
2130 fn test_display_math_with_attributes() {
2131 use crate::options::ParserOptions;
2132 use crate::syntax::SyntaxNode;
2133 use rowan::GreenNode;
2134
2135 let text = "$$ E = mc^2 $$ {#eq-einstein}";
2136 let mut config = ParserOptions::default();
2137 config.extensions.quarto_crossrefs = true; let mut builder = GreenNodeBuilder::new();
2140 builder.start_node(SyntaxKind::DOCUMENT.into()); parse_inline_text_recursive(&mut builder, text, &config);
2144
2145 builder.finish_node(); let green: GreenNode = builder.finish();
2147 let node = SyntaxNode::new_root(green);
2148
2149 assert_eq!(node.text().to_string(), text);
2151
2152 let has_display_math = node
2154 .descendants()
2155 .any(|n| n.kind() == SyntaxKind::DISPLAY_MATH);
2156 assert!(has_display_math, "Should have DISPLAY_MATH node");
2157
2158 let has_attributes = node
2160 .descendants()
2161 .any(|n| n.kind() == SyntaxKind::ATTRIBUTE);
2162 assert!(
2163 has_attributes,
2164 "Should have ATTRIBUTE node for {{#eq-einstein}}"
2165 );
2166
2167 let math_followed_by_text = node.descendants().any(|n| {
2169 n.kind() == SyntaxKind::DISPLAY_MATH
2170 && n.next_sibling()
2171 .map(|s| {
2172 s.kind() == SyntaxKind::TEXT
2173 && s.text().to_string().contains("{#eq-einstein}")
2174 })
2175 .unwrap_or(false)
2176 });
2177 assert!(
2178 !math_followed_by_text,
2179 "Attributes should not be parsed as TEXT"
2180 );
2181 }
2182
2183 #[test]
2184 fn test_parse_inline_text_gfm_inline_link_destination_not_autolinked() {
2185 use crate::options::{Extensions, Flavor};
2186
2187 let config = ParserOptions {
2188 flavor: Flavor::Gfm,
2189 extensions: Extensions::for_flavor(Flavor::Gfm),
2190 ..ParserOptions::default()
2191 };
2192
2193 let mut builder = GreenNodeBuilder::new();
2194 builder.start_node(SyntaxKind::PARAGRAPH.into());
2195 parse_inline_text_recursive(
2196 &mut builder,
2197 "Second Link [link_text](https://link.com)",
2198 &config,
2199 );
2200 builder.finish_node();
2201 let green = builder.finish();
2202 let root = SyntaxNode::new_root(green);
2203
2204 let links: Vec<_> = root
2205 .descendants()
2206 .filter(|n| n.kind() == SyntaxKind::LINK)
2207 .collect();
2208 assert_eq!(
2209 links.len(),
2210 1,
2211 "Expected exactly one LINK node for inline link, not nested bare URI autolink"
2212 );
2213
2214 let link = links[0].clone();
2215 let mut link_text = None::<String>;
2216 let mut link_dest = None::<String>;
2217
2218 for child in link.children() {
2219 match child.kind() {
2220 SyntaxKind::LINK_TEXT => link_text = Some(child.text().to_string()),
2221 SyntaxKind::LINK_DEST => link_dest = Some(child.text().to_string()),
2222 _ => {}
2223 }
2224 }
2225
2226 assert_eq!(link_text.as_deref(), Some("link_text"));
2227 assert_eq!(link_dest.as_deref(), Some("https://link.com"));
2228 }
2229
2230 #[test]
2231 fn test_autolink_bare_uri_utf8_boundary_safe() {
2232 let text = "§";
2233 let mut config = ParserOptions::default();
2234 config.extensions.autolink_bare_uris = true;
2235 let mut builder = GreenNodeBuilder::new();
2236
2237 builder.start_node(SyntaxKind::DOCUMENT.into());
2238 parse_inline_text_recursive(&mut builder, text, &config);
2239 builder.finish_node();
2240
2241 let green: GreenNode = builder.finish();
2242 let node = SyntaxNode::new_root(green);
2243 assert_eq!(node.text().to_string(), text);
2244 }
2245
2246 #[test]
2247 fn test_parse_emphasis_unicode_content_no_panic() {
2248 let text = "*§*";
2249 let config = ParserOptions::default();
2250 let mut builder = GreenNodeBuilder::new();
2251
2252 let result = try_parse_emphasis(text, 0, text.len(), &config, &mut builder);
2253 assert_eq!(result, Some((text.len(), 1)));
2254
2255 let green: GreenNode = builder.finish();
2256 let node = SyntaxNode::new_root(green);
2257 assert_eq!(node.kind(), SyntaxKind::EMPHASIS);
2258 assert_eq!(node.text().to_string(), text);
2259 }
2260}
2261
2262#[test]
2263fn test_two_with_nested_one_and_triple_closer() {
2264 use crate::options::ParserOptions;
2269 use crate::syntax::SyntaxNode;
2270 use rowan::GreenNode;
2271
2272 let text = "**bold with *italic***";
2273 let config = ParserOptions::default();
2274 let mut builder = GreenNodeBuilder::new();
2275
2276 parse_inline_range(text, 0, text.len(), &config, &mut builder);
2278
2279 let green: GreenNode = builder.finish();
2280 let node = SyntaxNode::new_root(green);
2281
2282 assert_eq!(node.text().to_string(), text, "Should be lossless");
2284
2285 assert_eq!(
2287 node.kind(),
2288 SyntaxKind::STRONG,
2289 "Root should be STRONG, got: {:?}",
2290 node.kind()
2291 );
2292
2293 let has_emphasis = node.children().any(|c| c.kind() == SyntaxKind::EMPHASIS);
2295 assert!(has_emphasis, "STRONG should contain EMPHASIS node");
2296}
2297
2298#[test]
2299fn test_emphasis_with_trailing_space_before_closer() {
2300 use crate::options::ParserOptions;
2304 use crate::syntax::SyntaxNode;
2305 use rowan::GreenNode;
2306
2307 let text = "*foo *";
2308 let config = ParserOptions::default();
2309 let mut builder = GreenNodeBuilder::new();
2310
2311 let result = try_parse_emphasis(text, 0, text.len(), &config, &mut builder);
2313
2314 assert_eq!(
2316 result,
2317 Some((6, 1)),
2318 "Should parse as emphasis, result: {:?}",
2319 result
2320 );
2321
2322 let green: GreenNode = builder.finish();
2324 let node = SyntaxNode::new_root(green);
2325
2326 assert_eq!(node.kind(), SyntaxKind::EMPHASIS);
2328
2329 assert_eq!(node.text().to_string(), text);
2331}
2332
2333#[test]
2334fn test_triple_emphasis_all_strong_nested() {
2335 use crate::options::ParserOptions;
2339 use crate::syntax::SyntaxNode;
2340 use rowan::GreenNode;
2341
2342 let text = "***foo** bar **baz***";
2343 let config = ParserOptions::default();
2344 let mut builder = GreenNodeBuilder::new();
2345
2346 parse_inline_range(text, 0, text.len(), &config, &mut builder);
2347
2348 let green: GreenNode = builder.finish();
2349 let node = SyntaxNode::new_root(green);
2350
2351 let emphasis_nodes: Vec<_> = node
2353 .descendants()
2354 .filter(|n| n.kind() == SyntaxKind::EMPHASIS)
2355 .collect();
2356 assert_eq!(
2357 emphasis_nodes.len(),
2358 1,
2359 "Should have exactly one EMPHASIS node, found: {}",
2360 emphasis_nodes.len()
2361 );
2362
2363 let emphasis_node = emphasis_nodes[0].clone();
2365 let strong_in_emphasis: Vec<_> = emphasis_node
2366 .children()
2367 .filter(|n| n.kind() == SyntaxKind::STRONG)
2368 .collect();
2369 assert_eq!(
2370 strong_in_emphasis.len(),
2371 2,
2372 "EMPHASIS should contain two STRONG nodes, found: {}",
2373 strong_in_emphasis.len()
2374 );
2375
2376 assert_eq!(node.text().to_string(), text);
2378}
2379
2380#[test]
2381fn test_triple_emphasis_all_emph_nested() {
2382 use crate::options::ParserOptions;
2386 use crate::syntax::SyntaxNode;
2387 use rowan::GreenNode;
2388
2389 let text = "***foo* bar *baz***";
2390 let config = ParserOptions::default();
2391 let mut builder = GreenNodeBuilder::new();
2392
2393 parse_inline_range(text, 0, text.len(), &config, &mut builder);
2394
2395 let green: GreenNode = builder.finish();
2396 let node = SyntaxNode::new_root(green);
2397
2398 let strong_nodes: Vec<_> = node
2400 .descendants()
2401 .filter(|n| n.kind() == SyntaxKind::STRONG)
2402 .collect();
2403 assert_eq!(
2404 strong_nodes.len(),
2405 1,
2406 "Should have exactly one STRONG node, found: {}",
2407 strong_nodes.len()
2408 );
2409
2410 let strong_node = strong_nodes[0].clone();
2412 let emph_in_strong: Vec<_> = strong_node
2413 .children()
2414 .filter(|n| n.kind() == SyntaxKind::EMPHASIS)
2415 .collect();
2416 assert_eq!(
2417 emph_in_strong.len(),
2418 2,
2419 "STRONG should contain two EMPHASIS nodes, found: {}",
2420 emph_in_strong.len()
2421 );
2422
2423 assert_eq!(node.text().to_string(), text);
2425}
2426
2427#[test]
2429fn test_parse_emphasis_multiline() {
2430 use crate::options::ParserOptions;
2432 use crate::syntax::SyntaxNode;
2433 use rowan::GreenNode;
2434
2435 let text = "*text on\nline two*";
2436 let config = ParserOptions::default();
2437 let mut builder = GreenNodeBuilder::new();
2438
2439 let result = try_parse_emphasis(text, 0, text.len(), &config, &mut builder);
2440
2441 assert_eq!(
2443 result,
2444 Some((text.len(), 1)),
2445 "Emphasis should parse multiline content"
2446 );
2447
2448 let green: GreenNode = builder.finish();
2450 let node = SyntaxNode::new_root(green);
2451
2452 assert_eq!(node.kind(), SyntaxKind::EMPHASIS);
2454
2455 assert_eq!(node.text().to_string(), text);
2457 assert!(
2458 node.text().to_string().contains('\n'),
2459 "Should preserve newline in emphasis content"
2460 );
2461}
2462
2463#[test]
2464fn test_parse_strong_multiline() {
2465 use crate::options::ParserOptions;
2467 use crate::syntax::SyntaxNode;
2468 use rowan::GreenNode;
2469
2470 let text = "**strong on\nline two**";
2471 let config = ParserOptions::default();
2472 let mut builder = GreenNodeBuilder::new();
2473
2474 let result = try_parse_emphasis(text, 0, text.len(), &config, &mut builder);
2475
2476 assert_eq!(
2478 result,
2479 Some((text.len(), 2)),
2480 "Strong emphasis should parse multiline content"
2481 );
2482
2483 let green: GreenNode = builder.finish();
2485 let node = SyntaxNode::new_root(green);
2486
2487 assert_eq!(node.kind(), SyntaxKind::STRONG);
2489
2490 assert_eq!(node.text().to_string(), text);
2492 assert!(
2493 node.text().to_string().contains('\n'),
2494 "Should preserve newline in strong content"
2495 );
2496}
2497
2498#[test]
2499fn test_parse_triple_emphasis_multiline() {
2500 use crate::options::ParserOptions;
2502 use crate::syntax::SyntaxNode;
2503 use rowan::GreenNode;
2504
2505 let text = "***both on\nline two***";
2506 let config = ParserOptions::default();
2507 let mut builder = GreenNodeBuilder::new();
2508
2509 let result = try_parse_emphasis(text, 0, text.len(), &config, &mut builder);
2510
2511 assert_eq!(
2513 result,
2514 Some((text.len(), 3)),
2515 "Triple emphasis should parse multiline content"
2516 );
2517
2518 let green: GreenNode = builder.finish();
2520 let node = SyntaxNode::new_root(green);
2521
2522 let has_strong = node.descendants().any(|n| n.kind() == SyntaxKind::STRONG);
2524 assert!(has_strong, "Should have STRONG node");
2525
2526 assert_eq!(node.text().to_string(), text);
2528 assert!(
2529 node.text().to_string().contains('\n'),
2530 "Should preserve newline in triple emphasis content"
2531 );
2532}