1use crate::options::{Dialect, ParserOptions};
13use crate::syntax::SyntaxKind;
14use rowan::GreenNodeBuilder;
15
16use super::inline_ir::{
17 BracketPlan, ConstructDispo, ConstructPlan, DelimChar, EmphasisKind, EmphasisPlan,
18};
19
20use super::bookdown::{
22 try_parse_bookdown_definition, try_parse_bookdown_reference, try_parse_bookdown_text_reference,
23};
24use super::bracketed_spans::{emit_bracketed_span, try_parse_bracketed_span};
25use super::citations::{
26 emit_bare_citation, emit_bracketed_citation, try_parse_bare_citation,
27 try_parse_bracketed_citation,
28};
29use super::code_spans::{emit_code_span, try_parse_code_span};
30use super::emoji::{emit_emoji, try_parse_emoji};
31use super::escapes::{EscapeType, emit_escape, try_parse_escape};
32use super::inline_executable::{emit_inline_executable, try_parse_inline_executable};
33use super::inline_footnotes::{
34 emit_footnote_reference, emit_inline_footnote, try_parse_footnote_reference,
35 try_parse_inline_footnote,
36};
37use super::inline_html::{emit_inline_html, try_parse_inline_html};
38use super::latex::{parse_latex_command, try_parse_latex_command};
39use super::links::{
40 LinkScanContext, emit_autolink, emit_bare_uri_link, emit_inline_image, emit_inline_link,
41 emit_reference_image, emit_reference_link, try_parse_autolink, try_parse_bare_uri,
42 try_parse_inline_image, try_parse_inline_link, try_parse_reference_image,
43 try_parse_reference_link,
44};
45use super::mark::{emit_mark, try_parse_mark};
46use super::math::{
47 emit_display_math, emit_display_math_environment, emit_double_backslash_display_math,
48 emit_double_backslash_inline_math, emit_gfm_inline_math, emit_inline_math,
49 emit_single_backslash_display_math, emit_single_backslash_inline_math, try_parse_display_math,
50 try_parse_double_backslash_display_math, try_parse_double_backslash_inline_math,
51 try_parse_gfm_inline_math, try_parse_inline_math, try_parse_math_environment,
52 try_parse_single_backslash_display_math, try_parse_single_backslash_inline_math,
53};
54use super::native_spans::{emit_native_span, try_parse_native_span};
55use super::raw_inline::is_raw_inline;
56use super::shortcodes::{emit_shortcode, try_parse_shortcode};
57use super::strikeout::{emit_strikeout, try_parse_strikeout};
58use super::subscript::{emit_subscript, try_parse_subscript};
59use super::superscript::{emit_superscript, try_parse_superscript};
60
61pub fn parse_inline_text_recursive(
76 builder: &mut GreenNodeBuilder,
77 text: &str,
78 config: &ParserOptions,
79) {
80 log::trace!(
81 "Recursive inline parsing: {:?} ({} bytes)",
82 &text[..text.len().min(40)],
83 text.len()
84 );
85
86 let mask = structural_byte_mask(config);
87 if try_emit_plain_text_fast_path_with_mask(builder, text, &mask) {
88 log::trace!("Recursive inline parsing complete (plain-text fast path)");
89 return;
90 }
91
92 let plans = super::inline_ir::build_full_plans(text, 0, text.len(), config);
93 parse_inline_range_impl(
94 text,
95 0,
96 text.len(),
97 config,
98 builder,
99 false,
100 &plans.emphasis,
101 &plans.brackets,
102 &plans.constructs,
103 false,
104 &mask,
105 );
106
107 log::trace!("Recursive inline parsing complete");
108}
109
110pub fn parse_inline_text(
125 builder: &mut GreenNodeBuilder,
126 text: &str,
127 config: &ParserOptions,
128 suppress_inner_links: bool,
129) {
130 log::trace!(
131 "Parsing inline text (nested in link): {:?} ({} bytes)",
132 &text[..text.len().min(40)],
133 text.len()
134 );
135
136 let mask = structural_byte_mask(config);
137 if try_emit_plain_text_fast_path_with_mask(builder, text, &mask) {
138 return;
139 }
140
141 let plans = super::inline_ir::build_full_plans(text, 0, text.len(), config);
142 parse_inline_range_impl(
143 text,
144 0,
145 text.len(),
146 config,
147 builder,
148 true,
149 &plans.emphasis,
150 &plans.brackets,
151 &plans.constructs,
152 suppress_inner_links,
153 &mask,
154 );
155}
156
157fn try_emit_plain_text_fast_path_with_mask(
171 builder: &mut GreenNodeBuilder,
172 text: &str,
173 mask: &[bool; 256],
174) -> bool {
175 if text.is_empty() {
176 return false;
177 }
178 for &b in text.as_bytes() {
179 if mask[b as usize] {
180 return false;
181 }
182 }
183 builder.token(SyntaxKind::TEXT.into(), text);
184 true
185}
186
187fn structural_byte_mask(config: &ParserOptions) -> [bool; 256] {
192 let mut mask = [false; 256];
193 let exts = &config.extensions;
194 let pandoc = config.dialect == Dialect::Pandoc;
195
196 mask[b'\n' as usize] = true;
202 mask[b'\r' as usize] = true;
203 mask[b'\\' as usize] = true;
204 mask[b'`' as usize] = true;
205 mask[b'*' as usize] = true;
206 mask[b'_' as usize] = true;
207
208 if exts.inline_links
214 || exts.reference_links
215 || exts.inline_images
216 || exts.bracketed_spans
217 || exts.footnotes
218 || exts.citations
219 {
220 mask[b'[' as usize] = true;
221 mask[b']' as usize] = true;
222 }
223 if exts.inline_images || exts.reference_links {
224 mask[b'!' as usize] = true;
225 }
226
227 if exts.autolinks || exts.raw_html || exts.native_spans {
229 mask[b'<' as usize] = true;
230 }
231
232 if exts.inline_footnotes || exts.superscript {
235 mask[b'^' as usize] = true;
236 }
237
238 if exts.citations || exts.quarto_crossrefs {
245 mask[b'@' as usize] = true;
246 if pandoc {
247 mask[b'-' as usize] = true;
248 }
249 }
250
251 if exts.tex_math_dollars || exts.tex_math_gfm {
253 mask[b'$' as usize] = true;
254 }
255
256 if exts.subscript || exts.strikeout {
258 mask[b'~' as usize] = true;
259 }
260
261 if exts.mark {
262 mask[b'=' as usize] = true;
263 }
264 if exts.emoji {
265 mask[b':' as usize] = true;
266 }
267 if exts.bookdown_references {
268 mask[b'(' as usize] = true;
269 }
270 mask[b'{' as usize] = true;
274
275 if exts.autolink_bare_uris {
284 for b in b'a'..=b'z' {
285 mask[b as usize] = true;
286 }
287 for b in b'A'..=b'Z' {
288 mask[b as usize] = true;
289 }
290 }
291
292 mask
293}
294
295fn is_emoji_boundary(text: &str, pos: usize) -> bool {
296 if pos > 0 {
297 let prev = text.as_bytes()[pos - 1] as char;
298 if prev.is_ascii_alphanumeric() || prev == '_' {
299 return false;
300 }
301 }
302 true
303}
304
305#[inline]
306fn advance_char_boundary(text: &str, pos: usize, end: usize) -> usize {
307 if pos >= end || pos >= text.len() {
308 return pos;
309 }
310 let ch_len = text[pos..]
311 .chars()
312 .next()
313 .map_or(1, std::primitive::char::len_utf8);
314 (pos + ch_len).min(end)
315}
316
317#[allow(clippy::too_many_arguments)]
318fn parse_inline_range_impl(
319 text: &str,
320 start: usize,
321 end: usize,
322 config: &ParserOptions,
323 builder: &mut GreenNodeBuilder,
324 nested_in_link: bool,
325 plan: &EmphasisPlan,
326 bracket_plan: &BracketPlan,
327 construct_plan: &ConstructPlan,
328 suppress_inner_links: bool,
329 mask: &[bool; 256],
330) {
331 log::trace!(
332 "parse_inline_range: start={}, end={}, text={:?}",
333 start,
334 end,
335 &text[start..end]
336 );
337 let mut pos = start;
338 let mut text_start = start;
339 let bytes = text.as_bytes();
340
341 while pos < end {
342 if !mask[bytes[pos] as usize] {
349 let mut next = pos + 1;
350 while next < end && !mask[bytes[next] as usize] {
351 next += 1;
352 }
353 pos = next;
354 if pos >= end {
355 break;
356 }
357 }
358 if let Some(dispo) = construct_plan.lookup(pos) {
367 match *dispo {
368 ConstructDispo::InlineFootnote { end: dispo_end } => {
369 if dispo_end <= end
370 && let Some((len, content)) = try_parse_inline_footnote(&text[pos..])
371 && pos + len == dispo_end
372 {
373 if pos > text_start {
374 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
375 }
376 log::trace!("IR: matched inline footnote at pos {}", pos);
377 emit_inline_footnote(builder, content, config);
378 pos += len;
379 text_start = pos;
380 continue;
381 }
382 }
383 ConstructDispo::NativeSpan { end: dispo_end } => {
384 if dispo_end <= end
385 && let Some((len, content, attributes)) =
386 try_parse_native_span(&text[pos..])
387 && pos + len == dispo_end
388 {
389 if pos > text_start {
390 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
391 }
392 log::trace!("IR: matched native span at pos {}", pos);
393 emit_native_span(builder, content, &attributes, config);
394 pos += len;
395 text_start = pos;
396 continue;
397 }
398 }
399 ConstructDispo::FootnoteReference { end: dispo_end } => {
400 if dispo_end <= end
401 && let Some((len, id)) = try_parse_footnote_reference(&text[pos..])
402 && pos + len == dispo_end
403 {
404 if pos > text_start {
405 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
406 }
407 log::trace!("IR: matched footnote reference at pos {}", pos);
408 emit_footnote_reference(builder, &id);
409 pos += len;
410 text_start = pos;
411 continue;
412 }
413 }
414 ConstructDispo::BracketedCitation { end: dispo_end } => {
415 if dispo_end <= end
416 && let Some((len, content)) = try_parse_bracketed_citation(&text[pos..])
417 && pos + len == dispo_end
418 {
419 if pos > text_start {
420 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
421 }
422 log::trace!("IR: matched bracketed citation at pos {}", pos);
423 emit_bracketed_citation(builder, content);
424 pos += len;
425 text_start = pos;
426 continue;
427 }
428 }
429 ConstructDispo::BareCitation { end: dispo_end } => {
430 if dispo_end <= end
431 && let Some((len, key, has_suppress)) =
432 try_parse_bare_citation(&text[pos..])
433 && pos + len == dispo_end
434 {
435 let is_crossref = config.extensions.quarto_crossrefs
436 && super::citations::is_quarto_crossref_key(key);
437 if is_crossref || config.extensions.citations {
438 if pos > text_start {
439 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
440 }
441 if is_crossref {
442 log::trace!("IR: matched Quarto crossref at pos {}: {}", pos, key);
443 super::citations::emit_crossref(builder, key, has_suppress);
444 } else {
445 log::trace!("IR: matched bare citation at pos {}: {}", pos, key);
446 emit_bare_citation(builder, key, has_suppress);
447 }
448 pos += len;
449 text_start = pos;
450 continue;
451 }
452 }
453 }
454 ConstructDispo::BracketedSpan { end: dispo_end } => {
455 if dispo_end <= end
456 && let Some((len, content, attrs)) = try_parse_bracketed_span(&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 span at pos {}", pos);
463 emit_bracketed_span(builder, &content, &attrs, config);
464 pos += len;
465 text_start = pos;
466 continue;
467 }
468 }
469 }
470 }
471
472 if let Some(super::inline_ir::BracketDispo::Open {
492 is_image,
493 suffix_end,
494 ..
495 }) = bracket_plan.lookup(pos)
496 {
497 let is_image = *is_image;
498 let dispo_suffix_end = *suffix_end;
499 let suppress = suppress_inner_links && !is_image;
500 if !suppress {
501 let ctx = LinkScanContext::from_options(config);
502 let allow_shortcut = config.extensions.shortcut_reference_links;
503 let is_commonmark = config.dialect == Dialect::CommonMark;
504 if is_image {
505 if config.extensions.inline_images
506 && let Some((len, alt_text, dest, attributes)) =
507 try_parse_inline_image(&text[pos..], ctx)
508 && pos + len >= dispo_suffix_end
509 && pos + len <= end
510 {
511 if pos > text_start {
512 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
513 }
514 log::trace!("IR: matched inline image at pos {}", pos);
515 emit_inline_image(
516 builder,
517 &text[pos..pos + len],
518 alt_text,
519 dest,
520 attributes,
521 config,
522 );
523 pos += len;
524 text_start = pos;
525 continue;
526 }
527 if config.extensions.reference_links
528 && let Some((len, alt_text, reference, is_shortcut)) =
529 try_parse_reference_image(&text[pos..], allow_shortcut)
530 && pos + len == dispo_suffix_end
531 && pos + len <= end
532 {
533 if pos > text_start {
534 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
535 }
536 log::trace!("IR: matched reference image at pos {}", pos);
537 emit_reference_image(builder, alt_text, &reference, is_shortcut, config);
538 pos += len;
539 text_start = pos;
540 continue;
541 }
542 } else {
543 if config.extensions.inline_links
544 && let Some((len, link_text, dest, attributes)) =
545 try_parse_inline_link(&text[pos..], is_commonmark, ctx)
546 && pos + len >= dispo_suffix_end
547 && pos + len <= end
548 {
549 if pos > text_start {
550 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
551 }
552 log::trace!("IR: matched inline link at pos {}", pos);
553 emit_inline_link(
554 builder,
555 &text[pos..pos + len],
556 link_text,
557 dest,
558 attributes,
559 config,
560 );
561 pos += len;
562 text_start = pos;
563 continue;
564 }
565 if config.extensions.reference_links
566 && let Some((len, link_text, reference, is_shortcut)) =
567 try_parse_reference_link(
568 &text[pos..],
569 allow_shortcut,
570 config.extensions.inline_links,
571 ctx,
572 )
573 && pos + len == dispo_suffix_end
574 && pos + len <= end
575 {
576 if pos > text_start {
577 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
578 }
579 log::trace!("IR: matched reference link at pos {}", pos);
580 emit_reference_link(builder, link_text, &reference, is_shortcut, config);
581 pos += len;
582 text_start = pos;
583 continue;
584 }
585 }
586 }
587 }
588
589 let byte = text.as_bytes()[pos];
590
591 if byte == b'\\' {
593 if config.extensions.tex_math_double_backslash {
595 if let Some((len, content)) = try_parse_double_backslash_display_math(&text[pos..])
596 {
597 if pos > text_start {
598 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
599 }
600 log::trace!("Matched double backslash display math at pos {}", pos);
601 emit_double_backslash_display_math(builder, content);
602 pos += len;
603 text_start = pos;
604 continue;
605 }
606
607 if let Some((len, content)) = try_parse_double_backslash_inline_math(&text[pos..]) {
609 if pos > text_start {
610 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
611 }
612 log::trace!("Matched double backslash inline math at pos {}", pos);
613 emit_double_backslash_inline_math(builder, content);
614 pos += len;
615 text_start = pos;
616 continue;
617 }
618 }
619
620 if config.extensions.tex_math_single_backslash {
622 if let Some((len, content)) = try_parse_single_backslash_display_math(&text[pos..])
623 {
624 if pos > text_start {
625 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
626 }
627 log::trace!("Matched single backslash display math at pos {}", pos);
628 emit_single_backslash_display_math(builder, content);
629 pos += len;
630 text_start = pos;
631 continue;
632 }
633
634 if let Some((len, content)) = try_parse_single_backslash_inline_math(&text[pos..]) {
636 if pos > text_start {
637 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
638 }
639 log::trace!("Matched single backslash inline math at pos {}", pos);
640 emit_single_backslash_inline_math(builder, content);
641 pos += len;
642 text_start = pos;
643 continue;
644 }
645 }
646
647 if config.extensions.raw_tex
649 && let Some((len, begin_marker, content, end_marker)) =
650 try_parse_math_environment(&text[pos..])
651 {
652 if pos > text_start {
653 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
654 }
655 log::trace!("Matched math environment at pos {}", pos);
656 emit_display_math_environment(builder, begin_marker, content, end_marker);
657 pos += len;
658 text_start = pos;
659 continue;
660 }
661
662 if config.extensions.bookdown_references
664 && let Some((len, label)) = try_parse_bookdown_reference(&text[pos..])
665 {
666 if pos > text_start {
667 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
668 }
669 log::trace!("Matched bookdown reference at pos {}: {}", pos, label);
670 super::citations::emit_bookdown_crossref(builder, label);
671 pos += len;
672 text_start = pos;
673 continue;
674 }
675
676 if let Some((len, ch, escape_type)) = try_parse_escape(&text[pos..]) {
678 let escape_enabled = match escape_type {
679 EscapeType::HardLineBreak => config.extensions.escaped_line_breaks,
680 EscapeType::NonbreakingSpace => config.extensions.all_symbols_escapable,
681 EscapeType::Literal => {
682 const BASE_ESCAPABLE: &str = "\\`*_{}[]()>#+-.!|~";
695 BASE_ESCAPABLE.contains(ch)
696 || config.extensions.all_symbols_escapable
697 || (config.dialect == crate::Dialect::CommonMark
698 && ch.is_ascii_punctuation())
699 }
700 };
701 if !escape_enabled {
702 pos = advance_char_boundary(text, pos, end);
705 continue;
706 }
707
708 if pos > text_start {
710 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
711 }
712
713 log::trace!("Matched escape at pos {}: \\{}", pos, ch);
714 emit_escape(builder, ch, escape_type);
715 pos += len;
716 text_start = pos;
717 continue;
718 }
719
720 if config.extensions.raw_tex
722 && let Some(len) = try_parse_latex_command(&text[pos..])
723 {
724 if pos > text_start {
725 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
726 }
727 log::trace!("Matched LaTeX command at pos {}", pos);
728 parse_latex_command(builder, &text[pos..], len);
729 pos += len;
730 text_start = pos;
731 continue;
732 }
733 }
734
735 if byte == b'{'
737 && pos + 1 < text.len()
738 && text.as_bytes()[pos + 1] == b'{'
739 && let Some((len, name, attrs)) = try_parse_shortcode(&text[pos..])
740 {
741 if pos > text_start {
742 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
743 }
744 log::trace!("Matched shortcode at pos {}: {}", pos, &name);
745 emit_shortcode(builder, &name, attrs);
746 pos += len;
747 text_start = pos;
748 continue;
749 }
750
751 if byte == b'`'
753 && let Some(m) = try_parse_inline_executable(
754 &text[pos..],
755 config.extensions.rmarkdown_inline_code,
756 config.extensions.quarto_inline_code,
757 )
758 {
759 if pos > text_start {
760 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
761 }
762 log::trace!("Matched inline executable code at pos {}", pos);
763 emit_inline_executable(builder, &m);
764 pos += m.total_len;
765 text_start = pos;
766 continue;
767 }
768
769 if byte == b'`' {
771 if let Some((len, content, backtick_count, attributes)) =
772 try_parse_code_span(&text[pos..])
773 {
774 if pos > text_start {
776 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
777 }
778
779 log::trace!(
780 "Matched code span at pos {}: {} backticks",
781 pos,
782 backtick_count
783 );
784
785 if let Some(ref attrs) = attributes
787 && config.extensions.raw_attribute
788 && let Some(format) = is_raw_inline(attrs)
789 {
790 use super::raw_inline::emit_raw_inline;
791 log::trace!("Matched raw inline span at pos {}: format={}", pos, format);
792 emit_raw_inline(builder, content, backtick_count, format);
793 } else if !config.extensions.inline_code_attributes && attributes.is_some() {
794 let code_span_len = backtick_count * 2 + content.len();
795 emit_code_span(builder, content, backtick_count, None);
796 pos += code_span_len;
797 text_start = pos;
798 continue;
799 } else {
800 emit_code_span(builder, content, backtick_count, attributes);
801 }
802
803 pos += len;
804 text_start = pos;
805 continue;
806 }
807
808 if config.dialect == Dialect::CommonMark {
817 let run_len = text[pos..].bytes().take_while(|&b| b == b'`').count();
818 pos += run_len;
819 continue;
820 }
821 }
822
823 if byte == b':'
825 && config.extensions.emoji
826 && is_emoji_boundary(text, pos)
827 && let Some((len, _alias)) = try_parse_emoji(&text[pos..])
828 {
829 if pos > text_start {
830 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
831 }
832 log::trace!("Matched emoji at pos {}", pos);
833 emit_emoji(builder, &text[pos..pos + len]);
834 pos += len;
835 text_start = pos;
836 continue;
837 }
838
839 if byte == b'^'
844 && pos + 1 < text.len()
845 && text.as_bytes()[pos + 1] == b'['
846 && config.dialect == Dialect::CommonMark
847 && config.extensions.inline_footnotes
848 && let Some((len, content)) = try_parse_inline_footnote(&text[pos..])
849 {
850 if pos > text_start {
851 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
852 }
853 log::trace!("Matched inline footnote at pos {}", pos);
854 emit_inline_footnote(builder, content, config);
855 pos += len;
856 text_start = pos;
857 continue;
858 }
859
860 if byte == b'^'
862 && config.extensions.superscript
863 && let Some((len, content)) = try_parse_superscript(&text[pos..])
864 {
865 if pos > text_start {
866 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
867 }
868 log::trace!("Matched superscript at pos {}", pos);
869 emit_superscript(builder, content, config);
870 pos += len;
871 text_start = pos;
872 continue;
873 }
874
875 if byte == b'(' && config.extensions.bookdown_references {
877 if let Some((len, label)) = try_parse_bookdown_definition(&text[pos..]) {
878 if pos > text_start {
879 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
880 }
881 log::trace!("Matched bookdown definition at pos {}: {}", pos, label);
882 builder.token(SyntaxKind::TEXT.into(), &text[pos..pos + len]);
883 pos += len;
884 text_start = pos;
885 continue;
886 }
887 if let Some((len, label)) = try_parse_bookdown_text_reference(&text[pos..]) {
888 if pos > text_start {
889 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
890 }
891 log::trace!("Matched bookdown text reference at pos {}: {}", pos, label);
892 builder.token(SyntaxKind::TEXT.into(), &text[pos..pos + len]);
893 pos += len;
894 text_start = pos;
895 continue;
896 }
897 }
898
899 if byte == b'~'
901 && config.extensions.subscript
902 && let Some((len, content)) = try_parse_subscript(&text[pos..])
903 {
904 if pos > text_start {
905 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
906 }
907 log::trace!("Matched subscript at pos {}", pos);
908 emit_subscript(builder, content, config);
909 pos += len;
910 text_start = pos;
911 continue;
912 }
913
914 if byte == b'~'
916 && config.extensions.strikeout
917 && let Some((len, content)) = try_parse_strikeout(&text[pos..])
918 {
919 if pos > text_start {
920 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
921 }
922 log::trace!("Matched strikeout at pos {}", pos);
923 emit_strikeout(builder, content, config);
924 pos += len;
925 text_start = pos;
926 continue;
927 }
928
929 if byte == b'='
931 && config.extensions.mark
932 && let Some((len, content)) = try_parse_mark(&text[pos..])
933 {
934 if pos > text_start {
935 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
936 }
937 log::trace!("Matched mark at pos {}", pos);
938 emit_mark(builder, content, config);
939 pos += len;
940 text_start = pos;
941 continue;
942 }
943
944 if byte == b'$'
946 && config.extensions.tex_math_gfm
947 && let Some((len, content)) = try_parse_gfm_inline_math(&text[pos..])
948 {
949 if pos > text_start {
950 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
951 }
952 log::trace!("Matched GFM inline math at pos {}", pos);
953 emit_gfm_inline_math(builder, content);
954 pos += len;
955 text_start = pos;
956 continue;
957 }
958
959 if byte == b'$' && config.extensions.tex_math_dollars {
961 if let Some((len, content)) = try_parse_display_math(&text[pos..]) {
963 if pos > text_start {
965 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
966 }
967
968 let dollar_count = text[pos..].chars().take_while(|&c| c == '$').count();
969 log::trace!(
970 "Matched display math at pos {}: {} dollars",
971 pos,
972 dollar_count
973 );
974
975 let after_math = &text[pos + len..];
977 let attr_len = if config.extensions.quarto_crossrefs {
978 use crate::parser::utils::attributes::try_parse_trailing_attributes;
979 if let Some((_attr_block, _)) = try_parse_trailing_attributes(after_math) {
980 let trimmed_after = after_math.trim_start();
981 if let Some(open_brace_pos) = trimmed_after.find('{') {
982 let ws_before_brace = after_math.len() - trimmed_after.len();
983 let attr_text_len = trimmed_after[open_brace_pos..]
984 .find('}')
985 .map(|close| close + 1)
986 .unwrap_or(0);
987 ws_before_brace + open_brace_pos + attr_text_len
988 } else {
989 0
990 }
991 } else {
992 0
993 }
994 } else {
995 0
996 };
997
998 let total_len = len + attr_len;
999 emit_display_math(builder, content, dollar_count);
1000
1001 if attr_len > 0 {
1003 use crate::parser::utils::attributes::{
1004 emit_attributes, try_parse_trailing_attributes,
1005 };
1006 let attr_text = &text[pos + len..pos + total_len];
1007 if let Some((attr_block, _text_before)) =
1008 try_parse_trailing_attributes(attr_text)
1009 {
1010 let trimmed_after = attr_text.trim_start();
1011 let ws_len = attr_text.len() - trimmed_after.len();
1012 if ws_len > 0 {
1013 builder.token(SyntaxKind::WHITESPACE.into(), &attr_text[..ws_len]);
1014 }
1015 emit_attributes(builder, &attr_block);
1016 }
1017 }
1018
1019 pos += total_len;
1020 text_start = pos;
1021 continue;
1022 }
1023
1024 if let Some((len, content)) = try_parse_inline_math(&text[pos..]) {
1026 if pos > text_start {
1028 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1029 }
1030
1031 log::trace!("Matched inline math at pos {}", pos);
1032 emit_inline_math(builder, content);
1033 pos += len;
1034 text_start = pos;
1035 continue;
1036 }
1037
1038 if pos > text_start {
1041 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1042 }
1043 builder.token(SyntaxKind::TEXT.into(), "$");
1044 pos = advance_char_boundary(text, pos, end);
1045 text_start = pos;
1046 continue;
1047 }
1048
1049 if byte == b'<'
1051 && config.extensions.autolinks
1052 && let Some((len, url)) = try_parse_autolink(
1053 &text[pos..],
1054 config.dialect == crate::options::Dialect::CommonMark,
1055 )
1056 {
1057 if pos > text_start {
1058 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1059 }
1060 log::trace!("Matched autolink at pos {}", pos);
1061 emit_autolink(builder, &text[pos..pos + len], url);
1062 pos += len;
1063 text_start = pos;
1064 continue;
1065 }
1066
1067 if !nested_in_link
1068 && config.extensions.autolink_bare_uris
1069 && let Some((len, url)) = try_parse_bare_uri(&text[pos..])
1070 {
1071 if pos > text_start {
1072 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1073 }
1074 log::trace!("Matched bare URI at pos {}", pos);
1075 emit_bare_uri_link(builder, url, config);
1076 pos += len;
1077 text_start = pos;
1078 continue;
1079 }
1080
1081 if byte == b'<'
1087 && config.dialect == Dialect::CommonMark
1088 && config.extensions.native_spans
1089 && let Some((len, content, attributes)) = try_parse_native_span(&text[pos..])
1090 {
1091 if pos > text_start {
1092 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1093 }
1094 log::trace!("Matched native span at pos {}", pos);
1095 emit_native_span(builder, content, &attributes, config);
1096 pos += len;
1097 text_start = pos;
1098 continue;
1099 }
1100
1101 if byte == b'<'
1105 && config.extensions.raw_html
1106 && let Some(len) = try_parse_inline_html(&text[pos..])
1107 {
1108 if pos > text_start {
1109 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1110 }
1111 log::trace!("Matched inline raw HTML at pos {}", pos);
1112 emit_inline_html(builder, &text[pos..pos + len]);
1113 pos += len;
1114 text_start = pos;
1115 continue;
1116 }
1117
1118 if byte == b'['
1126 && config.dialect == Dialect::CommonMark
1127 && config.extensions.footnotes
1128 && let Some((len, id)) = try_parse_footnote_reference(&text[pos..])
1129 {
1130 if pos > text_start {
1131 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1132 }
1133 log::trace!("Matched footnote reference at pos {}", pos);
1134 emit_footnote_reference(builder, &id);
1135 pos += len;
1136 text_start = pos;
1137 continue;
1138 }
1139 if byte == b'['
1140 && config.dialect == Dialect::CommonMark
1141 && config.extensions.citations
1142 && let Some((len, content)) = try_parse_bracketed_citation(&text[pos..])
1143 {
1144 if pos > text_start {
1145 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1146 }
1147 log::trace!("Matched bracketed citation at pos {}", pos);
1148 emit_bracketed_citation(builder, content);
1149 pos += len;
1150 text_start = pos;
1151 continue;
1152 }
1153
1154 if config.dialect == Dialect::CommonMark
1160 && byte == b'['
1161 && config.extensions.bracketed_spans
1162 && let Some((len, text_content, attrs)) = try_parse_bracketed_span(&text[pos..])
1163 {
1164 if pos > text_start {
1165 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1166 }
1167 log::trace!("Matched bracketed span at pos {}", pos);
1168 emit_bracketed_span(builder, &text_content, &attrs, config);
1169 pos += len;
1170 text_start = pos;
1171 continue;
1172 }
1173
1174 if config.dialect == Dialect::CommonMark
1180 && byte == b'@'
1181 && (config.extensions.citations || config.extensions.quarto_crossrefs)
1182 && let Some((len, key, has_suppress)) = try_parse_bare_citation(&text[pos..])
1183 {
1184 let is_crossref =
1185 config.extensions.quarto_crossrefs && super::citations::is_quarto_crossref_key(key);
1186 if is_crossref || config.extensions.citations {
1187 if pos > text_start {
1188 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1189 }
1190 if is_crossref {
1191 log::trace!("Matched Quarto crossref at pos {}: {}", pos, &key);
1192 super::citations::emit_crossref(builder, key, has_suppress);
1193 } else {
1194 log::trace!("Matched bare citation at pos {}: {}", pos, &key);
1195 emit_bare_citation(builder, key, has_suppress);
1196 }
1197 pos += len;
1198 text_start = pos;
1199 continue;
1200 }
1201 }
1202
1203 if config.dialect == Dialect::CommonMark
1208 && byte == b'-'
1209 && pos + 1 < text.len()
1210 && text.as_bytes()[pos + 1] == b'@'
1211 && (config.extensions.citations || config.extensions.quarto_crossrefs)
1212 && let Some((len, key, has_suppress)) = try_parse_bare_citation(&text[pos..])
1213 {
1214 let is_crossref =
1215 config.extensions.quarto_crossrefs && super::citations::is_quarto_crossref_key(key);
1216 if is_crossref || config.extensions.citations {
1217 if pos > text_start {
1218 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1219 }
1220 if is_crossref {
1221 log::trace!("Matched Quarto crossref at pos {}: {}", pos, &key);
1222 super::citations::emit_crossref(builder, key, has_suppress);
1223 } else {
1224 log::trace!("Matched suppress-author citation at pos {}: {}", pos, &key);
1225 emit_bare_citation(builder, key, has_suppress);
1226 }
1227 pos += len;
1228 text_start = pos;
1229 continue;
1230 }
1231 }
1232
1233 if byte == b'*' || byte == b'_' {
1238 match plan.lookup(pos) {
1239 Some(DelimChar::Open {
1240 len,
1241 partner,
1242 partner_len,
1243 kind,
1244 }) => {
1245 if pos > text_start {
1246 builder.token(SyntaxKind::TEXT.into(), &text[text_start..pos]);
1247 }
1248 let len = len as usize;
1249 let partner_len = partner_len as usize;
1250 let (wrapper_kind, marker_kind) = match kind {
1251 EmphasisKind::Strong => (SyntaxKind::STRONG, SyntaxKind::STRONG_MARKER),
1252 EmphasisKind::Emph => (SyntaxKind::EMPHASIS, SyntaxKind::EMPHASIS_MARKER),
1253 };
1254 builder.start_node(wrapper_kind.into());
1255 builder.token(marker_kind.into(), &text[pos..pos + len]);
1256 parse_inline_range_impl(
1257 text,
1258 pos + len,
1259 partner,
1260 config,
1261 builder,
1262 nested_in_link,
1263 plan,
1264 bracket_plan,
1265 construct_plan,
1266 suppress_inner_links,
1267 mask,
1268 );
1269 builder.token(marker_kind.into(), &text[partner..partner + partner_len]);
1270 builder.finish_node();
1271 pos = partner + partner_len;
1272 text_start = pos;
1273 continue;
1274 }
1275 Some(DelimChar::Close) => {
1276 pos += 1;
1283 continue;
1284 }
1285 Some(DelimChar::Literal) | None => {
1286 let bytes = text.as_bytes();
1292 let mut end_pos = pos + 1;
1293 while end_pos < end && bytes[end_pos] == byte {
1294 match plan.lookup(end_pos) {
1295 Some(DelimChar::Literal) | None => end_pos += 1,
1296 _ => break,
1297 }
1298 }
1299 pos = end_pos;
1300 continue;
1301 }
1302 }
1303 }
1304
1305 if byte == b'\r' && pos + 1 < end && text.as_bytes()[pos + 1] == b'\n' {
1307 let text_before = &text[text_start..pos];
1308
1309 let trailing_spaces = text_before.chars().rev().take_while(|&c| c == ' ').count();
1311 if trailing_spaces >= 2 {
1312 let text_content = &text_before[..text_before.len() - trailing_spaces];
1314 if !text_content.is_empty() {
1315 builder.token(SyntaxKind::TEXT.into(), text_content);
1316 }
1317 let spaces = " ".repeat(trailing_spaces);
1318 builder.token(
1319 SyntaxKind::HARD_LINE_BREAK.into(),
1320 &format!("{}\r\n", spaces),
1321 );
1322 pos += 2;
1323 text_start = pos;
1324 continue;
1325 }
1326
1327 if config.extensions.hard_line_breaks {
1329 if !text_before.is_empty() {
1330 builder.token(SyntaxKind::TEXT.into(), text_before);
1331 }
1332 builder.token(SyntaxKind::HARD_LINE_BREAK.into(), "\r\n");
1333 pos += 2;
1334 text_start = pos;
1335 continue;
1336 }
1337
1338 if !text_before.is_empty() {
1340 builder.token(SyntaxKind::TEXT.into(), text_before);
1341 }
1342 builder.token(SyntaxKind::NEWLINE.into(), "\r\n");
1343 pos += 2;
1344 text_start = pos;
1345 continue;
1346 }
1347
1348 if byte == b'\n' {
1349 let text_before = &text[text_start..pos];
1350
1351 let trailing_spaces = text_before.chars().rev().take_while(|&c| c == ' ').count();
1353 if trailing_spaces >= 2 {
1354 let text_content = &text_before[..text_before.len() - trailing_spaces];
1356 if !text_content.is_empty() {
1357 builder.token(SyntaxKind::TEXT.into(), text_content);
1358 }
1359 let spaces = " ".repeat(trailing_spaces);
1360 builder.token(SyntaxKind::HARD_LINE_BREAK.into(), &format!("{}\n", spaces));
1361 pos += 1;
1362 text_start = pos;
1363 continue;
1364 }
1365
1366 if config.extensions.hard_line_breaks {
1368 if !text_before.is_empty() {
1369 builder.token(SyntaxKind::TEXT.into(), text_before);
1370 }
1371 builder.token(SyntaxKind::HARD_LINE_BREAK.into(), "\n");
1372 pos += 1;
1373 text_start = pos;
1374 continue;
1375 }
1376
1377 if !text_before.is_empty() {
1379 builder.token(SyntaxKind::TEXT.into(), text_before);
1380 }
1381 builder.token(SyntaxKind::NEWLINE.into(), "\n");
1382 pos += 1;
1383 text_start = pos;
1384 continue;
1385 }
1386
1387 pos = advance_char_boundary(text, pos, end);
1389 }
1390
1391 if pos > text_start && text_start < end {
1393 log::trace!("Emitting remaining TEXT: {:?}", &text[text_start..end]);
1394 builder.token(SyntaxKind::TEXT.into(), &text[text_start..end]);
1395 }
1396
1397 log::trace!("parse_inline_range complete: start={}, end={}", start, end);
1398}
1399
1400#[cfg(test)]
1401mod tests {
1402 use super::*;
1403 use crate::syntax::{SyntaxKind, SyntaxNode};
1404 use rowan::GreenNode;
1405
1406 #[test]
1407 fn test_recursive_simple_emphasis() {
1408 let text = "*test*";
1409 let config = ParserOptions::default();
1410 let mut builder = GreenNodeBuilder::new();
1411
1412 parse_inline_text_recursive(&mut builder, text, &config);
1413
1414 let green: GreenNode = builder.finish();
1415 let node = SyntaxNode::new_root(green);
1416
1417 assert_eq!(node.text().to_string(), text);
1419
1420 let has_emph = node.descendants().any(|n| n.kind() == SyntaxKind::EMPHASIS);
1422 assert!(has_emph, "Should have EMPHASIS node");
1423 }
1424
1425 #[test]
1426 fn test_recursive_nested() {
1427 let text = "*foo **bar** baz*";
1428 let config = ParserOptions::default();
1429 let mut builder = GreenNodeBuilder::new();
1430
1431 builder.start_node(SyntaxKind::PARAGRAPH.into());
1433 parse_inline_text_recursive(&mut builder, text, &config);
1434 builder.finish_node();
1435
1436 let green: GreenNode = builder.finish();
1437 let node = SyntaxNode::new_root(green);
1438
1439 assert_eq!(node.text().to_string(), text);
1441
1442 let has_emph = node.descendants().any(|n| n.kind() == SyntaxKind::EMPHASIS);
1444 let has_strong = node.descendants().any(|n| n.kind() == SyntaxKind::STRONG);
1445
1446 assert!(has_emph, "Should have EMPHASIS node");
1447 assert!(has_strong, "Should have STRONG node");
1448 }
1449
1450 #[test]
1453 fn test_triple_emphasis_star_then_double_star() {
1454 use crate::options::ParserOptions;
1455 use crate::syntax::SyntaxNode;
1456 use rowan::GreenNode;
1457
1458 let text = "***foo* bar**";
1459 let config = ParserOptions::default();
1460 let mut builder = GreenNodeBuilder::new();
1461
1462 builder.start_node(SyntaxKind::DOCUMENT.into());
1463 parse_inline_text_recursive(&mut builder, text, &config);
1464 builder.finish_node();
1465
1466 let green: GreenNode = builder.finish();
1467 let node = SyntaxNode::new_root(green);
1468
1469 assert_eq!(node.text().to_string(), text);
1471
1472 let structure = format!("{:#?}", node);
1475
1476 assert!(structure.contains("STRONG"), "Should have STRONG node");
1478 assert!(structure.contains("EMPHASIS"), "Should have EMPHASIS node");
1479
1480 let mut found_strong = false;
1483 let mut found_emph_after_strong = false;
1484 for descendant in node.descendants() {
1485 if descendant.kind() == SyntaxKind::STRONG {
1486 found_strong = true;
1487 }
1488 if found_strong && descendant.kind() == SyntaxKind::EMPHASIS {
1489 found_emph_after_strong = true;
1490 break;
1491 }
1492 }
1493
1494 assert!(
1495 found_emph_after_strong,
1496 "EMPH should be inside STRONG, not before it. Current structure:\n{}",
1497 structure
1498 );
1499 }
1500
1501 #[test]
1504 fn test_triple_emphasis_double_star_then_star() {
1505 use crate::options::ParserOptions;
1506 use crate::syntax::SyntaxNode;
1507 use rowan::GreenNode;
1508
1509 let text = "***foo** bar*";
1510 let config = ParserOptions::default();
1511 let mut builder = GreenNodeBuilder::new();
1512
1513 builder.start_node(SyntaxKind::DOCUMENT.into());
1514 parse_inline_text_recursive(&mut builder, text, &config);
1515 builder.finish_node();
1516
1517 let green: GreenNode = builder.finish();
1518 let node = SyntaxNode::new_root(green);
1519
1520 assert_eq!(node.text().to_string(), text);
1522
1523 let structure = format!("{:#?}", node);
1525
1526 assert!(structure.contains("EMPHASIS"), "Should have EMPHASIS node");
1528 assert!(structure.contains("STRONG"), "Should have STRONG node");
1529
1530 let mut found_emph = false;
1532 let mut found_strong_after_emph = false;
1533 for descendant in node.descendants() {
1534 if descendant.kind() == SyntaxKind::EMPHASIS {
1535 found_emph = true;
1536 }
1537 if found_emph && descendant.kind() == SyntaxKind::STRONG {
1538 found_strong_after_emph = true;
1539 break;
1540 }
1541 }
1542
1543 assert!(
1544 found_strong_after_emph,
1545 "STRONG should be inside EMPH. Current structure:\n{}",
1546 structure
1547 );
1548 }
1549
1550 #[test]
1553 fn test_display_math_with_attributes() {
1554 use crate::options::ParserOptions;
1555 use crate::syntax::SyntaxNode;
1556 use rowan::GreenNode;
1557
1558 let text = "$$ E = mc^2 $$ {#eq-einstein}";
1559 let mut config = ParserOptions::default();
1560 config.extensions.quarto_crossrefs = true; let mut builder = GreenNodeBuilder::new();
1563 builder.start_node(SyntaxKind::DOCUMENT.into()); parse_inline_text_recursive(&mut builder, text, &config);
1567
1568 builder.finish_node(); let green: GreenNode = builder.finish();
1570 let node = SyntaxNode::new_root(green);
1571
1572 assert_eq!(node.text().to_string(), text);
1574
1575 let has_display_math = node
1577 .descendants()
1578 .any(|n| n.kind() == SyntaxKind::DISPLAY_MATH);
1579 assert!(has_display_math, "Should have DISPLAY_MATH node");
1580
1581 let has_attributes = node
1583 .descendants()
1584 .any(|n| n.kind() == SyntaxKind::ATTRIBUTE);
1585 assert!(
1586 has_attributes,
1587 "Should have ATTRIBUTE node for {{#eq-einstein}}"
1588 );
1589
1590 let math_followed_by_text = node.descendants().any(|n| {
1592 n.kind() == SyntaxKind::DISPLAY_MATH
1593 && n.next_sibling()
1594 .map(|s| {
1595 s.kind() == SyntaxKind::TEXT
1596 && s.text().to_string().contains("{#eq-einstein}")
1597 })
1598 .unwrap_or(false)
1599 });
1600 assert!(
1601 !math_followed_by_text,
1602 "Attributes should not be parsed as TEXT"
1603 );
1604 }
1605
1606 #[test]
1607 fn test_parse_inline_text_gfm_inline_link_destination_not_autolinked() {
1608 use crate::options::{Dialect, Extensions, Flavor};
1609
1610 let config = ParserOptions {
1611 flavor: Flavor::Gfm,
1612 dialect: Dialect::for_flavor(Flavor::Gfm),
1613 extensions: Extensions::for_flavor(Flavor::Gfm),
1614 ..ParserOptions::default()
1615 };
1616
1617 let mut builder = GreenNodeBuilder::new();
1618 builder.start_node(SyntaxKind::PARAGRAPH.into());
1619 parse_inline_text_recursive(
1620 &mut builder,
1621 "Second Link [link_text](https://link.com)",
1622 &config,
1623 );
1624 builder.finish_node();
1625 let green = builder.finish();
1626 let root = SyntaxNode::new_root(green);
1627
1628 let links: Vec<_> = root
1629 .descendants()
1630 .filter(|n| n.kind() == SyntaxKind::LINK)
1631 .collect();
1632 assert_eq!(
1633 links.len(),
1634 1,
1635 "Expected exactly one LINK node for inline link, not nested bare URI autolink"
1636 );
1637
1638 let link = links[0].clone();
1639 let mut link_text = None::<String>;
1640 let mut link_dest = None::<String>;
1641
1642 for child in link.children() {
1643 match child.kind() {
1644 SyntaxKind::LINK_TEXT => link_text = Some(child.text().to_string()),
1645 SyntaxKind::LINK_DEST => link_dest = Some(child.text().to_string()),
1646 _ => {}
1647 }
1648 }
1649
1650 assert_eq!(link_text.as_deref(), Some("link_text"));
1651 assert_eq!(link_dest.as_deref(), Some("https://link.com"));
1652 }
1653
1654 #[test]
1655 fn test_autolink_bare_uri_utf8_boundary_safe() {
1656 let text = "§";
1657 let mut config = ParserOptions::default();
1658 config.extensions.autolink_bare_uris = true;
1659 let mut builder = GreenNodeBuilder::new();
1660
1661 builder.start_node(SyntaxKind::DOCUMENT.into());
1662 parse_inline_text_recursive(&mut builder, text, &config);
1663 builder.finish_node();
1664
1665 let green: GreenNode = builder.finish();
1666 let node = SyntaxNode::new_root(green);
1667 assert_eq!(node.text().to_string(), text);
1668 }
1669
1670 #[test]
1671 fn test_parse_emphasis_unicode_content_no_panic() {
1672 let text = "*§*";
1673 let config = ParserOptions::default();
1674 let mut builder = GreenNodeBuilder::new();
1675
1676 builder.start_node(SyntaxKind::PARAGRAPH.into());
1677 parse_inline_text_recursive(&mut builder, text, &config);
1678 builder.finish_node();
1679
1680 let green: GreenNode = builder.finish();
1681 let node = SyntaxNode::new_root(green);
1682 let has_emph = node.descendants().any(|n| n.kind() == SyntaxKind::EMPHASIS);
1683 assert!(has_emph, "Should have EMPHASIS node");
1684 assert_eq!(node.text().to_string(), text);
1685 }
1686}
1687
1688#[test]
1689fn test_two_with_nested_one_and_triple_closer() {
1690 use crate::options::ParserOptions;
1695 use crate::syntax::SyntaxNode;
1696 use rowan::GreenNode;
1697
1698 let text = "**bold with *italic***";
1699 let config = ParserOptions::default();
1700 let mut builder = GreenNodeBuilder::new();
1701
1702 builder.start_node(SyntaxKind::PARAGRAPH.into());
1703 parse_inline_text_recursive(&mut builder, text, &config);
1704 builder.finish_node();
1705
1706 let green: GreenNode = builder.finish();
1707 let node = SyntaxNode::new_root(green);
1708
1709 assert_eq!(node.text().to_string(), text, "Should be lossless");
1710
1711 let strong_nodes: Vec<_> = node
1712 .descendants()
1713 .filter(|n| n.kind() == SyntaxKind::STRONG)
1714 .collect();
1715 assert_eq!(strong_nodes.len(), 1, "Should have exactly one STRONG node");
1716 let has_emphasis_in_strong = strong_nodes[0]
1717 .descendants()
1718 .any(|n| n.kind() == SyntaxKind::EMPHASIS);
1719 assert!(
1720 has_emphasis_in_strong,
1721 "STRONG should contain EMPHASIS node"
1722 );
1723}
1724
1725#[test]
1726fn test_emphasis_with_trailing_space_before_closer() {
1727 use crate::options::ParserOptions;
1731 use crate::syntax::SyntaxNode;
1732 use rowan::GreenNode;
1733
1734 let text = "*foo *";
1735 let config = ParserOptions::default();
1736 let mut builder = GreenNodeBuilder::new();
1737
1738 builder.start_node(SyntaxKind::PARAGRAPH.into());
1739 parse_inline_text_recursive(&mut builder, text, &config);
1740 builder.finish_node();
1741
1742 let green: GreenNode = builder.finish();
1743 let node = SyntaxNode::new_root(green);
1744
1745 let has_emph = node.descendants().any(|n| n.kind() == SyntaxKind::EMPHASIS);
1746 assert!(has_emph, "Should have EMPHASIS node");
1747 assert_eq!(node.text().to_string(), text);
1748}
1749
1750#[test]
1751fn test_triple_emphasis_all_strong_nested() {
1752 use crate::options::ParserOptions;
1756 use crate::syntax::SyntaxNode;
1757 use rowan::GreenNode;
1758
1759 let text = "***foo** bar **baz***";
1760 let config = ParserOptions::default();
1761 let mut builder = GreenNodeBuilder::new();
1762
1763 builder.start_node(SyntaxKind::DOCUMENT.into());
1764 parse_inline_text_recursive(&mut builder, text, &config);
1765 builder.finish_node();
1766
1767 let green: GreenNode = builder.finish();
1768 let node = SyntaxNode::new_root(green);
1769
1770 let emphasis_nodes: Vec<_> = node
1772 .descendants()
1773 .filter(|n| n.kind() == SyntaxKind::EMPHASIS)
1774 .collect();
1775 assert_eq!(
1776 emphasis_nodes.len(),
1777 1,
1778 "Should have exactly one EMPHASIS node, found: {}",
1779 emphasis_nodes.len()
1780 );
1781
1782 let emphasis_node = emphasis_nodes[0].clone();
1784 let strong_in_emphasis: Vec<_> = emphasis_node
1785 .children()
1786 .filter(|n| n.kind() == SyntaxKind::STRONG)
1787 .collect();
1788 assert_eq!(
1789 strong_in_emphasis.len(),
1790 2,
1791 "EMPHASIS should contain two STRONG nodes, found: {}",
1792 strong_in_emphasis.len()
1793 );
1794
1795 assert_eq!(node.text().to_string(), text);
1797}
1798
1799#[test]
1800fn test_triple_emphasis_all_emph_nested() {
1801 use crate::options::ParserOptions;
1805 use crate::syntax::SyntaxNode;
1806 use rowan::GreenNode;
1807
1808 let text = "***foo* bar *baz***";
1809 let config = ParserOptions::default();
1810 let mut builder = GreenNodeBuilder::new();
1811
1812 builder.start_node(SyntaxKind::DOCUMENT.into());
1813 parse_inline_text_recursive(&mut builder, text, &config);
1814 builder.finish_node();
1815
1816 let green: GreenNode = builder.finish();
1817 let node = SyntaxNode::new_root(green);
1818
1819 let strong_nodes: Vec<_> = node
1821 .descendants()
1822 .filter(|n| n.kind() == SyntaxKind::STRONG)
1823 .collect();
1824 assert_eq!(
1825 strong_nodes.len(),
1826 1,
1827 "Should have exactly one STRONG node, found: {}",
1828 strong_nodes.len()
1829 );
1830
1831 let strong_node = strong_nodes[0].clone();
1833 let emph_in_strong: Vec<_> = strong_node
1834 .children()
1835 .filter(|n| n.kind() == SyntaxKind::EMPHASIS)
1836 .collect();
1837 assert_eq!(
1838 emph_in_strong.len(),
1839 2,
1840 "STRONG should contain two EMPHASIS nodes, found: {}",
1841 emph_in_strong.len()
1842 );
1843
1844 assert_eq!(node.text().to_string(), text);
1846}
1847
1848#[test]
1850fn test_parse_emphasis_multiline() {
1851 use crate::options::ParserOptions;
1853 use crate::syntax::SyntaxNode;
1854 use rowan::GreenNode;
1855
1856 let text = "*text on\nline two*";
1857 let config = ParserOptions::default();
1858 let mut builder = GreenNodeBuilder::new();
1859
1860 builder.start_node(SyntaxKind::PARAGRAPH.into());
1861 parse_inline_text_recursive(&mut builder, text, &config);
1862 builder.finish_node();
1863
1864 let green: GreenNode = builder.finish();
1865 let node = SyntaxNode::new_root(green);
1866
1867 let has_emph = node.descendants().any(|n| n.kind() == SyntaxKind::EMPHASIS);
1868 assert!(has_emph, "Should have EMPHASIS node");
1869
1870 assert_eq!(node.text().to_string(), text);
1871 assert!(
1872 node.text().to_string().contains('\n'),
1873 "Should preserve newline in emphasis content"
1874 );
1875}
1876
1877#[test]
1878fn test_parse_strong_multiline() {
1879 use crate::options::ParserOptions;
1881 use crate::syntax::SyntaxNode;
1882 use rowan::GreenNode;
1883
1884 let text = "**strong on\nline two**";
1885 let config = ParserOptions::default();
1886 let mut builder = GreenNodeBuilder::new();
1887
1888 builder.start_node(SyntaxKind::PARAGRAPH.into());
1889 parse_inline_text_recursive(&mut builder, text, &config);
1890 builder.finish_node();
1891
1892 let green: GreenNode = builder.finish();
1893 let node = SyntaxNode::new_root(green);
1894
1895 let has_strong = node.descendants().any(|n| n.kind() == SyntaxKind::STRONG);
1896 assert!(has_strong, "Should have STRONG node");
1897
1898 assert_eq!(node.text().to_string(), text);
1899 assert!(
1900 node.text().to_string().contains('\n'),
1901 "Should preserve newline in strong content"
1902 );
1903}
1904
1905#[test]
1906fn test_parse_triple_emphasis_multiline() {
1907 use crate::options::ParserOptions;
1909 use crate::syntax::SyntaxNode;
1910 use rowan::GreenNode;
1911
1912 let text = "***both on\nline two***";
1913 let config = ParserOptions::default();
1914 let mut builder = GreenNodeBuilder::new();
1915
1916 builder.start_node(SyntaxKind::PARAGRAPH.into());
1917 parse_inline_text_recursive(&mut builder, text, &config);
1918 builder.finish_node();
1919
1920 let green: GreenNode = builder.finish();
1921 let node = SyntaxNode::new_root(green);
1922
1923 let has_strong = node.descendants().any(|n| n.kind() == SyntaxKind::STRONG);
1925 assert!(has_strong, "Should have STRONG node");
1926
1927 assert_eq!(node.text().to_string(), text);
1928 assert!(
1929 node.text().to_string().contains('\n'),
1930 "Should preserve newline in triple emphasis content"
1931 );
1932}