1use super::sink::InlineSink;
13use crate::options::{Dialect, ParserOptions};
14use crate::syntax::SyntaxKind;
15#[cfg(test)]
16use rowan::GreenNodeBuilder;
17
18use super::inline_ir::{
19 BracketPlan, ConstructDispo, ConstructPlan, DelimChar, EmphasisKind, EmphasisPlan,
20};
21
22use super::bookdown::{
24 try_parse_bookdown_definition, try_parse_bookdown_reference, try_parse_bookdown_text_reference,
25};
26use super::bracketed_spans::{emit_bracketed_span, try_parse_bracketed_span};
27use super::citations::{
28 emit_bare_citation, emit_bracketed_citation, try_parse_bare_citation,
29 try_parse_bracketed_citation,
30};
31use super::code_spans::{emit_code_span, try_parse_code_span};
32use super::emoji::{emit_emoji, try_parse_emoji};
33use super::escapes::{EscapeType, emit_escape, try_parse_escape};
34use super::inline_executable::{emit_inline_executable, try_parse_inline_executable};
35use super::inline_footnotes::{
36 emit_footnote_reference, emit_inline_footnote, try_parse_footnote_reference,
37 try_parse_inline_footnote,
38};
39use super::inline_html::{emit_inline_html, try_parse_inline_html};
40use super::latex::{parse_latex_command, try_parse_latex_command};
41use super::links::{
42 LinkScanContext, emit_autolink, emit_bare_uri_link, emit_inline_image, emit_inline_link,
43 emit_reference_image, emit_reference_link, emit_unresolved_reference, try_parse_autolink,
44 try_parse_bare_uri, try_parse_inline_image, try_parse_inline_link, try_parse_reference_image,
45 try_parse_reference_link,
46};
47use super::mark::{emit_mark, try_parse_mark};
48use super::math::{
49 emit_display_math, emit_display_math_environment, emit_double_backslash_display_math,
50 emit_double_backslash_inline_math, emit_gfm_inline_math, emit_inline_math,
51 emit_single_backslash_display_math, emit_single_backslash_inline_math, math_opts,
52 try_parse_display_math, try_parse_double_backslash_display_math,
53 try_parse_double_backslash_inline_math, try_parse_gfm_inline_math, try_parse_inline_math,
54 try_parse_math_environment, try_parse_single_backslash_display_math,
55 try_parse_single_backslash_inline_math,
56};
57use super::myst_roles::{emit_role, try_parse_role};
58use super::myst_substitutions::{emit_substitution, try_parse_substitution};
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};
65use super::svelte::{emit_svelte_template, try_parse_svelte_template};
66
67pub fn parse_inline_text_recursive(
86 builder: &mut impl InlineSink,
87 text: &str,
88 config: &ParserOptions,
89 suppress_footnote_refs: bool,
90) {
91 log::trace!(
92 "Recursive inline parsing: {:?} ({} bytes)",
93 &text[..text.len().min(40)],
94 text.len()
95 );
96
97 let mask = structural_byte_mask(config);
98 if try_emit_plain_text_fast_path_with_mask(builder, text, &mask) {
99 log::trace!("Recursive inline parsing complete (plain-text fast path)");
100 return;
101 }
102
103 let plans = super::inline_ir::build_full_plans(text, 0, text.len(), config);
104 parse_inline_range_impl(
105 text,
106 0,
107 text.len(),
108 config,
109 builder,
110 false,
111 &plans.emphasis,
112 &plans.brackets,
113 &plans.constructs,
114 false,
115 suppress_footnote_refs,
116 &mask,
117 );
118
119 log::trace!("Recursive inline parsing complete");
120}
121
122pub fn parse_inline_text(
137 builder: &mut impl InlineSink,
138 text: &str,
139 config: &ParserOptions,
140 suppress_inner_links: bool,
141 suppress_footnote_refs: bool,
142) {
143 log::trace!(
144 "Parsing inline text (nested in link): {:?} ({} bytes)",
145 &text[..text.len().min(40)],
146 text.len()
147 );
148
149 let mask = structural_byte_mask(config);
150 if try_emit_plain_text_fast_path_with_mask(builder, text, &mask) {
151 return;
152 }
153
154 let plans = super::inline_ir::build_full_plans(text, 0, text.len(), config);
155 parse_inline_range_impl(
156 text,
157 0,
158 text.len(),
159 config,
160 builder,
161 true,
162 &plans.emphasis,
163 &plans.brackets,
164 &plans.constructs,
165 suppress_inner_links,
166 suppress_footnote_refs,
167 &mask,
168 );
169}
170
171fn try_emit_plain_text_fast_path_with_mask(
185 builder: &mut impl InlineSink,
186 text: &str,
187 mask: &[bool; 256],
188) -> bool {
189 if text.is_empty() {
190 return false;
191 }
192 for &b in text.as_bytes() {
193 if mask[b as usize] {
194 return false;
195 }
196 }
197 builder.token(SyntaxKind::TEXT.into(), text);
198 true
199}
200
201fn structural_byte_mask(config: &ParserOptions) -> [bool; 256] {
206 let mut mask = [false; 256];
207 let exts = &config.extensions;
208 let pandoc = config.dialect == Dialect::Pandoc;
209
210 mask[b'\n' as usize] = true;
216 mask[b'\r' as usize] = true;
217 mask[b'\\' as usize] = true;
218 mask[b'`' as usize] = true;
219 mask[b'*' as usize] = true;
220 mask[b'_' as usize] = true;
221
222 if exts.inline_links
228 || exts.reference_links
229 || exts.inline_images
230 || exts.bracketed_spans
231 || exts.footnotes
232 || exts.citations
233 {
234 mask[b'[' as usize] = true;
235 mask[b']' as usize] = true;
236 }
237 if exts.inline_images || exts.reference_links {
238 mask[b'!' as usize] = true;
239 }
240
241 if exts.autolinks || exts.raw_html || exts.native_spans {
243 mask[b'<' as usize] = true;
244 }
245
246 if exts.inline_footnotes || exts.superscript {
249 mask[b'^' as usize] = true;
250 }
251
252 if exts.citations || exts.quarto_crossrefs {
259 mask[b'@' as usize] = true;
260 if pandoc {
261 mask[b'-' as usize] = true;
262 }
263 }
264
265 if exts.tex_math_dollars || exts.tex_math_gfm {
267 mask[b'$' as usize] = true;
268 }
269
270 if exts.subscript || exts.strikeout {
272 mask[b'~' as usize] = true;
273 }
274
275 if exts.mark {
276 mask[b'=' as usize] = true;
277 }
278 if exts.emoji {
279 mask[b':' as usize] = true;
280 }
281 if exts.bookdown_references {
282 mask[b'(' as usize] = true;
283 }
284 mask[b'{' as usize] = true;
288
289 if exts.autolink_bare_uris {
298 for b in b'a'..=b'z' {
299 mask[b as usize] = true;
300 }
301 for b in b'A'..=b'Z' {
302 mask[b as usize] = true;
303 }
304 }
305
306 mask
307}
308
309fn bare_uri_has_left_boundary(text: &str, pos: usize) -> bool {
318 if pos == 0 {
319 return true;
320 }
321 match text[..pos].chars().next_back() {
322 Some(prev) => !(prev.is_alphanumeric() || prev == '.'),
323 None => true,
324 }
325}
326
327fn is_emoji_boundary(text: &str, pos: usize) -> bool {
328 if pos > 0 {
329 let prev = text.as_bytes()[pos - 1] as char;
330 if prev.is_ascii_alphanumeric() || prev == '_' {
331 return false;
332 }
333 }
334 true
335}
336
337#[inline]
338fn advance_char_boundary(text: &str, pos: usize, end: usize) -> usize {
339 if pos >= end || pos >= text.len() {
340 return pos;
341 }
342 let ch_len = text[pos..]
343 .chars()
344 .next()
345 .map_or(1, std::primitive::char::len_utf8);
346 (pos + ch_len).min(end)
347}
348
349#[allow(clippy::too_many_arguments)]
350fn parse_inline_range_impl(
351 text: &str,
352 start: usize,
353 end: usize,
354 config: &ParserOptions,
355 builder: &mut impl InlineSink,
356 nested_in_link: bool,
357 plan: &EmphasisPlan,
358 bracket_plan: &BracketPlan,
359 construct_plan: &ConstructPlan,
360 suppress_inner_links: bool,
361 suppress_footnote_refs: bool,
362 mask: &[bool; 256],
363) {
364 log::trace!(
365 "parse_inline_range: start={}, end={}, text={:?}",
366 start,
367 end,
368 &text[start..end]
369 );
370 let mut pos = start;
371 let mut text_start = start;
372 let bytes = text.as_bytes();
373
374 while pos < end {
375 if !mask[bytes[pos] as usize] {
382 let mut next = pos + 1;
383 while next < end && !mask[bytes[next] as usize] {
384 next += 1;
385 }
386 pos = next;
387 if pos >= end {
388 break;
389 }
390 }
391 if let Some(dispo) = construct_plan.lookup(pos) {
400 match *dispo {
401 ConstructDispo::InlineFootnote { end: dispo_end } => {
402 if dispo_end <= end
403 && let Some((len, content)) = try_parse_inline_footnote(&text[pos..])
404 && pos + len == dispo_end
405 {
406 if pos > text_start {
407 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
408 }
409 log::trace!("IR: matched inline footnote at pos {}", pos);
410 emit_inline_footnote(builder, content, config, suppress_footnote_refs);
411 pos += len;
412 text_start = pos;
413 continue;
414 }
415 }
416 ConstructDispo::NativeSpan { end: dispo_end } => {
417 if dispo_end <= end
418 && let Some((len, content, _attributes)) =
419 try_parse_native_span(&text[pos..])
420 && pos + len == dispo_end
421 {
422 if pos > text_start {
423 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
424 }
425 log::trace!("IR: matched native span at pos {}", pos);
426 emit_native_span(
427 builder,
428 &text[pos..pos + len],
429 content,
430 config,
431 suppress_footnote_refs,
432 );
433 pos += len;
434 text_start = pos;
435 continue;
436 }
437 }
438 ConstructDispo::FootnoteReference { end: dispo_end } => {
439 if !suppress_footnote_refs
440 && dispo_end <= end
441 && let Some((len, id)) = try_parse_footnote_reference(&text[pos..])
442 && pos + len == dispo_end
443 {
444 if pos > text_start {
445 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
446 }
447 log::trace!("IR: matched footnote reference at pos {}", pos);
448 emit_footnote_reference(builder, &id);
449 pos += len;
450 text_start = pos;
451 continue;
452 }
453 }
454 ConstructDispo::BracketedCitation { end: dispo_end } => {
455 if dispo_end <= end
456 && let Some((len, content)) = try_parse_bracketed_citation(&text[pos..])
457 && pos + len == dispo_end
458 {
459 if pos > text_start {
460 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
461 }
462 log::trace!("IR: matched bracketed citation at pos {}", pos);
463 emit_bracketed_citation(builder, content);
464 pos += len;
465 text_start = pos;
466 continue;
467 }
468 }
469 ConstructDispo::BareCitation { end: dispo_end } => {
470 if dispo_end <= end
471 && let Some((len, key, has_suppress)) =
472 try_parse_bare_citation(&text[pos..])
473 && pos + len == dispo_end
474 {
475 let is_crossref = config.extensions.quarto_crossrefs
476 && super::citations::is_crossref_key(key, &config.crossref_prefixes);
477 if is_crossref || config.extensions.citations {
478 if pos > text_start {
479 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
480 }
481 if is_crossref {
482 log::trace!("IR: matched Quarto crossref at pos {}: {}", pos, key);
483 super::citations::emit_crossref(builder, key, has_suppress);
484 } else {
485 log::trace!("IR: matched bare citation at pos {}: {}", pos, key);
486 emit_bare_citation(builder, key, has_suppress);
487 }
488 pos += len;
489 text_start = pos;
490 continue;
491 }
492 }
493 }
494 ConstructDispo::BracketedSpan { end: dispo_end } => {
495 if dispo_end <= end
496 && let Some((len, content, attrs)) = try_parse_bracketed_span(&text[pos..])
497 && pos + len == dispo_end
498 {
499 if pos > text_start {
500 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
501 }
502 log::trace!("IR: matched bracketed span at pos {}", pos);
503 emit_bracketed_span(
504 builder,
505 &content,
506 &attrs,
507 config,
508 suppress_footnote_refs,
509 );
510 pos += len;
511 text_start = pos;
512 continue;
513 }
514 }
515 ConstructDispo::WikiLink { end: dispo_end } => {
516 if dispo_end <= end
517 && let Some(span) = super::wikilinks::try_parse_wikilink(text, pos, config)
518 && span.end == dispo_end
519 {
520 if pos > text_start {
521 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
522 }
523 log::trace!("IR: matched wikilink at pos {}", pos);
524 super::wikilinks::emit_wikilink(builder, text, span, config);
525 pos = span.end;
526 text_start = pos;
527 continue;
528 }
529 }
530 }
531 }
532
533 if let Some(super::inline_ir::BracketDispo::UnresolvedReference {
562 is_image,
563 text_start: ref_text_start,
564 text_end: ref_text_end,
565 end: ref_end,
566 }) = bracket_plan.lookup(pos)
567 {
568 let is_image = *is_image;
569 let dispo_suffix_end = *ref_end;
570 let suppress = suppress_inner_links && !is_image;
571 if !suppress {
572 let ctx = LinkScanContext::from_options(config);
573 let is_commonmark = config.dialect == Dialect::CommonMark;
574 if is_image {
575 if config.extensions.inline_images
576 && let Some((len, alt_text, dest, attributes)) =
577 try_parse_inline_image(&text[pos..], ctx)
578 && pos + len >= dispo_suffix_end
579 && pos + len <= end
580 {
581 if pos > text_start {
582 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
583 }
584 log::trace!(
585 "IR: dispatcher overrode UnresolvedReference with inline image at pos {}",
586 pos
587 );
588 emit_inline_image(
589 builder,
590 &text[pos..pos + len],
591 alt_text,
592 dest,
593 attributes,
594 config,
595 suppress_footnote_refs,
596 );
597 pos += len;
598 text_start = pos;
599 continue;
600 }
601 } else if config.extensions.inline_links
602 && let Some((len, link_text, dest, attributes)) =
603 try_parse_inline_link(&text[pos..], is_commonmark, ctx)
604 && pos + len >= dispo_suffix_end
605 && pos + len <= end
606 {
607 if pos > text_start {
608 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
609 }
610 log::trace!(
611 "IR: dispatcher overrode UnresolvedReference with inline link at pos {}",
612 pos
613 );
614 emit_inline_link(
615 builder,
616 &text[pos..pos + len],
617 link_text,
618 dest,
619 attributes,
620 config,
621 suppress_footnote_refs,
622 );
623 pos += len;
624 text_start = pos;
625 continue;
626 }
627 }
628
629 let inner_text = &text[*ref_text_start..*ref_text_end];
631 let suffix_start = *ref_text_end + 1;
632 let label_suffix = if suffix_start < *ref_end {
633 Some(&text[suffix_start..*ref_end])
634 } else {
635 None
636 };
637 if pos > text_start {
638 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
639 }
640 log::trace!(
641 "IR: unresolved Pandoc reference shape at pos {}..{}",
642 pos,
643 ref_end
644 );
645 emit_unresolved_reference(
646 builder,
647 is_image,
648 inner_text,
649 label_suffix,
650 config,
651 suppress_footnote_refs,
652 );
653 pos = *ref_end;
654 text_start = pos;
655 continue;
656 }
657
658 if let Some(super::inline_ir::BracketDispo::Open {
659 is_image,
660 suffix_end,
661 ..
662 }) = bracket_plan.lookup(pos)
663 {
664 let is_image = *is_image;
665 let dispo_suffix_end = *suffix_end;
666 let suppress = suppress_inner_links && !is_image;
667 if !suppress {
668 let ctx = LinkScanContext::from_options(config);
669 let allow_shortcut = config.extensions.shortcut_reference_links;
670 let is_commonmark = config.dialect == Dialect::CommonMark;
671 if is_image {
672 if config.extensions.inline_images
673 && let Some((len, alt_text, dest, attributes)) =
674 try_parse_inline_image(&text[pos..], ctx)
675 && pos + len >= dispo_suffix_end
676 && pos + len <= end
677 {
678 if pos > text_start {
679 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
680 }
681 log::trace!("IR: matched inline image at pos {}", pos);
682 emit_inline_image(
683 builder,
684 &text[pos..pos + len],
685 alt_text,
686 dest,
687 attributes,
688 config,
689 suppress_footnote_refs,
690 );
691 pos += len;
692 text_start = pos;
693 continue;
694 }
695 if config.extensions.reference_links
696 && let Some((len, alt_text, reference, gap, is_shortcut)) =
697 try_parse_reference_image(
698 &text[pos..],
699 allow_shortcut,
700 config.extensions.spaced_reference_links,
701 )
702 && pos + len == dispo_suffix_end
703 && pos + len <= end
704 {
705 if pos > text_start {
706 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
707 }
708 log::trace!("IR: matched reference image at pos {}", pos);
709 emit_reference_image(
710 builder,
711 alt_text,
712 &reference,
713 gap,
714 is_shortcut,
715 config,
716 suppress_footnote_refs,
717 );
718 pos += len;
719 text_start = pos;
720 continue;
721 }
722 } else {
723 if config.extensions.inline_links
724 && let Some((len, link_text, dest, attributes)) =
725 try_parse_inline_link(&text[pos..], is_commonmark, ctx)
726 && pos + len >= dispo_suffix_end
727 && pos + len <= end
728 {
729 if pos > text_start {
730 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
731 }
732 log::trace!("IR: matched inline link at pos {}", pos);
733 emit_inline_link(
734 builder,
735 &text[pos..pos + len],
736 link_text,
737 dest,
738 attributes,
739 config,
740 suppress_footnote_refs,
741 );
742 pos += len;
743 text_start = pos;
744 continue;
745 }
746 if config.extensions.reference_links
747 && let Some((len, link_text, reference, gap, is_shortcut)) =
748 try_parse_reference_link(
749 &text[pos..],
750 allow_shortcut,
751 config.extensions.inline_links,
752 config.extensions.spaced_reference_links,
753 ctx,
754 )
755 && pos + len == dispo_suffix_end
756 && pos + len <= end
757 {
758 if pos > text_start {
759 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
760 }
761 log::trace!("IR: matched reference link at pos {}", pos);
762 emit_reference_link(
763 builder,
764 link_text,
765 &reference,
766 gap,
767 is_shortcut,
768 config,
769 suppress_footnote_refs,
770 );
771 pos += len;
772 text_start = pos;
773 continue;
774 }
775 }
776 }
777 }
778
779 let byte = text.as_bytes()[pos];
780
781 if byte == b'\\' {
783 if config.extensions.tex_math_double_backslash {
785 if let Some((len, content)) = try_parse_double_backslash_display_math(&text[pos..])
786 {
787 if pos > text_start {
788 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
789 }
790 log::trace!("Matched double backslash display math at pos {}", pos);
791 emit_double_backslash_display_math(builder, content, math_opts(config));
792 pos += len;
793 text_start = pos;
794 continue;
795 }
796
797 if let Some((len, content)) = try_parse_double_backslash_inline_math(
799 &text[pos..],
800 config.dialect == Dialect::Pandoc,
801 ) {
802 if pos > text_start {
803 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
804 }
805 log::trace!("Matched double backslash inline math at pos {}", pos);
806 emit_double_backslash_inline_math(builder, content, math_opts(config));
807 pos += len;
808 text_start = pos;
809 continue;
810 }
811 }
812
813 if config.extensions.tex_math_single_backslash {
815 if let Some((len, content)) = try_parse_single_backslash_display_math(&text[pos..])
816 {
817 if pos > text_start {
818 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
819 }
820 log::trace!("Matched single backslash display math at pos {}", pos);
821 emit_single_backslash_display_math(builder, content, math_opts(config));
822 pos += len;
823 text_start = pos;
824 continue;
825 }
826
827 if let Some((len, content)) = try_parse_single_backslash_inline_math(
829 &text[pos..],
830 config.dialect == Dialect::Pandoc,
831 ) {
832 if pos > text_start {
833 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
834 }
835 log::trace!("Matched single backslash inline math at pos {}", pos);
836 emit_single_backslash_inline_math(builder, content, math_opts(config));
837 pos += len;
838 text_start = pos;
839 continue;
840 }
841 }
842
843 if config.extensions.raw_tex
845 && let Some((len, begin_marker, content, end_marker)) =
846 try_parse_math_environment(&text[pos..])
847 {
848 if pos > text_start {
849 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
850 }
851 log::trace!("Matched math environment at pos {}", pos);
852 emit_display_math_environment(
853 builder,
854 begin_marker,
855 content,
856 end_marker,
857 math_opts(config),
858 );
859 pos += len;
860 text_start = pos;
861 continue;
862 }
863
864 if config.extensions.bookdown_references
866 && let Some((len, label)) = try_parse_bookdown_reference(&text[pos..])
867 {
868 if pos > text_start {
869 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
870 }
871 log::trace!("Matched bookdown reference at pos {}: {}", pos, label);
872 super::citations::emit_bookdown_crossref(builder, label);
873 pos += len;
874 text_start = pos;
875 continue;
876 }
877
878 if let Some((len, ch, escape_type)) = try_parse_escape(&text[pos..]) {
880 let escape_enabled = match escape_type {
881 EscapeType::HardLineBreak => config.extensions.escaped_line_breaks,
882 EscapeType::NonbreakingSpace => config.extensions.all_symbols_escapable,
883 EscapeType::Literal => {
884 const BASE_ESCAPABLE: &str = "\\`*_{}[]()>#+-.!|~";
897 BASE_ESCAPABLE.contains(ch)
898 || config.extensions.all_symbols_escapable
899 || (config.dialect == crate::Dialect::CommonMark
900 && ch.is_ascii_punctuation())
901 }
902 };
903 if !escape_enabled {
904 pos = advance_char_boundary(text, pos, end);
907 continue;
908 }
909
910 if pos > text_start {
912 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
913 }
914
915 log::trace!("Matched escape at pos {}: \\{}", pos, ch);
916 emit_escape(builder, ch, escape_type);
917 pos += len;
918 text_start = pos;
919 continue;
920 }
921
922 if config.extensions.raw_tex
924 && let Some(len) = try_parse_latex_command(&text[pos..])
925 {
926 if pos > text_start {
927 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
928 }
929 log::trace!("Matched LaTeX command at pos {}", pos);
930 parse_latex_command(builder, &text[pos..], len);
931 pos += len;
932 text_start = pos;
933 continue;
934 }
935 }
936
937 if byte == b'{'
942 && config.extensions.svelte_template
943 && let Some((len, kind, content)) = try_parse_svelte_template(&text[pos..])
944 {
945 if pos > text_start {
946 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
947 }
948 log::trace!("Matched Svelte template span at pos {}: {:?}", pos, kind);
949 emit_svelte_template(builder, kind, &content);
950 pos += len;
951 text_start = pos;
952 continue;
953 }
954
955 if byte == b'{'
960 && config.extensions.myst_roles
961 && let Some(role) = try_parse_role(&text[pos..])
962 {
963 if pos > text_start {
964 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
965 }
966 emit_role(builder, &text[pos..pos + role.total_len], role);
967 pos += role.total_len;
968 text_start = pos;
969 continue;
970 }
971
972 if byte == b'{'
975 && config.extensions.myst_substitutions
976 && let Some((len, inner_len)) = try_parse_substitution(&text[pos..])
977 {
978 if pos > text_start {
979 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
980 }
981 emit_substitution(builder, &text[pos..pos + len], inner_len);
982 pos += len;
983 text_start = pos;
984 continue;
985 }
986
987 if byte == b'{'
989 && pos + 1 < text.len()
990 && text.as_bytes()[pos + 1] == b'{'
991 && let Some((len, name, attrs)) = try_parse_shortcode(&text[pos..])
992 {
993 if pos > text_start {
994 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
995 }
996 log::trace!("Matched shortcode at pos {}: {}", pos, &name);
997 emit_shortcode(builder, &name, attrs);
998 pos += len;
999 text_start = pos;
1000 continue;
1001 }
1002
1003 if byte == b'`'
1005 && let Some(m) = try_parse_inline_executable(
1006 &text[pos..],
1007 config.extensions.rmarkdown_inline_code,
1008 config.extensions.quarto_inline_code,
1009 )
1010 {
1011 if pos > text_start {
1012 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1013 }
1014 log::trace!("Matched inline executable code at pos {}", pos);
1015 emit_inline_executable(builder, &m);
1016 pos += m.total_len;
1017 text_start = pos;
1018 continue;
1019 }
1020
1021 if byte == b'`' {
1023 if let Some((len, content, backtick_count, attributes)) =
1024 try_parse_code_span(&text[pos..])
1025 {
1026 if pos > text_start {
1028 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1029 }
1030
1031 log::trace!(
1032 "Matched code span at pos {}: {} backticks",
1033 pos,
1034 backtick_count
1035 );
1036
1037 if let Some((ref attrs, raw_attr)) = attributes
1039 && config.extensions.raw_attribute
1040 && let Some(format) = is_raw_inline(attrs)
1041 {
1042 use super::raw_inline::emit_raw_inline;
1043 log::trace!("Matched raw inline span at pos {}: format={}", pos, format);
1044 emit_raw_inline(builder, content, backtick_count, raw_attr);
1045 } else if !config.extensions.inline_code_attributes && attributes.is_some() {
1046 let code_span_len = backtick_count * 2 + content.len();
1047 emit_code_span(builder, content, backtick_count, None);
1048 pos += code_span_len;
1049 text_start = pos;
1050 continue;
1051 } else {
1052 emit_code_span(
1053 builder,
1054 content,
1055 backtick_count,
1056 attributes.as_ref().map(|(_, raw)| *raw),
1057 );
1058 }
1059
1060 pos += len;
1061 text_start = pos;
1062 continue;
1063 }
1064
1065 if config.dialect == Dialect::CommonMark {
1074 let run_len = text[pos..].bytes().take_while(|&b| b == b'`').count();
1075 pos += run_len;
1076 continue;
1077 }
1078 }
1079
1080 if byte == b':'
1082 && config.extensions.emoji
1083 && is_emoji_boundary(text, pos)
1084 && let Some((len, _alias)) = try_parse_emoji(&text[pos..])
1085 {
1086 if pos > text_start {
1087 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1088 }
1089 log::trace!("Matched emoji at pos {}", pos);
1090 emit_emoji(builder, &text[pos..pos + len]);
1091 pos += len;
1092 text_start = pos;
1093 continue;
1094 }
1095
1096 if byte == b'^'
1101 && pos + 1 < text.len()
1102 && text.as_bytes()[pos + 1] == b'['
1103 && config.dialect == Dialect::CommonMark
1104 && config.extensions.inline_footnotes
1105 && let Some((len, content)) = try_parse_inline_footnote(&text[pos..])
1106 {
1107 if pos > text_start {
1108 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1109 }
1110 log::trace!("Matched inline footnote at pos {}", pos);
1111 emit_inline_footnote(builder, content, config, suppress_footnote_refs);
1112 pos += len;
1113 text_start = pos;
1114 continue;
1115 }
1116
1117 if byte == b'^'
1119 && config.extensions.superscript
1120 && let Some((len, content)) = try_parse_superscript(&text[pos..])
1121 {
1122 if pos > text_start {
1123 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1124 }
1125 log::trace!("Matched superscript at pos {}", pos);
1126 emit_superscript(builder, content, config, suppress_footnote_refs);
1127 pos += len;
1128 text_start = pos;
1129 continue;
1130 }
1131
1132 if byte == b'(' && config.extensions.bookdown_references {
1134 if let Some((len, label)) = try_parse_bookdown_definition(&text[pos..]) {
1135 if pos > text_start {
1136 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1137 }
1138 log::trace!("Matched bookdown definition at pos {}: {}", pos, label);
1139 builder.token(SyntaxKind::TEXT.into(), &text[pos..pos + len]);
1140 pos += len;
1141 text_start = pos;
1142 continue;
1143 }
1144 if let Some((len, label)) = try_parse_bookdown_text_reference(&text[pos..]) {
1145 if pos > text_start {
1146 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1147 }
1148 log::trace!("Matched bookdown text reference at pos {}: {}", pos, label);
1149 builder.token(SyntaxKind::TEXT.into(), &text[pos..pos + len]);
1150 pos += len;
1151 text_start = pos;
1152 continue;
1153 }
1154 }
1155
1156 if byte == b'~'
1162 && config.extensions.strikeout
1163 && let Some((len, content)) = try_parse_strikeout(&text[pos..])
1164 {
1165 if pos > text_start {
1166 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1167 }
1168 log::trace!("Matched strikeout at pos {}", pos);
1169 emit_strikeout(builder, content, config, suppress_footnote_refs);
1170 pos += len;
1171 text_start = pos;
1172 continue;
1173 }
1174
1175 if byte == b'~'
1178 && config.extensions.subscript
1179 && let Some((len, content)) = try_parse_subscript(&text[pos..])
1180 {
1181 if pos > text_start {
1182 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1183 }
1184 log::trace!("Matched subscript at pos {}", pos);
1185 emit_subscript(builder, content, config, suppress_footnote_refs);
1186 pos += len;
1187 text_start = pos;
1188 continue;
1189 }
1190
1191 if byte == b'='
1193 && config.extensions.mark
1194 && let Some((len, content)) = try_parse_mark(&text[pos..])
1195 {
1196 if pos > text_start {
1197 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1198 }
1199 log::trace!("Matched mark at pos {}", pos);
1200 emit_mark(builder, content, config, suppress_footnote_refs);
1201 pos += len;
1202 text_start = pos;
1203 continue;
1204 }
1205
1206 if byte == b'$'
1208 && config.extensions.tex_math_gfm
1209 && let Some((len, content)) =
1210 try_parse_gfm_inline_math(&text[pos..], config.dialect == Dialect::Pandoc)
1211 {
1212 if pos > text_start {
1213 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1214 }
1215 log::trace!("Matched GFM inline math at pos {}", pos);
1216 emit_gfm_inline_math(builder, content, math_opts(config));
1217 pos += len;
1218 text_start = pos;
1219 continue;
1220 }
1221
1222 if byte == b'$' && config.extensions.tex_math_dollars {
1224 if let Some((len, content)) = try_parse_display_math(&text[pos..]) {
1226 if pos > text_start {
1228 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1229 }
1230
1231 let dollar_count = text[pos..].chars().take_while(|&c| c == '$').count();
1232 log::trace!(
1233 "Matched display math at pos {}: {} dollars",
1234 pos,
1235 dollar_count
1236 );
1237
1238 let after_math = &text[pos + len..];
1244 let line_end = after_math.find('\n').unwrap_or(after_math.len());
1245 let line_segment = &after_math[..line_end];
1246 let attr_len = if config.extensions.quarto_crossrefs {
1247 use crate::parser::utils::attributes::try_parse_trailing_attributes;
1248 if let Some((_attr_block, _)) = try_parse_trailing_attributes(line_segment) {
1249 let trimmed_after = line_segment.trim_start();
1250 if let Some(open_brace_pos) = trimmed_after.find('{') {
1251 let ws_before_brace = line_segment.len() - trimmed_after.len();
1252 let attr_text_len = trimmed_after[open_brace_pos..]
1253 .find('}')
1254 .map(|close| close + 1)
1255 .unwrap_or(0);
1256 ws_before_brace + open_brace_pos + attr_text_len
1257 } else {
1258 0
1259 }
1260 } else {
1261 0
1262 }
1263 } else {
1264 0
1265 };
1266
1267 let total_len = len + attr_len;
1268 emit_display_math(builder, content, dollar_count, math_opts(config));
1269
1270 if attr_len > 0 {
1273 use crate::parser::utils::attributes::emit_attribute_node;
1274 let attr_text = &text[pos + len..pos + total_len];
1275 let trimmed_after = attr_text.trim_start();
1276 let ws_len = attr_text.len() - trimmed_after.len();
1277 if ws_len > 0 {
1278 builder.token(SyntaxKind::WHITESPACE.into(), &attr_text[..ws_len]);
1279 }
1280 emit_attribute_node(builder, trimmed_after);
1281 }
1282
1283 pos += total_len;
1284 text_start = pos;
1285 continue;
1286 }
1287
1288 if let Some((len, content)) =
1290 try_parse_inline_math(&text[pos..], config.dialect == Dialect::Pandoc)
1291 {
1292 if pos > text_start {
1294 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1295 }
1296
1297 log::trace!("Matched inline math at pos {}", pos);
1298 emit_inline_math(builder, content, math_opts(config));
1299 pos += len;
1300 text_start = pos;
1301 continue;
1302 }
1303
1304 if pos > text_start {
1307 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1308 }
1309 builder.token(SyntaxKind::TEXT.into(), "$");
1310 pos = advance_char_boundary(text, pos, end);
1311 text_start = pos;
1312 continue;
1313 }
1314
1315 if byte == b'<'
1317 && config.extensions.autolinks
1318 && let Some((len, url)) = try_parse_autolink(
1319 &text[pos..],
1320 config.dialect == crate::options::Dialect::CommonMark,
1321 )
1322 {
1323 if pos > text_start {
1324 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1325 }
1326 log::trace!("Matched autolink at pos {}", pos);
1327 emit_autolink(builder, &text[pos..pos + len], url);
1328 pos += len;
1329 text_start = pos;
1330 continue;
1331 }
1332
1333 if !nested_in_link
1334 && config.extensions.autolink_bare_uris
1335 && bare_uri_has_left_boundary(text, pos)
1336 && let Some((len, url)) = try_parse_bare_uri(&text[pos..])
1337 {
1338 if pos > text_start {
1339 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1340 }
1341 log::trace!("Matched bare URI at pos {}", pos);
1342 emit_bare_uri_link(builder, url, config);
1343 pos += len;
1344 text_start = pos;
1345 continue;
1346 }
1347
1348 if byte == b'<'
1354 && config.dialect == Dialect::CommonMark
1355 && config.extensions.native_spans
1356 && let Some((len, content, _attributes)) = try_parse_native_span(&text[pos..])
1357 {
1358 if pos > text_start {
1359 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1360 }
1361 log::trace!("Matched native span at pos {}", pos);
1362 emit_native_span(
1363 builder,
1364 &text[pos..pos + len],
1365 content,
1366 config,
1367 suppress_footnote_refs,
1368 );
1369 pos += len;
1370 text_start = pos;
1371 continue;
1372 }
1373
1374 if byte == b'<'
1378 && config.extensions.raw_html
1379 && let Some(len) = try_parse_inline_html(&text[pos..], config.dialect)
1380 {
1381 if pos > text_start {
1382 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1383 }
1384 log::trace!("Matched inline raw HTML at pos {}", pos);
1385 emit_inline_html(builder, &text[pos..pos + len]);
1386 pos += len;
1387 text_start = pos;
1388 continue;
1389 }
1390
1391 if byte == b'['
1399 && config.dialect == Dialect::CommonMark
1400 && config.extensions.footnotes
1401 && !suppress_footnote_refs
1402 && let Some((len, id)) = try_parse_footnote_reference(&text[pos..])
1403 {
1404 if pos > text_start {
1405 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1406 }
1407 log::trace!("Matched footnote reference at pos {}", pos);
1408 emit_footnote_reference(builder, &id);
1409 pos += len;
1410 text_start = pos;
1411 continue;
1412 }
1413 if byte == b'['
1414 && config.dialect == Dialect::CommonMark
1415 && config.extensions.citations
1416 && let Some((len, content)) = try_parse_bracketed_citation(&text[pos..])
1417 {
1418 if pos > text_start {
1419 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1420 }
1421 log::trace!("Matched bracketed citation at pos {}", pos);
1422 emit_bracketed_citation(builder, content);
1423 pos += len;
1424 text_start = pos;
1425 continue;
1426 }
1427
1428 if config.dialect == Dialect::CommonMark
1434 && byte == b'['
1435 && config.extensions.bracketed_spans
1436 && let Some((len, text_content, attrs)) = try_parse_bracketed_span(&text[pos..])
1437 {
1438 if pos > text_start {
1439 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1440 }
1441 log::trace!("Matched bracketed span at pos {}", pos);
1442 emit_bracketed_span(
1443 builder,
1444 &text_content,
1445 &attrs,
1446 config,
1447 suppress_footnote_refs,
1448 );
1449 pos += len;
1450 text_start = pos;
1451 continue;
1452 }
1453
1454 if config.dialect == Dialect::CommonMark
1460 && byte == b'@'
1461 && (config.extensions.citations || config.extensions.quarto_crossrefs)
1462 && let Some((len, key, has_suppress)) = try_parse_bare_citation(&text[pos..])
1463 {
1464 let is_crossref = config.extensions.quarto_crossrefs
1465 && super::citations::is_crossref_key(key, &config.crossref_prefixes);
1466 if is_crossref || config.extensions.citations {
1467 if pos > text_start {
1468 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1469 }
1470 if is_crossref {
1471 log::trace!("Matched Quarto crossref at pos {}: {}", pos, &key);
1472 super::citations::emit_crossref(builder, key, has_suppress);
1473 } else {
1474 log::trace!("Matched bare citation at pos {}: {}", pos, &key);
1475 emit_bare_citation(builder, key, has_suppress);
1476 }
1477 pos += len;
1478 text_start = pos;
1479 continue;
1480 }
1481 }
1482
1483 if config.dialect == Dialect::CommonMark
1488 && byte == b'-'
1489 && pos + 1 < text.len()
1490 && text.as_bytes()[pos + 1] == b'@'
1491 && (config.extensions.citations || config.extensions.quarto_crossrefs)
1492 && let Some((len, key, has_suppress)) = try_parse_bare_citation(&text[pos..])
1493 {
1494 let is_crossref = config.extensions.quarto_crossrefs
1495 && super::citations::is_crossref_key(key, &config.crossref_prefixes);
1496 if is_crossref || config.extensions.citations {
1497 if pos > text_start {
1498 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1499 }
1500 if is_crossref {
1501 log::trace!("Matched Quarto crossref at pos {}: {}", pos, &key);
1502 super::citations::emit_crossref(builder, key, has_suppress);
1503 } else {
1504 log::trace!("Matched suppress-author citation at pos {}: {}", pos, &key);
1505 emit_bare_citation(builder, key, has_suppress);
1506 }
1507 pos += len;
1508 text_start = pos;
1509 continue;
1510 }
1511 }
1512
1513 if byte == b'*' || byte == b'_' {
1518 match plan.lookup(pos) {
1519 Some(DelimChar::Open {
1520 len,
1521 partner,
1522 partner_len,
1523 kind,
1524 }) => {
1525 if pos > text_start {
1526 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1527 }
1528 let len = len as usize;
1529 let partner_len = partner_len as usize;
1530 let (wrapper_kind, marker_kind) = match kind {
1531 EmphasisKind::Strong => (SyntaxKind::STRONG, SyntaxKind::STRONG_MARKER),
1532 EmphasisKind::Emph => (SyntaxKind::EMPHASIS, SyntaxKind::EMPHASIS_MARKER),
1533 };
1534 builder.start_node(wrapper_kind.into());
1535 builder.token(marker_kind.into(), &text[pos..pos + len]);
1536 parse_inline_range_impl(
1537 text,
1538 pos + len,
1539 partner,
1540 config,
1541 builder,
1542 nested_in_link,
1543 plan,
1544 bracket_plan,
1545 construct_plan,
1546 suppress_inner_links,
1547 suppress_footnote_refs,
1548 mask,
1549 );
1550 builder.token(marker_kind.into(), &text[partner..partner + partner_len]);
1551 builder.finish_node();
1552 pos = partner + partner_len;
1553 text_start = pos;
1554 continue;
1555 }
1556 Some(DelimChar::Close) => {
1557 pos += 1;
1564 continue;
1565 }
1566 Some(DelimChar::Literal) | None => {
1567 let bytes = text.as_bytes();
1573 let mut end_pos = pos + 1;
1574 while end_pos < end && bytes[end_pos] == byte {
1575 match plan.lookup(end_pos) {
1576 Some(DelimChar::Literal) | None => end_pos += 1,
1577 _ => break,
1578 }
1579 }
1580 pos = end_pos;
1581 continue;
1582 }
1583 }
1584 }
1585
1586 if byte == b'\r' && pos + 1 < end && text.as_bytes()[pos + 1] == b'\n' {
1588 let text_before = &text[text_start..pos];
1589
1590 let trailing_spaces = text_before.chars().rev().take_while(|&c| c == ' ').count();
1592 if trailing_spaces >= 2 {
1593 let text_content = &text_before[..text_before.len() - trailing_spaces];
1595 if !text_content.is_empty() {
1596 builder.token(SyntaxKind::TEXT.into(), text_content);
1597 }
1598 let spaces = " ".repeat(trailing_spaces);
1599 builder.token(
1600 SyntaxKind::HARD_LINE_BREAK.into(),
1601 &format!("{}\r\n", spaces),
1602 );
1603 pos += 2;
1604 text_start = pos;
1605 continue;
1606 }
1607
1608 if config.extensions.hard_line_breaks {
1610 if !text_before.is_empty() {
1611 builder.token(SyntaxKind::TEXT.into(), text_before);
1612 }
1613 builder.token(SyntaxKind::HARD_LINE_BREAK.into(), "\r\n");
1614 pos += 2;
1615 text_start = pos;
1616 continue;
1617 }
1618
1619 if !text_before.is_empty() {
1621 builder.token(SyntaxKind::TEXT.into(), text_before);
1622 }
1623 builder.token(SyntaxKind::NEWLINE.into(), "\r\n");
1624 pos += 2;
1625 text_start = pos;
1626 continue;
1627 }
1628
1629 if byte == b'\n' {
1630 let text_before = &text[text_start..pos];
1631
1632 let trailing_spaces = text_before.chars().rev().take_while(|&c| c == ' ').count();
1634 if trailing_spaces >= 2 {
1635 let text_content = &text_before[..text_before.len() - trailing_spaces];
1637 if !text_content.is_empty() {
1638 builder.token(SyntaxKind::TEXT.into(), text_content);
1639 }
1640 let spaces = " ".repeat(trailing_spaces);
1641 builder.token(SyntaxKind::HARD_LINE_BREAK.into(), &format!("{}\n", spaces));
1642 pos += 1;
1643 text_start = pos;
1644 continue;
1645 }
1646
1647 if config.extensions.hard_line_breaks {
1649 if !text_before.is_empty() {
1650 builder.token(SyntaxKind::TEXT.into(), text_before);
1651 }
1652 builder.token(SyntaxKind::HARD_LINE_BREAK.into(), "\n");
1653 pos += 1;
1654 text_start = pos;
1655 continue;
1656 }
1657
1658 if !text_before.is_empty() {
1660 builder.token(SyntaxKind::TEXT.into(), text_before);
1661 }
1662 builder.token(SyntaxKind::NEWLINE.into(), "\n");
1663 pos += 1;
1664 text_start = pos;
1665 continue;
1666 }
1667
1668 pos = advance_char_boundary(text, pos, end);
1670 }
1671
1672 if pos > text_start && text_start < end {
1674 log::trace!("Emitting remaining TEXT: {:?}", &text[text_start..end]);
1675 builder.token(SyntaxKind::TEXT.into(), &text[text_start..end]);
1676 }
1677
1678 log::trace!("parse_inline_range complete: start={}, end={}", start, end);
1679}
1680
1681#[cfg(test)]
1682mod tests {
1683 use super::*;
1684 use crate::syntax::{SyntaxKind, SyntaxNode};
1685 use rowan::GreenNode;
1686
1687 #[test]
1688 fn test_recursive_simple_emphasis() {
1689 let text = "*test*";
1690 let config = ParserOptions::default();
1691 let mut builder = GreenNodeBuilder::new();
1692
1693 parse_inline_text_recursive(&mut builder, text, &config, false);
1694
1695 let green: GreenNode = builder.finish();
1696 let node = SyntaxNode::new_root(green);
1697
1698 assert_eq!(node.text().to_string(), text);
1700
1701 let has_emph = node.descendants().any(|n| n.kind() == SyntaxKind::EMPHASIS);
1703 assert!(has_emph, "Should have EMPHASIS node");
1704 }
1705
1706 #[test]
1707 fn test_recursive_nested() {
1708 let text = "*foo **bar** baz*";
1709 let config = ParserOptions::default();
1710 let mut builder = GreenNodeBuilder::new();
1711
1712 builder.start_node(SyntaxKind::PARAGRAPH.into());
1714 parse_inline_text_recursive(&mut builder, text, &config, false);
1715 builder.finish_node();
1716
1717 let green: GreenNode = builder.finish();
1718 let node = SyntaxNode::new_root(green);
1719
1720 assert_eq!(node.text().to_string(), text);
1722
1723 let has_emph = node.descendants().any(|n| n.kind() == SyntaxKind::EMPHASIS);
1725 let has_strong = node.descendants().any(|n| n.kind() == SyntaxKind::STRONG);
1726
1727 assert!(has_emph, "Should have EMPHASIS node");
1728 assert!(has_strong, "Should have STRONG node");
1729 }
1730
1731 #[test]
1734 fn test_triple_emphasis_star_then_double_star() {
1735 use crate::options::ParserOptions;
1736 use crate::syntax::SyntaxNode;
1737 use rowan::GreenNode;
1738
1739 let text = "***foo* bar**";
1740 let config = ParserOptions::default();
1741 let mut builder = GreenNodeBuilder::new();
1742
1743 builder.start_node(SyntaxKind::DOCUMENT.into());
1744 parse_inline_text_recursive(&mut builder, text, &config, false);
1745 builder.finish_node();
1746
1747 let green: GreenNode = builder.finish();
1748 let node = SyntaxNode::new_root(green);
1749
1750 assert_eq!(node.text().to_string(), text);
1752
1753 let structure = format!("{:#?}", node);
1756
1757 assert!(structure.contains("STRONG"), "Should have STRONG node");
1759 assert!(structure.contains("EMPHASIS"), "Should have EMPHASIS node");
1760
1761 let mut found_strong = false;
1764 let mut found_emph_after_strong = false;
1765 for descendant in node.descendants() {
1766 if descendant.kind() == SyntaxKind::STRONG {
1767 found_strong = true;
1768 }
1769 if found_strong && descendant.kind() == SyntaxKind::EMPHASIS {
1770 found_emph_after_strong = true;
1771 break;
1772 }
1773 }
1774
1775 assert!(
1776 found_emph_after_strong,
1777 "EMPH should be inside STRONG, not before it. Current structure:\n{}",
1778 structure
1779 );
1780 }
1781
1782 #[test]
1785 fn test_triple_emphasis_double_star_then_star() {
1786 use crate::options::ParserOptions;
1787 use crate::syntax::SyntaxNode;
1788 use rowan::GreenNode;
1789
1790 let text = "***foo** bar*";
1791 let config = ParserOptions::default();
1792 let mut builder = GreenNodeBuilder::new();
1793
1794 builder.start_node(SyntaxKind::DOCUMENT.into());
1795 parse_inline_text_recursive(&mut builder, text, &config, false);
1796 builder.finish_node();
1797
1798 let green: GreenNode = builder.finish();
1799 let node = SyntaxNode::new_root(green);
1800
1801 assert_eq!(node.text().to_string(), text);
1803
1804 let structure = format!("{:#?}", node);
1806
1807 assert!(structure.contains("EMPHASIS"), "Should have EMPHASIS node");
1809 assert!(structure.contains("STRONG"), "Should have STRONG node");
1810
1811 let mut found_emph = false;
1813 let mut found_strong_after_emph = false;
1814 for descendant in node.descendants() {
1815 if descendant.kind() == SyntaxKind::EMPHASIS {
1816 found_emph = true;
1817 }
1818 if found_emph && descendant.kind() == SyntaxKind::STRONG {
1819 found_strong_after_emph = true;
1820 break;
1821 }
1822 }
1823
1824 assert!(
1825 found_strong_after_emph,
1826 "STRONG should be inside EMPH. Current structure:\n{}",
1827 structure
1828 );
1829 }
1830
1831 #[test]
1834 fn test_display_math_with_attributes() {
1835 use crate::options::ParserOptions;
1836 use crate::syntax::SyntaxNode;
1837 use rowan::GreenNode;
1838
1839 let text = "$$ E = mc^2 $$ {#eq-einstein}";
1840 let mut config = ParserOptions::default();
1841 config.extensions.quarto_crossrefs = true; let mut builder = GreenNodeBuilder::new();
1844 builder.start_node(SyntaxKind::DOCUMENT.into()); parse_inline_text_recursive(&mut builder, text, &config, false);
1848
1849 builder.finish_node(); let green: GreenNode = builder.finish();
1851 let node = SyntaxNode::new_root(green);
1852
1853 assert_eq!(node.text().to_string(), text);
1855
1856 let has_display_math = node
1858 .descendants()
1859 .any(|n| n.kind() == SyntaxKind::DISPLAY_MATH);
1860 assert!(has_display_math, "Should have DISPLAY_MATH node");
1861
1862 let has_attributes = node
1864 .descendants()
1865 .any(|n| n.kind() == SyntaxKind::ATTRIBUTE);
1866 assert!(
1867 has_attributes,
1868 "Should have ATTRIBUTE node for {{#eq-einstein}}"
1869 );
1870
1871 let math_followed_by_text = node.descendants().any(|n| {
1873 n.kind() == SyntaxKind::DISPLAY_MATH
1874 && n.next_sibling()
1875 .map(|s| {
1876 s.kind() == SyntaxKind::TEXT
1877 && s.text().to_string().contains("{#eq-einstein}")
1878 })
1879 .unwrap_or(false)
1880 });
1881 assert!(
1882 !math_followed_by_text,
1883 "Attributes should not be parsed as TEXT"
1884 );
1885 }
1886
1887 #[test]
1888 fn test_parse_inline_text_gfm_inline_link_destination_not_autolinked() {
1889 use crate::options::{Dialect, Extensions, Flavor};
1890
1891 let config = ParserOptions {
1892 flavor: Flavor::Gfm,
1893 dialect: Dialect::for_flavor(Flavor::Gfm),
1894 extensions: Extensions::for_flavor(Flavor::Gfm),
1895 ..ParserOptions::default()
1896 };
1897
1898 let mut builder = GreenNodeBuilder::new();
1899 builder.start_node(SyntaxKind::PARAGRAPH.into());
1900 parse_inline_text_recursive(
1901 &mut builder,
1902 "Second Link [link_text](https://link.com)",
1903 &config,
1904 false,
1905 );
1906 builder.finish_node();
1907 let green = builder.finish();
1908 let root = SyntaxNode::new_root(green);
1909
1910 let links: Vec<_> = root
1911 .descendants()
1912 .filter(|n| n.kind() == SyntaxKind::LINK)
1913 .collect();
1914 assert_eq!(
1915 links.len(),
1916 1,
1917 "Expected exactly one LINK node for inline link, not nested bare URI autolink"
1918 );
1919
1920 let link = links[0].clone();
1921 let mut link_text = None::<String>;
1922 let mut link_dest = None::<String>;
1923
1924 for child in link.children() {
1925 match child.kind() {
1926 SyntaxKind::LINK_TEXT => link_text = Some(child.text().to_string()),
1927 SyntaxKind::LINK_DEST => link_dest = Some(child.text().to_string()),
1928 _ => {}
1929 }
1930 }
1931
1932 assert_eq!(link_text.as_deref(), Some("link_text"));
1933 assert_eq!(link_dest.as_deref(), Some("https://link.com"));
1934 }
1935
1936 #[test]
1937 fn test_autolink_bare_uri_utf8_boundary_safe() {
1938 let text = "§";
1939 let mut config = ParserOptions::default();
1940 config.extensions.autolink_bare_uris = true;
1941 let mut builder = GreenNodeBuilder::new();
1942
1943 builder.start_node(SyntaxKind::DOCUMENT.into());
1944 parse_inline_text_recursive(&mut builder, text, &config, false);
1945 builder.finish_node();
1946
1947 let green: GreenNode = builder.finish();
1948 let node = SyntaxNode::new_root(green);
1949 assert_eq!(node.text().to_string(), text);
1950 }
1951
1952 #[test]
1953 fn test_autolink_bare_uri_is_lossless_without_synthetic_brackets() {
1954 let text = "https://example.com/path";
1959 let mut config = ParserOptions::default();
1960 config.extensions.autolink_bare_uris = true;
1961 let mut builder = GreenNodeBuilder::new();
1962
1963 builder.start_node(SyntaxKind::PARAGRAPH.into());
1964 parse_inline_text_recursive(&mut builder, text, &config, false);
1965 builder.finish_node();
1966
1967 let green: GreenNode = builder.finish();
1968 let node = SyntaxNode::new_root(green);
1969
1970 assert_eq!(node.text().to_string(), text);
1972 assert!(
1974 node.descendants()
1975 .any(|n| n.kind() == SyntaxKind::AUTO_LINK),
1976 "bare URI should be an AUTO_LINK node"
1977 );
1978 assert!(
1979 !node.descendants().any(|n| n.kind() == SyntaxKind::LINK),
1980 "bare URI must not become a bracketed LINK node"
1981 );
1982 }
1983
1984 #[test]
1985 fn test_bare_uri_requires_left_word_boundary() {
1986 let text = "**Nonlinear least squares:** Gauss-Newton";
1993 let mut config = ParserOptions::default();
1994 config.extensions.autolink_bare_uris = true;
1995 let mut builder = GreenNodeBuilder::new();
1996
1997 builder.start_node(SyntaxKind::PARAGRAPH.into());
1998 parse_inline_text_recursive(&mut builder, text, &config, false);
1999 builder.finish_node();
2000
2001 let green: GreenNode = builder.finish();
2002 let node = SyntaxNode::new_root(green);
2003
2004 assert_eq!(node.text().to_string(), text);
2005 assert!(
2006 !node
2007 .descendants()
2008 .any(|n| n.kind() == SyntaxKind::AUTO_LINK),
2009 "mid-word `res:` must not become an AUTO_LINK"
2010 );
2011 }
2012
2013 #[test]
2014 fn test_bare_uri_still_matches_at_word_boundary() {
2015 let text = "see res:foo here";
2018 let mut config = ParserOptions::default();
2019 config.extensions.autolink_bare_uris = true;
2020 let mut builder = GreenNodeBuilder::new();
2021
2022 builder.start_node(SyntaxKind::PARAGRAPH.into());
2023 parse_inline_text_recursive(&mut builder, text, &config, false);
2024 builder.finish_node();
2025
2026 let green: GreenNode = builder.finish();
2027 let node = SyntaxNode::new_root(green);
2028
2029 assert_eq!(node.text().to_string(), text);
2030 assert!(
2031 node.descendants()
2032 .any(|n| n.kind() == SyntaxKind::AUTO_LINK),
2033 "standalone bare URI should still be an AUTO_LINK"
2034 );
2035 }
2036
2037 #[test]
2038 fn test_parse_emphasis_unicode_content_no_panic() {
2039 let text = "*§*";
2040 let config = ParserOptions::default();
2041 let mut builder = GreenNodeBuilder::new();
2042
2043 builder.start_node(SyntaxKind::PARAGRAPH.into());
2044 parse_inline_text_recursive(&mut builder, text, &config, false);
2045 builder.finish_node();
2046
2047 let green: GreenNode = builder.finish();
2048 let node = SyntaxNode::new_root(green);
2049 let has_emph = node.descendants().any(|n| n.kind() == SyntaxKind::EMPHASIS);
2050 assert!(has_emph, "Should have EMPHASIS node");
2051 assert_eq!(node.text().to_string(), text);
2052 }
2053}
2054
2055#[test]
2056fn test_two_with_nested_one_and_triple_closer() {
2057 use crate::options::ParserOptions;
2062 use crate::syntax::SyntaxNode;
2063 use rowan::GreenNode;
2064
2065 let text = "**bold with *italic***";
2066 let config = ParserOptions::default();
2067 let mut builder = GreenNodeBuilder::new();
2068
2069 builder.start_node(SyntaxKind::PARAGRAPH.into());
2070 parse_inline_text_recursive(&mut builder, text, &config, false);
2071 builder.finish_node();
2072
2073 let green: GreenNode = builder.finish();
2074 let node = SyntaxNode::new_root(green);
2075
2076 assert_eq!(node.text().to_string(), text, "Should be lossless");
2077
2078 let strong_nodes: Vec<_> = node
2079 .descendants()
2080 .filter(|n| n.kind() == SyntaxKind::STRONG)
2081 .collect();
2082 assert_eq!(strong_nodes.len(), 1, "Should have exactly one STRONG node");
2083 let has_emphasis_in_strong = strong_nodes[0]
2084 .descendants()
2085 .any(|n| n.kind() == SyntaxKind::EMPHASIS);
2086 assert!(
2087 has_emphasis_in_strong,
2088 "STRONG should contain EMPHASIS node"
2089 );
2090}
2091
2092#[test]
2093fn test_emphasis_with_trailing_space_before_closer() {
2094 use crate::options::ParserOptions;
2098 use crate::syntax::SyntaxNode;
2099 use rowan::GreenNode;
2100
2101 let text = "*foo *";
2102 let config = ParserOptions::default();
2103 let mut builder = GreenNodeBuilder::new();
2104
2105 builder.start_node(SyntaxKind::PARAGRAPH.into());
2106 parse_inline_text_recursive(&mut builder, text, &config, false);
2107 builder.finish_node();
2108
2109 let green: GreenNode = builder.finish();
2110 let node = SyntaxNode::new_root(green);
2111
2112 let has_emph = node.descendants().any(|n| n.kind() == SyntaxKind::EMPHASIS);
2113 assert!(has_emph, "Should have EMPHASIS node");
2114 assert_eq!(node.text().to_string(), text);
2115}
2116
2117#[test]
2118fn test_triple_emphasis_all_strong_nested() {
2119 use crate::options::ParserOptions;
2123 use crate::syntax::SyntaxNode;
2124 use rowan::GreenNode;
2125
2126 let text = "***foo** bar **baz***";
2127 let config = ParserOptions::default();
2128 let mut builder = GreenNodeBuilder::new();
2129
2130 builder.start_node(SyntaxKind::DOCUMENT.into());
2131 parse_inline_text_recursive(&mut builder, text, &config, false);
2132 builder.finish_node();
2133
2134 let green: GreenNode = builder.finish();
2135 let node = SyntaxNode::new_root(green);
2136
2137 let emphasis_nodes: Vec<_> = node
2139 .descendants()
2140 .filter(|n| n.kind() == SyntaxKind::EMPHASIS)
2141 .collect();
2142 assert_eq!(
2143 emphasis_nodes.len(),
2144 1,
2145 "Should have exactly one EMPHASIS node, found: {}",
2146 emphasis_nodes.len()
2147 );
2148
2149 let emphasis_node = emphasis_nodes[0].clone();
2151 let strong_in_emphasis: Vec<_> = emphasis_node
2152 .children()
2153 .filter(|n| n.kind() == SyntaxKind::STRONG)
2154 .collect();
2155 assert_eq!(
2156 strong_in_emphasis.len(),
2157 2,
2158 "EMPHASIS should contain two STRONG nodes, found: {}",
2159 strong_in_emphasis.len()
2160 );
2161
2162 assert_eq!(node.text().to_string(), text);
2164}
2165
2166#[test]
2167fn test_triple_emphasis_all_emph_nested() {
2168 use crate::options::ParserOptions;
2172 use crate::syntax::SyntaxNode;
2173 use rowan::GreenNode;
2174
2175 let text = "***foo* bar *baz***";
2176 let config = ParserOptions::default();
2177 let mut builder = GreenNodeBuilder::new();
2178
2179 builder.start_node(SyntaxKind::DOCUMENT.into());
2180 parse_inline_text_recursive(&mut builder, text, &config, false);
2181 builder.finish_node();
2182
2183 let green: GreenNode = builder.finish();
2184 let node = SyntaxNode::new_root(green);
2185
2186 let strong_nodes: Vec<_> = node
2188 .descendants()
2189 .filter(|n| n.kind() == SyntaxKind::STRONG)
2190 .collect();
2191 assert_eq!(
2192 strong_nodes.len(),
2193 1,
2194 "Should have exactly one STRONG node, found: {}",
2195 strong_nodes.len()
2196 );
2197
2198 let strong_node = strong_nodes[0].clone();
2200 let emph_in_strong: Vec<_> = strong_node
2201 .children()
2202 .filter(|n| n.kind() == SyntaxKind::EMPHASIS)
2203 .collect();
2204 assert_eq!(
2205 emph_in_strong.len(),
2206 2,
2207 "STRONG should contain two EMPHASIS nodes, found: {}",
2208 emph_in_strong.len()
2209 );
2210
2211 assert_eq!(node.text().to_string(), text);
2213}
2214
2215#[test]
2217fn test_parse_emphasis_multiline() {
2218 use crate::options::ParserOptions;
2220 use crate::syntax::SyntaxNode;
2221 use rowan::GreenNode;
2222
2223 let text = "*text on\nline two*";
2224 let config = ParserOptions::default();
2225 let mut builder = GreenNodeBuilder::new();
2226
2227 builder.start_node(SyntaxKind::PARAGRAPH.into());
2228 parse_inline_text_recursive(&mut builder, text, &config, false);
2229 builder.finish_node();
2230
2231 let green: GreenNode = builder.finish();
2232 let node = SyntaxNode::new_root(green);
2233
2234 let has_emph = node.descendants().any(|n| n.kind() == SyntaxKind::EMPHASIS);
2235 assert!(has_emph, "Should have EMPHASIS node");
2236
2237 assert_eq!(node.text().to_string(), text);
2238 assert!(
2239 node.text().to_string().contains('\n'),
2240 "Should preserve newline in emphasis content"
2241 );
2242}
2243
2244#[test]
2245fn test_parse_strong_multiline() {
2246 use crate::options::ParserOptions;
2248 use crate::syntax::SyntaxNode;
2249 use rowan::GreenNode;
2250
2251 let text = "**strong on\nline two**";
2252 let config = ParserOptions::default();
2253 let mut builder = GreenNodeBuilder::new();
2254
2255 builder.start_node(SyntaxKind::PARAGRAPH.into());
2256 parse_inline_text_recursive(&mut builder, text, &config, false);
2257 builder.finish_node();
2258
2259 let green: GreenNode = builder.finish();
2260 let node = SyntaxNode::new_root(green);
2261
2262 let has_strong = node.descendants().any(|n| n.kind() == SyntaxKind::STRONG);
2263 assert!(has_strong, "Should have STRONG node");
2264
2265 assert_eq!(node.text().to_string(), text);
2266 assert!(
2267 node.text().to_string().contains('\n'),
2268 "Should preserve newline in strong content"
2269 );
2270}
2271
2272#[test]
2273fn test_parse_triple_emphasis_multiline() {
2274 use crate::options::ParserOptions;
2276 use crate::syntax::SyntaxNode;
2277 use rowan::GreenNode;
2278
2279 let text = "***both on\nline two***";
2280 let config = ParserOptions::default();
2281 let mut builder = GreenNodeBuilder::new();
2282
2283 builder.start_node(SyntaxKind::PARAGRAPH.into());
2284 parse_inline_text_recursive(&mut builder, text, &config, false);
2285 builder.finish_node();
2286
2287 let green: GreenNode = builder.finish();
2288 let node = SyntaxNode::new_root(green);
2289
2290 let has_strong = node.descendants().any(|n| n.kind() == SyntaxKind::STRONG);
2292 assert!(has_strong, "Should have STRONG node");
2293
2294 assert_eq!(node.text().to_string(), text);
2295 assert!(
2296 node.text().to_string().contains('\n'),
2297 "Should preserve newline in triple emphasis content"
2298 );
2299}