1use alloc::{borrow::Cow, collections::BTreeMap, string::String, vec, vec::Vec};
7
8use crate::{
9 ast::*,
10 diagnostic::{Diagnostic, DiagnosticCode, DiagnosticSeverity},
11 entities::named_character_reference,
12 memo::{
13 bracket_walk, path_walk, pattern_starts, BracketMemo, BracketStep, PathMemo, PersistentMap,
14 Positions, Step,
15 },
16 options::{SyntaxConfigError, SyntaxOptions},
17 span::Span,
18 validate::is_directive_name,
19};
20
21mod nul;
22#[cfg(test)]
23mod scan_tests;
24
25#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct ParseOutput {
29 pub document: Document,
31 pub diagnostics: Vec<Diagnostic>,
33}
34
35#[derive(Clone, Debug, Eq, PartialEq)]
37pub enum ParseStrictError {
38 Config(SyntaxConfigError),
40 Diagnostic(Diagnostic),
42}
43
44#[derive(Clone, Debug, Eq, PartialEq)]
45struct ParsedLinkResource {
46 destination: String,
47 destination_kind: LinkDestinationKind,
48 title: Option<String>,
49 title_kind: Option<LinkTitleKind>,
50}
51
52const REFERENCE_LABEL_MAX_CHARS: usize = 999;
53const WIKILINK_MAX_BYTES: usize = 999;
54
55#[derive(Clone, Copy, Debug)]
56struct Line<'a> {
57 text: &'a str,
58 eol: &'a str,
59 start: usize,
60 end: usize,
61 end_with_eol: usize,
62 lazy: bool,
67}
68
69#[derive(Clone, Copy, Debug)]
70struct ListMarkerInfo<'a> {
71 ordered: bool,
72 start: Option<u64>,
73 delimiter: ListDelimiter,
74 indent: usize,
75 marker_len: usize,
76 content_indent: usize,
77 content: &'a str,
78}
79
80#[derive(Clone, Copy, Debug)]
81struct DescriptionMarker<'a> {
82 content_offset: usize,
83 content: &'a str,
84}
85
86#[derive(Clone, Debug)]
87struct DescriptionTerm {
88 marker_index: usize,
89 term_end: usize,
90 blank_after_term: bool,
91 source: String,
92 source_offset: usize,
93}
94
95#[derive(Clone, Copy, Debug, Eq, PartialEq)]
96enum HtmlBlockKind {
97 RawTag,
98 BlockTag,
99 Until(&'static str),
100 UntilBlank,
101}
102
103pub fn parse(input: &str) -> ParseOutput {
106 SyntaxOptions::default().parse(input)
107}
108
109impl SyntaxOptions {
110 pub fn parse(&self, input: &str) -> ParseOutput {
115 match parse_checked(input, self) {
116 Ok(output) => output,
117 Err(error) => ParseOutput {
118 document: Document::default(),
119 diagnostics: vec![Diagnostic::new(
120 DiagnosticSeverity::Error,
121 DiagnosticCode::StrictParse,
122 Span::new(0, input.len()),
123 error.message(),
124 )],
125 },
126 }
127 }
128
129 pub fn parse_strict(&self, input: &str) -> Result<ParseOutput, ParseStrictError> {
132 let output = parse_checked(input, self).map_err(ParseStrictError::Config)?;
133 if let Some(diagnostic) = output
134 .diagnostics
135 .iter()
136 .find(|diagnostic| diagnostic.severity == DiagnosticSeverity::Error)
137 {
138 return Err(ParseStrictError::Diagnostic(diagnostic.clone()));
139 }
140 Ok(output)
141 }
142}
143
144fn parse_checked(input: &str, options: &SyntaxOptions) -> Result<ParseOutput, SyntaxConfigError> {
145 options.validate()?;
146 let mut diagnostics = Vec::new();
147 let start = if input.starts_with('\u{feff}') {
150 '\u{feff}'.len_utf8()
151 } else {
152 0
153 };
154 let source = &input[start..];
155 let definitions = collect_definitions(source, start, options);
156 let children = parse_blocks(
157 source,
158 start,
159 true,
160 options,
161 &definitions,
162 &mut diagnostics,
163 0,
164 );
165 let mut document = Document {
166 meta: NodeMeta::new(Some(Span::new(0, input.len()))),
167 children,
168 };
169 if source.contains('\0') {
170 nul::replace_in_document(&mut document);
171 }
172
173 Ok(ParseOutput {
174 document,
175 diagnostics,
176 })
177}
178
179fn source_char(char: char) -> char {
183 if char == '\0' {
184 '\u{FFFD}'
185 } else {
186 char
187 }
188}
189
190const MAX_BLOCK_NESTING: usize = 32;
195
196fn parse_blocks(
197 input: &str,
198 base_offset: usize,
199 allow_frontmatter: bool,
200 options: &SyntaxOptions,
201 definitions: &[String],
202 diagnostics: &mut Vec<Diagnostic>,
203 depth: usize,
204) -> Vec<Block> {
205 let lines = collect_lines(input, base_offset);
206 parse_blocks_from_lines(
207 &lines,
208 allow_frontmatter,
209 options,
210 definitions,
211 diagnostics,
212 depth,
213 )
214}
215
216fn parse_blocks_from_lines(
217 lines: &[Line<'_>],
218 allow_frontmatter: bool,
219 options: &SyntaxOptions,
220 definitions: &[String],
221 diagnostics: &mut Vec<Diagnostic>,
222 depth: usize,
223) -> Vec<Block> {
224 let mut blocks = Vec::new();
225 let mut index = 0;
226 let nest_containers = depth < MAX_BLOCK_NESTING;
229 let mut container_closes = BracketMemo::default();
232 let mut mdx_flow = MdxFlowScan::default();
233
234 while index < lines.len() {
235 let line = lines[index];
236 if line.text.trim().is_empty() {
237 index += 1;
238 continue;
239 }
240 let after_definition_unbroken = index > 0
241 && !lines[index - 1].text.trim().is_empty()
242 && matches!(blocks.last(), Some(Block::Definition(_)));
243
244 if allow_frontmatter && index == 0 {
245 if let Some((block, next)) = parse_frontmatter(lines, index, options) {
246 blocks.push(block);
247 index = next;
248 continue;
249 }
250 }
251
252 if let Some((block, next)) = nest_containers
253 .then(|| {
254 parse_container_directive(lines, index, options, definitions, diagnostics, depth)
255 })
256 .flatten()
257 {
258 blocks.push(block);
259 index = next;
260 continue;
261 }
262
263 if let Some((block, next)) = parse_math_block(lines, index, options) {
264 blocks.push(block);
265 index = next;
266 continue;
267 }
268
269 if let Some((block, next)) = parse_fenced_code(lines, index, options) {
270 blocks.push(block);
271 index = next;
272 continue;
273 }
274
275 if let Some((block, next)) = nest_containers
276 .then(|| parse_block_quote(lines, index, options, definitions, diagnostics, depth))
277 .flatten()
278 {
279 blocks.push(block);
280 index = next;
281 continue;
282 }
283
284 if let Some(block) = parse_atx_heading(line, options, definitions) {
285 blocks.push(block);
286 index += 1;
287 continue;
288 }
289
290 if let Some(block) = parse_thematic_break(line) {
291 blocks.push(block);
292 index += 1;
293 continue;
294 }
295
296 if let Some((block, next)) = nest_containers
297 .then(|| parse_list(lines, index, options, definitions, diagnostics, depth))
298 .flatten()
299 {
300 blocks.push(block);
301 index = next;
302 continue;
303 }
304
305 if let Some((block, next)) = nest_containers
306 .then(|| {
307 parse_footnote_definition(lines, index, options, definitions, diagnostics, depth)
308 })
309 .flatten()
310 {
311 blocks.push(block);
312 index = next;
313 continue;
314 }
315
316 if let Some((block, next)) =
317 parse_definition(lines, index, options, after_definition_unbroken)
318 {
319 blocks.push(block);
320 index = next;
321 continue;
322 }
323
324 if let Some(block) = parse_leaf_directive(line, options, definitions, diagnostics) {
325 blocks.push(block);
326 index += 1;
327 continue;
328 }
329
330 if let Some((block, next)) = nest_containers
331 .then(|| {
332 parse_html_container(
333 lines,
334 index,
335 options,
336 definitions,
337 diagnostics,
338 depth,
339 &mut container_closes,
340 )
341 })
342 .flatten()
343 {
344 blocks.push(block);
345 index = next;
346 continue;
347 }
348
349 if let Some((block, next)) = parse_html_block(lines, index, options) {
350 blocks.push(block);
351 index = next;
352 continue;
353 }
354
355 if let Some((block, next)) =
356 parse_mdx_flow(lines, index, options, diagnostics, &mut mdx_flow)
357 {
358 blocks.push(block);
359 index = next;
360 continue;
361 }
362
363 if !after_definition_unbroken {
364 if let Some((block, next)) = parse_indented_code(lines, index, options) {
365 blocks.push(block);
366 index = next;
367 continue;
368 }
369 }
370
371 if let Some((block, next)) = parse_table(lines, index, options, definitions, diagnostics) {
372 blocks.push(block);
373 index = next;
374 continue;
375 }
376
377 if let Some((block, next)) = parse_setext_heading(lines, index, options, definitions) {
378 blocks.push(block);
379 index = next;
380 continue;
381 }
382
383 if let Some((block, next)) = nest_containers
384 .then(|| parse_description_list(lines, index, options, definitions, diagnostics, depth))
385 .flatten()
386 {
387 blocks.push(block);
388 index = next;
389 continue;
390 }
391
392 let (block, next) = parse_paragraph(lines, index, options, definitions, diagnostics);
393 blocks.push(block);
394 index = next;
395 }
396
397 blocks
398}
399
400fn collect_lines(input: &str, base_offset: usize) -> Vec<Line<'_>> {
401 let bytes = input.as_bytes();
402 let mut lines = Vec::new();
403 let mut start = 0;
404 let mut index = 0;
405
406 while index < bytes.len() {
407 match bytes[index] {
408 b'\n' => {
409 let end = index;
410 lines.push(Line {
411 text: &input[start..end],
412 eol: &input[index..index + 1],
413 start: base_offset + start,
414 end: base_offset + end,
415 end_with_eol: base_offset + index + 1,
416 lazy: false,
417 });
418 index += 1;
419 start = index;
420 }
421 b'\r' => {
422 let end = index;
423 let eol_end = if index + 1 < bytes.len() && bytes[index + 1] == b'\n' {
424 index + 2
425 } else {
426 index + 1
427 };
428 lines.push(Line {
429 text: &input[start..end],
430 eol: &input[index..eol_end],
431 start: base_offset + start,
432 end: base_offset + end,
433 end_with_eol: base_offset + eol_end,
434 lazy: false,
435 });
436 index = eol_end;
437 start = index;
438 }
439 _ => index += 1,
440 }
441 }
442
443 if start < bytes.len() || input.is_empty() {
444 lines.push(Line {
445 text: &input[start..],
446 eol: "",
447 start: base_offset + start,
448 end: base_offset + bytes.len(),
449 end_with_eol: base_offset + bytes.len(),
450 lazy: false,
451 });
452 }
453
454 lines
455}
456
457fn collect_definitions(input: &str, base_offset: usize, options: &SyntaxOptions) -> Vec<String> {
458 let mut diagnostics = Vec::new();
459 let blocks = parse_blocks(input, base_offset, true, options, &[], &mut diagnostics, 0);
460 let mut definitions = Vec::new();
461 collect_definition_refs_from_blocks(&blocks, &mut definitions);
462 definitions.sort_unstable();
464 definitions.dedup();
465 definitions
466}
467
468fn collect_definition_refs_from_blocks(blocks: &[Block], definitions: &mut Vec<String>) {
469 for block in blocks {
470 match block {
471 Block::Definition(definition) => {
472 definitions.push(definition.identifier.clone());
473 }
474 Block::BlockQuote(node) => {
475 collect_definition_refs_from_blocks(&node.children, definitions);
476 }
477 Block::Alert(node) => {
478 collect_definition_refs_from_blocks(&node.children, definitions);
479 }
480 Block::List(node) => {
481 for item in &node.children {
482 collect_definition_refs_from_blocks(&item.children, definitions);
483 }
484 }
485 Block::DescriptionList(node) => {
486 for item in &node.children {
487 for details in &item.details {
488 collect_definition_refs_from_blocks(&details.children, definitions);
489 }
490 }
491 }
492 Block::FootnoteDefinition(node) => {
493 collect_definition_refs_from_blocks(&node.children, definitions);
494 }
495 Block::HtmlContainer(node) => {
496 if let HtmlContainerContent::Blocks(children) = &node.content {
497 collect_definition_refs_from_blocks(children, definitions);
498 }
499 }
500 Block::ContainerDirective(node) => {
501 collect_definition_refs_from_blocks(&node.children, definitions);
502 }
503 _ => {}
504 }
505 }
506}
507
508fn parse_frontmatter(
509 lines: &[Line<'_>],
510 index: usize,
511 options: &SyntaxOptions,
512) -> Option<(Block, usize)> {
513 if !options.constructs.frontmatter {
514 return None;
515 }
516 let kind = frontmatter_fence_kind(lines[index].text)?;
517
518 let mut value = String::new();
519 let mut cursor = index + 1;
520 while cursor < lines.len() {
521 if frontmatter_fence_kind(lines[cursor].text) == Some(kind) {
522 let span = Span::new(lines[index].start, lines[cursor].end_with_eol);
523 return Some((
524 Block::Frontmatter(Frontmatter {
525 meta: NodeMeta::new(Some(span)),
526 kind,
527 value,
528 }),
529 cursor + 1,
530 ));
531 }
532 push_line(&mut value, lines[cursor].text);
533 cursor += 1;
534 }
535
536 None
537}
538
539fn frontmatter_fence_kind(line: &str) -> Option<FrontmatterKind> {
540 match line.trim_end_matches([' ', '\t']) {
541 "---" => Some(FrontmatterKind::Yaml),
542 "+++" => Some(FrontmatterKind::Toml),
543 _ => None,
544 }
545}
546
547fn parse_container_directive(
548 lines: &[Line<'_>],
549 index: usize,
550 options: &SyntaxOptions,
551 definitions: &[String],
552 diagnostics: &mut Vec<Diagnostic>,
553 depth: usize,
554) -> Option<(Block, usize)> {
555 if !options.constructs.directive_container {
556 return None;
557 }
558 let trimmed = trim_up_to_three_spaces(lines[index].text)?;
559 let Some((fence_len, opener_rest)) = directive_container_opener_prefix(trimmed) else {
560 return None;
561 };
562 let opener_base = lines[index].start + (lines[index].text.len() - trimmed.len()) + fence_len;
563
564 let Some((name, label_source, attributes, _consumed)) = parse_directive_opener(opener_rest)
565 else {
566 diagnostics.push(Diagnostic::new(
567 DiagnosticSeverity::Error,
568 DiagnosticCode::InvalidDirectiveName,
569 Span::new(lines[index].start, lines[index].end),
570 "container directive must have a valid name",
571 ));
572 return None;
573 };
574 let label_base = opener_base + name.len() + 1;
575
576 let mut content = String::new();
577 let mut cursor = index + 1;
578 let mut nested_fences = Vec::new();
579 while cursor < lines.len() {
580 let line = lines[cursor].text;
581 let trimmed = trim_up_to_three_spaces(line);
582 if let Some(trimmed) = trimmed {
583 if let Some(nested_len) = nested_fences.last().copied() {
584 if directive_container_closing_fence(trimmed, nested_len).is_some() {
585 nested_fences.pop();
586 push_line(&mut content, line);
587 cursor += 1;
588 continue;
589 }
590 } else if directive_container_closing_fence(trimmed, fence_len).is_some() {
591 let label = label_source
592 .map(|source| {
593 parse_inlines(source, label_base, options, definitions, diagnostics)
594 })
595 .unwrap_or_default();
596 let children = parse_blocks(
597 &content,
598 lines[index + 1].start,
599 false,
600 options,
601 definitions,
602 diagnostics,
603 depth + 1,
604 );
605 return Some((
606 Block::ContainerDirective(ContainerDirective {
607 meta: NodeMeta::new(Some(Span::new(
608 lines[index].start,
609 lines[cursor].end_with_eol,
610 ))),
611 name,
612 label,
613 attributes,
614 children,
615 }),
616 cursor + 1,
617 ));
618 }
619
620 if let Some((nested_len, nested_rest)) = directive_container_opener_prefix(trimmed) {
621 if parse_directive_opener(nested_rest).is_some() {
622 nested_fences.push(nested_len);
623 }
624 }
625 }
626
627 push_line(&mut content, line);
628 cursor += 1;
629 }
630
631 diagnostics.push(Diagnostic::new(
632 DiagnosticSeverity::Error,
633 DiagnosticCode::UnclosedDirectiveContainer,
634 Span::new(lines[index].start, lines[index].end),
635 "container directive is missing a closing fence",
636 ));
637 Some((
638 Block::ContainerDirective(ContainerDirective {
639 meta: NodeMeta::new(Some(Span::new(
640 lines[index].start,
641 lines.last()?.end_with_eol,
642 ))),
643 name,
644 label: label_source
645 .map(|source| parse_inlines(source, label_base, options, definitions, diagnostics))
646 .unwrap_or_default(),
647 attributes,
648 children: parse_blocks(
649 &content,
650 lines
651 .get(index + 1)
652 .map(|line| line.start)
653 .unwrap_or(lines[index].end),
654 false,
655 options,
656 definitions,
657 diagnostics,
658 depth + 1,
659 ),
660 }),
661 lines.len(),
662 ))
663}
664
665fn directive_container_opener_prefix(input: &str) -> Option<(usize, &str)> {
666 let fence_len = input
667 .as_bytes()
668 .iter()
669 .take_while(|byte| **byte == b':')
670 .count();
671 if fence_len >= 3 {
672 Some((fence_len, &input[fence_len..]))
673 } else {
674 None
675 }
676}
677
678fn directive_container_closing_fence(input: &str, min_len: usize) -> Option<usize> {
679 let fence_len = input
680 .as_bytes()
681 .iter()
682 .take_while(|byte| **byte == b':')
683 .count();
684 if fence_len >= min_len && input[fence_len..].trim().is_empty() {
685 Some(fence_len)
686 } else {
687 None
688 }
689}
690
691fn parse_math_block(
692 lines: &[Line<'_>],
693 index: usize,
694 options: &SyntaxOptions,
695) -> Option<(Block, usize)> {
696 if !options.constructs.math_block {
697 return None;
698 }
699 let opener = trim_up_to_three_spaces(lines[index].text)?;
705 let fence_length = math_block_fence_length(opener)?;
706 let opening_indent = leading_indent_columns(lines[index].text);
707
708 let mut value = String::new();
709 let mut content_lines = 0usize;
710 let mut cursor = index + 1;
711 while cursor < lines.len() {
712 if let Some(close_line) = trim_up_to_three_spaces(lines[cursor].text) {
713 if math_block_fence_closes(close_line, fence_length) {
714 return Some((
715 Block::MathBlock(MathBlock {
716 meta: NodeMeta::new(Some(Span::new(
717 lines[index].start,
718 lines[cursor].end_with_eol,
719 ))),
720 value,
721 }),
722 cursor + 1,
723 ));
724 }
725 }
726 if content_lines > 0 {
727 ensure_line_separator(&mut value);
731 }
732 let stripped = strip_leading_indent_columns(lines[cursor].text, opening_indent);
733 value.push_str(&stripped);
734 value.push_str(lines[cursor].eol);
735 content_lines += 1;
736 cursor += 1;
737 }
738
739 Some((
742 Block::MathBlock(MathBlock {
743 meta: NodeMeta::new(Some(Span::new(
744 lines[index].start,
745 lines.last()?.end_with_eol,
746 ))),
747 value,
748 }),
749 lines.len(),
750 ))
751}
752
753fn math_block_fence_length(input: &str) -> Option<usize> {
756 let length = input
757 .as_bytes()
758 .iter()
759 .take_while(|byte| **byte == b'$')
760 .count();
761 if length < 2 || input[length..].contains('$') {
762 return None;
763 }
764 Some(length)
765}
766
767fn math_block_fence_closes(input: &str, length: usize) -> bool {
770 let count = input
771 .as_bytes()
772 .iter()
773 .take_while(|byte| **byte == b'$')
774 .count();
775 count >= length && input[count..].trim().is_empty()
776}
777
778fn parse_fenced_code(
779 lines: &[Line<'_>],
780 index: usize,
781 options: &SyntaxOptions,
782) -> Option<(Block, usize)> {
783 let line = fence_line(lines[index].text, options)?;
784 let (marker, length) = fence_start(line)?;
785 let opening_indent = leading_indent_columns(lines[index].text);
788 let info = line[length..].trim();
789 if marker == FenceMarker::Backtick && info.contains('`') {
790 return None;
791 }
792 let info = if info.is_empty() {
793 None
794 } else {
795 Some(unescape_string(info))
796 };
797
798 let mut value = String::new();
799 let mut content_lines = 0usize;
804 let mut cursor = index + 1;
805 while cursor < lines.len() {
806 if let Some(close_line) = fence_line(lines[cursor].text, options) {
807 if fence_close(close_line, marker, length) {
808 return Some((
809 Block::CodeBlock(CodeBlock {
810 meta: NodeMeta::new(Some(Span::new(
811 lines[index].start,
812 lines[cursor].end_with_eol,
813 ))),
814 kind: CodeBlockKind::Fenced { marker, length },
815 info,
816 value,
817 }),
818 cursor + 1,
819 ));
820 }
821 }
822 if content_lines > 0 {
823 ensure_line_separator(&mut value);
827 }
828 let stripped = strip_leading_indent_columns(lines[cursor].text, opening_indent);
829 value.push_str(&stripped);
830 value.push_str(lines[cursor].eol);
831 content_lines += 1;
832 cursor += 1;
833 }
834 Some((
835 Block::CodeBlock(CodeBlock {
836 meta: NodeMeta::new(Some(Span::new(
837 lines[index].start,
838 lines.last()?.end_with_eol,
839 ))),
840 kind: CodeBlockKind::Fenced { marker, length },
841 info,
842 value,
843 }),
844 lines.len(),
845 ))
846}
847
848fn fence_line<'a>(line: &'a str, options: &SyntaxOptions) -> Option<&'a str> {
849 if options.constructs.indented_code {
850 trim_up_to_three_spaces(line)
851 } else {
852 Some(trim_ascii_start(line))
853 }
854}
855
856fn container_closed_after_unclosed_fence(
857 lines: &[Line<'_>],
858 cursor: usize,
859 last_content_index: usize,
860 content: &str,
861 options: &SyntaxOptions,
862) -> bool {
863 !lines[last_content_index].eol.is_empty()
864 && (cursor >= lines.len() || lines[cursor].text.trim().is_empty())
865 && content_has_unclosed_fenced_code(content, options)
866}
867
868fn content_has_unclosed_fenced_code(content: &str, options: &SyntaxOptions) -> bool {
869 let lines = collect_lines(content, 0);
870 let mut open_fence = None;
871 for line in lines {
872 let Some(trimmed) = fence_line(line.text, options) else {
873 continue;
874 };
875 if let Some((marker, length, has_nonblank_content)) = open_fence {
876 if fence_close(trimmed, marker, length) {
877 open_fence = None;
878 } else {
879 open_fence = Some((
880 marker,
881 length,
882 has_nonblank_content || !trimmed.trim().is_empty(),
883 ));
884 }
885 continue;
886 }
887 let Some((marker, length)) = fence_start(trimmed) else {
888 continue;
889 };
890 let info = trimmed[length..].trim();
891 if marker != FenceMarker::Backtick || !info.contains('`') {
892 open_fence = Some((marker, length, false));
893 }
894 }
895 open_fence.is_some_and(|(_, _, has_nonblank_content)| !has_nonblank_content)
896}
897
898fn block_quote_content_paragraph_open(content: &str, options: &SyntaxOptions) -> bool {
908 let mut source = Cow::Borrowed(content);
912 let mut offset = 0;
913 for _ in 0..=MAX_BLOCK_NESTING {
914 let Some(trimmed) = trim_up_to_three_spaces(&source[offset..]) else {
915 return false;
917 };
918 if trimmed.is_empty() {
919 return false;
920 }
921 let rest = if let Some(rest) = trimmed.strip_prefix('>') {
922 Cow::Borrowed(rest.strip_prefix(' ').unwrap_or(rest))
923 } else if let Some(marker) = list_marker_info(trimmed) {
924 list_marker_first_content(trimmed, marker)
925 } else {
926 return !lazy_line_starts_block(trimmed, options);
927 };
928 match rest {
929 Cow::Borrowed(rest) => offset = source.len() - rest.len(),
931 Cow::Owned(rest) => {
932 source = Cow::Owned(rest);
933 offset = 0;
934 }
935 }
936 }
937 true
938}
939
940fn lazy_line_starts_block(input: &str, options: &SyntaxOptions) -> bool {
946 likely_block_start(input, options)
947 || (options.constructs.html_block && line_starts_html_block(input))
948 || trim_up_to_three_spaces(input).is_some_and(|t| t.starts_with('`') || t.starts_with('~'))
953}
954
955fn parse_block_quote(
956 lines: &[Line<'_>],
957 index: usize,
958 options: &SyntaxOptions,
959 definitions: &[String],
960 diagnostics: &mut Vec<Diagnostic>,
961 depth: usize,
962) -> Option<(Block, usize)> {
963 if !trim_up_to_three_spaces(lines[index].text)?.starts_with('>') {
964 return None;
965 }
966
967 let mut content = String::new();
968 let mut lazy_flags: Vec<bool> = Vec::new();
972 let mut cursor = index;
973 let mut paragraph_open = false;
974 let mut in_table = false;
975 let mut last_content_line: Option<String> = None;
976 let mut content_base_offset = None;
977 while cursor < lines.len() {
978 let raw = lines[cursor].text;
979 let trimmed_opt = trim_up_to_three_spaces(raw);
980 let marked = trimmed_opt.is_some_and(|trimmed| trimmed.starts_with('>'));
981 let quote_rest_owned: String;
982 if let Some(trimmed) = trimmed_opt {
983 if trimmed.is_empty() {
984 break;
985 }
986 }
987 let (line, line_start) = if marked {
988 let trimmed = trimmed_opt.expect("marked implies a trimmed line");
989 let trimmed_start = lines[cursor].start + (raw.len() - trimmed.len());
990 let mut rest_start = 1;
991 let mut rest = &trimmed[rest_start..];
992 if rest.starts_with(' ') {
993 rest_start += 1;
994 rest = &rest[1..];
995 } else if rest.starts_with('\t') {
996 let marker_end_column = leading_indent_columns(raw) + 1;
997 match strip_leading_indent_columns_from(rest, 1, marker_end_column) {
998 Cow::Borrowed(stripped) => rest = stripped,
999 Cow::Owned(stripped) => {
1000 quote_rest_owned = stripped;
1001 rest = "e_rest_owned;
1002 }
1003 }
1004 }
1005 (rest, trimmed_start + rest_start)
1006 } else if in_table {
1007 break;
1010 } else if paragraph_open && !lazy_line_starts_block(raw, options) {
1011 (raw, lines[cursor].start)
1015 } else {
1016 break;
1017 };
1018
1019 let mut escaped_lazy = String::new();
1020 let line = if !marked
1021 && last_content_line.as_deref().is_some_and(|previous| {
1022 table_can_start_source(
1023 previous,
1024 line,
1025 options.constructs.indented_code,
1026 options.constructs.spoiler,
1027 )
1028 }) {
1029 escaped_lazy.push_str(line);
1030 if let Some(offset) = escaped_lazy.find('-') {
1031 escaped_lazy.insert(offset, '\\');
1032 }
1033 &escaped_lazy
1034 } else {
1035 line
1036 };
1037
1038 let starts_table = last_content_line.as_deref().is_some_and(|previous| {
1039 table_can_start_source(
1040 previous,
1041 line,
1042 options.constructs.indented_code,
1043 options.constructs.spoiler,
1044 )
1045 });
1046 if marked && starts_table {
1047 paragraph_open = false;
1048 in_table = true;
1049 } else if marked && in_table && block_quote_table_body_row(line, options) {
1050 paragraph_open = false;
1051 } else {
1052 in_table = false;
1053 paragraph_open = block_quote_content_paragraph_open(line, options);
1056 }
1057 last_content_line = Some(line.into());
1058 if content_base_offset.is_none() {
1059 content_base_offset = Some(line_start);
1060 }
1061 push_line(&mut content, line);
1062 lazy_flags.push(!marked);
1063 cursor += 1;
1064 }
1065
1066 let span = Span::new(lines[index].start, lines[cursor - 1].end_with_eol);
1067 let child_base_offset = content_base_offset.unwrap_or(lines[index].start);
1068 if !lines[cursor - 1].eol.is_empty() && !ends_with_line_ending(&content) {
1069 content.push_str(lines[cursor - 1].eol);
1070 }
1071 if container_closed_after_unclosed_fence(lines, cursor, cursor - 1, &content, options) {
1072 content.push('\n');
1073 }
1074 if let Some(alert) = parse_alert_from_block_quote(
1075 &content,
1076 child_base_offset,
1077 span,
1078 options,
1079 definitions,
1080 diagnostics,
1081 depth,
1082 ) {
1083 return Some((alert, cursor));
1084 }
1085
1086 let mut child_lines = collect_lines(&content, child_base_offset);
1087 for (child, &lazy) in child_lines.iter_mut().zip(lazy_flags.iter()) {
1088 child.lazy = lazy;
1089 }
1090 let children = parse_blocks_from_lines(
1091 &child_lines,
1092 false,
1093 options,
1094 definitions,
1095 diagnostics,
1096 depth + 1,
1097 );
1098 Some((
1099 Block::BlockQuote(BlockQuote {
1100 meta: NodeMeta::new(Some(span)),
1101 children,
1102 }),
1103 cursor,
1104 ))
1105}
1106
1107fn parse_alert_from_block_quote(
1108 content: &str,
1109 base_offset: usize,
1110 span: Span,
1111 options: &SyntaxOptions,
1112 definitions: &[String],
1113 diagnostics: &mut Vec<Diagnostic>,
1114 depth: usize,
1115) -> Option<Block> {
1116 if !options.constructs.gfm_alert {
1117 return None;
1118 }
1119 let (first_line, rest) = content.split_once('\n').unwrap_or((content, ""));
1120 let (kind, title) = parse_alert_marker(first_line)?;
1121 let rest_base_offset = base_offset + first_line.len() + usize::from(!rest.is_empty());
1122 let children = if rest.is_empty() {
1123 Vec::new()
1124 } else {
1125 parse_blocks(
1126 rest,
1127 rest_base_offset,
1128 false,
1129 options,
1130 definitions,
1131 diagnostics,
1132 depth + 1,
1133 )
1134 };
1135 Some(Block::Alert(Alert {
1136 meta: NodeMeta::new(Some(span)),
1137 kind,
1138 title,
1139 children,
1140 }))
1141}
1142
1143fn parse_alert_marker(line: &str) -> Option<(AlertKind, Option<String>)> {
1144 let close = line.find(']')?;
1145 let marker = line.get(0..close + 1)?;
1146 if !marker.starts_with("[!") {
1147 return None;
1148 }
1149 let kind = match &marker[2..close].to_ascii_lowercase()[..] {
1150 "note" => AlertKind::Note,
1151 "tip" => AlertKind::Tip,
1152 "important" => AlertKind::Important,
1153 "warning" => AlertKind::Warning,
1154 "caution" => AlertKind::Caution,
1155 _ => return None,
1156 };
1157 let title = line[close + 1..].trim();
1158 Some((
1159 kind,
1160 if title.is_empty() {
1161 None
1162 } else {
1163 Some(title.into())
1164 },
1165 ))
1166}
1167
1168fn block_quote_table_body_row(line: &str, options: &SyntaxOptions) -> bool {
1169 table_indent_line(line, options.constructs.indented_code).is_some_and(|row| {
1170 !row.trim().is_empty() && contains_unescaped_pipe(row, options.constructs.spoiler)
1171 })
1172}
1173
1174fn parse_list(
1175 lines: &[Line<'_>],
1176 index: usize,
1177 options: &SyntaxOptions,
1178 definitions: &[String],
1179 diagnostics: &mut Vec<Diagnostic>,
1180 depth: usize,
1181) -> Option<(Block, usize)> {
1182 let first_marker = list_marker_info(lines[index].text)?;
1183 let mut items = Vec::new();
1184 let mut cursor = index;
1185 let mut tight = true;
1186
1187 while cursor < lines.len() {
1188 if parse_thematic_break(lines[cursor]).is_some() {
1193 break;
1194 }
1195 let Some(marker) = list_marker_info(lines[cursor].text) else {
1196 break;
1197 };
1198 if !same_list_marker(first_marker, marker) {
1199 break;
1200 }
1201
1202 let item_start = cursor;
1203 let mut item_end = cursor;
1204 let mut item_tight = true;
1205 let mut item_blank_offsets: Vec<usize> = Vec::new();
1211 let mut content = String::new();
1212 let mut lazy_flags: Vec<bool> = Vec::new();
1219 let mut open_fence = None;
1220 let first_content = list_marker_first_content(lines[cursor].text, marker);
1221 let mut last_content_line: Option<String> = Some(first_content.as_ref().into());
1222 let mut paragraph_open = list_item_paragraph_stays_open(None, &first_content, options);
1223 let mut item_started_blank = first_content.trim().is_empty();
1228 push_line(&mut content, &first_content);
1229 lazy_flags.push(false);
1230 update_list_item_fence(&first_content, &mut open_fence);
1231 cursor += 1;
1232
1233 while cursor < lines.len() {
1234 if lines[cursor].text.trim().is_empty() {
1235 if open_fence.is_some() {
1238 let stripped = strip_list_continuation(
1239 lines[cursor].text,
1240 marker.content_indent,
1241 first_marker.indent,
1242 );
1243 push_line(&mut content, &stripped);
1244 lazy_flags.push(false);
1245 update_list_item_fence(&stripped, &mut open_fence);
1246 item_end = cursor;
1247 cursor += 1;
1248 continue;
1249 }
1250 let next = next_nonblank_line(lines, cursor + 1);
1251 if item_started_blank
1252 || next >= lines.len()
1253 || sibling_list_marker_at_line(
1254 lines[next].text,
1255 first_marker,
1256 marker.content_indent,
1257 )
1258 || leading_indent_columns(lines[next].text) < marker.content_indent
1259 {
1260 if next < lines.len()
1261 && sibling_list_marker_at_line(
1262 lines[next].text,
1263 first_marker,
1264 marker.content_indent,
1265 )
1266 {
1267 item_tight = false;
1268 }
1269 cursor = next;
1270 break;
1271 }
1272 item_blank_offsets.push(content.len() + usize::from(!content.is_empty()));
1280 paragraph_open = false;
1281 push_line(&mut content, "");
1282 lazy_flags.push(false);
1283 item_end = cursor;
1284 cursor += 1;
1285 continue;
1286 }
1287
1288 item_started_blank = false;
1289
1290 if sibling_list_marker_at_line(lines[cursor].text, first_marker, marker.content_indent)
1291 {
1292 break;
1293 }
1294
1295 if leading_indent_columns(lines[cursor].text) < marker.content_indent
1300 && !same_list_marker_line(lines[cursor].text, first_marker)
1301 && list_marker_info(lines[cursor].text).is_some()
1302 {
1303 break;
1304 }
1305
1306 if leading_indent_columns(lines[cursor].text) < marker.content_indent {
1307 if likely_block_start(lines[cursor].text, options) || !paragraph_open {
1308 break;
1309 }
1310 }
1311
1312 let lazy = paragraph_open
1318 && leading_indent_columns(lines[cursor].text) < marker.content_indent;
1319 let stripped = strip_list_continuation(
1320 lines[cursor].text,
1321 marker.content_indent,
1322 first_marker.indent,
1323 );
1324 let starts_table = last_content_line.as_deref().is_some_and(|previous| {
1325 table_can_start_source(
1326 previous,
1327 &stripped,
1328 options.constructs.indented_code,
1329 options.constructs.spoiler,
1330 )
1331 });
1332 paragraph_open = if starts_table {
1333 false
1334 } else {
1335 list_item_paragraph_stays_open(Some(paragraph_open), &stripped, options)
1336 };
1337 push_line(&mut content, &stripped);
1338 lazy_flags.push(lazy);
1339 update_list_item_fence(&stripped, &mut open_fence);
1340 last_content_line = Some(stripped.into_owned());
1341 item_end = cursor;
1342 cursor += 1;
1343 }
1344
1345 let child_base = lines[item_start].start + marker.content_indent;
1346 if !lines[item_end].eol.is_empty() && !ends_with_line_ending(&content) {
1347 content.push_str(lines[item_end].eol);
1348 }
1349 if container_closed_after_unclosed_fence(lines, cursor, item_end, &content, options) {
1350 content.push('\n');
1351 }
1352 let mut child_lines = collect_lines(&content, child_base);
1353 for (child, &lazy) in child_lines.iter_mut().zip(lazy_flags.iter()) {
1354 child.lazy = lazy;
1355 }
1356 let mut children = parse_blocks_from_lines(
1357 &child_lines,
1358 false,
1359 options,
1360 definitions,
1361 diagnostics,
1362 depth + 1,
1363 );
1364 let checked = if options.constructs.gfm_task_list_item {
1365 take_task_marker_from_children(&mut children)
1366 } else {
1367 None
1368 };
1369
1370 if item_tight
1371 && blank_separates_top_level_blocks(&item_blank_offsets, &children, child_base)
1372 {
1373 item_tight = false;
1374 }
1375 tight = tight && item_tight;
1376 items.push(ListItem {
1377 meta: NodeMeta::new(Some(Span::new(
1378 lines[item_start].start,
1379 lines[item_end].end_with_eol,
1380 ))),
1381 checked,
1382 children,
1383 });
1384 }
1385
1386 Some((
1387 Block::List(List {
1388 meta: NodeMeta::new(Some(Span::new(
1389 lines[index].start,
1390 lines[cursor - 1].end_with_eol,
1391 ))),
1392 ordered: first_marker.ordered,
1393 start: first_marker.start,
1394 delimiter: first_marker.delimiter,
1395 tight,
1396 children: items,
1397 }),
1398 cursor,
1399 ))
1400}
1401
1402fn blank_separates_top_level_blocks(
1415 blank_offsets: &[usize],
1416 children: &[Block],
1417 child_base: usize,
1418) -> bool {
1419 if blank_offsets.is_empty() || children.len() < 2 {
1420 return false;
1421 }
1422 let Some(&first_blank) = blank_offsets.iter().min() else {
1423 return false;
1424 };
1425 children.iter().any(|child| {
1426 block_span(child).is_some_and(|span| span.start.saturating_sub(child_base) > first_blank)
1427 })
1428}
1429
1430fn block_span(block: &Block) -> Option<Span> {
1431 let meta = match block {
1432 Block::Paragraph(node) => &node.meta,
1433 Block::Heading(node) => &node.meta,
1434 Block::ThematicBreak(node) => &node.meta,
1435 Block::BlockQuote(node) => &node.meta,
1436 Block::Alert(node) => &node.meta,
1437 Block::List(node) => &node.meta,
1438 Block::DescriptionList(node) => &node.meta,
1439 Block::CodeBlock(node) => &node.meta,
1440 Block::HtmlBlock(node) => &node.meta,
1441 Block::HtmlContainer(node) => &node.meta,
1442 Block::Definition(node) => &node.meta,
1443 Block::FootnoteDefinition(node) => &node.meta,
1444 Block::Table(node) => &node.meta,
1445 Block::MathBlock(node) => &node.meta,
1446 Block::Frontmatter(node) => &node.meta,
1447 Block::MdxEsm(node) => &node.meta,
1448 Block::MdxExpression(node) => &node.meta,
1449 Block::MdxJsx(node) => &node.meta,
1450 Block::LeafDirective(node) => &node.meta,
1451 Block::ContainerDirective(node) => &node.meta,
1452 };
1453 meta.span
1454}
1455
1456fn list_item_paragraph_stays_open(
1457 previous_open: Option<bool>,
1458 line: &str,
1459 options: &SyntaxOptions,
1460) -> bool {
1461 if line.trim().is_empty() {
1462 return false;
1463 }
1464 if previous_open == Some(false) {
1465 return false;
1466 }
1467 block_quote_content_paragraph_open(line, options)
1468}
1469
1470fn parse_description_list(
1471 lines: &[Line<'_>],
1472 index: usize,
1473 options: &SyntaxOptions,
1474 definitions: &[String],
1475 diagnostics: &mut Vec<Diagnostic>,
1476 depth: usize,
1477) -> Option<(Block, usize)> {
1478 if !options.constructs.description_list || !is_description_term_line(lines[index].text, options)
1479 {
1480 return None;
1481 }
1482
1483 let mut cursor = index;
1484 let mut items = Vec::new();
1485 let mut tight = true;
1486 let mut list_end = lines[index].end_with_eol;
1487
1488 while cursor < lines.len() {
1489 if !is_description_term_line(lines[cursor].text, options) {
1490 break;
1491 }
1492 let Some(term) = description_term(lines, cursor, options) else {
1493 break;
1494 };
1495 let term_line = lines[cursor];
1496 let mut details = Vec::new();
1497 let item_start = term_line.start;
1498 let mut item_end = lines[term.term_end].end_with_eol;
1499 tight = tight && !term.blank_after_term;
1500 cursor = term.marker_index;
1501
1502 loop {
1503 let Some(marker) = description_marker(lines[cursor].text) else {
1504 break;
1505 };
1506 let (detail, next, detail_tight) = parse_description_details(
1507 lines,
1508 cursor,
1509 marker,
1510 options,
1511 definitions,
1512 diagnostics,
1513 depth,
1514 )?;
1515 tight = tight && detail_tight;
1516 item_end = detail
1517 .meta
1518 .span
1519 .map(|span| span.end)
1520 .unwrap_or(lines[cursor].end_with_eol);
1521 details.push(detail);
1522 cursor = next;
1523
1524 let next_nonblank = next_nonblank_line(lines, cursor);
1525 if next_nonblank < lines.len()
1526 && description_marker(lines[next_nonblank].text).is_some()
1527 {
1528 if next_nonblank != cursor {
1529 tight = false;
1530 }
1531 cursor = next_nonblank;
1532 continue;
1533 }
1534 break;
1535 }
1536
1537 if details.is_empty() {
1538 return None;
1539 }
1540 list_end = item_end;
1541 items.push(DescriptionItem {
1542 meta: NodeMeta::new(Some(Span::new(item_start, item_end))),
1543 term: parse_inlines(
1544 &term.source,
1545 term.source_offset,
1546 options,
1547 definitions,
1548 diagnostics,
1549 ),
1550 details,
1551 });
1552
1553 let next_item = next_nonblank_line(lines, cursor);
1554 if next_item >= lines.len() {
1555 cursor = next_item;
1556 break;
1557 }
1558 if description_term(lines, next_item, options).is_some() {
1559 if next_item != cursor {
1560 tight = false;
1561 }
1562 cursor = next_item;
1563 continue;
1564 }
1565 cursor = next_item;
1566 break;
1567 }
1568
1569 (!items.is_empty()).then_some((
1570 Block::DescriptionList(DescriptionList {
1571 meta: NodeMeta::new(Some(Span::new(lines[index].start, list_end))),
1572 tight,
1573 children: items,
1574 }),
1575 cursor,
1576 ))
1577}
1578
1579fn parse_description_details(
1580 lines: &[Line<'_>],
1581 index: usize,
1582 marker: DescriptionMarker<'_>,
1583 options: &SyntaxOptions,
1584 definitions: &[String],
1585 diagnostics: &mut Vec<Diagnostic>,
1586 depth: usize,
1587) -> Option<(DescriptionDetails, usize, bool)> {
1588 let mut content = String::new();
1589 push_line(&mut content, marker.content);
1590 let mut cursor = index + 1;
1591 let mut end = lines[index].end_with_eol;
1592 let mut tight = true;
1593 let mut paragraph_open = paragraph_stays_open(marker.content, options);
1594
1595 while cursor < lines.len() {
1596 if lines[cursor].text.trim().is_empty() {
1597 let next = next_nonblank_line(lines, cursor + 1);
1598 if next >= lines.len() || description_term(lines, next, options).is_some() {
1605 cursor = next;
1606 break;
1607 }
1608 if description_marker(lines[next].text).is_some() {
1609 tight = false;
1610 cursor = next;
1611 break;
1612 }
1613 if strip_indent_continuation(lines[next].text).is_none() {
1614 break;
1615 }
1616 push_line(&mut content, "");
1617 paragraph_open = false;
1618 tight = false;
1619 end = lines[cursor].end_with_eol;
1620 cursor += 1;
1621 continue;
1622 }
1623
1624 if description_marker(lines[cursor].text).is_some()
1625 || description_term(lines, cursor, options).is_some()
1626 {
1627 break;
1628 }
1629
1630 let continuation = if let Some(continuation) = strip_indent_continuation(lines[cursor].text)
1631 {
1632 continuation
1633 } else if paragraph_open && !likely_block_start(lines[cursor].text, options) {
1634 trim_ascii_start(lines[cursor].text)
1635 } else {
1636 break;
1637 };
1638 paragraph_open = paragraph_stays_open(continuation, options);
1639 push_line(&mut content, continuation);
1640 end = lines[cursor].end_with_eol;
1641 cursor += 1;
1642 }
1643
1644 if content.trim().is_empty() {
1645 return None;
1646 }
1647
1648 Some((
1649 DescriptionDetails {
1650 meta: NodeMeta::new(Some(Span::new(lines[index].start, end))),
1651 children: parse_blocks(
1652 &content,
1653 lines[index].start + marker.content_offset,
1654 false,
1655 options,
1656 definitions,
1657 diagnostics,
1658 depth + 1,
1659 ),
1660 },
1661 cursor,
1662 tight,
1663 ))
1664}
1665
1666fn description_term(
1667 lines: &[Line<'_>],
1668 term_index: usize,
1669 options: &SyntaxOptions,
1670) -> Option<DescriptionTerm> {
1671 if term_index >= lines.len() || !is_description_term_line(lines[term_index].text, options) {
1672 return None;
1673 }
1674 let mut source = String::new();
1675 let mut term_end = term_index;
1676 let mut cursor = term_index;
1677 while cursor < lines.len() && is_description_term_line(lines[cursor].text, options) {
1678 if !source.is_empty() {
1679 source.push('\n');
1680 }
1681 source.push_str(trim_ascii_start(lines[cursor].text).trim_end());
1682 term_end = cursor;
1683 cursor += 1;
1684 }
1685
1686 let mut marker_index = cursor;
1687 let mut blank_after_term = false;
1688 while marker_index < lines.len() && lines[marker_index].text.trim().is_empty() {
1689 blank_after_term = true;
1690 marker_index += 1;
1691 }
1692 (marker_index < lines.len() && description_marker(lines[marker_index].text).is_some()).then(
1693 || DescriptionTerm {
1694 marker_index,
1695 term_end,
1696 blank_after_term,
1697 source,
1698 source_offset: lines[term_index].start + leading_trim_bytes(lines[term_index].text),
1699 },
1700 )
1701}
1702
1703fn is_description_term_line(line: &str, options: &SyntaxOptions) -> bool {
1704 leading_indent_columns(line) <= 3
1705 && !line.trim().is_empty()
1706 && description_marker(line).is_none()
1707 && !likely_block_start(line, options)
1708}
1709
1710fn description_marker(line: &str) -> Option<DescriptionMarker<'_>> {
1711 let (columns, bytes) = leading_indent(line);
1712 if columns > 2 || !matches!(line.as_bytes().get(bytes), Some(b':' | b'~')) {
1713 return None;
1714 }
1715 if line
1716 .as_bytes()
1717 .get(bytes + 1)
1718 .is_some_and(|byte| !matches!(*byte, b' ' | b'\t'))
1719 {
1720 return None;
1721 }
1722 let mut content_offset = bytes + 1;
1723 while line
1724 .as_bytes()
1725 .get(content_offset)
1726 .is_some_and(|byte| matches!(*byte, b' ' | b'\t'))
1727 {
1728 content_offset += 1;
1729 }
1730 Some(DescriptionMarker {
1731 content_offset,
1732 content: &line[content_offset..],
1733 })
1734}
1735
1736fn paragraph_stays_open(line: &str, options: &SyntaxOptions) -> bool {
1740 !line.trim().is_empty() && !likely_block_start(line, options)
1741}
1742
1743fn strip_indent_continuation(input: &str) -> Option<&str> {
1745 input
1746 .strip_prefix(" ")
1747 .or_else(|| input.strip_prefix('\t'))
1748}
1749
1750fn parse_atx_heading(
1751 line: Line<'_>,
1752 options: &SyntaxOptions,
1753 definitions: &[String],
1754) -> Option<Block> {
1755 let text = trim_up_to_three_spaces(line.text)?;
1756 let depth = text
1757 .as_bytes()
1758 .iter()
1759 .take_while(|byte| **byte == b'#')
1760 .count();
1761 if depth == 0 || depth > 6 {
1762 return None;
1763 }
1764 if text
1765 .as_bytes()
1766 .get(depth)
1767 .is_some_and(|byte| !matches!(*byte, b' ' | b'\t'))
1768 && text.len() != depth
1769 {
1770 return None;
1771 }
1772 let after_opening = &text[depth..];
1773 let content_start_in_text = depth + leading_trim_bytes(after_opening);
1774 let content = trim_closing_hashes(after_opening.trim_start());
1775 let content_start = line.start + (line.text.len() - text.len()) + content_start_in_text;
1776 Some(Block::Heading(Heading {
1777 meta: NodeMeta::new(Some(Span::new(line.start, line.end))),
1778 depth: depth as u8,
1779 kind: HeadingKind::Atx,
1780 children: parse_inlines(
1781 content,
1782 content_start,
1783 options,
1784 definitions,
1785 &mut Vec::new(),
1786 ),
1787 }))
1788}
1789
1790fn parse_thematic_break(line: Line<'_>) -> Option<Block> {
1791 let text = trim_up_to_three_spaces(line.text)?.trim();
1792 let mut marker = None;
1793 let mut count = 0;
1794 for char in text.chars() {
1795 if char == ' ' || char == '\t' {
1796 continue;
1797 }
1798 let current = match char {
1799 '-' => ThematicBreakMarker::Dash,
1800 '*' => ThematicBreakMarker::Asterisk,
1801 '_' => ThematicBreakMarker::Underscore,
1802 _ => return None,
1803 };
1804 if marker.is_some_and(|marker| marker != current) {
1805 return None;
1806 }
1807 marker = Some(current);
1808 count += 1;
1809 }
1810 if count >= 3 {
1811 Some(Block::ThematicBreak(ThematicBreak {
1812 meta: NodeMeta::new(Some(Span::new(line.start, line.end))),
1813 marker: marker?,
1814 }))
1815 } else {
1816 None
1817 }
1818}
1819
1820fn parse_definition(
1821 lines: &[Line<'_>],
1822 index: usize,
1823 options: &SyntaxOptions,
1824 allow_subsequent_indent: bool,
1825) -> Option<(Block, usize)> {
1826 let line = lines[index];
1827 let text = trim_definition_start(line.text, allow_subsequent_indent)?;
1828 if !text.starts_with('[') {
1829 return None;
1830 }
1831
1832 let mut accumulated = String::from(text);
1840 let mut label_end_line = index;
1841 let close = loop {
1842 if let Some(close) = find_reference_label_end(&accumulated, 0) {
1843 if accumulated.as_bytes().get(close + 1) == Some(&b':') {
1844 break close;
1845 }
1846 return None;
1848 }
1849 let next = label_end_line + 1;
1850 if next >= lines.len() || lines[next].text.trim().is_empty() {
1851 return None;
1852 }
1853 if likely_block_start(lines[next].text, options)
1860 || setext_underline_depth(lines[next].text).is_some()
1861 || table_can_start(lines, next, options)
1862 {
1863 return None;
1864 }
1865 accumulated.push('\n');
1866 accumulated.push_str(lines[next].text);
1867 label_end_line = next;
1868 };
1869 let label = String::from(&accumulated[1..close]);
1870 if normalize_label(&label).is_empty() {
1871 return None;
1872 }
1873 let label = label.as_str();
1874 let mut source = String::from(&accumulated[close + 2..]);
1875 let mut cursor = label_end_line;
1876 let mut best_without_title = None;
1877
1878 loop {
1879 if let Some(resource) = parse_definition_destination_title(&source) {
1880 if resource.title.is_some() {
1881 return Some((
1882 Block::Definition(Definition {
1883 meta: NodeMeta::new(Some(Span::new(
1884 line.start,
1885 lines[cursor].end_with_eol,
1886 ))),
1887 label: label.into(),
1888 identifier: normalize_label(label),
1889 destination: resource.destination,
1890 destination_kind: resource.destination_kind,
1891 title: resource.title,
1892 title_kind: resource.title_kind,
1893 }),
1894 cursor + 1,
1895 ));
1896 }
1897
1898 best_without_title = Some((resource, cursor + 1));
1899 let next = cursor + 1;
1900 if next >= lines.len()
1901 || lines[next].text.trim().is_empty()
1902 || !line_can_start_definition_title(lines[next].text)
1903 {
1904 break;
1905 }
1906 }
1907
1908 let next = cursor + 1;
1909 if next >= lines.len() || lines[next].text.trim().is_empty() {
1910 break;
1911 }
1912 if likely_block_start(lines[next].text, options)
1918 || setext_underline_depth(lines[next].text).is_some()
1919 {
1920 break;
1921 }
1922 source.push('\n');
1923 source.push_str(lines[next].text);
1924 cursor = next;
1925 }
1926
1927 let (resource, next) = best_without_title?;
1928 let end = lines[next - 1].end_with_eol;
1929 Some((
1930 Block::Definition(Definition {
1931 meta: NodeMeta::new(Some(Span::new(line.start, end))),
1932 label: label.into(),
1933 identifier: normalize_label(label),
1934 destination: resource.destination,
1935 destination_kind: resource.destination_kind,
1936 title: resource.title,
1937 title_kind: resource.title_kind,
1938 }),
1939 next,
1940 ))
1941}
1942
1943fn trim_definition_start(input: &str, allow_subsequent_indent: bool) -> Option<&str> {
1944 if let Some(trimmed) = trim_up_to_three_spaces(input) {
1945 return Some(trimmed);
1946 }
1947 if allow_subsequent_indent {
1948 let (columns, bytes) = leading_indent(input);
1949 if columns == 4 {
1950 return Some(&input[bytes..]);
1951 }
1952 }
1953 None
1954}
1955
1956fn parse_footnote_definition(
1957 lines: &[Line<'_>],
1958 index: usize,
1959 options: &SyntaxOptions,
1960 definitions: &[String],
1961 diagnostics: &mut Vec<Diagnostic>,
1962 depth: usize,
1963) -> Option<(Block, usize)> {
1964 if !options.constructs.footnote_definition {
1965 return None;
1966 }
1967 let line = lines[index];
1968 let text = line.text.trim();
1969 if !text.starts_with("[^") {
1970 return None;
1971 }
1972 let close = find_footnote_definition_label_end(text)?;
1973 let label = &text[2..close];
1974 if !is_footnote_label(label) {
1975 return None;
1976 }
1977 let rest = text[close + 2..].trim();
1978 let mut content = String::new();
1979 push_line(&mut content, rest);
1980 let mut cursor = index + 1;
1981 let mut end = line.end_with_eol;
1982 let mut paragraph_open = paragraph_stays_open(rest, options);
1983
1984 while cursor < lines.len() {
1985 if lines[cursor].text.trim().is_empty() {
1986 let next = next_nonblank_line(lines, cursor + 1);
1987 if next >= lines.len() || !is_footnote_continuation(lines[next].text) {
1988 break;
1989 }
1990 push_line(&mut content, "");
1991 paragraph_open = false;
1992 end = lines[cursor].end_with_eol;
1993 cursor += 1;
1994 continue;
1995 }
1996
1997 let continuation = if let Some(continuation) = strip_indent_continuation(lines[cursor].text)
1998 {
1999 continuation
2000 } else if paragraph_open && !likely_block_start(lines[cursor].text, options) {
2001 trim_ascii_start(lines[cursor].text)
2002 } else {
2003 break;
2004 };
2005 paragraph_open = paragraph_stays_open(continuation, options);
2006 push_line(&mut content, continuation);
2007 end = lines[cursor].end_with_eol;
2008 cursor += 1;
2009 }
2010
2011 Some((
2012 Block::FootnoteDefinition(FootnoteDefinition {
2013 meta: NodeMeta::new(Some(Span::new(line.start, end))),
2014 label: label.into(),
2015 identifier: normalize_label(label),
2016 children: parse_blocks(
2017 &content,
2018 line.end.saturating_sub(rest.len()),
2019 false,
2020 options,
2021 definitions,
2022 diagnostics,
2023 depth + 1,
2024 ),
2025 }),
2026 cursor,
2027 ))
2028}
2029
2030fn is_footnote_continuation(input: &str) -> bool {
2031 strip_indent_continuation(input).is_some()
2032}
2033
2034fn parse_leaf_directive(
2035 line: Line<'_>,
2036 options: &SyntaxOptions,
2037 definitions: &[String],
2038 diagnostics: &mut Vec<Diagnostic>,
2039) -> Option<Block> {
2040 if !options.constructs.directive_leaf {
2041 return None;
2042 }
2043 let trimmed = line.text.trim_start();
2044 if trimmed.starts_with(":::") || !trimmed.starts_with("::") {
2045 return None;
2046 }
2047 let opener_base = line.start + (line.text.len() - trimmed.len()) + 2;
2048 let Some((name, label_source, attributes, _)) = parse_directive_opener(&trimmed[2..]) else {
2049 diagnostics.push(Diagnostic::new(
2050 DiagnosticSeverity::Error,
2051 DiagnosticCode::InvalidDirectiveName,
2052 Span::new(line.start, line.end),
2053 "leaf directive must have a valid name",
2054 ));
2055 return None;
2056 };
2057 let label = label_source
2058 .map(|source| {
2059 parse_inlines(
2060 source,
2061 opener_base + name.len() + 1,
2062 options,
2063 definitions,
2064 diagnostics,
2065 )
2066 })
2067 .unwrap_or_default();
2068 Some(Block::LeafDirective(LeafDirective {
2069 meta: NodeMeta::new(Some(Span::new(line.start, line.end))),
2070 name,
2071 label,
2072 attributes,
2073 }))
2074}
2075
2076fn parse_html_container(
2077 lines: &[Line<'_>],
2078 index: usize,
2079 options: &SyntaxOptions,
2080 definitions: &[String],
2081 diagnostics: &mut Vec<Diagnostic>,
2082 depth: usize,
2083 container_closes: &mut BracketMemo,
2084) -> Option<(Block, usize)> {
2085 if !options.constructs.html_container {
2086 return None;
2087 }
2088
2089 let (opening, leading_summary) =
2090 parse_details_container_opening_line(lines[index], options, definitions, diagnostics)?;
2091 let close_index = container_closes.resolve(lines.len() + 1, index + 1, |cursor| {
2092 html_container_close_step(lines, cursor, "details")
2093 })?;
2094 let closing =
2095 parse_html_container_tag_line(lines[close_index], "details", HtmlContainerTag::Closing)?;
2096 let children = parse_details_container_children(
2097 &lines[index + 1..close_index],
2098 leading_summary,
2099 options,
2100 definitions,
2101 diagnostics,
2102 depth,
2103 );
2104
2105 Some((
2106 Block::HtmlContainer(HtmlContainer {
2107 meta: NodeMeta::new(Some(Span::new(
2108 opening
2109 .meta
2110 .span
2111 .map(|span| span.start)
2112 .unwrap_or(lines[index].start),
2113 lines[close_index].end_with_eol,
2114 ))),
2115 opening,
2116 content: HtmlContainerContent::Blocks(children),
2117 closing,
2118 }),
2119 close_index + 1,
2120 ))
2121}
2122
2123#[derive(Clone, Copy, Eq, PartialEq)]
2124enum HtmlContainerTag {
2125 Opening,
2126 Closing,
2127}
2128
2129#[derive(Clone, Copy)]
2130struct HtmlContainerFence {
2131 marker: FenceMarker,
2132 length: usize,
2133}
2134
2135fn parse_details_container_children(
2136 lines: &[Line<'_>],
2137 leading_summary: Option<Block>,
2138 options: &SyntaxOptions,
2139 definitions: &[String],
2140 diagnostics: &mut Vec<Diagnostic>,
2141 depth: usize,
2142) -> Vec<Block> {
2143 let mut children = Vec::new();
2144 if let Some(summary) = leading_summary {
2145 children.push(summary);
2146 children.extend(parse_blocks_from_lines(
2147 lines,
2148 false,
2149 options,
2150 definitions,
2151 diagnostics,
2152 depth + 1,
2153 ));
2154 return children;
2155 }
2156
2157 let Some(summary_index) = lines.iter().position(|line| !line.text.trim().is_empty()) else {
2158 return children;
2159 };
2160
2161 if let Some((summary, next)) =
2162 parse_summary_container(lines, summary_index, options, definitions, diagnostics)
2163 {
2164 children.push(summary);
2165 children.extend(parse_blocks_from_lines(
2166 &lines[next..],
2167 false,
2168 options,
2169 definitions,
2170 diagnostics,
2171 depth + 1,
2172 ));
2173 return children;
2174 }
2175
2176 parse_blocks_from_lines(lines, false, options, definitions, diagnostics, depth + 1)
2177}
2178
2179fn parse_summary_container(
2180 lines: &[Line<'_>],
2181 index: usize,
2182 options: &SyntaxOptions,
2183 definitions: &[String],
2184 diagnostics: &mut Vec<Diagnostic>,
2185) -> Option<(Block, usize)> {
2186 let line = lines[index];
2187 let (trimmed, indent_bytes) = trim_html_container_line(line.text)?;
2188 parse_summary_container_source(
2189 trimmed,
2190 line.start + indent_bytes,
2191 options,
2192 definitions,
2193 diagnostics,
2194 )
2195 .map(|block| (block, index + 1))
2196}
2197
2198fn parse_details_container_opening_line(
2199 line: Line<'_>,
2200 options: &SyntaxOptions,
2201 definitions: &[String],
2202 diagnostics: &mut Vec<Diagnostic>,
2203) -> Option<(HtmlTag, Option<Block>)> {
2204 let (trimmed, indent_bytes) = trim_html_container_line(line.text)?;
2205 let (open_end, name) = parse_html_tag(trimmed, 0)?;
2206 if !name.eq_ignore_ascii_case("details")
2207 || html_tag_is_closing(trimmed, 0)
2208 || html_tag_is_self_closing(&trimmed[..open_end])
2209 {
2210 return None;
2211 }
2212
2213 let tag_start = line.start + indent_bytes;
2214 let rest_start = open_end + leading_ascii_whitespace_len(&trimmed[open_end..]);
2215 let summary = if trimmed[rest_start..].is_empty() {
2216 None
2217 } else {
2218 Some(parse_summary_container_source(
2219 &trimmed[rest_start..],
2220 tag_start + rest_start,
2221 options,
2222 definitions,
2223 diagnostics,
2224 )?)
2225 };
2226
2227 Some((
2228 HtmlTag {
2229 meta: NodeMeta::new(Some(Span::new(tag_start, tag_start + open_end))),
2230 name: "details".into(),
2231 raw: trimmed[..open_end].into(),
2232 },
2233 summary,
2234 ))
2235}
2236
2237fn parse_summary_container_source(
2238 source: &str,
2239 base_offset: usize,
2240 options: &SyntaxOptions,
2241 definitions: &[String],
2242 diagnostics: &mut Vec<Diagnostic>,
2243) -> Option<Block> {
2244 let (open_end, open_name) = parse_html_tag(source, 0)?;
2245 if !open_name.eq_ignore_ascii_case("summary")
2246 || html_tag_is_closing(source, 0)
2247 || html_tag_is_self_closing(&source[..open_end])
2248 {
2249 return None;
2250 }
2251
2252 let close_start = source[open_end..]
2253 .find("</")
2254 .map(|offset| open_end + offset)?;
2255 let (close_end, close_name) = parse_html_tag(source, close_start)?;
2256 if !close_name.eq_ignore_ascii_case("summary")
2257 || !html_tag_is_closing(source, close_start)
2258 || !source[close_end..].trim().is_empty()
2259 {
2260 return None;
2261 }
2262
2263 let content = &source[open_end..close_start];
2264 let content_base = base_offset + open_end;
2265 let opening = HtmlTag {
2266 meta: NodeMeta::new(Some(Span::new(base_offset, base_offset + open_end))),
2267 name: "summary".into(),
2268 raw: source[..open_end].into(),
2269 };
2270 let closing = HtmlTag {
2271 meta: NodeMeta::new(Some(Span::new(
2272 base_offset + close_start,
2273 base_offset + close_end,
2274 ))),
2275 name: "summary".into(),
2276 raw: source[close_start..close_end].into(),
2277 };
2278
2279 Some(Block::HtmlContainer(HtmlContainer {
2280 meta: NodeMeta::new(Some(Span::new(base_offset, base_offset + source.len()))),
2281 opening,
2282 content: HtmlContainerContent::Inlines(parse_inlines(
2283 content,
2284 content_base,
2285 options,
2286 definitions,
2287 diagnostics,
2288 )),
2289 closing,
2290 }))
2291}
2292
2293fn html_container_close_step(lines: &[Line<'_>], cursor: usize, tag: &str) -> BracketStep {
2297 let Some(line) = lines.get(cursor) else {
2298 return BracketStep::End;
2299 };
2300 if let Some(fence) = html_container_fence_opens(line.text) {
2301 let after_fence = (cursor + 1..lines.len())
2302 .find(|close| html_container_fence_closes(lines[*close].text, fence))
2303 .map_or(lines.len(), |close| close + 1);
2304 return BracketStep::Pass(after_fence);
2305 }
2306 if parse_html_container_tag_line(*line, tag, HtmlContainerTag::Closing).is_some() {
2307 return BracketStep::Close(cursor + 1);
2308 }
2309 if parse_html_container_opening_line(*line, tag).is_some() {
2310 return BracketStep::Open(cursor + 1);
2311 }
2312 BracketStep::Pass(cursor + 1)
2313}
2314
2315fn parse_html_container_tag_line(
2316 line: Line<'_>,
2317 tag: &str,
2318 kind: HtmlContainerTag,
2319) -> Option<HtmlTag> {
2320 let (trimmed, indent_bytes) = trim_html_container_line(line.text)?;
2321 let (end, name) = parse_html_tag(trimmed, 0)?;
2322 if !name.eq_ignore_ascii_case(tag) || !trimmed[end..].trim().is_empty() {
2323 return None;
2324 }
2325
2326 let closing = html_tag_is_closing(trimmed, 0);
2327 if (kind == HtmlContainerTag::Opening && closing)
2328 || (kind == HtmlContainerTag::Closing && !closing)
2329 || html_tag_is_self_closing(&trimmed[..end])
2330 {
2331 return None;
2332 }
2333
2334 let start = line.start + indent_bytes;
2335 Some(HtmlTag {
2336 meta: NodeMeta::new(Some(Span::new(start, start + end))),
2337 name: tag.into(),
2338 raw: trimmed[..end].into(),
2339 })
2340}
2341
2342fn parse_html_container_opening_line(line: Line<'_>, tag: &str) -> Option<HtmlTag> {
2343 let (trimmed, indent_bytes) = trim_html_container_line(line.text)?;
2344 let (end, name) = parse_html_tag(trimmed, 0)?;
2345 if !name.eq_ignore_ascii_case(tag)
2346 || html_tag_is_closing(trimmed, 0)
2347 || html_tag_is_self_closing(&trimmed[..end])
2348 {
2349 return None;
2350 }
2351
2352 let rest_start = end + leading_ascii_whitespace_len(&trimmed[end..]);
2353 if !trimmed[rest_start..].is_empty() && !html_container_line_has_summary(&trimmed[rest_start..])
2354 {
2355 return None;
2356 }
2357
2358 let start = line.start + indent_bytes;
2359 Some(HtmlTag {
2360 meta: NodeMeta::new(Some(Span::new(start, start + end))),
2361 name: tag.into(),
2362 raw: trimmed[..end].into(),
2363 })
2364}
2365
2366fn html_container_line_has_summary(input: &str) -> bool {
2367 let Some((open_end, name)) = parse_html_tag(input, 0) else {
2368 return false;
2369 };
2370 if !name.eq_ignore_ascii_case("summary")
2371 || html_tag_is_closing(input, 0)
2372 || html_tag_is_self_closing(&input[..open_end])
2373 {
2374 return false;
2375 }
2376 let Some(close_start) = input[open_end..].find("</").map(|offset| open_end + offset) else {
2377 return false;
2378 };
2379 let Some((close_end, close_name)) = parse_html_tag(input, close_start) else {
2380 return false;
2381 };
2382 close_name.eq_ignore_ascii_case("summary")
2383 && html_tag_is_closing(input, close_start)
2384 && input[close_end..].trim().is_empty()
2385}
2386
2387fn trim_html_container_line(input: &str) -> Option<(&str, usize)> {
2388 let trimmed = trim_up_to_three_spaces(input)?;
2389 Some((trimmed, input.len() - trimmed.len()))
2390}
2391
2392fn leading_ascii_whitespace_len(input: &str) -> usize {
2393 input
2394 .as_bytes()
2395 .iter()
2396 .take_while(|byte| byte.is_ascii_whitespace())
2397 .count()
2398}
2399
2400fn html_tag_is_closing(input: &str, index: usize) -> bool {
2401 input.as_bytes().get(index + 1) == Some(&b'/')
2402}
2403
2404fn html_tag_is_self_closing(input: &str) -> bool {
2405 input.trim_end().ends_with("/>")
2406}
2407
2408fn html_container_fence_opens(line: &str) -> Option<HtmlContainerFence> {
2409 let trimmed = trim_up_to_three_spaces(line)?;
2410 let (marker, length) = fence_start(trimmed)?;
2411 Some(HtmlContainerFence { marker, length })
2412}
2413
2414fn html_container_fence_closes(line: &str, fence: HtmlContainerFence) -> bool {
2415 trim_up_to_three_spaces(line)
2416 .is_some_and(|trimmed| fence_close(trimmed, fence.marker, fence.length))
2417}
2418
2419fn parse_html_block(
2420 lines: &[Line<'_>],
2421 index: usize,
2422 options: &SyntaxOptions,
2423) -> Option<(Block, usize)> {
2424 if !options.constructs.html_block {
2425 return None;
2426 }
2427
2428 let trimmed = trim_up_to_three_spaces(lines[index].text)?;
2429 let kind = html_block_start(trimmed)?;
2430 let mut value = String::new();
2431 let mut cursor = index;
2432 match kind {
2433 HtmlBlockKind::RawTag => {
2434 while cursor < lines.len() {
2438 push_line(&mut value, lines[cursor].text);
2439 if ["script", "pre", "style", "textarea"]
2440 .iter()
2441 .any(|tag| line_contains_raw_closing_tag(lines[cursor].text, tag))
2442 {
2443 cursor += 1;
2444 break;
2445 }
2446 cursor += 1;
2447 }
2448 }
2449 HtmlBlockKind::BlockTag => {
2450 while cursor < lines.len() && !lines[cursor].text.trim().is_empty() {
2451 push_line(&mut value, lines[cursor].text);
2452 cursor += 1;
2453 }
2454 }
2455 HtmlBlockKind::Until(end) => {
2456 while cursor < lines.len() {
2457 push_line(&mut value, lines[cursor].text);
2458 if lines[cursor].text.contains(end) {
2459 cursor += 1;
2460 break;
2461 }
2462 cursor += 1;
2463 }
2464 }
2465 HtmlBlockKind::UntilBlank => {
2466 while cursor < lines.len() && !lines[cursor].text.trim().is_empty() {
2467 push_line(&mut value, lines[cursor].text);
2468 cursor += 1;
2469 }
2470 }
2471 }
2472 Some((
2473 Block::HtmlBlock(HtmlBlock {
2474 meta: NodeMeta::new(Some(Span::new(
2475 lines[index].start,
2476 lines[cursor - 1].end_with_eol,
2477 ))),
2478 value,
2479 }),
2480 cursor,
2481 ))
2482}
2483
2484fn html_block_start(input: &str) -> Option<HtmlBlockKind> {
2485 let trimmed = input.trim_end();
2486 if !trimmed.starts_with('<') {
2487 return None;
2488 }
2489
2490 if raw_html_tag_start(trimmed) {
2491 return Some(HtmlBlockKind::RawTag);
2492 }
2493 if trimmed.starts_with("<!--") {
2494 return Some(HtmlBlockKind::Until("-->"));
2495 }
2496 if trimmed.starts_with("<?") {
2497 return Some(HtmlBlockKind::Until("?>"));
2498 }
2499 if is_declaration_start(trimmed) {
2500 return Some(HtmlBlockKind::Until(">"));
2501 }
2502 if trimmed.starts_with("<![CDATA[") {
2503 return Some(HtmlBlockKind::Until("]]>"));
2504 }
2505
2506 if html_block_tag_start(trimmed) {
2507 return Some(HtmlBlockKind::BlockTag);
2508 }
2509
2510 let Some((end, _tag_name)) = parse_html_tag(trimmed, 0) else {
2511 return None;
2512 };
2513 let rest = trimmed[end..].trim();
2514 if rest.is_empty() {
2515 Some(HtmlBlockKind::UntilBlank)
2516 } else {
2517 None
2518 }
2519}
2520
2521pub(crate) fn line_starts_html_block(input: &str) -> bool {
2522 trim_up_to_three_spaces(input)
2523 .and_then(html_block_start)
2524 .is_some()
2525}
2526
2527fn line_starts_html_container(input: &str) -> bool {
2528 let line = Line {
2529 text: input,
2530 eol: "",
2531 start: 0,
2532 end: input.len(),
2533 end_with_eol: input.len(),
2534 lazy: false,
2535 };
2536 parse_html_container_opening_line(line, "details").is_some()
2537}
2538
2539fn raw_html_tag_start(input: &str) -> bool {
2540 for tag in ["script", "pre", "style", "textarea"] {
2541 if html_raw_open_tag_prefix(input, tag) {
2542 return true;
2543 }
2544 }
2545 false
2546}
2547
2548fn html_raw_open_tag_prefix(input: &str, tag: &str) -> bool {
2549 let Some(rest) = input.strip_prefix('<') else {
2550 return false;
2551 };
2552 if rest.starts_with('/') || rest.len() < tag.len() {
2553 return false;
2554 }
2555 let rest_bytes = rest.as_bytes();
2556 let tag_bytes = tag.as_bytes();
2557 if !rest_bytes
2558 .get(..tag_bytes.len())
2559 .is_some_and(|name| name.eq_ignore_ascii_case(tag_bytes))
2560 {
2561 return false;
2562 }
2563 match rest_bytes.get(tag.len()) {
2564 None => true,
2565 Some(b' ' | b'\t' | b'\n' | b'\r' | b'>') => true,
2566 Some(b'/') => {
2567 rest_bytes.get(tag.len() + 1) == Some(&b'>') && rest_bytes.get(tag.len() + 2).is_none()
2568 }
2569 _ => false,
2570 }
2571}
2572
2573fn line_contains_raw_closing_tag(input: &str, tag: &str) -> bool {
2574 let bytes = input.as_bytes();
2575 let tag_bytes = tag.as_bytes();
2576 let mut cursor = 0;
2577
2578 while cursor + 2 + tag_bytes.len() <= bytes.len() {
2579 let tag_start = cursor + 2;
2580 let tag_end = tag_start + tag_bytes.len();
2581 if bytes.get(cursor) == Some(&b'<')
2582 && bytes.get(cursor + 1) == Some(&b'/')
2583 && bytes
2584 .get(tag_start..tag_end)
2585 .is_some_and(|name| name.eq_ignore_ascii_case(tag_bytes))
2586 {
2587 match bytes.get(tag_end) {
2588 Some(b'>') => return true,
2589 Some(byte) if byte.is_ascii_whitespace() => {
2590 let mut after_space = tag_end;
2591 while bytes
2592 .get(after_space)
2593 .is_some_and(|byte| byte.is_ascii_whitespace())
2594 {
2595 after_space += 1;
2596 }
2597 if bytes.get(after_space) == Some(&b'>') {
2598 return true;
2599 }
2600 }
2601 _ => {}
2602 }
2603 }
2604 cursor += 1;
2605 }
2606
2607 false
2608}
2609
2610fn html_block_tag_start(input: &str) -> bool {
2611 let bytes = input.as_bytes();
2612 if bytes.first() != Some(&b'<') {
2613 return false;
2614 }
2615
2616 let mut cursor = 1;
2617 if bytes.get(cursor) == Some(&b'/') {
2618 cursor += 1;
2619 }
2620
2621 let name_start = cursor;
2622 if !bytes
2623 .get(cursor)
2624 .is_some_and(|byte| byte.is_ascii_alphabetic())
2625 {
2626 return false;
2627 }
2628 cursor += 1;
2629 while bytes.get(cursor).is_some_and(|byte| html_name_byte(*byte)) {
2630 cursor += 1;
2631 }
2632
2633 let name = &input[name_start..cursor];
2634 if !html_block_tag(name) {
2635 return false;
2636 }
2637
2638 match bytes.get(cursor) {
2639 None | Some(b' ' | b'\t' | b'\n' | b'\r' | b'>') => true,
2640 Some(b'/') if bytes.get(cursor + 1) == Some(&b'>') => true,
2641 _ => false,
2642 }
2643}
2644
2645fn html_block_tag(tag: &str) -> bool {
2646 matches!(
2647 tag.to_ascii_lowercase().as_str(),
2648 "address"
2649 | "article"
2650 | "aside"
2651 | "base"
2652 | "basefont"
2653 | "blockquote"
2654 | "body"
2655 | "caption"
2656 | "center"
2657 | "col"
2658 | "colgroup"
2659 | "dd"
2660 | "details"
2661 | "dialog"
2662 | "dir"
2663 | "div"
2664 | "dl"
2665 | "dt"
2666 | "fieldset"
2667 | "figcaption"
2668 | "figure"
2669 | "footer"
2670 | "form"
2671 | "frame"
2672 | "frameset"
2673 | "h1"
2674 | "h2"
2675 | "h3"
2676 | "h4"
2677 | "h5"
2678 | "h6"
2679 | "head"
2680 | "header"
2681 | "hr"
2682 | "html"
2683 | "iframe"
2684 | "legend"
2685 | "li"
2686 | "link"
2687 | "main"
2688 | "menu"
2689 | "menuitem"
2690 | "nav"
2691 | "noframes"
2692 | "ol"
2693 | "optgroup"
2694 | "option"
2695 | "p"
2696 | "param"
2697 | "search"
2698 | "section"
2699 | "summary"
2700 | "table"
2701 | "tbody"
2702 | "td"
2703 | "tfoot"
2704 | "th"
2705 | "thead"
2706 | "title"
2707 | "tr"
2708 | "track"
2709 | "ul"
2710 )
2711}
2712
2713fn is_declaration_start(input: &str) -> bool {
2714 input
2715 .as_bytes()
2716 .get(2)
2717 .is_some_and(|byte| input.starts_with("<!") && byte.is_ascii_alphabetic())
2718}
2719
2720fn parse_mdx_flow(
2721 lines: &[Line<'_>],
2722 index: usize,
2723 options: &SyntaxOptions,
2724 diagnostics: &mut Vec<Diagnostic>,
2725 flow: &mut MdxFlowScan,
2726) -> Option<(Block, usize)> {
2727 if options.constructs.mdx_esm {
2728 if let Some((block, next)) = parse_mdx_esm_flow(lines, index, diagnostics) {
2729 return Some((block, next));
2730 }
2731 }
2732
2733 let line = lines[index];
2734 let trimmed = line.text.trim_start();
2735 if options.constructs.mdx_expression_block && trimmed.starts_with('{') {
2736 let open_byte = line.text.len() - trimmed.len();
2737 if let Some((close_line, close_byte)) = flow.expression_close(lines, index, open_byte) {
2738 return Some((
2739 Block::MdxExpression(MdxExpression {
2740 meta: NodeMeta::new(Some(Span::new(line.start, lines[close_line].end))),
2741 value: collect_mdx_expression_value(
2742 lines, index, open_byte, close_line, close_byte,
2743 ),
2744 }),
2745 close_line + 1,
2746 ));
2747 }
2748 diagnostics.push(Diagnostic::new(
2749 DiagnosticSeverity::Error,
2750 DiagnosticCode::InvalidMdx,
2751 Span::new(line.start + open_byte, lines.last()?.end_with_eol),
2752 "MDX expression block is missing a closing brace",
2753 ));
2754 }
2755 if options.constructs.mdx_jsx_block && trimmed.starts_with('<') {
2756 let start_byte = line.text.len() - trimmed.len();
2757 if let Some(close_line) = flow.jsx_close_line(lines, index, start_byte) {
2758 return Some((
2759 Block::MdxJsx(MdxJsx {
2760 meta: NodeMeta::new(Some(Span::new(line.start, lines[close_line].end))),
2761 value: collect_line_range(lines, index, close_line),
2762 }),
2763 close_line + 1,
2764 ));
2765 }
2766 if let Some(root) = mdx_jsx_tag_start(line.text, start_byte) {
2767 if !root.closing {
2768 if let Some(self_closing) = flow.jsx_tag_self_closing(lines, index, start_byte) {
2769 if !self_closing {
2770 diagnostics.push(Diagnostic::new(
2771 DiagnosticSeverity::Error,
2772 DiagnosticCode::InvalidMdx,
2773 Span::new(line.start + start_byte, lines.last()?.end_with_eol),
2774 "MDX JSX block is missing a closing tag",
2775 ));
2776 }
2777 }
2778 }
2779 }
2780 }
2781 None
2782}
2783
2784#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
2785struct MdxEsmState {
2786 brace_depth: usize,
2787 bracket_depth: usize,
2788 paren_depth: usize,
2789 block_comment: bool,
2790 quote: Option<u8>,
2791 escaped: bool,
2792}
2793
2794#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2795enum MdxJsxTag<'a> {
2796 Fragment,
2797 Named(&'a str),
2798}
2799
2800#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2801struct MdxJsxTagStart<'a> {
2802 tag: MdxJsxTag<'a>,
2803 closing: bool,
2804}
2805
2806fn parse_mdx_esm_flow(
2807 lines: &[Line<'_>],
2808 index: usize,
2809 diagnostics: &mut Vec<Diagnostic>,
2810) -> Option<(Block, usize)> {
2811 if !is_mdx_esm_start(lines[index].text) {
2812 return None;
2813 }
2814
2815 let mut value = String::new();
2816 let mut state = MdxEsmState::default();
2817 let mut cursor = index;
2818 while cursor < lines.len() {
2819 let line = lines[cursor].text;
2820 if cursor > index && !is_mdx_esm_continuation(line, &state) {
2821 break;
2822 }
2823 if cursor > index {
2824 value.push('\n');
2825 }
2826 value.push_str(line);
2827 update_mdx_esm_state(line, &mut state);
2828 cursor += 1;
2829 }
2830 if cursor >= lines.len() && state_has_open_mdx_esm_construct(&state) {
2831 diagnostics.push(Diagnostic::new(
2832 DiagnosticSeverity::Error,
2833 DiagnosticCode::InvalidMdx,
2834 Span::new(lines[index].start, lines[cursor - 1].end_with_eol),
2835 "MDX ESM block is missing a closing delimiter",
2836 ));
2837 }
2838
2839 Some((
2840 Block::MdxEsm(MdxEsm {
2841 meta: NodeMeta::new(Some(Span::new(lines[index].start, lines[cursor - 1].end))),
2842 value,
2843 }),
2844 cursor,
2845 ))
2846}
2847
2848fn is_mdx_esm_start(line: &str) -> bool {
2849 line.starts_with("import ") || line.starts_with("export ")
2850}
2851
2852fn is_mdx_esm_continuation(line: &str, state: &MdxEsmState) -> bool {
2853 if state_has_open_mdx_esm_construct(state) {
2854 return true;
2855 }
2856 let trimmed = line.trim_start();
2857 if trimmed.is_empty() {
2858 return false;
2859 }
2860 is_mdx_esm_start(line) || trimmed.starts_with("//") || trimmed.starts_with("/*")
2861}
2862
2863fn state_has_open_mdx_esm_construct(state: &MdxEsmState) -> bool {
2864 state.brace_depth > 0
2865 || state.bracket_depth > 0
2866 || state.paren_depth > 0
2867 || state.block_comment
2868 || state.quote == Some(b'`')
2869}
2870
2871fn update_mdx_esm_state(line: &str, state: &mut MdxEsmState) {
2872 let bytes = line.as_bytes();
2873 let mut index = 0;
2874 while index < bytes.len() {
2875 let byte = bytes[index];
2876 if state.block_comment {
2877 if byte == b'*' && bytes.get(index + 1) == Some(&b'/') {
2878 state.block_comment = false;
2879 index += 1;
2880 }
2881 index += 1;
2882 continue;
2883 }
2884
2885 if let Some(delimiter) = state.quote {
2886 if state.escaped {
2887 state.escaped = false;
2888 } else if byte == b'\\' {
2889 state.escaped = true;
2890 } else if byte == delimiter {
2891 state.quote = None;
2892 }
2893 index += 1;
2894 continue;
2895 }
2896
2897 match byte {
2898 b'\'' | b'"' | b'`' => state.quote = Some(byte),
2899 b'/' if bytes.get(index + 1) == Some(&b'/') => break,
2900 b'/' if bytes.get(index + 1) == Some(&b'*') => {
2901 state.block_comment = true;
2902 index += 1;
2903 }
2904 b'{' => state.brace_depth += 1,
2905 b'}' => state.brace_depth = state.brace_depth.saturating_sub(1),
2906 b'[' => state.bracket_depth += 1,
2907 b']' => state.bracket_depth = state.bracket_depth.saturating_sub(1),
2908 b'(' => state.paren_depth += 1,
2909 b')' => state.paren_depth = state.paren_depth.saturating_sub(1),
2910 _ => {}
2911 }
2912 index += 1;
2913 }
2914}
2915const MDX_BRACE_STATES: usize = 6;
2917const MDX_NORMAL: usize = 0;
2918const MDX_SINGLE_QUOTED: usize = 1;
2919const MDX_DOUBLE_QUOTED: usize = 2;
2920const MDX_TEMPLATE: usize = 3;
2921const MDX_LINE_COMMENT: usize = 4;
2922const MDX_BLOCK_COMMENT: usize = 5;
2923
2924fn after_escaped_byte(text: &str, escape: usize, lines_joined: bool) -> usize {
2932 let mut escaped = escape + 1;
2933 if lines_joined {
2934 while text.as_bytes().get(escaped) == Some(&b'\n') {
2935 escaped += 1;
2936 }
2937 }
2938 (escaped + 1).min(text.len())
2939}
2940
2941fn mdx_expression_step(input: &str, node: usize, lines_joined: bool) -> BracketStep {
2942 let bytes = input.as_bytes();
2943 let (cursor, state) = (node / MDX_BRACE_STATES, node % MDX_BRACE_STATES);
2944 let at = |position: usize, state: usize| position.min(bytes.len()) * MDX_BRACE_STATES + state;
2945 let Some(&byte) = bytes.get(cursor) else {
2946 return BracketStep::End;
2947 };
2948 let following = bytes.get(cursor + 1).copied();
2949 match state {
2950 MDX_NORMAL => match byte {
2951 b'\'' => BracketStep::Pass(at(cursor + 1, MDX_SINGLE_QUOTED)),
2952 b'"' => BracketStep::Pass(at(cursor + 1, MDX_DOUBLE_QUOTED)),
2953 b'`' => BracketStep::Pass(at(cursor + 1, MDX_TEMPLATE)),
2954 b'/' if following == Some(b'/') => BracketStep::Pass(at(cursor + 2, MDX_LINE_COMMENT)),
2955 b'/' if following == Some(b'*') => BracketStep::Pass(at(cursor + 2, MDX_BLOCK_COMMENT)),
2956 b'{' => BracketStep::Open(at(cursor + 1, MDX_NORMAL)),
2957 b'}' => BracketStep::Close(at(cursor + 1, MDX_NORMAL)),
2958 _ => BracketStep::Pass(at(cursor + 1, MDX_NORMAL)),
2959 },
2960 MDX_LINE_COMMENT => BracketStep::Pass(at(
2961 cursor + 1,
2962 if byte == b'\n' {
2963 MDX_NORMAL
2964 } else {
2965 MDX_LINE_COMMENT
2966 },
2967 )),
2968 MDX_BLOCK_COMMENT => {
2969 if byte == b'*' && following == Some(b'/') {
2970 BracketStep::Pass(at(cursor + 2, MDX_NORMAL))
2971 } else {
2972 BracketStep::Pass(at(cursor + 1, MDX_BLOCK_COMMENT))
2973 }
2974 }
2975 quoted => {
2976 let delimiter = [b'\'', b'"', b'`'][quoted - MDX_SINGLE_QUOTED];
2977 if byte == b'\\' {
2978 BracketStep::Pass(at(after_escaped_byte(input, cursor, lines_joined), quoted))
2979 } else if byte == delimiter {
2980 BracketStep::Pass(at(cursor + 1, MDX_NORMAL))
2981 } else {
2982 BracketStep::Pass(at(cursor + 1, quoted))
2983 }
2984 }
2985 }
2986}
2987
2988fn collect_mdx_expression_value(
2989 lines: &[Line<'_>],
2990 start_line: usize,
2991 open_byte: usize,
2992 close_line: usize,
2993 close_byte: usize,
2994) -> String {
2995 let mut value = String::new();
2996 let mut cursor = start_line;
2997 while cursor <= close_line {
2998 if cursor > start_line {
2999 value.push('\n');
3000 }
3001 let line = lines[cursor].text;
3002 let segment = if cursor == start_line && cursor == close_line {
3003 &line[open_byte + 1..close_byte]
3004 } else if cursor == start_line {
3005 &line[open_byte + 1..]
3006 } else if cursor == close_line {
3007 &line[..close_byte]
3008 } else {
3009 line
3010 };
3011 value.push_str(segment);
3012 cursor += 1;
3013 }
3014 value
3015}
3016
3017fn mdx_jsx_tag_start(input: &str, start: usize) -> Option<MdxJsxTagStart<'_>> {
3018 let bytes = input.as_bytes();
3019 if bytes.get(start) != Some(&b'<') {
3020 return None;
3021 }
3022
3023 match bytes.get(start + 1) {
3024 Some(b'>') => {
3025 return Some(MdxJsxTagStart {
3026 tag: MdxJsxTag::Fragment,
3027 closing: false,
3028 });
3029 }
3030 Some(b'/') if bytes.get(start + 2) == Some(&b'>') => {
3031 return Some(MdxJsxTagStart {
3032 tag: MdxJsxTag::Fragment,
3033 closing: true,
3034 });
3035 }
3036 Some(b'!' | b'?') | None => return None,
3037 _ => {}
3038 }
3039
3040 let closing = bytes.get(start + 1) == Some(&b'/');
3041 let name_start = start + if closing { 2 } else { 1 };
3042 if !bytes
3043 .get(name_start)
3044 .is_some_and(|byte| is_mdx_jsx_name_start_byte(*byte))
3045 {
3046 return None;
3047 }
3048
3049 let mut name_end = name_start + 1;
3050 while bytes
3051 .get(name_end)
3052 .is_some_and(|byte| is_mdx_jsx_name_byte(*byte))
3053 {
3054 name_end += 1;
3055 }
3056 if name_end == name_start {
3057 return None;
3058 }
3059 if bytes
3060 .get(name_end)
3061 .is_some_and(|byte| !is_mdx_jsx_name_delimiter(*byte))
3062 {
3063 return None;
3064 }
3065 Some(MdxJsxTagStart {
3066 tag: MdxJsxTag::Named(&input[name_start..name_end]),
3067 closing,
3068 })
3069}
3070
3071fn previous_nonspace_before_text(input: &str, byte_index: usize) -> Option<u8> {
3072 input.as_bytes()[..byte_index]
3073 .iter()
3074 .rev()
3075 .copied()
3076 .find(|byte| !byte.is_ascii_whitespace())
3077}
3078
3079const JSX_TAG_QUOTE_STATES: usize = 3;
3082
3083fn jsx_tag_end_step(
3088 text: &str,
3089 node: usize,
3090 lines_joined: bool,
3091 expression_close: &mut impl FnMut(usize) -> Option<usize>,
3092) -> Step {
3093 let (cursor, quote) = (node / JSX_TAG_QUOTE_STATES, node % JSX_TAG_QUOTE_STATES);
3094 let at = |position: usize, quote: usize| Step::Next(position * JSX_TAG_QUOTE_STATES + quote);
3095 let Some(&byte) = text.as_bytes().get(cursor) else {
3096 return Step::Done(None);
3097 };
3098 if quote != 0 {
3099 let delimiter = if quote == 1 { b'\'' } else { b'"' };
3100 return if byte == b'\\' {
3101 at(after_escaped_byte(text, cursor, lines_joined), quote)
3102 } else if byte == delimiter {
3103 at(cursor + 1, 0)
3104 } else {
3105 at(cursor + 1, quote)
3106 };
3107 }
3108 match byte {
3109 b'\'' => at(cursor + 1, 1),
3110 b'"' => at(cursor + 1, 2),
3111 b'{' => match expression_close(cursor) {
3112 Some(close) => at(close + 1, 0),
3113 None => Step::Done(None),
3114 },
3115 b'>' => Step::Done(Some(cursor)),
3116 _ => at(cursor + 1, 0),
3117 }
3118}
3119
3120struct JsxIndex {
3128 starts: Vec<usize>,
3129 closing: Vec<bool>,
3130 ends: Vec<Option<usize>>,
3131 self_closing: Vec<bool>,
3132 next_same_name: Vec<usize>,
3136 matches: BracketMemo,
3137}
3138
3139impl JsxIndex {
3140 fn new(text: &str, lines_joined: bool, expressions: &mut BracketMemo) -> Self {
3141 let bytes = text.as_bytes();
3142 let mut starts = Vec::new();
3143 let mut closing = Vec::new();
3144 let mut names = Vec::new();
3145 for (position, byte) in bytes.iter().enumerate() {
3146 if *byte == b'<' {
3147 if let Some(tag) = mdx_jsx_tag_start(text, position) {
3148 starts.push(position);
3149 closing.push(tag.closing);
3150 names.push(match tag.tag {
3151 MdxJsxTag::Fragment => None,
3152 MdxJsxTag::Named(name) => Some(name),
3153 });
3154 }
3155 }
3156 }
3157
3158 let mut tag_ends = PathMemo::default();
3159 let mut self_closing_by_end = BTreeMap::new();
3160 let mut ends = Vec::with_capacity(starts.len());
3161 let mut self_closing = Vec::with_capacity(starts.len());
3162 for &start in &starts {
3163 let end = tag_ends.resolve(
3164 (text.len() + 1) * JSX_TAG_QUOTE_STATES,
3165 (start + 1) * JSX_TAG_QUOTE_STATES,
3166 |node| {
3167 jsx_tag_end_step(text, node, lines_joined, &mut |open| {
3168 expressions
3169 .resolve(
3170 (text.len() + 1) * MDX_BRACE_STATES,
3171 (open + 1) * MDX_BRACE_STATES,
3172 |node| mdx_expression_step(text, node, lines_joined),
3173 )
3174 .map(|node| node / MDX_BRACE_STATES)
3175 })
3176 },
3177 );
3178 ends.push(end);
3179 self_closing.push(end.is_some_and(|end| {
3180 *self_closing_by_end
3181 .entry(end)
3182 .or_insert_with(|| previous_nonspace_before_text(text, end) == Some(b'/'))
3183 }));
3184 }
3185
3186 let mut name_ids = BTreeMap::new();
3187 for name in &names {
3188 let next_id = name_ids.len();
3189 name_ids.entry(*name).or_insert(next_id);
3190 }
3191 let count = starts.len();
3192 let mut maps = PersistentMap::new(name_ids.len());
3193 let mut versions = alloc::vec![PersistentMap::EMPTY; count];
3194 let mut next_same_name = alloc::vec![count; count];
3195 for tag in (0..count).rev() {
3196 let Some(end) = ends[tag] else {
3198 continue;
3199 };
3200 let after = starts.partition_point(|start| *start <= end);
3201 let rest = versions.get(after).copied().unwrap_or(PersistentMap::EMPTY);
3202 let name = name_ids[&names[tag]];
3203 next_same_name[tag] = maps.get(rest, name).unwrap_or(count);
3204 versions[tag] = maps.insert(rest, name, tag);
3205 }
3206
3207 Self {
3208 starts,
3209 closing,
3210 ends,
3211 self_closing,
3212 next_same_name,
3213 matches: BracketMemo::default(),
3214 }
3215 }
3216
3217 fn element_end(&mut self, start: usize) -> Option<usize> {
3220 let tag = self.starts.binary_search(&start).ok()?;
3221 if self.closing[tag] {
3222 return None;
3223 }
3224 let end = self.ends[tag]?;
3225 if self.self_closing[tag] {
3226 return Some(end);
3227 }
3228 let count = self.starts.len();
3229 let (closing, self_closing, next) =
3230 (&self.closing, &self.self_closing, &self.next_same_name);
3231 let close = self.matches.resolve(count + 1, next[tag], |tag| {
3232 if tag == count {
3233 BracketStep::End
3234 } else if closing[tag] {
3235 BracketStep::Close(next[tag])
3236 } else if self_closing[tag] {
3237 BracketStep::Pass(next[tag])
3238 } else {
3239 BracketStep::Open(next[tag])
3240 }
3241 })?;
3242 self.ends[close]
3243 }
3244
3245 fn tag_end(&self, start: usize) -> Option<(usize, bool)> {
3248 let tag = self.starts.binary_search(&start).ok()?;
3249 Some((self.ends[tag]?, self.self_closing[tag]))
3250 }
3251}
3252
3253#[derive(Default)]
3257struct MdxFlowScan {
3258 joined: Option<JoinedLines>,
3259}
3260
3261struct JoinedLines {
3262 text: String,
3263 line_starts: Vec<usize>,
3264 expressions: BracketMemo,
3265 jsx: Option<JsxIndex>,
3266}
3267
3268impl JoinedLines {
3269 fn new(lines: &[Line<'_>]) -> Self {
3270 let mut text = String::new();
3271 let mut line_starts = Vec::with_capacity(lines.len());
3272 for (index, line) in lines.iter().enumerate() {
3273 if index > 0 {
3274 text.push('\n');
3275 }
3276 line_starts.push(text.len());
3277 text.push_str(line.text);
3278 }
3279 Self {
3280 text,
3281 line_starts,
3282 expressions: BracketMemo::default(),
3283 jsx: None,
3284 }
3285 }
3286
3287 fn locate(&self, position: usize) -> (usize, usize) {
3289 let line = self.line_starts.partition_point(|start| *start <= position) - 1;
3290 (line, position - self.line_starts[line])
3291 }
3292
3293 fn jsx(&mut self) -> &mut JsxIndex {
3294 let Self {
3295 text,
3296 expressions,
3297 jsx,
3298 ..
3299 } = self;
3300 jsx.get_or_insert_with(|| JsxIndex::new(text, true, expressions))
3301 }
3302}
3303
3304impl MdxFlowScan {
3305 fn joined(&mut self, lines: &[Line<'_>]) -> &mut JoinedLines {
3306 self.joined.get_or_insert_with(|| JoinedLines::new(lines))
3307 }
3308
3309 fn expression_close(
3313 &mut self,
3314 lines: &[Line<'_>],
3315 index: usize,
3316 open_byte: usize,
3317 ) -> Option<(usize, usize)> {
3318 let joined = self.joined(lines);
3319 let open = joined.line_starts[index] + open_byte;
3320 let text = &joined.text;
3321 let start = (open + 1) * MDX_BRACE_STATES;
3322 let close =
3323 joined
3324 .expressions
3325 .resolve((text.len() + 1) * MDX_BRACE_STATES, start, |node| {
3326 mdx_expression_step(text, node, true)
3327 })?
3328 / MDX_BRACE_STATES;
3329 let (line, byte) = joined.locate(close);
3330 lines[line].text[byte + 1..]
3331 .trim()
3332 .is_empty()
3333 .then_some((line, byte))
3334 }
3335
3336 fn jsx_close_line(
3339 &mut self,
3340 lines: &[Line<'_>],
3341 index: usize,
3342 start_byte: usize,
3343 ) -> Option<usize> {
3344 let joined = self.joined(lines);
3345 let start = joined.line_starts[index] + start_byte;
3346 let end = joined.jsx().element_end(start)?;
3347 Some(joined.locate(end).0)
3348 }
3349
3350 fn jsx_tag_self_closing(
3353 &mut self,
3354 lines: &[Line<'_>],
3355 index: usize,
3356 start_byte: usize,
3357 ) -> Option<bool> {
3358 let joined = self.joined(lines);
3359 let start = joined.line_starts[index] + start_byte;
3360 joined
3361 .jsx()
3362 .tag_end(start)
3363 .map(|(_, self_closing)| self_closing)
3364 }
3365}
3366
3367fn is_mdx_jsx_name_start_byte(byte: u8) -> bool {
3368 byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$')
3369}
3370
3371fn is_mdx_jsx_name_byte(byte: u8) -> bool {
3372 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b':' | b'_' | b'-' | b'$')
3373}
3374
3375fn is_mdx_jsx_name_delimiter(byte: u8) -> bool {
3376 byte.is_ascii_whitespace() || matches!(byte, b'/' | b'>' | b'{' | b'}')
3377}
3378
3379fn collect_line_range(lines: &[Line<'_>], start: usize, end: usize) -> String {
3380 let mut value = String::new();
3381 let mut cursor = start;
3382 while cursor <= end {
3383 if cursor > start {
3384 value.push('\n');
3385 }
3386 value.push_str(lines[cursor].text);
3387 cursor += 1;
3388 }
3389 value
3390}
3391
3392fn parse_indented_code(
3393 lines: &[Line<'_>],
3394 index: usize,
3395 options: &SyntaxOptions,
3396) -> Option<(Block, usize)> {
3397 if !options.constructs.indented_code || strip_indented_code_prefix(lines[index].text).is_none()
3398 {
3399 return None;
3400 }
3401 let mut value = String::new();
3402 let mut cursor = index;
3403 let mut content_end = index;
3406 let mut content_end_len = 0usize;
3407 while cursor < lines.len() {
3408 if let Some(text) = strip_indented_code_prefix(lines[cursor].text) {
3409 ensure_line_separator(&mut value);
3410 value.push_str(text);
3411 value.push_str(lines[cursor].eol);
3412 if !text.trim().is_empty() {
3413 content_end = cursor;
3414 content_end_len = value.len();
3415 }
3416 cursor += 1;
3417 continue;
3418 }
3419
3420 if !lines[cursor].text.trim().is_empty() {
3421 break;
3422 }
3423 ensure_line_separator(&mut value);
3424 value.push_str(lines[cursor].eol);
3425 cursor += 1;
3426 }
3427 value.truncate(content_end_len);
3429 Some((
3430 Block::CodeBlock(CodeBlock {
3431 meta: NodeMeta::new(Some(Span::new(
3432 lines[index].start,
3433 lines[content_end].end_with_eol,
3434 ))),
3435 kind: CodeBlockKind::Indented,
3436 info: None,
3437 value,
3438 }),
3439 cursor,
3440 ))
3441}
3442
3443fn strip_indented_code_prefix(input: &str) -> Option<&str> {
3444 let mut column = 0usize;
3445 for (index, byte) in input.as_bytes().iter().enumerate() {
3446 match *byte {
3447 b' ' => {
3448 column += 1;
3449 if column == 4 {
3450 return Some(&input[index + 1..]);
3451 }
3452 }
3453 b'\t' => {
3454 column += 4 - (column % 4);
3455 if column >= 4 {
3456 return Some(&input[index + 1..]);
3457 }
3458 }
3459 _ => return None,
3460 }
3461 }
3462 None
3463}
3464
3465fn parse_table(
3466 lines: &[Line<'_>],
3467 index: usize,
3468 options: &SyntaxOptions,
3469 definitions: &[String],
3470 diagnostics: &mut Vec<Diagnostic>,
3471) -> Option<(Block, usize)> {
3472 if !options.constructs.gfm_table || index + 1 >= lines.len() {
3473 return None;
3474 }
3475 let delimiter = table_indent_line(lines[index + 1].text, options.constructs.indented_code)?;
3476 if list_marker_info(delimiter).is_some() {
3477 return None;
3478 }
3479 if !table_has_separator(lines[index].text, delimiter, options.constructs.spoiler) {
3480 return None;
3481 }
3482 let alignments = parse_table_delimiter(delimiter, options.constructs.spoiler)?;
3483 let headers = split_table_row(lines[index].text, options.constructs.spoiler);
3484 if headers.len() != alignments.len() {
3485 return None;
3486 }
3487
3488 let mut rows = Vec::new();
3489 rows.push(TableRow {
3490 meta: NodeMeta::new(Some(Span::new(lines[index].start, lines[index].end))),
3491 cells: headers
3492 .iter()
3493 .map(|cell| TableCell {
3494 meta: NodeMeta::default(),
3495 children: parse_inlines(
3496 cell.trim(),
3497 lines[index].start,
3498 options,
3499 definitions,
3500 diagnostics,
3501 ),
3502 })
3503 .collect(),
3504 });
3505
3506 let mut cursor = index + 2;
3507 while cursor < lines.len() {
3508 let Some(row) = table_indent_line(lines[cursor].text, options.constructs.indented_code)
3509 else {
3510 break;
3511 };
3512 if row.trim().is_empty() || table_body_line_ends_table(lines[cursor].text, options) {
3516 break;
3517 }
3518 let cells = split_table_row(row, options.constructs.spoiler);
3519 rows.push(TableRow {
3520 meta: NodeMeta::new(Some(Span::new(lines[cursor].start, lines[cursor].end))),
3521 cells: alignments
3522 .iter()
3523 .enumerate()
3524 .map(|(cell_index, _)| {
3525 let value = cells.get(cell_index).map(String::as_str).unwrap_or("");
3526 TableCell {
3527 meta: NodeMeta::default(),
3528 children: parse_inlines(
3529 value.trim(),
3530 lines[cursor].start,
3531 options,
3532 definitions,
3533 diagnostics,
3534 ),
3535 }
3536 })
3537 .collect(),
3538 });
3539 cursor += 1;
3540 }
3541
3542 Some((
3543 Block::Table(Table {
3544 meta: NodeMeta::new(Some(Span::new(
3545 lines[index].start,
3546 lines[cursor - 1].end_with_eol,
3547 ))),
3548 alignments,
3549 rows,
3550 }),
3551 cursor,
3552 ))
3553}
3554
3555fn parse_setext_heading(
3556 lines: &[Line<'_>],
3557 index: usize,
3558 options: &SyntaxOptions,
3559 definitions: &[String],
3560) -> Option<(Block, usize)> {
3561 if index + 1 >= lines.len() || lines[index].text.trim().is_empty() {
3562 return None;
3563 }
3564
3565 let mut underline_index = index + 1;
3570 loop {
3571 let underline_depth = if lines[underline_index].lazy {
3577 None
3578 } else {
3579 setext_underline_depth(lines[underline_index].text)
3580 };
3581 if let Some(depth) = underline_depth {
3582 let mut value = String::new();
3583 for line in &lines[index..underline_index] {
3584 push_line(&mut value, trim_ascii_start(line.text));
3587 }
3588 return Some((
3589 Block::Heading(Heading {
3590 meta: NodeMeta::new(Some(Span::new(
3591 lines[index].start,
3592 lines[underline_index].end,
3593 ))),
3594 depth,
3595 kind: HeadingKind::Setext,
3596 children: parse_inlines(
3597 &value,
3598 lines[index].start,
3599 options,
3600 definitions,
3601 &mut Vec::new(),
3602 ),
3603 }),
3604 underline_index + 1,
3605 ));
3606 }
3607
3608 let line = lines[underline_index].text;
3611 if line.trim().is_empty()
3612 || table_can_start(lines, underline_index, options)
3613 || likely_block_start(line, options)
3614 {
3615 return None;
3616 }
3617 underline_index += 1;
3618 if underline_index >= lines.len() {
3619 return None;
3620 }
3621 }
3622}
3623
3624fn setext_underline_depth(input: &str) -> Option<u8> {
3625 let underline = trim_up_to_three_spaces(input)?.trim();
3626 match underline {
3627 text if !text.is_empty() && text.chars().all(|char| char == '=') => Some(1),
3628 text if !text.is_empty() && text.chars().all(|char| char == '-') => Some(2),
3629 _ => None,
3630 }
3631}
3632
3633fn parse_paragraph(
3634 lines: &[Line<'_>],
3635 index: usize,
3636 options: &SyntaxOptions,
3637 definitions: &[String],
3638 diagnostics: &mut Vec<Diagnostic>,
3639) -> (Block, usize) {
3640 let mut value = String::new();
3641 let start = lines[index].start;
3642 let mut cursor = index;
3643 while cursor < lines.len() {
3644 if lines[cursor].text.trim().is_empty() {
3645 break;
3646 }
3647 if cursor > index && !lines[cursor].lazy {
3651 if table_can_start(lines, cursor, options) {
3652 break;
3653 }
3654 if likely_block_start(lines[cursor].text, options) {
3655 break;
3656 }
3657 }
3658 if !value.is_empty() {
3659 value.push('\n');
3660 }
3661 value.push_str(trim_ascii_start(lines[cursor].text));
3662 cursor += 1;
3663 }
3664
3665 let end = lines[cursor - 1].end;
3666 (
3667 Block::Paragraph(Paragraph {
3668 meta: NodeMeta::new(Some(Span::new(start, end))),
3669 children: parse_inlines(&value, start, options, definitions, diagnostics),
3670 }),
3671 cursor,
3672 )
3673}
3674
3675#[derive(Clone, Copy)]
3686struct DelimMarker {
3687 node_index: usize,
3691 marker: u8,
3692 length: usize,
3694 can_open: bool,
3695 can_close: bool,
3696 single_open: bool,
3698 single_close: bool,
3699 strike: bool,
3701 recloses: usize,
3704 enclosed_from: usize,
3707 position: usize,
3709 line: usize,
3712}
3713
3714impl DelimMarker {
3715 fn nests(&self) -> bool {
3718 self.marker != b'|'
3719 }
3720}
3721
3722const NIL: usize = usize::MAX;
3724
3725const MAX_EMPHASIS_NESTING: usize = 16;
3730
3731#[derive(Clone, Copy, Default)]
3739struct Nesting {
3740 emphasis: usize,
3741 total: usize,
3742}
3743
3744impl Nesting {
3745 fn max(self, other: Self) -> Self {
3746 Self {
3747 emphasis: self.emphasis.max(other.emphasis),
3748 total: self.total.max(other.total),
3749 }
3750 }
3751}
3752
3753struct DelimiterLinks {
3757 prev: Vec<usize>,
3758 next: Vec<usize>,
3759 live: Vec<bool>,
3760 nesting_after: Vec<Nesting>,
3761}
3762
3763impl DelimiterLinks {
3764 fn new(count: usize) -> Self {
3765 Self {
3766 prev: (0..count).map(|index| index.wrapping_sub(1)).collect(),
3767 next: (1..=count)
3768 .map(|index| if index < count { index } else { NIL })
3769 .collect(),
3770 live: alloc::vec![true; count],
3771 nesting_after: alloc::vec![Nesting::default(); count],
3772 }
3773 }
3774
3775 fn unlink(&mut self, index: usize) {
3777 let (before, after) = (self.prev[index], self.next[index]);
3778 self.live[index] = false;
3779 if before != NIL {
3780 self.next[before] = after;
3781 self.nesting_after[before] = self.nesting_after[before].max(self.nesting_after[index]);
3782 }
3783 if after != NIL {
3784 self.prev[after] = before;
3785 }
3786 }
3787
3788 fn close_span(&mut self, opener: usize, closer: usize) -> Nesting {
3791 let mut nesting = self.nesting_after[opener];
3792 let mut inner = self.next[opener];
3793 while inner != closer {
3794 nesting = nesting.max(self.nesting_after[inner]);
3795 self.live[inner] = false;
3796 inner = self.next[inner];
3797 }
3798 self.next[opener] = closer;
3799 self.prev[closer] = opener;
3800 nesting
3801 }
3802}
3803
3804struct InlineList {
3809 slots: Vec<Option<Inline>>,
3810 prev: Vec<usize>,
3811 next: Vec<usize>,
3812 head: usize,
3813}
3814
3815impl InlineList {
3816 fn new(nodes: Vec<Inline>) -> Self {
3817 let count = nodes.len();
3818 Self {
3819 slots: nodes.into_iter().map(Some).collect(),
3820 prev: (0..count).map(|slot| slot.wrapping_sub(1)).collect(),
3821 next: (1..=count)
3822 .map(|slot| if slot < count { slot } else { NIL })
3823 .collect(),
3824 head: if count == 0 { NIL } else { 0 },
3825 }
3826 }
3827
3828 fn node_mut(&mut self, slot: usize) -> &mut Inline {
3829 self.slots[slot]
3830 .as_mut()
3831 .expect("delimiter placeholder slot is live")
3832 }
3833
3834 fn wrap_between(
3837 &mut self,
3838 before: usize,
3839 after: usize,
3840 wrap: impl FnOnce(Vec<Inline>) -> Inline,
3841 ) {
3842 let mut children = Vec::new();
3843 let mut cursor = self.next[before];
3844 while cursor != after {
3845 children.push(self.slots[cursor].take().expect("linked slot is live"));
3846 cursor = self.next[cursor];
3847 }
3848 let slot = self.slots.len();
3849 self.slots.push(Some(wrap(children)));
3850 self.prev.push(before);
3851 self.next.push(after);
3852 self.next[before] = slot;
3853 self.prev[after] = slot;
3854 }
3855
3856 fn remove(&mut self, slot: usize) {
3857 let (prev, next) = (self.prev[slot], self.next[slot]);
3858 if prev == NIL {
3859 self.head = next;
3860 } else {
3861 self.next[prev] = next;
3862 }
3863 if next != NIL {
3864 self.prev[next] = prev;
3865 }
3866 self.slots[slot] = None;
3867 }
3868
3869 fn into_vec(mut self) -> Vec<Inline> {
3870 let mut nodes = Vec::new();
3871 let mut cursor = self.head;
3872 while cursor != NIL {
3873 nodes.push(self.slots[cursor].take().expect("linked slot is live"));
3874 cursor = self.next[cursor];
3875 }
3876 nodes
3877 }
3878}
3879
3880#[derive(Default)]
3883struct LineCounter {
3884 position: usize,
3885 line: usize,
3886}
3887
3888impl LineCounter {
3889 fn line_at(&mut self, input: &str, position: usize) -> usize {
3890 self.line += input.as_bytes()[self.position..position]
3891 .iter()
3892 .filter(|byte| matches!(byte, b'\n' | b'\r'))
3893 .count();
3894 self.position = position;
3895 self.line
3896 }
3897}
3898
3899#[derive(Clone, Copy, Default)]
3901struct DelimRoles {
3902 can_open: bool,
3903 can_close: bool,
3904 single_open: bool,
3905 single_close: bool,
3906 strike: bool,
3909 recloses: usize,
3910}
3911
3912fn push_text(nodes: &mut Vec<Inline>, start: usize, value: &str) {
3914 nodes.push(Inline::Text(Text {
3915 meta: NodeMeta::new(Some(Span::new(start, start + value.len()))),
3916 value: value.into(),
3917 }));
3918}
3919
3920#[allow(clippy::too_many_arguments)]
3924fn close_bracket(
3925 input: &str,
3926 base_offset: usize,
3927 close: usize,
3928 options: &SyntaxOptions,
3929 definitions: &[String],
3930 nodes: &mut Vec<Inline>,
3931 delimiters: &mut Vec<DelimMarker>,
3932 brackets: &mut Brackets,
3933 pass: &mut InlinePass,
3934) -> Option<usize> {
3935 let mut opener = brackets.openers.pop()?;
3936 let depth = pass.state.depth + brackets.openers.len();
3937 let link_active = brackets.openers.len() >= brackets.link_floor;
3940 brackets.link_floor = brackets.link_floor.min(brackets.openers.len());
3941
3942 if opener.kind == BracketKind::InlineFootnote {
3943 if !input[opener.position + 2..close].trim().is_empty() {
3944 let (children, marks) =
3945 take_label(input, options, nodes, delimiters, brackets, &opener, depth);
3946 nodes.truncate(opener.node_index - 1);
3947 if let Some(caret) = opener.caret {
3948 delimiters.truncate(caret);
3949 }
3950 nodes.push(Inline::InlineFootnote(InlineFootnote {
3951 meta: NodeMeta::new(Some(Span::new(
3952 base_offset + opener.position,
3953 base_offset + close + 1,
3954 ))),
3955 children,
3956 }));
3957 brackets.formed.push((nodes.len() - 1, marks + 1));
3958 return Some(close + 1);
3959 }
3960 if let Some(caret) = opener.caret {
3963 delimiters[caret].single_close = true;
3964 }
3965 opener.kind = BracketKind::Link;
3966 opener.position += 1;
3967 }
3968
3969 if opener.kind == BracketKind::Image {
3970 let target = match_link_target(
3971 &mut pass.scan,
3972 input,
3973 opener.position + 2,
3974 close,
3975 true,
3976 definitions,
3977 );
3978 if let Some((end, target)) = target {
3979 let (children, marks) =
3980 take_label(input, options, nodes, delimiters, brackets, &opener, depth);
3981 nodes.truncate(opener.node_index - 1);
3982 let span = Span::new(base_offset + opener.position, base_offset + end);
3983 nodes.push(link_node(target, true, span, children));
3984 brackets.formed.push((nodes.len() - 1, marks + 1));
3985 return Some(end);
3986 }
3987 if let Some((end, wikilink)) = opener.wikilink.take() {
3988 if end > close {
3989 drop_label(nodes, delimiters, brackets, &opener);
3990 nodes.push(wikilink);
3991 return Some(end);
3992 }
3993 }
3994 opener.kind = BracketKind::Link;
3996 opener.position += 1;
3997 }
3998
3999 if link_active {
4000 let target = match_link_target(
4001 &mut pass.scan,
4002 input,
4003 opener.position + 1,
4004 close,
4005 false,
4006 definitions,
4007 );
4008 if let Some((end, target)) = target {
4009 let (children, marks) =
4010 take_label(input, options, nodes, delimiters, brackets, &opener, depth);
4011 nodes.truncate(opener.node_index);
4012 let span = Span::new(base_offset + opener.position, base_offset + end);
4013 nodes.push(link_node(target, false, span, children));
4014 brackets.formed.push((nodes.len() - 1, marks + 1));
4015 brackets.close_links();
4017 return Some(end);
4018 }
4019 }
4020
4021 let holds_formed = brackets
4024 .formed
4025 .last()
4026 .is_some_and(|&(node, _)| node > opener.node_index);
4027 if options.constructs.footnote_reference
4028 && !holds_formed
4029 && input[opener.position + 1..].starts_with('^')
4030 {
4031 let label = &input[opener.position + 2..close];
4032 if is_footnote_label(label) {
4033 drop_label(nodes, delimiters, brackets, &opener);
4034 nodes.push(Inline::FootnoteReference(FootnoteReference {
4035 meta: NodeMeta::new(Some(Span::new(
4036 base_offset + opener.position,
4037 base_offset + close + 1,
4038 ))),
4039 label: label.into(),
4040 identifier: normalize_label(label),
4041 }));
4042 return Some(close + 1);
4043 }
4044 }
4045 None
4046}
4047
4048fn drop_label(
4050 nodes: &mut Vec<Inline>,
4051 delimiters: &mut Vec<DelimMarker>,
4052 brackets: &mut Brackets,
4053 opener: &BracketOpener,
4054) {
4055 nodes.truncate(opener.node_index);
4056 delimiters.truncate(opener.delimiter_bottom);
4057 let kept = brackets
4058 .formed
4059 .partition_point(|&(node, _)| node < opener.node_index);
4060 brackets.formed.truncate(kept);
4061}
4062
4063#[allow(clippy::too_many_arguments)]
4066fn push_delimiter(
4067 nodes: &mut Vec<Inline>,
4068 delimiters: &mut Vec<DelimMarker>,
4069 lines: &mut LineCounter,
4070 input: &str,
4071 index: usize,
4072 base_offset: usize,
4073 marker: u8,
4074 length: usize,
4075 roles: DelimRoles,
4076) {
4077 let node_index = nodes.len();
4078 nodes.push(Inline::Text(Text {
4079 meta: NodeMeta::new(Some(Span::new(
4080 base_offset + index,
4081 base_offset + index + length,
4082 ))),
4083 value: String::from(marker as char).repeat(length),
4084 }));
4085 delimiters.push(DelimMarker {
4086 node_index,
4087 marker,
4088 length,
4089 can_open: roles.can_open,
4090 can_close: roles.can_close,
4091 single_open: roles.single_open,
4092 single_close: roles.single_close,
4093 strike: roles.strike,
4094 recloses: roles.recloses,
4095 enclosed_from: NIL,
4096 position: index,
4097 line: lines.line_at(input, index),
4098 });
4099}
4100
4101fn emphasis_roles(
4116 input: &str,
4117 index: usize,
4118 length: usize,
4119 marker: u8,
4120 strikethrough: bool,
4121 bounded_before: bool,
4122 bounded_after: bool,
4123) -> (bool, bool) {
4124 let flanking = delimiter_flanking_within(input, index, length, bounded_before, bounded_after);
4125 let (mut can_open, mut can_close) = if marker == b'_' {
4126 (
4127 flanking.left
4128 && (!flanking.right || flanking.previous.is_some_and(|c| c.is_ascii_punctuation())),
4129 flanking.right
4130 && (!flanking.left || flanking.next.is_some_and(|c| c.is_ascii_punctuation())),
4131 )
4132 } else {
4133 (flanking.left, flanking.right)
4134 };
4135
4136 if strikethrough && marker != b'~' {
4140 if flanking.next == Some('~') {
4141 can_open = true;
4142 }
4143 if flanking.previous == Some('~') {
4144 can_close = true;
4145 }
4146 }
4147 (can_open, can_close)
4148}
4149
4150fn assign_emphasis_roles(
4156 input: &str,
4157 delimiters: &mut [DelimMarker],
4158 strikethrough: bool,
4159 underline: bool,
4160) {
4161 let underline_mark = |run: &DelimMarker| underline && run.marker == b'_' && run.length == 2;
4162 let mut opens_span = alloc::vec![false; delimiters.len()];
4163 let mut closes_span = alloc::vec![false; delimiters.len()];
4164 let mut open_marks: [Vec<usize>; 3] = [Vec::new(), Vec::new(), Vec::new()];
4165 let mut pending: [Option<usize>; 3] = [None; 3];
4166 let mut span_close = alloc::vec![NIL; delimiters.len()];
4168 for (index, run) in delimiters.iter().enumerate() {
4169 if underline_mark(run) {
4170 let open = &mut open_marks[2];
4171 if can_close_underscore(input, run.position, 2) {
4172 if let Some(opener) = open.pop() {
4173 opens_span[opener] = true;
4174 closes_span[index] = true;
4175 span_close[opener] = index;
4176 continue;
4177 }
4178 }
4179 if can_open_underscore(input, run.position, 2) {
4180 open.push(index);
4181 }
4182 continue;
4183 }
4184 if matches!(run.marker, b'+' | b'=') {
4185 let open = &mut open_marks[usize::from(run.marker == b'=')];
4186 let mut closed = 0;
4187 if run.can_close {
4188 while closed <= run.recloses {
4189 let Some(opener) = open.pop() else {
4190 break;
4191 };
4192 opens_span[opener] = true;
4193 closes_span[index] = true;
4194 span_close[opener] = index;
4195 closed += 1;
4196 }
4197 }
4198 if run.can_open && run.length >= 2 * closed + 2 {
4200 open.push(index);
4201 }
4202 continue;
4203 }
4204 let Some(kind) = first_closer_kind(run) else {
4205 continue;
4206 };
4207 if let Some(opener) = pending[kind] {
4208 let opener_run = &delimiters[opener];
4209 if opener_run.line != run.line {
4210 pending[kind] = None;
4211 } else if first_closer_can_close(run) {
4212 pending[kind] = None;
4213 if opener_run.position + opener_run.length != run.position {
4214 opens_span[opener] = true;
4215 closes_span[index] = true;
4216 span_close[opener] = index;
4217 if run.marker == b'|' && run.can_open && run.length >= 4 {
4219 pending[kind] = Some(index);
4220 }
4221 continue;
4222 }
4223 }
4224 }
4225 if pending[kind].is_none() && first_closer_can_open(run) {
4226 pending[kind] = Some(index);
4227 }
4228 }
4229
4230 for index in 0..delimiters.len() {
4231 let run = delimiters[index];
4232 if !matches!(run.marker, b'*' | b'_') && !run.strike {
4233 continue;
4234 }
4235 let bounded_before = index > 0 && {
4236 let before = &delimiters[index - 1];
4237 before.position + before.length == run.position && opens_span[index - 1]
4238 };
4239 let bounded_after = index + 1 < delimiters.len() && {
4240 let after = &delimiters[index + 1];
4241 after.position == run.position + run.length && closes_span[index + 1]
4242 };
4243 let (can_open, can_close) = emphasis_roles(
4244 input,
4245 run.position,
4246 run.length,
4247 run.marker,
4248 strikethrough,
4249 bounded_before,
4250 bounded_after,
4251 );
4252 delimiters[index].can_open = can_open;
4253 delimiters[index].can_close = can_close;
4254 }
4255
4256 let mut open_spans: Vec<usize> = Vec::new();
4260 for (index, run) in delimiters.iter_mut().enumerate() {
4261 while open_spans
4262 .last()
4263 .is_some_and(|&opener| span_close[opener] <= index)
4264 {
4265 open_spans.pop();
4266 }
4267 if let Some(&opener) = open_spans.last() {
4268 run.enclosed_from = opener;
4269 }
4270 if span_close[index] != NIL {
4271 open_spans.push(index);
4272 }
4273 }
4274}
4275
4276fn double_mark_roles(input: &str, index: usize, length: usize, marker: u8) -> DelimRoles {
4281 let after_escaped_mark =
4282 index > 0 && input.as_bytes()[index - 1] == marker && is_escaped_at(input, index - 1);
4283 let can_close = !after_escaped_mark && can_close_delimited(input, index, 2);
4284 let recloses = if can_close {
4285 (1..length / 2)
4286 .take_while(|pair| can_close_delimited(input, index + 2 * pair, 2))
4287 .count()
4288 } else {
4289 0
4290 };
4291 DelimRoles {
4292 can_open: can_open_delimited(input, index + length - 2, 2),
4293 can_close,
4294 recloses,
4295 ..DelimRoles::default()
4296 }
4297}
4298
4299#[derive(Clone, Copy)]
4301struct EmphasisContext {
4302 underline: bool,
4304 depth: usize,
4307}
4308
4309fn process_emphasis(
4318 nodes: Vec<Inline>,
4319 mut delimiters: Vec<DelimMarker>,
4320 context: EmphasisContext,
4321 formed: &[(usize, usize)],
4322) -> (Vec<Inline>, usize) {
4323 let mut deepest = formed.iter().map(|&(_, depth)| depth).max().unwrap_or(0);
4324 if delimiters.is_empty() {
4325 let mut nodes = nodes;
4326 merge_adjacent_text(&mut nodes);
4327 return (nodes, deepest);
4328 }
4329 let mut nodes = InlineList::new(nodes);
4330
4331 let mut links = DelimiterLinks::new(delimiters.len());
4335 for &(node, depth) in formed {
4336 let before = delimiters.partition_point(|run| run.node_index < node);
4337 if before > 0 {
4338 let gap = &mut links.nesting_after[before - 1].total;
4339 *gap = (*gap).max(depth);
4340 }
4341 }
4342
4343 let mut openers_bottom: [Option<usize>; NESTED_MARKERS * 6] = [None; NESTED_MARKERS * 6];
4348 let mut span_bottom: Vec<Vec<(usize, usize)>> = Vec::new();
4349 let mut pending: [Option<usize>; 3] = [None; 3];
4351 let mut closer_idx = 0;
4352 let mut visited = NIL;
4355
4356 while closer_idx < delimiters.len() {
4359 let closer = delimiters[closer_idx];
4360
4361 let mut first = None;
4362 let kind = first_closer_kind(&closer);
4363 if let Some(kind) = kind {
4364 if visited != closer_idx {
4365 if let Some(opener) = pending[kind] {
4366 let opener_run = delimiters[opener];
4367 let usable = links.live[opener]
4368 && first_closer_can_open(&opener_run)
4369 && opener_run.line == closer.line;
4370 if !usable {
4371 pending[kind] = None;
4372 } else if first_closer_can_close(&closer) {
4373 if opener_run.position + opener_run.length == closer.position {
4374 pending[kind] = None;
4376 } else {
4377 first = Some(opener);
4378 }
4379 }
4380 }
4381 }
4382 }
4383 visited = closer_idx;
4384
4385 let mut nested = None;
4388 if closer.can_close && closer.nests() {
4389 nested = nested_opener(
4390 &delimiters,
4391 &links,
4392 &openers_bottom,
4393 &span_bottom,
4394 closer_idx,
4395 first.map(|opener| opener + 1),
4396 );
4397 }
4398
4399 let choice = match (nested, first) {
4400 (Some(nested), Some(first)) => Some((nested.max(first), nested > first)),
4401 (Some(nested), None) => Some((nested, true)),
4402 (None, Some(first)) => Some((first, false)),
4403 (None, None) => None,
4404 };
4405
4406 let Some((opener_idx, is_nested)) = choice else {
4407 if closer.can_close && closer.nests() {
4408 let key = openers_bottom_key(&closer);
4411 match span_floor(&closer) {
4412 Some(span) => {
4413 if span_bottom.len() <= span {
4414 span_bottom.resize_with(span + 1, Vec::new);
4415 }
4416 match span_bottom[span].iter_mut().find(|(k, _)| *k == key) {
4417 Some(bound) => bound.1 = closer_idx,
4418 None => span_bottom[span].push((key, closer_idx)),
4419 }
4420 }
4421 None => openers_bottom[key] = Some(closer_idx),
4422 }
4423 }
4424 if let Some(kind) = kind {
4425 if pending[kind].is_none() && first_closer_can_open(&closer) {
4426 pending[kind] = Some(closer_idx);
4427 }
4428 }
4429 if !closer.can_open && !closer.single_open {
4430 links.unlink(closer_idx);
4431 }
4432 closer_idx += 1;
4433 continue;
4434 };
4435
4436 if !is_nested {
4437 if let Some(kind) = kind {
4438 pending[kind] = None;
4439 }
4440 }
4441 let (used, wrap) = pair_shape(
4442 &delimiters[opener_idx],
4443 &delimiters[closer_idx],
4444 is_nested,
4445 context,
4446 );
4447
4448 let inner = links.close_span(opener_idx, closer_idx);
4451 let fits = context.depth + inner.total < MAX_INLINE_NESTING
4452 && (wrap.is_mark() || inner.emphasis < MAX_EMPHASIS_NESTING);
4453 if fits {
4454 apply_emphasis(
4455 &mut nodes,
4456 &mut delimiters,
4457 opener_idx,
4458 closer_idx,
4459 used,
4460 wrap,
4461 );
4462 links.nesting_after[opener_idx] = Nesting {
4463 emphasis: if wrap.is_mark() {
4464 0
4465 } else {
4466 inner.emphasis + 1
4467 },
4468 total: inner.total + 1,
4469 };
4470 deepest = deepest.max(inner.total + 1);
4471 } else {
4472 delimiters[opener_idx].length = 0;
4476 delimiters[closer_idx].length = 0;
4477 links.nesting_after[opener_idx] = inner;
4478 }
4479 if matches!(delimiters[opener_idx].marker, b'+' | b'=' | b'|') {
4480 delimiters[opener_idx].can_open = false;
4482 }
4483 if is_nested && matches!(closer.marker, b'+' | b'=') {
4484 let run = &mut delimiters[closer_idx];
4487 run.can_close = run.recloses > 0;
4488 run.recloses = run.recloses.saturating_sub(1);
4489 }
4490 if !is_nested {
4491 if closer.marker == b'~' {
4494 delimiters[closer_idx].can_open = false;
4495 delimiters[closer_idx].can_close = false;
4496 }
4497 delimiters[closer_idx].single_close = false;
4498 }
4499
4500 if delimiters[opener_idx].length == 0 {
4501 links.unlink(opener_idx);
4502 }
4503 let leftover = delimiters[closer_idx];
4504 if leftover.length == 0 {
4505 links.unlink(closer_idx);
4506 closer_idx += 1;
4507 } else if !is_nested {
4508 if let Some(kind) = kind {
4511 if first_closer_can_open(&leftover) {
4512 pending[kind] = Some(closer_idx);
4513 }
4514 }
4515 if !first_closer_can_open(&leftover) {
4516 links.unlink(closer_idx);
4517 }
4518 closer_idx += 1;
4519 }
4520 }
4524
4525 let mut nodes = nodes.into_vec();
4529 merge_adjacent_text(&mut nodes);
4530 (nodes, deepest)
4531}
4532
4533const NESTED_MARKERS: usize = 5;
4535
4536fn nested_opener(
4544 delimiters: &[DelimMarker],
4545 links: &DelimiterLinks,
4546 openers_bottom: &[Option<usize>],
4547 span_bottom: &[Vec<(usize, usize)>],
4548 closer_idx: usize,
4549 floor: Option<usize>,
4550) -> Option<usize> {
4551 let closer = &delimiters[closer_idx];
4552 let key = openers_bottom_key(closer);
4553 let mut bottom = openers_bottom[key].max(floor);
4554 if let Some(span) = span_floor(closer) {
4555 let span_floor = span_bottom
4556 .get(span)
4557 .and_then(|bounds| bounds.iter().find(|(k, _)| *k == key))
4558 .map_or(span + 1, |&(_, bound)| bound);
4559 bottom = bottom.max(Some(span_floor));
4560 }
4561 let mut search = links.prev[closer_idx];
4562 while search != NIL {
4563 if bottom.is_some_and(|bottom| search < bottom) {
4564 return None;
4565 }
4566 let candidate = &delimiters[search];
4567 if candidate.marker == closer.marker
4568 && candidate.can_open
4569 && emphasis_delimiters_match(candidate, closer)
4570 {
4571 return Some(search);
4572 }
4573 search = links.prev[search];
4574 }
4575 None
4576}
4577
4578fn span_floor(closer: &DelimMarker) -> Option<usize> {
4581 (closer.can_open && closer.enclosed_from != NIL).then_some(closer.enclosed_from)
4582}
4583
4584fn first_closer_kind(run: &DelimMarker) -> Option<usize> {
4586 match run.marker {
4587 b'|' => Some(0),
4588 b'~' if run.single_open || run.single_close => Some(1),
4589 b'^' => Some(2),
4590 _ => None,
4591 }
4592}
4593
4594fn first_closer_can_open(run: &DelimMarker) -> bool {
4595 if run.marker == b'|' {
4596 run.can_open && run.length >= 2
4597 } else {
4598 run.single_open
4599 }
4600}
4601
4602fn first_closer_can_close(run: &DelimMarker) -> bool {
4603 if run.marker == b'|' {
4604 run.can_close && run.length >= 2
4605 } else {
4606 run.single_close
4607 }
4608}
4609
4610fn pair_shape(
4612 opener: &DelimMarker,
4613 closer: &DelimMarker,
4614 nested: bool,
4615 context: EmphasisContext,
4616) -> (usize, EmphasisWrap) {
4617 match closer.marker {
4618 b'~' if nested => {
4619 let marker = if closer.length >= 2 {
4622 DeleteMarker::DoubleTilde
4623 } else {
4624 DeleteMarker::SingleTilde
4625 };
4626 (closer.length, EmphasisWrap::Delete(marker))
4627 }
4628 b'~' => (1, EmphasisWrap::Subscript),
4629 b'^' => (1, EmphasisWrap::Superscript),
4630 b'+' => (2, EmphasisWrap::Insert),
4631 b'=' => (2, EmphasisWrap::Mark),
4632 b'|' => (2, EmphasisWrap::Spoiler),
4633 marker => {
4634 let strong = opener.length >= 2 && closer.length >= 2;
4635 if !strong {
4636 (1, EmphasisWrap::Emphasis)
4637 } else if marker == b'_' && context.underline {
4638 (2, EmphasisWrap::Underline)
4639 } else {
4640 (2, EmphasisWrap::Strong)
4641 }
4642 }
4643 }
4644}
4645
4646fn merge_adjacent_text(nodes: &mut Vec<Inline>) {
4650 let mut write = 0;
4651 for read in 0..nodes.len() {
4652 if read != write {
4653 nodes.swap(read, write);
4654 }
4655 if write > 0 {
4656 let (head, tail) = nodes.split_at_mut(write);
4657 if let (Inline::Text(previous), Inline::Text(current)) =
4658 (&mut head[write - 1], &tail[0])
4659 {
4660 previous.value.push_str(¤t.value);
4661 if let (Some(previous_span), Some(current_span)) =
4662 (previous.meta.span.as_mut(), current.meta.span)
4663 {
4664 previous_span.end = current_span.end;
4665 }
4666 continue;
4667 }
4668 }
4669 write += 1;
4670 }
4671 nodes.truncate(write);
4672
4673 for node in nodes.iter_mut() {
4674 match node {
4675 Inline::Emphasis(node) => merge_adjacent_text(&mut node.children),
4676 Inline::Strong(node) => merge_adjacent_text(&mut node.children),
4677 Inline::Delete(node) => merge_adjacent_text(&mut node.children),
4678 Inline::Underline(node) => merge_adjacent_text(&mut node.children),
4679 Inline::Insert(node) => merge_adjacent_text(&mut node.children),
4680 Inline::Mark(node) => merge_adjacent_text(&mut node.children),
4681 Inline::Spoiler(node) => merge_adjacent_text(&mut node.children),
4682 Inline::Subscript(node) => merge_adjacent_text(&mut node.children),
4683 Inline::Superscript(node) => merge_adjacent_text(&mut node.children),
4684 _ => {}
4685 }
4686 }
4687}
4688
4689fn openers_bottom_key(closer: &DelimMarker) -> usize {
4692 let marker = match closer.marker {
4693 b'_' => 1,
4694 b'~' => 2,
4695 b'+' => 3,
4696 b'=' => 4,
4697 _ => 0,
4698 };
4699 let both = usize::from(closer.can_open && closer.can_close);
4700 let modulo = closer.length % 3;
4701 ((marker * 2) + both) * 3 + modulo
4702}
4703
4704fn emphasis_delimiters_match(opener: &DelimMarker, closer: &DelimMarker) -> bool {
4706 match opener.marker {
4707 b'~' => opener.length == closer.length,
4710 b'+' | b'=' => opener.length >= 2 && closer.length >= 2,
4712 _ => {
4713 let opener_both = opener.can_open && opener.can_close;
4717 let closer_both = closer.can_open && closer.can_close;
4718 if opener_both || closer_both {
4719 let sum = opener.length + closer.length;
4720 if sum % 3 == 0 && !(opener.length % 3 == 0 && closer.length % 3 == 0) {
4721 return false;
4722 }
4723 }
4724 true
4725 }
4726 }
4727}
4728
4729#[derive(Clone, Copy)]
4731enum EmphasisWrap {
4732 Emphasis,
4733 Strong,
4734 Delete(DeleteMarker),
4735 Underline,
4736 Insert,
4737 Mark,
4738 Spoiler,
4739 Subscript,
4740 Superscript,
4741}
4742
4743impl EmphasisWrap {
4744 fn is_mark(self) -> bool {
4747 !matches!(self, Self::Emphasis | Self::Strong | Self::Delete(_))
4748 }
4749}
4750
4751fn apply_emphasis(
4754 nodes: &mut InlineList,
4755 delimiters: &mut [DelimMarker],
4756 opener_idx: usize,
4757 closer_idx: usize,
4758 used: usize,
4759 wrap: EmphasisWrap,
4760) {
4761 let opener_node = delimiters[opener_idx].node_index;
4762 let closer_node = delimiters[closer_idx].node_index;
4763
4764 let span_start = nodes
4768 .node_mut(opener_node)
4769 .span()
4770 .map(|span| span.end - used);
4771 let span_end = nodes
4772 .node_mut(closer_node)
4773 .span()
4774 .map(|span| span.start + used);
4775 trim_delimiter_text_tail(nodes.node_mut(opener_node), used);
4778 trim_delimiter_text_head(nodes.node_mut(closer_node), used);
4779 delimiters[opener_idx].length -= used;
4780 let closer = &mut delimiters[closer_idx];
4781 closer.length -= used;
4782 closer.position += used;
4783
4784 let meta = NodeMeta::new(
4787 span_start
4788 .zip(span_end)
4789 .map(|(start, end)| Span::new(start, end)),
4790 );
4791 nodes.wrap_between(opener_node, closer_node, |children| match wrap {
4792 EmphasisWrap::Strong => Inline::Strong(Strong { meta, children }),
4793 EmphasisWrap::Emphasis => Inline::Emphasis(Emphasis { meta, children }),
4794 EmphasisWrap::Delete(marker) => Inline::Delete(Delete {
4795 meta,
4796 marker,
4797 children,
4798 }),
4799 EmphasisWrap::Underline => Inline::Underline(Underline { meta, children }),
4800 EmphasisWrap::Insert => Inline::Insert(Insert { meta, children }),
4801 EmphasisWrap::Mark => Inline::Mark(Mark { meta, children }),
4802 EmphasisWrap::Spoiler => Inline::Spoiler(Spoiler { meta, children }),
4803 EmphasisWrap::Subscript => Inline::Subscript(Subscript { meta, children }),
4804 EmphasisWrap::Superscript => Inline::Superscript(Superscript { meta, children }),
4805 });
4806
4807 if delimiters[closer_idx].length == 0 {
4810 nodes.remove(closer_node);
4811 }
4812 if delimiters[opener_idx].length == 0 {
4813 nodes.remove(opener_node);
4814 }
4815}
4816
4817#[derive(Clone, Copy, PartialEq, Eq)]
4819enum BracketKind {
4820 Link,
4821 Image,
4822 InlineFootnote,
4823}
4824
4825struct BracketOpener {
4828 kind: BracketKind,
4829 node_index: usize,
4831 position: usize,
4833 delimiter_bottom: usize,
4836 caret: Option<usize>,
4839 wikilink: Option<(usize, Inline)>,
4842}
4843
4844enum LinkTarget {
4846 Resource(ParsedLinkResource),
4847 Reference {
4848 identifier: String,
4849 kind: ReferenceKind,
4850 },
4851}
4852
4853fn match_link_target(
4859 scan: &mut InlineScan,
4860 input: &str,
4861 label_start: usize,
4862 close: usize,
4863 image: bool,
4864 definitions: &[String],
4865) -> Option<(usize, LinkTarget)> {
4866 let label = &input[label_start..close];
4867 let after = close + 1;
4868 if input.as_bytes().get(after) == Some(&b'(') {
4869 match parse_link_resource(&mut scan.lookups, input, after) {
4870 Some((end, resource)) => return Some((end, LinkTarget::Resource(resource))),
4871 None if image => return None,
4875 None => {}
4876 }
4877 }
4878 let reference_close = if input.as_bytes().get(after) == Some(&b'[') {
4881 scan.reference_label_end(after)
4882 } else {
4883 None
4884 };
4885 if let Some(reference_close) = reference_close {
4886 let reference = &input[after + 1..reference_close];
4887 let identifier = if reference.is_empty() {
4888 label
4889 } else {
4890 reference
4891 };
4892 return definition_exists(definitions, identifier).then(|| {
4896 let kind = if reference.is_empty() {
4897 ReferenceKind::Collapsed
4898 } else {
4899 ReferenceKind::Full
4900 };
4901 (
4902 reference_close + 1,
4903 LinkTarget::Reference {
4904 identifier: identifier.into(),
4905 kind,
4906 },
4907 )
4908 });
4909 }
4910 definition_exists(definitions, label).then(|| {
4911 (
4912 after,
4913 LinkTarget::Reference {
4914 identifier: label.into(),
4915 kind: ReferenceKind::Shortcut,
4916 },
4917 )
4918 })
4919}
4920
4921fn link_node(target: LinkTarget, image: bool, span: Span, mut children: Vec<Inline>) -> Inline {
4923 let meta = NodeMeta::new(Some(span));
4924 if !image {
4925 demote_links(&mut children);
4926 }
4927 match (target, image) {
4928 (LinkTarget::Resource(resource), false) => Inline::Link(Link {
4929 meta,
4930 destination: resource.destination,
4931 destination_kind: resource.destination_kind,
4932 title: resource.title,
4933 title_kind: resource.title_kind,
4934 children,
4935 }),
4936 (LinkTarget::Resource(resource), true) => Inline::Image(Image {
4937 meta,
4938 destination: resource.destination,
4939 destination_kind: resource.destination_kind,
4940 title: resource.title,
4941 title_kind: resource.title_kind,
4942 alt: children,
4943 }),
4944 (LinkTarget::Reference { identifier, kind }, false) => {
4945 Inline::LinkReference(LinkReference {
4946 meta,
4947 identifier: normalize_label(&identifier),
4948 label: identifier,
4949 kind,
4950 children,
4951 })
4952 }
4953 (LinkTarget::Reference { identifier, kind }, true) => {
4954 Inline::ImageReference(ImageReference {
4955 meta,
4956 identifier: normalize_label(&identifier),
4957 label: identifier,
4958 kind,
4959 alt: children,
4960 })
4961 }
4962 }
4963}
4964
4965fn demote_links(nodes: &mut Vec<Inline>) {
4969 for node in nodes.iter_mut() {
4970 match node {
4971 Inline::Autolink(autolink) => {
4972 let value = match &autolink.kind {
4973 AutolinkKind::Angle => autolink.destination.clone(),
4974 AutolinkKind::GfmLiteral { original } => original.clone(),
4975 };
4976 *node = Inline::Text(Text {
4977 meta: autolink.meta.clone(),
4978 value,
4979 });
4980 }
4981 Inline::Emphasis(node) => demote_links(&mut node.children),
4982 Inline::Strong(node) => demote_links(&mut node.children),
4983 Inline::Underline(node) => demote_links(&mut node.children),
4984 Inline::Delete(node) => demote_links(&mut node.children),
4985 Inline::Insert(node) => demote_links(&mut node.children),
4986 Inline::Mark(node) => demote_links(&mut node.children),
4987 Inline::Spoiler(node) => demote_links(&mut node.children),
4988 Inline::Subscript(node) => demote_links(&mut node.children),
4989 Inline::Superscript(node) => demote_links(&mut node.children),
4990 Inline::InlineFootnote(node) => demote_links(&mut node.children),
4991 Inline::TextDirective(node) => demote_links(&mut node.label),
4992 _ => {}
4993 }
4994 }
4995 merge_adjacent_text(nodes);
4996}
4997
4998struct Brackets {
5000 openers: Vec<BracketOpener>,
5001 link_floor: usize,
5004 overflow: usize,
5007 formed: Vec<(usize, usize)>,
5010}
5011
5012impl Brackets {
5013 fn new() -> Self {
5014 Self {
5015 openers: Vec::new(),
5016 link_floor: 0,
5017 overflow: 0,
5018 formed: Vec::new(),
5019 }
5020 }
5021
5022 fn close_links(&mut self) {
5024 self.link_floor = self.openers.len();
5025 }
5026}
5027
5028fn take_label(
5032 input: &str,
5033 options: &SyntaxOptions,
5034 nodes: &mut Vec<Inline>,
5035 delimiters: &mut Vec<DelimMarker>,
5036 brackets: &mut Brackets,
5037 opener: &BracketOpener,
5038 depth: usize,
5039) -> (Vec<Inline>, usize) {
5040 let first = opener.node_index + 1;
5041 let children: Vec<Inline> = nodes.drain(first..).collect();
5042 let mut runs = delimiters.split_off(opener.delimiter_bottom);
5043 for run in &mut runs {
5044 run.node_index -= first;
5045 }
5046 let split = brackets.formed.partition_point(|&(node, _)| node < first);
5047 let formed: Vec<(usize, usize)> = brackets
5048 .formed
5049 .split_off(split)
5050 .into_iter()
5051 .map(|(node, marks)| (node - first, marks))
5052 .collect();
5053 assign_emphasis_roles(
5054 input,
5055 &mut runs,
5056 options.constructs.gfm_strikethrough,
5057 options.constructs.underline,
5058 );
5059 process_emphasis(
5060 children,
5061 runs,
5062 EmphasisContext {
5063 underline: options.constructs.underline,
5064 depth,
5065 },
5066 &formed,
5067 )
5068}
5069
5070fn trim_delimiter_text_tail(node: &mut Inline, count: usize) {
5072 if let Inline::Text(text) = node {
5073 let new_len = text.value.len().saturating_sub(count);
5074 text.value.truncate(new_len);
5075 if let Some(span) = text.meta.span.as_mut() {
5076 span.end = span.end.saturating_sub(count);
5077 }
5078 }
5079}
5080
5081fn trim_delimiter_text_head(node: &mut Inline, count: usize) {
5085 if let Inline::Text(text) = node {
5086 let count = count.min(text.value.len());
5087 text.value.truncate(text.value.len() - count);
5088 if let Some(span) = text.meta.span.as_mut() {
5089 span.start += count;
5090 }
5091 }
5092}
5093
5094trait Lookups {
5099 fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize>;
5101 fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize>;
5104 fn angle_destination_stop(&mut self, from: usize) -> Option<usize>;
5106}
5107
5108struct DirectLookups<'a> {
5109 input: &'a str,
5110}
5111
5112impl Lookups for DirectLookups<'_> {
5113 fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize> {
5114 self.input[from..].find(pattern).map(|offset| from + offset)
5115 }
5116
5117 fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize> {
5118 find_code_span_close(self.input, start, len)
5119 }
5120
5121 fn angle_destination_stop(&mut self, from: usize) -> Option<usize> {
5122 (from..self.input.len()).find(|index| is_angle_destination_stop(self.input, *index))
5123 }
5124}
5125
5126const CACHED_PATTERNS: [&str; 7] = [">", "-->", "?>", "]]>", "\"", "'", "`$"];
5128
5129struct InlineLookups<'a> {
5132 input: &'a str,
5133 patterns: [Positions; CACHED_PATTERNS.len()],
5134 backtick_runs: Option<BTreeMap<usize, Vec<usize>>>,
5135 angle_destination_stops: Positions,
5136}
5137
5138impl<'a> InlineLookups<'a> {
5139 fn new(input: &'a str) -> Self {
5140 Self {
5141 input,
5142 patterns: Default::default(),
5143 backtick_runs: None,
5144 angle_destination_stops: Positions::default(),
5145 }
5146 }
5147}
5148
5149impl Lookups for InlineLookups<'_> {
5150 fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize> {
5151 let input = self.input;
5152 let Some(slot) = CACHED_PATTERNS.iter().position(|cached| *cached == pattern) else {
5153 return DirectLookups { input }.find(pattern, from);
5154 };
5155 self.patterns[slot].first_at_or_after(from, || pattern_starts(input, pattern))
5156 }
5157
5158 fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize> {
5159 let input = self.input;
5160 let runs = self
5161 .backtick_runs
5162 .get_or_insert_with(|| backtick_runs_by_length(input));
5163 let starts = runs.get(&len)?;
5164 starts
5165 .get(starts.partition_point(|run| *run < start))
5166 .copied()
5167 }
5168
5169 fn angle_destination_stop(&mut self, from: usize) -> Option<usize> {
5170 let input = self.input;
5171 self.angle_destination_stops.first_at_or_after(from, || {
5172 (0..input.len())
5173 .filter(|index| is_angle_destination_stop(input, *index))
5174 .collect()
5175 })
5176 }
5177}
5178
5179fn backtick_runs_by_length(input: &str) -> BTreeMap<usize, Vec<usize>> {
5181 let mut runs: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
5182 let mut index = 0;
5183 while index < input.len() {
5184 if input.as_bytes()[index] == b'`' {
5185 let len = backtick_run_len(input, index);
5186 runs.entry(len).or_default().push(index);
5187 index += len;
5188 } else {
5189 index += 1;
5190 }
5191 }
5192 runs
5193}
5194
5195fn backtick_run_len(input: &str, index: usize) -> usize {
5196 input.as_bytes()[index..]
5197 .iter()
5198 .take_while(|byte| **byte == b'`')
5199 .count()
5200}
5201
5202fn is_angle_destination_stop(input: &str, index: usize) -> bool {
5203 match input.as_bytes()[index] {
5204 b'<' | b'>' => !is_escaped_at(input, index),
5205 b'\n' | b'\r' => true,
5206 _ => false,
5207 }
5208}
5209
5210struct InlineScan<'a> {
5213 input: &'a str,
5214 lookups: InlineLookups<'a>,
5215 label_ends: BracketMemo,
5216 reference_label_ends: PathMemo,
5217 wikilink_closes: PathMemo,
5218 directive_attribute_closes: PathMemo,
5219 mdx_expression_closes: BracketMemo,
5220 jsx: Option<JsxIndex>,
5221 single_tilde_delete_closes: Positions,
5222 literal_autolinks: LiteralAutolinkScan,
5223}
5224
5225impl<'a> InlineScan<'a> {
5226 fn new(input: &'a str) -> Self {
5227 Self {
5228 input,
5229 lookups: InlineLookups::new(input),
5230 label_ends: BracketMemo::default(),
5231 reference_label_ends: PathMemo::default(),
5232 wikilink_closes: PathMemo::default(),
5233 directive_attribute_closes: PathMemo::default(),
5234 mdx_expression_closes: BracketMemo::default(),
5235 jsx: None,
5236 single_tilde_delete_closes: Positions::default(),
5237 literal_autolinks: LiteralAutolinkScan::default(),
5238 }
5239 }
5240
5241 fn link_label_end(&mut self, open: usize) -> Option<usize> {
5243 let input = self.input;
5244 if input.as_bytes().get(open) != Some(&b'[') {
5245 return None;
5246 }
5247 let lookups = &mut self.lookups;
5248 self.label_ends
5249 .resolve(input.len() + 1, open + 1, |cursor| {
5250 link_label_step(lookups, input, cursor)
5251 })
5252 }
5253
5254 fn reference_label_end(&mut self, open: usize) -> Option<usize> {
5256 let input = self.input;
5257 if input.as_bytes().get(open) != Some(&b'[') {
5258 return None;
5259 }
5260 let close = self
5261 .reference_label_ends
5262 .resolve(input.len() + 1, open + 1, |cursor| {
5263 reference_label_step(input, cursor)
5264 })?;
5265 reference_label_is_within_limit(&input[open + 1..close]).then_some(close)
5266 }
5267
5268 fn wikilink_close(&mut self, start: usize) -> Option<usize> {
5271 let input = self.input;
5272 self.wikilink_closes
5273 .resolve(input.len() + 1, start, |cursor| {
5274 wikilink_close_step(input, cursor)
5275 })
5276 }
5277
5278 fn mdx_expression_close(&mut self, open: usize) -> Option<usize> {
5280 let input = self.input;
5281 if input.as_bytes().get(open) != Some(&b'{') {
5282 return None;
5283 }
5284 self.mdx_expression_closes
5285 .resolve(
5286 (input.len() + 1) * MDX_BRACE_STATES,
5287 (open + 1) * MDX_BRACE_STATES,
5288 |node| mdx_expression_step(input, node, false),
5289 )
5290 .map(|node| node / MDX_BRACE_STATES)
5291 }
5292
5293 fn mdx_jsx_end(&mut self, start: usize) -> Option<usize> {
5295 let input = self.input;
5296 let expressions = &mut self.mdx_expression_closes;
5297 self.jsx
5298 .get_or_insert_with(|| JsxIndex::new(input, false, expressions))
5299 .element_end(start)
5300 .map(|end| end + 1)
5301 }
5302
5303 fn single_tilde_delete_close(&mut self, start: usize) -> Option<usize> {
5305 let input = self.input;
5306 self.single_tilde_delete_closes
5307 .first_at_or_after(start, || {
5308 (0..input.len())
5309 .filter(|index| is_single_tilde_delete_close(input, *index))
5310 .collect()
5311 })
5312 }
5313
5314 fn directive_attributes_close(&mut self, open: usize) -> Option<usize> {
5316 let input = self.input;
5317 if input.as_bytes().get(open) != Some(&b'{') {
5318 return None;
5319 }
5320 self.directive_attribute_closes.resolve(
5321 (input.len() + 1) * DIRECTIVE_QUOTE_STATES,
5322 (open + 1) * DIRECTIVE_QUOTE_STATES,
5323 |node| directive_attributes_step(input, node),
5324 )
5325 }
5326}
5327
5328fn parse_inlines(
5329 input: &str,
5330 base_offset: usize,
5331 options: &SyntaxOptions,
5332 definitions: &[String],
5333 diagnostics: &mut Vec<Diagnostic>,
5334) -> Vec<Inline> {
5335 parse_inlines_with_context(
5336 input,
5337 base_offset,
5338 options,
5339 definitions,
5340 diagnostics,
5341 &mut InlineState::default(),
5342 )
5343}
5344
5345const MAX_INLINE_NESTING: usize = 32;
5350
5351#[derive(Default)]
5355struct InlineState {
5356 depth: usize,
5358}
5359
5360struct InlinePass<'p, 'a> {
5363 state: &'p mut InlineState,
5364 scan: InlineScan<'a>,
5365}
5366
5367fn parse_inlines_with_context(
5368 input: &str,
5369 base_offset: usize,
5370 options: &SyntaxOptions,
5371 definitions: &[String],
5372 diagnostics: &mut Vec<Diagnostic>,
5373 state: &mut InlineState,
5374) -> Vec<Inline> {
5375 if state.depth >= MAX_INLINE_NESTING {
5376 if input.is_empty() {
5377 return Vec::new();
5378 }
5379 return vec![Inline::Text(Text {
5380 meta: NodeMeta::new(Some(Span::new(base_offset, base_offset + input.len()))),
5381 value: input.into(),
5382 })];
5383 }
5384 state.depth += 1;
5385 let nodes = parse_inline_content(input, base_offset, options, definitions, diagnostics, state);
5386 state.depth -= 1;
5387 nodes
5388}
5389
5390fn parse_inline_content(
5391 input: &str,
5392 base_offset: usize,
5393 options: &SyntaxOptions,
5394 definitions: &[String],
5395 diagnostics: &mut Vec<Diagnostic>,
5396 state: &mut InlineState,
5397) -> Vec<Inline> {
5398 let bytes = input.as_bytes();
5399 let mut nodes = Vec::new();
5400 let mut text_start = 0;
5401 let mut text = String::new();
5402 let mut index = 0;
5403 let mut delimiters: Vec<DelimMarker> = Vec::new();
5408 let mut lines = LineCounter::default();
5409 let mut brackets = Brackets::new();
5410 let mut superscript_open: Option<(usize, usize)> = None;
5412 let mut pass = InlinePass {
5413 state,
5414 scan: InlineScan::new(input),
5415 };
5416
5417 while index < bytes.len() {
5418 if bytes[index] == b'\\' {
5419 if let Some((next_index, char)) = next_char(input, index + 1) {
5420 if char.is_ascii_punctuation() {
5421 if options.parse.preserve_character_escapes {
5422 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5423 nodes.push(Inline::Escape(Escape {
5424 meta: NodeMeta::new(Some(Span::new(
5425 base_offset + index,
5426 base_offset + next_index,
5427 ))),
5428 value: char,
5429 }));
5430 index = next_index;
5431 text_start = index;
5432 continue;
5433 }
5434 if text.is_empty() {
5435 text_start = base_offset + index;
5436 }
5437 text.push(char);
5438 index = next_index;
5439 continue;
5440 }
5441 }
5442 }
5443
5444 if bytes[index] == b'&' {
5445 if let Some((end, value)) = parse_character_reference(input, index) {
5446 if options.parse.preserve_character_references {
5447 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5448 nodes.push(Inline::CharacterReference(CharacterReference {
5449 meta: NodeMeta::new(Some(Span::new(
5450 base_offset + index,
5451 base_offset + end,
5452 ))),
5453 reference: input[index..end].into(),
5454 value,
5455 }));
5456 index = end;
5457 text_start = index;
5458 continue;
5459 }
5460 if text.is_empty() {
5461 text_start = base_offset + index;
5462 }
5463 text.push_str(&value);
5464 index = end;
5465 continue;
5466 }
5467 }
5468
5469 if bytes[index] == b'\n' {
5470 if text.ends_with('\\') {
5471 text.pop();
5472 flush_text(
5473 &mut nodes,
5474 &mut text,
5475 text_start,
5476 base_offset + index.saturating_sub(1),
5477 );
5478 nodes.push(Inline::LineBreak(LineBreak {
5479 meta: NodeMeta::new(Some(Span::new(
5480 base_offset + index.saturating_sub(1),
5481 base_offset + index + 1,
5482 ))),
5483 kind: LineBreakKind::Backslash,
5484 }));
5485 index += 1;
5486 text_start = index;
5487 continue;
5488 }
5489 let trailing_spaces = trailing_space_count(&text);
5490 if is_hard_break_suffix(&text, trailing_spaces) {
5491 text.truncate(text.len() - trailing_spaces);
5492 flush_text(
5493 &mut nodes,
5494 &mut text,
5495 text_start,
5496 base_offset + index.saturating_sub(trailing_spaces),
5497 );
5498 nodes.push(Inline::LineBreak(LineBreak {
5499 meta: NodeMeta::new(Some(Span::new(
5500 base_offset + index.saturating_sub(trailing_spaces),
5501 base_offset + index + 1,
5502 ))),
5503 kind: LineBreakKind::Spaces,
5504 }));
5505 index += 1;
5506 text_start = index;
5507 continue;
5508 }
5509 if trailing_spaces > 0 {
5510 text.truncate(text.len() - trailing_spaces);
5511 }
5512 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5513 nodes.push(Inline::SoftBreak(SoftBreak {
5514 meta: NodeMeta::new(Some(Span::new(
5515 base_offset + index,
5516 base_offset + index + 1,
5517 ))),
5518 }));
5519 index += 1;
5520 text_start = index;
5521 continue;
5522 }
5523
5524 if bytes[index] == b'`' {
5525 if let Some((end, code_span)) =
5526 parse_code_span_with(&mut pass.scan.lookups, input, index)
5527 {
5528 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5529 nodes.push(Inline::Code(CodeInline {
5530 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
5531 value: code_span.value,
5532 raw: code_span.raw,
5533 fence_length: code_span.fence_length,
5534 }));
5535 index = end;
5536 text_start = index;
5537 continue;
5538 } else {
5539 let run = bytes[index..]
5545 .iter()
5546 .take_while(|byte| **byte == b'`')
5547 .count();
5548 if text.is_empty() {
5549 text_start = base_offset + index;
5550 }
5551 for _ in 0..run {
5552 text.push('`');
5553 }
5554 index += run;
5555 continue;
5556 }
5557 }
5558
5559 if options.constructs.spoiler && bytes[index] == b'|' {
5560 let run_len = delimiter_byte_run_len(input, index, b'|');
5561 if run_len >= 2 {
5562 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5563 let roles = DelimRoles {
5566 can_open: true,
5567 can_close: index == 0 || bytes[index - 1] != b'|',
5568 ..DelimRoles::default()
5569 };
5570 push_delimiter(
5571 &mut nodes,
5572 &mut delimiters,
5573 &mut lines,
5574 input,
5575 index,
5576 base_offset,
5577 b'|',
5578 run_len,
5579 roles,
5580 );
5581 index += run_len;
5582 text_start = index;
5583 continue;
5584 }
5585 }
5586
5587 if bytes[index] == b'*' && delimiter_byte_run_start(input, index, b'*') == index {
5588 let run_len = delimiter_byte_run_len(input, index, b'*');
5589 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5590 let roles = DelimRoles::default();
5592 push_delimiter(
5593 &mut nodes,
5594 &mut delimiters,
5595 &mut lines,
5596 input,
5597 index,
5598 base_offset,
5599 b'*',
5600 run_len,
5601 roles,
5602 );
5603 index += run_len;
5604 text_start = index;
5605 continue;
5606 }
5607
5608 if bytes[index] == b'_' && delimiter_byte_run_start(input, index, b'_') == index {
5612 if options.constructs.gfm_autolink_literal || options.constructs.relaxed_autolinks {
5617 if let Some((end, destination)) = parse_literal_autolink(
5618 input,
5619 index,
5620 options.constructs.gfm_autolink_literal,
5621 options.constructs.relaxed_autolinks,
5622 &mut pass.scan.literal_autolinks,
5623 ) {
5624 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5625 nodes.push(Inline::Autolink(Autolink {
5626 meta: NodeMeta::new(Some(Span::new(
5627 base_offset + index,
5628 base_offset + end,
5629 ))),
5630 destination,
5631 kind: AutolinkKind::GfmLiteral {
5632 original: input[index..end].into(),
5633 },
5634 }));
5635 index = end;
5636 text_start = index;
5637 continue;
5638 }
5639 }
5640 let run_len = delimiter_byte_run_len(input, index, b'_');
5641 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5642 let roles = DelimRoles::default();
5644 push_delimiter(
5645 &mut nodes,
5646 &mut delimiters,
5647 &mut lines,
5648 input,
5649 index,
5650 base_offset,
5651 b'_',
5652 run_len,
5653 roles,
5654 );
5655 index += run_len;
5656 text_start = index;
5657 continue;
5658 }
5659
5660 if (options.constructs.insert && bytes[index] == b'+')
5661 || (options.constructs.highlight && bytes[index] == b'=')
5662 {
5663 let marker = bytes[index];
5664 let run_len = delimiter_byte_run_len(input, index, marker);
5665 if run_len >= 2 {
5666 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5667 let roles = double_mark_roles(input, index, run_len, marker);
5668 push_delimiter(
5669 &mut nodes,
5670 &mut delimiters,
5671 &mut lines,
5672 input,
5673 index,
5674 base_offset,
5675 marker,
5676 run_len,
5677 roles,
5678 );
5679 index += run_len;
5680 text_start = index;
5681 continue;
5682 }
5683 }
5684
5685 let bracket_room = pass.state.depth - 1 + brackets.openers.len() < MAX_INLINE_NESTING;
5686
5687 let closes_superscript = options.constructs.superscript
5690 && bytes[index] == b'^'
5691 && superscript_open.is_some_and(|(run, position)| {
5692 delimiters.get(run).is_some_and(|run| {
5694 run.position == position && run.line == lines.line_at(input, index)
5695 }) && position + 1 != index
5696 });
5697
5698 if options.constructs.inline_footnote
5701 && options.constructs.footnote_reference
5702 && bracket_room
5703 && !closes_superscript
5704 && bytes[index] == b'^'
5705 && bytes.get(index + 1) == Some(&b'[')
5706 {
5707 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5708 let caret = if options.constructs.superscript {
5709 push_delimiter(
5710 &mut nodes,
5711 &mut delimiters,
5712 &mut lines,
5713 input,
5714 index,
5715 base_offset,
5716 b'^',
5717 1,
5718 DelimRoles::default(),
5719 );
5720 Some(delimiters.len() - 1)
5721 } else {
5722 push_text(&mut nodes, base_offset + index, "^");
5723 None
5724 };
5725 push_text(&mut nodes, base_offset + index + 1, "[");
5726 brackets.openers.push(BracketOpener {
5727 kind: BracketKind::InlineFootnote,
5728 node_index: nodes.len() - 1,
5729 position: index,
5730 delimiter_bottom: delimiters.len(),
5731 caret,
5732 wikilink: None,
5733 });
5734 index += 2;
5735 text_start = index;
5736 continue;
5737 }
5738
5739 if options.constructs.superscript && bytes[index] == b'^' {
5742 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5743 let roles = DelimRoles {
5744 single_open: !(options.constructs.inline_footnote
5745 && bytes.get(index + 1) == Some(&b'[')),
5746 single_close: true,
5747 ..DelimRoles::default()
5748 };
5749 push_delimiter(
5750 &mut nodes,
5751 &mut delimiters,
5752 &mut lines,
5753 input,
5754 index,
5755 base_offset,
5756 b'^',
5757 1,
5758 roles,
5759 );
5760 superscript_open = if !closes_superscript && roles.single_open {
5763 Some((delimiters.len() - 1, index))
5764 } else {
5765 None
5766 };
5767 index += 1;
5768 text_start = index;
5769 continue;
5770 }
5771
5772 if (options.constructs.gfm_strikethrough || options.constructs.subscript)
5777 && bytes[index] == b'~'
5778 {
5779 let run_len = delimiter_byte_run_len(input, index, b'~');
5780 let mut roles = DelimRoles::default();
5781 if options.constructs.gfm_strikethrough
5782 && delimiter_byte_run_start(input, index, b'~') == index
5783 && (run_len == 2 || (run_len == 1 && options.parse.single_tilde_strikethrough))
5784 {
5785 roles.strike = true;
5786 }
5787 if options.constructs.subscript {
5788 roles.single_open = starts_exact_byte_run(input, index, b'~', 1)
5789 && !single_tilde_delete_takes_precedence(options, input, index, &mut pass.scan);
5790 roles.single_close = true;
5791 }
5792 if roles.strike || roles.single_open || roles.single_close {
5793 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5794 push_delimiter(
5795 &mut nodes,
5796 &mut delimiters,
5797 &mut lines,
5798 input,
5799 index,
5800 base_offset,
5801 b'~',
5802 run_len,
5803 roles,
5804 );
5805 index += run_len;
5806 text_start = index;
5807 continue;
5808 }
5809 }
5810
5811 if bytes[index] == b'!' && bytes.get(index + 1) == Some(&b'[') && bracket_room {
5812 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5813 let wikilink = parse_wikilink(input, index + 1, base_offset, options, &mut pass.scan);
5814 if let Some((end, wikilink)) = wikilink.clone() {
5815 if pass.scan.link_label_end(index + 1).is_none() {
5818 push_text(&mut nodes, base_offset + index, "!");
5819 nodes.push(wikilink);
5820 index = end;
5821 text_start = index;
5822 continue;
5823 }
5824 }
5825 push_text(&mut nodes, base_offset + index, "!");
5826 push_text(&mut nodes, base_offset + index + 1, "[");
5827 brackets.openers.push(BracketOpener {
5828 kind: BracketKind::Image,
5829 node_index: nodes.len() - 1,
5830 position: index,
5831 delimiter_bottom: delimiters.len(),
5832 caret: None,
5833 wikilink,
5834 });
5835 index += 2;
5836 text_start = index;
5837 continue;
5838 }
5839
5840 if bytes[index] == b'[' {
5841 if let Some((end, wikilink)) =
5842 parse_wikilink(input, index, base_offset, options, &mut pass.scan)
5843 {
5844 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5845 nodes.push(wikilink);
5846 index = end;
5847 text_start = index;
5848 continue;
5849 }
5850 if bracket_room {
5851 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5852 push_text(&mut nodes, base_offset + index, "[");
5853 brackets.openers.push(BracketOpener {
5854 kind: BracketKind::Link,
5855 node_index: nodes.len() - 1,
5856 position: index,
5857 delimiter_bottom: delimiters.len(),
5858 caret: None,
5859 wikilink: None,
5860 });
5861 index += 1;
5862 text_start = index;
5863 continue;
5864 }
5865 brackets.overflow += 1;
5866 }
5867
5868 if bytes[index] == b']' && brackets.overflow > 0 {
5869 brackets.overflow -= 1;
5871 } else if bytes[index] == b']' && !brackets.openers.is_empty() {
5872 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5873 if let Some(end) = close_bracket(
5874 input,
5875 base_offset,
5876 index,
5877 options,
5878 definitions,
5879 &mut nodes,
5880 &mut delimiters,
5881 &mut brackets,
5882 &mut pass,
5883 ) {
5884 index = end;
5885 text_start = index;
5886 continue;
5887 }
5888 }
5889
5890 if bytes[index] == b'$' && options.constructs.math_inline {
5891 if let Some((end, value, kind)) =
5892 parse_math_inline(&mut pass.scan.lookups, input, index)
5893 {
5894 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5895 nodes.push(Inline::Math(MathInline {
5896 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
5897 value,
5898 kind,
5899 }));
5900 index = end;
5901 text_start = index;
5902 continue;
5903 }
5904 let run = bytes[index..]
5911 .iter()
5912 .take_while(|byte| **byte == b'$')
5913 .count();
5914 if run > 1 {
5915 if text.is_empty() {
5916 text_start = base_offset + index;
5917 }
5918 text.push_str(&input[index..index + run]);
5919 index += run;
5920 continue;
5921 }
5922 }
5923
5924 if options.constructs.gfm_autolink_literal || options.constructs.relaxed_autolinks {
5927 if let Some((end, destination)) = parse_literal_autolink(
5928 input,
5929 index,
5930 options.constructs.gfm_autolink_literal,
5931 options.constructs.relaxed_autolinks,
5932 &mut pass.scan.literal_autolinks,
5933 ) {
5934 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5935 nodes.push(Inline::Autolink(Autolink {
5936 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
5937 destination,
5938 kind: AutolinkKind::GfmLiteral {
5939 original: input[index..end].into(),
5940 },
5941 }));
5942 index = end;
5943 text_start = index;
5944 continue;
5945 }
5946 }
5947
5948 if bytes[index] == b'<' {
5949 if let Some(end) = pass.scan.lookups.find(">", index).map(|close| close + 1) {
5950 let raw = &input[index..end];
5951 if is_autolink(raw) {
5952 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5953 nodes.push(Inline::Autolink(Autolink {
5954 meta: NodeMeta::new(Some(Span::new(
5955 base_offset + index,
5956 base_offset + end,
5957 ))),
5958 destination: raw[1..raw.len() - 1].into(),
5959 kind: AutolinkKind::Angle,
5960 }));
5961 index = end;
5962 text_start = index;
5963 continue;
5964 }
5965 }
5966 if options.constructs.mdx_jsx_inline {
5967 if let Some((end, raw)) = pass
5968 .scan
5969 .mdx_jsx_end(index)
5970 .map(|end| (end, String::from(&input[index..end])))
5971 {
5972 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5973 nodes.push(Inline::MdxJsx(MdxJsxInline {
5974 meta: NodeMeta::new(Some(Span::new(
5975 base_offset + index,
5976 base_offset + end,
5977 ))),
5978 value: raw,
5979 }));
5980 index = end;
5981 text_start = index;
5982 continue;
5983 }
5984 }
5985 if options.constructs.html_inline {
5986 if let Some(end) = html_inline_end(&mut pass.scan.lookups, input, index) {
5987 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
5988 nodes.push(Inline::Html(HtmlInline {
5989 meta: NodeMeta::new(Some(Span::new(
5990 base_offset + index,
5991 base_offset + end,
5992 ))),
5993 value: input[index..end].into(),
5994 }));
5995 index = end;
5996 text_start = index;
5997 continue;
5998 }
5999 }
6000 }
6001
6002 if bytes[index] == b'{' && options.constructs.mdx_expression_inline {
6003 if let Some(end) = pass.scan.mdx_expression_close(index) {
6004 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
6005 nodes.push(Inline::MdxExpression(MdxExpressionInline {
6006 meta: NodeMeta::new(Some(Span::new(
6007 base_offset + index,
6008 base_offset + end + 1,
6009 ))),
6010 value: input[index + 1..end].into(),
6011 }));
6012 index = end + 1;
6013 text_start = index;
6014 continue;
6015 } else {
6016 diagnostics.push(Diagnostic::new(
6017 DiagnosticSeverity::Error,
6018 DiagnosticCode::InvalidMdx,
6019 Span::new(base_offset + index, base_offset + input.len()),
6020 "MDX expression is missing a closing brace",
6021 ));
6022 }
6023 }
6024
6025 if bytes[index] == b':' && options.constructs.shortcode {
6026 if let Some((end, name)) = parse_shortcode(input, index) {
6027 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
6028 nodes.push(Inline::Shortcode(Shortcode {
6029 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
6030 name,
6031 }));
6032 index = end;
6033 text_start = index;
6034 continue;
6035 }
6036 }
6037
6038 if bytes[index] == b':'
6041 && options.constructs.directive_text
6042 && pass.state.depth + brackets.openers.len() <= MAX_INLINE_NESTING
6043 {
6044 pass.state.depth += brackets.openers.len();
6047 let directive = parse_text_directive(
6048 input,
6049 index,
6050 base_offset,
6051 options,
6052 definitions,
6053 diagnostics,
6054 &mut pass,
6055 );
6056 pass.state.depth -= brackets.openers.len();
6057 if let Some((end, directive)) = directive {
6058 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
6059 if let Inline::TextDirective(node) = &directive {
6060 if contains_link_inline(&node.label) {
6063 brackets.close_links();
6064 }
6065 let depth = inline_nesting(&node.label) + 1;
6066 brackets.formed.push((nodes.len(), depth));
6067 }
6068 nodes.push(directive);
6069 index = end;
6070 text_start = index;
6071 continue;
6072 }
6073 }
6074
6075 let (next_index, char) = next_char(input, index).expect("valid UTF-8 byte index");
6076 if text.is_empty() {
6077 text_start = base_offset + index;
6078 }
6079 text.push(char);
6080 index = next_index;
6081 }
6082
6083 flush_text(&mut nodes, &mut text, text_start, base_offset + input.len());
6084 for opener in &brackets.openers {
6087 if let Some(caret) = opener.caret {
6088 delimiters[caret].single_close = true;
6089 }
6090 }
6091 assign_emphasis_roles(
6092 input,
6093 &mut delimiters,
6094 options.constructs.gfm_strikethrough,
6095 options.constructs.underline,
6096 );
6097 process_emphasis(
6098 nodes,
6099 delimiters,
6100 EmphasisContext {
6101 underline: options.constructs.underline,
6102 depth: pass.state.depth - 1,
6103 },
6104 &brackets.formed,
6105 )
6106 .0
6107}
6108
6109fn parse_shortcode(input: &str, index: usize) -> Option<(usize, String)> {
6110 if input[index..].starts_with("::") {
6111 return None;
6112 }
6113
6114 let mut cursor = index + 1;
6115 while let Some((next, char)) = next_char(input, cursor) {
6116 if char == ':' {
6117 if cursor == index + 1 {
6118 return None;
6119 }
6120 return Some((next, input[index + 1..cursor].into()));
6121 }
6122 if !(char.is_ascii_alphanumeric() || matches!(char, '_' | '-' | '+')) {
6123 return None;
6124 }
6125 cursor = next;
6126 }
6127 None
6128}
6129
6130fn parse_wikilink(
6131 input: &str,
6132 index: usize,
6133 base_offset: usize,
6134 options: &SyntaxOptions,
6135 scan: &mut InlineScan,
6136) -> Option<(usize, Inline)> {
6137 let configured_order = if options.constructs.wikilink_title_after_pipe {
6138 WikiLinkLabelOrder::AfterPipe
6139 } else if options.constructs.wikilink_title_before_pipe {
6140 WikiLinkLabelOrder::BeforePipe
6141 } else {
6142 return None;
6143 };
6144 if input.as_bytes().get(index) != Some(&b'[') || input.as_bytes().get(index + 1) != Some(&b'[')
6145 {
6146 return None;
6147 }
6148
6149 let close = scan.wikilink_close(index + 2)?;
6150 let source = &input[index + 2..close];
6151 if source.is_empty() || source.len() > WIKILINK_MAX_BYTES {
6152 return None;
6153 }
6154
6155 let (target_source, label_source, label_order) =
6156 if let Some(separator) = find_wikilink_separator(source) {
6157 match configured_order {
6158 WikiLinkLabelOrder::AfterPipe => (
6159 &source[..separator],
6160 &source[separator + 1..],
6161 WikiLinkLabelOrder::AfterPipe,
6162 ),
6163 WikiLinkLabelOrder::BeforePipe => (
6164 &source[separator + 1..],
6165 &source[..separator],
6166 WikiLinkLabelOrder::BeforePipe,
6167 ),
6168 }
6169 } else {
6170 (source, source, configured_order)
6171 };
6172
6173 let target = unescape_string(target_source);
6174 if target.is_empty() {
6175 return None;
6176 }
6177 let label = unescape_string(label_source);
6178 let end = close + 2;
6179 Some((
6180 end,
6181 Inline::WikiLink(WikiLink {
6182 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
6183 target,
6184 label,
6185 label_order,
6186 }),
6187 ))
6188}
6189
6190fn wikilink_close_step(input: &str, cursor: usize) -> Step {
6193 let bytes = input.as_bytes();
6194 match bytes.get(cursor) {
6195 None | Some(b'\n' | b'\r') => Step::Done(None),
6196 Some(b'\\') => {
6197 let escaped = cursor + 1;
6198 Step::Next(next_char(input, escaped).map_or(escaped, |(after_escape, _)| after_escape))
6199 }
6200 Some(b']') if bytes.get(cursor + 1) == Some(&b']') => Step::Done(Some(cursor)),
6201 Some(_) => Step::Next(next_char(input, cursor).map_or(input.len(), |(next, _)| next)),
6202 }
6203}
6204
6205fn find_wikilink_separator(input: &str) -> Option<usize> {
6206 let bytes = input.as_bytes();
6207 let mut cursor = 0;
6208 while cursor < input.len() {
6209 match bytes[cursor] {
6210 b'\\' => {
6211 cursor += 1;
6212 if cursor < input.len() {
6213 cursor = next_char(input, cursor)?.0;
6214 }
6215 }
6216 b'|' => return Some(cursor),
6217 _ => cursor = next_char(input, cursor)?.0,
6218 }
6219 }
6220 None
6221}
6222
6223fn trailing_space_count(input: &str) -> usize {
6224 input
6225 .as_bytes()
6226 .iter()
6227 .rev()
6228 .take_while(|byte| matches!(**byte, b' ' | b'\t'))
6229 .count()
6230}
6231
6232fn is_hard_break_suffix(input: &str, trailing: usize) -> bool {
6233 let bytes = input.as_bytes();
6237 trailing >= 2
6238 && bytes[bytes.len() - trailing..]
6239 .iter()
6240 .all(|byte| *byte == b' ')
6241}
6242
6243fn find_reference_label_end(input: &str, open: usize) -> Option<usize> {
6244 if input.as_bytes().get(open) != Some(&b'[') {
6247 return None;
6248 }
6249 let close = path_walk(open + 1, |cursor| reference_label_step(input, cursor))?;
6250 reference_label_is_within_limit(&input[open + 1..close]).then_some(close)
6251}
6252
6253fn reference_label_step(input: &str, cursor: usize) -> Step {
6255 let Some((next, char)) = next_char(input, cursor) else {
6256 return Step::Done(None);
6257 };
6258 match char {
6259 '\\' => Step::Next(next_char(input, next).map_or(next, |(after_escape, _)| after_escape)),
6260 '[' => Step::Done(None),
6261 ']' => Step::Done(Some(cursor)),
6262 _ => Step::Next(next),
6263 }
6264}
6265
6266fn inline_nesting(inlines: &[Inline]) -> usize {
6268 let mut deepest = 0;
6269 let mut pending = vec![(inlines, 0)];
6270 while let Some((nodes, depth)) = pending.pop() {
6271 for node in nodes {
6272 if is_inline_container(node) {
6273 deepest = deepest.max(depth + 1);
6274 pending.push((node.children(), depth + 1));
6275 }
6276 }
6277 }
6278 deepest
6279}
6280
6281fn is_inline_container(inline: &Inline) -> bool {
6282 matches!(
6283 inline,
6284 Inline::Emphasis(_)
6285 | Inline::Strong(_)
6286 | Inline::Underline(_)
6287 | Inline::Delete(_)
6288 | Inline::Insert(_)
6289 | Inline::Mark(_)
6290 | Inline::Subscript(_)
6291 | Inline::Superscript(_)
6292 | Inline::Spoiler(_)
6293 | Inline::Link(_)
6294 | Inline::Image(_)
6295 | Inline::LinkReference(_)
6296 | Inline::ImageReference(_)
6297 | Inline::InlineFootnote(_)
6298 | Inline::TextDirective(_)
6299 )
6300}
6301
6302fn contains_link_inline(inlines: &[Inline]) -> bool {
6303 inlines.iter().any(|inline| {
6304 matches!(inline, Inline::Link(_) | Inline::LinkReference(_))
6305 || contains_link_inline(inline.children())
6306 })
6307}
6308
6309fn find_link_label_end(input: &str, open: usize) -> Option<usize> {
6310 if input.as_bytes().get(open) != Some(&b'[') {
6311 return None;
6312 }
6313 let mut lookups = DirectLookups { input };
6314 bracket_walk(open + 1, |cursor| {
6315 link_label_step(&mut lookups, input, cursor)
6316 })
6317}
6318
6319fn link_label_step(lookups: &mut impl Lookups, input: &str, cursor: usize) -> BracketStep {
6323 let Some((next, char)) = next_char(input, cursor) else {
6324 return BracketStep::End;
6325 };
6326 match char {
6327 '\\' => {
6328 BracketStep::Pass(next_char(input, next).map_or(next, |(after_escape, _)| after_escape))
6329 }
6330 '`' => BracketStep::Pass(code_span_end(lookups, input, cursor).unwrap_or(next)),
6331 '<' => BracketStep::Pass(
6332 autolink_end(lookups, input, cursor)
6333 .or_else(|| html_inline_end(lookups, input, cursor))
6334 .unwrap_or(next),
6335 ),
6336 '[' => BracketStep::Open(next),
6337 ']' => BracketStep::Close(next),
6338 _ => BracketStep::Pass(next),
6339 }
6340}
6341
6342fn parse_text_directive(
6343 input: &str,
6344 index: usize,
6345 base_offset: usize,
6346 options: &SyntaxOptions,
6347 definitions: &[String],
6348 diagnostics: &mut Vec<Diagnostic>,
6349 pass: &mut InlinePass,
6350) -> Option<(usize, Inline)> {
6351 if input[index..].starts_with("::") {
6352 return None;
6353 }
6354 if index > 0 {
6355 let previous = input[..index].chars().next_back()?;
6356 if !previous.is_whitespace() && !matches!(previous, '(' | '[' | '{') {
6357 return None;
6358 }
6359 }
6360 let opener_source = &input[index + 1..];
6361 let opener_offset = index + 1;
6362 let opener = parse_directive_opener_with(opener_source, |close, open| {
6363 let found = match close {
6364 DirectiveClose::Label => pass.scan.link_label_end(opener_offset + open),
6365 DirectiveClose::Attributes => {
6366 pass.scan.directive_attributes_close(opener_offset + open)
6367 }
6368 };
6369 found.map(|position| position - opener_offset)
6370 });
6371 let (name, label_source, attributes, consumed) = match opener {
6372 Some(opener) => opener,
6373 None => {
6374 if directive_opener_looks_malformed(opener_source) {
6375 diagnostics.push(Diagnostic::new(
6376 DiagnosticSeverity::Error,
6377 DiagnosticCode::InvalidDirectiveName,
6378 Span::new(base_offset + index, base_offset + input.len()),
6379 "text directive opener is malformed",
6380 ));
6381 }
6382 return None;
6383 }
6384 };
6385 let label = label_source
6386 .map(|source| {
6387 parse_inlines_with_context(
6388 source,
6389 base_offset + index + 1 + name.len() + 1,
6390 options,
6391 definitions,
6392 diagnostics,
6393 pass.state,
6394 )
6395 })
6396 .unwrap_or_default();
6397 Some((
6398 index + 1 + consumed,
6399 Inline::TextDirective(TextDirective {
6400 meta: NodeMeta::new(Some(Span::new(
6401 base_offset + index,
6402 base_offset + index + 1 + consumed,
6403 ))),
6404 name,
6405 label,
6406 attributes,
6407 }),
6408 ))
6409}
6410
6411fn parse_directive_opener(
6412 input: &str,
6413) -> Option<(String, Option<&str>, Vec<DirectiveAttribute>, usize)> {
6414 parse_directive_opener_with(input, |close, open| match close {
6415 DirectiveClose::Label => find_link_label_end(input, open),
6416 DirectiveClose::Attributes => find_directive_attributes_close(input, open),
6417 })
6418}
6419
6420#[derive(Clone, Copy)]
6422enum DirectiveClose {
6423 Label,
6425 Attributes,
6427}
6428
6429fn parse_directive_opener_with(
6430 input: &str,
6431 mut find_close: impl FnMut(DirectiveClose, usize) -> Option<usize>,
6432) -> Option<(String, Option<&str>, Vec<DirectiveAttribute>, usize)> {
6433 let mut index = 0;
6434 while let Some((next, char)) = next_char(input, index) {
6435 if char.is_ascii_alphanumeric() || char == '_' || char == '-' {
6436 index = next;
6437 } else {
6438 break;
6439 }
6440 }
6441 let name = &input[..index];
6442 if !is_directive_name(name) {
6443 return None;
6444 }
6445
6446 let mut label = None;
6447 let mut attributes = Vec::new();
6448 let mut consumed = index;
6449 if input.as_bytes().get(consumed) == Some(&b'[') {
6450 let close = find_close(DirectiveClose::Label, consumed)?;
6451 label = Some(&input[consumed + 1..close]);
6452 consumed = close + 1;
6453 }
6454 if input.as_bytes().get(consumed) == Some(&b'{') {
6455 let close = find_close(DirectiveClose::Attributes, consumed)?;
6456 attributes = parse_attributes(&input[consumed + 1..close]);
6457 consumed = close + 1;
6458 }
6459
6460 Some((name.into(), label, attributes, consumed))
6461}
6462
6463fn directive_opener_looks_malformed(input: &str) -> bool {
6464 let mut index = 0;
6465 while let Some((next, char)) = next_char(input, index) {
6466 if char.is_ascii_alphanumeric() || char == '_' || char == '-' {
6467 index = next;
6468 } else {
6469 break;
6470 }
6471 }
6472 index > 0
6473 && is_directive_name(&input[..index])
6474 && matches!(input.as_bytes().get(index), Some(b'[' | b'{'))
6475}
6476
6477const DIRECTIVE_QUOTE_STATES: usize = 3;
6480
6481fn find_directive_attributes_close(input: &str, open: usize) -> Option<usize> {
6482 if input.as_bytes().get(open) != Some(&b'{') {
6483 return None;
6484 }
6485 path_walk((open + 1) * DIRECTIVE_QUOTE_STATES, |node| {
6486 directive_attributes_step(input, node)
6487 })
6488}
6489
6490fn directive_attributes_step(input: &str, node: usize) -> Step {
6494 let bytes = input.as_bytes();
6495 let (cursor, quote) = (node / DIRECTIVE_QUOTE_STATES, node % DIRECTIVE_QUOTE_STATES);
6496 let at = |position: usize, quote: usize| Step::Next(position * DIRECTIVE_QUOTE_STATES + quote);
6497 let Some(&byte) = bytes.get(cursor) else {
6498 return Step::Done(None);
6499 };
6500 if byte == b'\\' {
6501 return at((cursor + 2).min(bytes.len()), quote);
6502 }
6503 match (quote, byte) {
6504 (0, b'"') => at(cursor + 1, 1),
6505 (0, b'\'') => at(cursor + 1, 2),
6506 (0, b'}') => Step::Done(Some(cursor)),
6507 (1, b'"') | (2, b'\'') => at(cursor + 1, 0),
6508 _ => at(cursor + 1, quote),
6509 }
6510}
6511
6512fn parse_attributes(input: &str) -> Vec<DirectiveAttribute> {
6513 let mut attributes = Vec::new();
6514 let mut cursor = 0;
6515 while cursor < input.len() {
6516 cursor = skip_spaces(input, cursor);
6517 if cursor >= input.len() {
6518 break;
6519 }
6520
6521 if input.as_bytes().get(cursor) == Some(&b'#') {
6522 let (id, next) = parse_attribute_token(input, cursor + 1);
6523 if !id.is_empty() {
6524 attributes.push(DirectiveAttribute {
6525 name: "id".into(),
6526 value: Some(id.into()),
6527 });
6528 }
6529 cursor = next;
6530 continue;
6531 }
6532
6533 if input.as_bytes().get(cursor) == Some(&b'.') {
6534 let (class, next) = parse_attribute_token(input, cursor + 1);
6535 if !class.is_empty() {
6536 attributes.push(DirectiveAttribute {
6537 name: "class".into(),
6538 value: Some(class.into()),
6539 });
6540 }
6541 cursor = next;
6542 continue;
6543 }
6544
6545 let (name, next) = parse_attribute_name(input, cursor);
6546 if name.is_empty() {
6547 break;
6548 }
6549 cursor = skip_spaces(input, next);
6550 if input.as_bytes().get(cursor) == Some(&b'=') {
6551 cursor = skip_spaces(input, cursor + 1);
6552 if let Some((value, next)) = parse_attribute_value(input, cursor) {
6553 attributes.push(DirectiveAttribute {
6554 name: name.into(),
6555 value: Some(value),
6556 });
6557 cursor = next;
6558 } else {
6559 attributes.push(DirectiveAttribute {
6560 name: name.into(),
6561 value: Some(String::new()),
6562 });
6563 }
6564 } else {
6565 attributes.push(DirectiveAttribute {
6566 name: name.into(),
6567 value: None,
6568 });
6569 }
6570 }
6571 attributes
6572}
6573
6574fn parse_attribute_token(input: &str, index: usize) -> (&str, usize) {
6575 let mut cursor = index;
6576 while let Some((next, char)) = next_char(input, cursor) {
6577 if char.is_whitespace() {
6578 break;
6579 }
6580 cursor = next;
6581 }
6582 (&input[index..cursor], cursor)
6583}
6584
6585fn parse_attribute_name(input: &str, index: usize) -> (&str, usize) {
6586 let mut cursor = index;
6587 while let Some((next, char)) = next_char(input, cursor) {
6588 if char.is_whitespace() || char == '=' {
6589 break;
6590 }
6591 cursor = next;
6592 }
6593 (&input[index..cursor], cursor)
6594}
6595
6596fn parse_attribute_value(input: &str, index: usize) -> Option<(String, usize)> {
6597 let quote = input.as_bytes().get(index).copied();
6598 if matches!(quote, Some(b'"' | b'\'')) {
6599 let quote = quote?;
6600 let mut cursor = index + 1;
6601 while cursor < input.len() {
6602 let (next, char) = next_char(input, cursor)?;
6603 if char as u8 == quote && !is_escaped_at(input, cursor) {
6604 return Some((unescape_ascii_punctuation(&input[index + 1..cursor]), next));
6605 }
6606 cursor = next;
6607 }
6608 return None;
6609 }
6610
6611 let (value, next) = parse_attribute_token(input, index);
6612 Some((
6613 unescape_selected(value, |char| matches!(char, '\\' | '&')),
6614 next,
6615 ))
6616}
6617
6618struct CodeSpanSource {
6619 value: String,
6620 raw: String,
6621 fence_length: usize,
6622}
6623
6624fn parse_code_span_with(
6625 lookups: &mut impl Lookups,
6626 input: &str,
6627 index: usize,
6628) -> Option<(usize, CodeSpanSource)> {
6629 let len = backtick_run_len(input, index);
6630 let search_start = index + len;
6631 let close = lookups.code_span_close(search_start, len)?;
6632 let raw = &input[search_start..close];
6633 Some((
6634 close + len,
6635 CodeSpanSource {
6636 value: normalize_code_span(raw),
6637 raw: raw.into(),
6638 fence_length: len,
6639 },
6640 ))
6641}
6642
6643fn code_span_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
6645 let len = backtick_run_len(input, index);
6646 Some(lookups.code_span_close(index + len, len)? + len)
6647}
6648
6649fn find_code_span_close(input: &str, start: usize, marker_len: usize) -> Option<usize> {
6650 let bytes = input.as_bytes();
6651 let mut cursor = start;
6652 while cursor < bytes.len() {
6653 if bytes[cursor] != b'`' {
6654 cursor = next_char(input, cursor)
6655 .map(|(next, _)| next)
6656 .unwrap_or(bytes.len());
6657 continue;
6658 }
6659 let run_len = bytes[cursor..]
6660 .iter()
6661 .take_while(|byte| **byte == b'`')
6662 .count();
6663 if run_len == marker_len {
6664 return Some(cursor);
6665 }
6666 cursor += run_len;
6667 }
6668 None
6669}
6670
6671fn normalize_code_span(input: &str) -> String {
6672 let mut normalized = String::new();
6673 let mut cursor = 0;
6674 while cursor < input.len() {
6675 let (next, char) = next_char(input, cursor).expect("valid UTF-8 byte index");
6676 if char == '\r' {
6677 if input.as_bytes().get(next) == Some(&b'\n') {
6678 cursor = next + 1;
6679 } else {
6680 cursor = next;
6681 }
6682 normalized.push(' ');
6683 continue;
6684 }
6685 if char == '\n' {
6686 normalized.push(' ');
6687 cursor = next;
6688 continue;
6689 }
6690 normalized.push(char);
6691 cursor = next;
6692 }
6693
6694 if normalized.starts_with(' ')
6695 && normalized.ends_with(' ')
6696 && normalized.chars().any(|char| char != ' ')
6697 {
6698 normalized[1..normalized.len() - 1].into()
6699 } else {
6700 normalized
6701 }
6702}
6703
6704fn can_open_delimited(input: &str, index: usize, marker_len: usize) -> bool {
6705 delimiter_flanking(input, index, marker_len).left
6706}
6707
6708fn can_close_delimited(input: &str, index: usize, marker_len: usize) -> bool {
6709 delimiter_flanking(input, index, marker_len).right
6710}
6711
6712fn is_single_tilde_delete_close(input: &str, index: usize) -> bool {
6713 input.as_bytes()[index] == b'~'
6714 && !is_escaped_at(input, index)
6715 && single_tilde_can_close_delete(input, index)
6716}
6717
6718fn single_tilde_can_open_delete(input: &str, index: usize) -> bool {
6719 starts_exact_byte_run(input, index, b'~', 1)
6720 && can_open_delimited(input, index, 1)
6721 && !tilde_is_alphanumeric_interior(input, index)
6722}
6723
6724fn single_tilde_can_close_delete(input: &str, index: usize) -> bool {
6725 starts_exact_byte_run(input, index, b'~', 1)
6726 && can_close_delimited(input, index, 1)
6727 && !tilde_is_alphanumeric_interior(input, index)
6728}
6729
6730fn single_tilde_delete_takes_precedence(
6731 options: &SyntaxOptions,
6732 input: &str,
6733 index: usize,
6734 scan: &mut InlineScan,
6735) -> bool {
6736 options.constructs.gfm_strikethrough
6737 && options.parse.single_tilde_strikethrough
6738 && single_tilde_can_open_delete(input, index)
6739 && scan.single_tilde_delete_close(index + 1).is_some()
6740}
6741
6742fn tilde_is_alphanumeric_interior(input: &str, index: usize) -> bool {
6743 let previous = input[..index].chars().next_back();
6744 let next = input[index + 1..].chars().next();
6745 previous.is_some_and(|char| char.is_alphanumeric())
6746 && next.is_some_and(|char| char.is_alphanumeric())
6747}
6748
6749fn starts_exact_byte_run(input: &str, index: usize, marker: u8, len: usize) -> bool {
6750 input.as_bytes().get(index) == Some(&marker)
6751 && delimiter_byte_run_start(input, index, marker) == index
6752 && delimiter_byte_run_len(input, index, marker) == len
6753}
6754
6755fn delimiter_byte_run_start(input: &str, index: usize, marker: u8) -> usize {
6756 let bytes = input.as_bytes();
6757 let mut start = index;
6758 while start > 0 && bytes[start - 1] == marker && !is_escaped_at(input, start - 1) {
6759 start -= 1;
6760 }
6761 start
6762}
6763
6764fn delimiter_byte_run_len(input: &str, index: usize, marker: u8) -> usize {
6765 let bytes = input.as_bytes();
6766 let mut cursor = index;
6767 while bytes.get(cursor) == Some(&marker) {
6768 cursor += 1;
6769 }
6770 cursor - index
6771}
6772
6773fn can_open_underscore(input: &str, index: usize, marker_len: usize) -> bool {
6774 let flanking = delimiter_flanking(input, index, marker_len);
6775 flanking.left
6776 && (!flanking.right || flanking.previous.is_some_and(|c| c.is_ascii_punctuation()))
6777}
6778
6779fn can_close_underscore(input: &str, index: usize, marker_len: usize) -> bool {
6780 let flanking = delimiter_flanking(input, index, marker_len);
6781 flanking.right && (!flanking.left || flanking.next.is_some_and(|c| c.is_ascii_punctuation()))
6782}
6783
6784#[derive(Clone, Copy)]
6785struct DelimiterFlanking {
6786 left: bool,
6787 right: bool,
6788 previous: Option<char>,
6789 next: Option<char>,
6790}
6791
6792fn delimiter_flanking(input: &str, index: usize, marker_len: usize) -> DelimiterFlanking {
6793 delimiter_flanking_within(input, index, marker_len, false, false)
6794}
6795
6796fn delimiter_flanking_within(
6800 input: &str,
6801 index: usize,
6802 marker_len: usize,
6803 bounded_before: bool,
6804 bounded_after: bool,
6805) -> DelimiterFlanking {
6806 let previous = if bounded_before {
6807 None
6808 } else {
6809 input[..index].chars().next_back().map(source_char)
6810 };
6811 let next = if bounded_after {
6812 None
6813 } else {
6814 input[index + marker_len..].chars().next().map(source_char)
6815 };
6816
6817 let previous_whitespace = previous.is_none_or(char::is_whitespace);
6818 let next_whitespace = next.is_none_or(char::is_whitespace);
6819 let previous_punctuation = previous.is_some_and(is_flanking_punctuation);
6820 let next_punctuation = next.is_some_and(is_flanking_punctuation);
6821
6822 let left = next.is_some()
6823 && !next_whitespace
6824 && !(next_punctuation && !previous_whitespace && !previous_punctuation);
6825 let right = previous.is_some()
6826 && !previous_whitespace
6827 && !(previous_punctuation && !next_whitespace && !next_punctuation);
6828
6829 DelimiterFlanking {
6830 left,
6831 right,
6832 previous,
6833 next,
6834 }
6835}
6836
6837fn parse_math_inline(
6858 lookups: &mut impl Lookups,
6859 input: &str,
6860 index: usize,
6861) -> Option<(usize, String, MathInlineKind)> {
6862 if let Some((end, value)) = parse_math_code_inline(lookups, input, index) {
6863 return Some((end, value, MathInlineKind::Code));
6864 }
6865
6866 let bytes = input.as_bytes();
6867 let open_dollars = bytes[index..]
6868 .iter()
6869 .take_while(|byte| **byte == b'$')
6870 .count();
6871 if open_dollars == 0 || open_dollars > 2 {
6874 return None;
6875 }
6876
6877 let content_start = index + open_dollars;
6878 let close = scan_to_closing_dollar(input, content_start, open_dollars)?;
6879 let content_end = close - open_dollars;
6880 if content_end <= content_start {
6883 return None;
6884 }
6885
6886 let raw = &input[content_start..content_end];
6887 let value = if open_dollars == 1 {
6888 normalize_math_text(raw)
6889 } else {
6890 raw.into()
6891 };
6892 let dollars = u8::try_from(open_dollars).unwrap_or(u8::MAX);
6893 Some((close, value, MathInlineKind::Dollar { dollars }))
6894}
6895
6896fn scan_to_closing_dollar(input: &str, start: usize, open_dollars: usize) -> Option<usize> {
6900 let bytes = input.as_bytes();
6901 if open_dollars == 1 && bytes.get(start).is_some_and(|byte| is_math_space(*byte)) {
6903 return None;
6904 }
6905
6906 let mut cursor = start;
6907 loop {
6908 while cursor < bytes.len() && bytes[cursor] != b'$' {
6909 cursor += 1;
6910 }
6911 if cursor >= bytes.len() {
6912 return None;
6913 }
6914 let prev = bytes[cursor - 1];
6917 if open_dollars == 1 && is_math_space(prev) {
6918 return None;
6919 }
6920 if open_dollars == 1 && prev == b'\\' {
6921 cursor += 1;
6924 continue;
6925 }
6926 let run = bytes[cursor..]
6927 .iter()
6928 .take(open_dollars)
6929 .take_while(|byte| **byte == b'$')
6930 .count();
6931 if open_dollars == 1 && bytes.get(cursor + run).is_some_and(u8::is_ascii_digit) {
6933 return None;
6934 }
6935 if run == open_dollars {
6936 return Some(cursor + run);
6937 }
6938 cursor += run;
6939 }
6940}
6941
6942fn is_math_space(byte: u8) -> bool {
6944 matches!(byte, b'\t' | b'\n' | b'\r' | b' ')
6945}
6946
6947fn normalize_math_text(input: &str) -> String {
6951 let mut normalized = String::new();
6952 let mut cursor = 0;
6953 while cursor < input.len() {
6954 let (next, char) = next_char(input, cursor).expect("valid UTF-8 byte index");
6955 if char == '\r' {
6956 if input.as_bytes().get(next) == Some(&b'\n') {
6957 cursor = next + 1;
6958 } else {
6959 cursor = next;
6960 }
6961 normalized.push(' ');
6962 continue;
6963 }
6964 if char == '\n' {
6965 normalized.push(' ');
6966 cursor = next;
6967 continue;
6968 }
6969 normalized.push(char);
6970 cursor = next;
6971 }
6972
6973 if normalized.starts_with(' ')
6974 && normalized.ends_with(' ')
6975 && normalized.chars().any(|char| char != ' ')
6976 {
6977 normalized[1..normalized.len() - 1].into()
6978 } else {
6979 normalized
6980 }
6981}
6982
6983fn parse_math_code_inline(
6984 lookups: &mut impl Lookups,
6985 input: &str,
6986 index: usize,
6987) -> Option<(usize, String)> {
6988 if !input[index..].starts_with("$`") {
6989 return None;
6990 }
6991
6992 let search_start = index + 2;
6993 let close = lookups.find("`$", search_start)?;
6994 if close == search_start {
6995 return None;
6996 }
6997
6998 Some((close + 2, input[search_start..close].into()))
6999}
7000
7001fn parse_link_resource(
7002 lookups: &mut impl Lookups,
7003 input: &str,
7004 open: usize,
7005) -> Option<(usize, ParsedLinkResource)> {
7006 let bytes = input.as_bytes();
7007 if bytes.get(open) != Some(&b'(') {
7008 return None;
7009 }
7010 let (mut cursor, initial_space) = skip_link_resource_space_with_info(input, open + 1)?;
7011 if bytes.get(cursor) == Some(&b')') {
7012 return Some((
7013 cursor + 1,
7014 ParsedLinkResource {
7015 destination: String::new(),
7016 destination_kind: LinkDestinationKind::Omitted,
7017 title: None,
7018 title_kind: None,
7019 },
7020 ));
7021 }
7022 if initial_space && matches!(bytes.get(cursor), Some(b'"' | b'\'' | b'(')) {
7023 let (title, title_kind, next) = parse_link_title(input, cursor)?;
7024 cursor = skip_link_resource_space(input, next)?;
7025 if bytes.get(cursor) == Some(&b')') {
7026 return Some((
7027 cursor + 1,
7028 ParsedLinkResource {
7029 destination: String::new(),
7030 destination_kind: LinkDestinationKind::Omitted,
7031 title: Some(title),
7032 title_kind: Some(title_kind),
7033 },
7034 ));
7035 }
7036 return None;
7037 }
7038 let (destination, destination_kind, next) = parse_link_destination(lookups, input, cursor)?;
7039 let (after_destination, had_space) = skip_link_resource_space_with_info(input, next)?;
7040 cursor = after_destination;
7041 if bytes.get(cursor) == Some(&b')') {
7042 return Some((
7043 cursor + 1,
7044 ParsedLinkResource {
7045 destination,
7046 destination_kind,
7047 title: None,
7048 title_kind: None,
7049 },
7050 ));
7051 }
7052 if !had_space {
7053 return None;
7054 }
7055
7056 let (title, title_kind, next) = parse_link_title(input, cursor)?;
7057 cursor = skip_link_resource_space(input, next)?;
7058 if bytes.get(cursor) == Some(&b')') {
7059 Some((
7060 cursor + 1,
7061 ParsedLinkResource {
7062 destination,
7063 destination_kind,
7064 title: Some(title),
7065 title_kind: Some(title_kind),
7066 },
7067 ))
7068 } else {
7069 None
7070 }
7071}
7072
7073fn parse_link_destination(
7074 lookups: &mut impl Lookups,
7075 input: &str,
7076 index: usize,
7077) -> Option<(String, LinkDestinationKind, usize)> {
7078 if input.as_bytes().get(index) == Some(&b'<') {
7079 let stop = lookups.angle_destination_stop(index + 1)?;
7080 if input.as_bytes()[stop] != b'>' {
7081 return None;
7082 }
7083 return Some((
7084 unescape_ascii_punctuation(&input[index + 1..stop]),
7085 LinkDestinationKind::Angle,
7086 stop + 1,
7087 ));
7088 }
7089
7090 let mut cursor = index;
7091 let mut depth = 0usize;
7092 while cursor < input.len() {
7093 let (next, char) = next_char(input, cursor)?;
7094 if (char == ' ' || source_char(char).is_ascii_control()) && depth == 0 {
7099 break;
7100 }
7101 if char == '(' && !is_escaped_at(input, cursor) {
7102 depth += 1;
7103 if depth > 32 {
7105 return None;
7106 }
7107 } else if char == ')' && !is_escaped_at(input, cursor) {
7108 if depth == 0 {
7109 break;
7110 }
7111 depth -= 1;
7112 }
7113 cursor = next;
7114 }
7115
7116 if cursor == index || depth > 0 {
7117 None
7118 } else {
7119 Some((
7120 unescape_ascii_punctuation(&input[index..cursor]),
7121 LinkDestinationKind::Bare,
7122 cursor,
7123 ))
7124 }
7125}
7126
7127fn parse_link_title(input: &str, index: usize) -> Option<(String, LinkTitleKind, usize)> {
7128 let opener = input.as_bytes().get(index).copied()?;
7129 let (closer, title_kind) = match opener {
7130 b'"' => ('"', LinkTitleKind::DoubleQuote),
7131 b'\'' => ('\'', LinkTitleKind::SingleQuote),
7132 b'(' => (')', LinkTitleKind::Paren),
7133 _ => return None,
7134 };
7135 let mut cursor = index + 1;
7136 while cursor < input.len() {
7137 let (next, char) = next_char(input, cursor)?;
7138 if char == closer && !is_escaped_at(input, cursor) {
7139 if contains_blank_line(&input[index + 1..cursor]) {
7140 return None;
7141 }
7142 return Some((
7143 unescape_ascii_punctuation(&input[index + 1..cursor]),
7144 title_kind,
7145 next,
7146 ));
7147 }
7148 if opener == b'(' && char == '(' && !is_escaped_at(input, cursor) {
7149 return None;
7150 }
7151 cursor = next;
7152 }
7153 None
7154}
7155
7156fn contains_blank_line(input: &str) -> bool {
7157 if !input.bytes().any(|byte| matches!(byte, b'\n' | b'\r')) {
7158 return false;
7159 }
7160 let lines = collect_lines(input, 0);
7165 let interior = lines.len().saturating_sub(1);
7166 lines
7167 .iter()
7168 .take(interior)
7169 .skip(1)
7170 .any(|line| line.text.trim().is_empty())
7171}
7172
7173fn skip_link_resource_space(input: &str, index: usize) -> Option<usize> {
7174 skip_link_resource_space_with_info(input, index).map(|(index, _)| index)
7175}
7176
7177fn skip_link_resource_space_with_info(input: &str, mut index: usize) -> Option<(usize, bool)> {
7178 let mut line_breaks = 0usize;
7179 let mut had_space = false;
7180 while input
7181 .as_bytes()
7182 .get(index)
7183 .is_some_and(|byte| matches!(*byte, b' ' | b'\t' | b'\n' | b'\r'))
7184 {
7185 had_space = true;
7186 match input.as_bytes()[index] {
7187 b'\n' => {
7188 line_breaks += 1;
7189 if line_breaks > 1 {
7190 return None;
7191 }
7192 index += 1;
7193 }
7194 b'\r' => {
7195 line_breaks += 1;
7196 if line_breaks > 1 {
7197 return None;
7198 }
7199 if input.as_bytes().get(index + 1) == Some(&b'\n') {
7200 index += 2;
7201 } else {
7202 index += 1;
7203 }
7204 }
7205 _ => index += 1,
7206 }
7207 }
7208 Some((index, had_space))
7209}
7210
7211pub(crate) fn parse_character_reference(input: &str, index: usize) -> Option<(usize, String)> {
7212 let rest = input.get(index..)?;
7213 if let Some(rest) = rest
7214 .strip_prefix("&#x")
7215 .or_else(|| rest.strip_prefix("&#X"))
7216 {
7217 let digits = find_reference_terminator(rest, 6)?;
7218 if digits == 0 || !rest[..digits].bytes().all(|byte| byte.is_ascii_hexdigit()) {
7219 return None;
7220 }
7221 let value = u32::from_str_radix(&rest[..digits], 16).ok()?;
7222 return Some((
7223 index + 3 + digits + 1,
7224 character_reference_value(value).into(),
7225 ));
7226 }
7227 if let Some(rest) = rest.strip_prefix("&#") {
7228 let digits = find_reference_terminator(rest, 7)?;
7229 if digits == 0 || !rest[..digits].bytes().all(|byte| byte.is_ascii_digit()) {
7230 return None;
7231 }
7232 let value = rest[..digits].parse::<u32>().ok()?;
7233 return Some((
7234 index + 2 + digits + 1,
7235 character_reference_value(value).into(),
7236 ));
7237 }
7238
7239 let name_end = find_reference_terminator(rest, 32)?;
7240 if name_end == 0 {
7241 return None;
7242 }
7243 let name = &rest[1..name_end];
7244 named_character_reference(name).map(|value| (index + name_end + 1, value.into()))
7245}
7246
7247fn find_reference_terminator(rest: &str, max_len: usize) -> Option<usize> {
7251 rest.as_bytes()
7252 .iter()
7253 .take(max_len + 1)
7254 .position(|byte| *byte == b';')
7255}
7256
7257pub(crate) fn character_reference_value(value: u32) -> char {
7274 if value == 0 {
7275 '\u{FFFD}'
7276 } else {
7277 char::from_u32(value).unwrap_or('\u{FFFD}')
7278 }
7279}
7280
7281pub(crate) fn is_escaped_at(input: &str, index: usize) -> bool {
7282 let bytes = input.as_bytes();
7283 let mut cursor = index;
7284 let mut count = 0;
7285 while cursor > 0 && bytes[cursor - 1] == b'\\' {
7286 count += 1;
7287 cursor -= 1;
7288 }
7289 count % 2 == 1
7290}
7291
7292fn parse_definition_destination_title(input: &str) -> Option<ParsedLinkResource> {
7293 let (mut cursor, _) = skip_link_resource_space_with_info(input, 0)?;
7294 let (destination, destination_kind, next) =
7295 parse_link_destination(&mut DirectLookups { input }, input, cursor)?;
7296 cursor = next;
7297
7298 let (next, had_space) = skip_link_resource_space_with_info(input, cursor)?;
7299 cursor = next;
7300 if cursor >= input.len() {
7301 return Some(ParsedLinkResource {
7302 destination,
7303 destination_kind,
7304 title: None,
7305 title_kind: None,
7306 });
7307 }
7308 if !had_space {
7309 return None;
7310 }
7311
7312 let (title, title_kind, next) = parse_link_title(input, cursor)?;
7313 let after_title = skip_link_resource_space(input, next)?;
7314 (after_title == input.len()).then_some(ParsedLinkResource {
7315 destination,
7316 destination_kind,
7317 title: Some(title),
7318 title_kind: Some(title_kind),
7319 })
7320}
7321
7322fn line_can_start_definition_title(input: &str) -> bool {
7323 let trimmed = input.trim_start();
7324 matches!(trimmed.as_bytes().first(), Some(b'"' | b'\'' | b'('))
7325}
7326
7327fn unescape_ascii_punctuation(input: &str) -> String {
7328 unescape_selected(input, |char| char.is_ascii_punctuation())
7330}
7331
7332fn unescape_string(input: &str) -> String {
7333 unescape_selected(input, |char| char.is_ascii_punctuation() || char == '&')
7334}
7335
7336fn unescape_selected(input: &str, should_unescape: impl Fn(char) -> bool) -> String {
7337 let mut output = String::new();
7338 let mut cursor = 0;
7339 while cursor < input.len() {
7340 if input.as_bytes().get(cursor) == Some(&b'&') {
7341 if let Some((end, value)) = parse_character_reference(input, cursor) {
7342 output.push_str(&value);
7343 cursor = end;
7344 continue;
7345 }
7346 }
7347 let (next, char) = next_char(input, cursor).expect("valid UTF-8 byte index");
7348 if char == '\\' {
7349 if let Some((after_escape, escaped)) = next_char(input, next) {
7350 if should_unescape(escaped) {
7351 output.push(escaped);
7352 } else {
7353 output.push(char);
7354 output.push(escaped);
7355 }
7356 cursor = after_escape;
7357 } else {
7358 output.push(char);
7359 cursor = next;
7360 }
7361 } else {
7362 output.push(char);
7363 cursor = next;
7364 }
7365 }
7366 output
7367}
7368
7369fn push_line(output: &mut String, line: &str) {
7370 if !output.is_empty() {
7371 output.push('\n');
7372 }
7373 output.push_str(line);
7374}
7375
7376fn ensure_line_separator(output: &mut String) {
7377 if !output.is_empty() && !ends_with_line_ending(output) {
7378 output.push('\n');
7379 }
7380}
7381
7382fn ends_with_line_ending(input: &str) -> bool {
7383 input.ends_with('\n') || input.ends_with('\r')
7384}
7385
7386fn flush_text(nodes: &mut Vec<Inline>, text: &mut String, text_start: usize, end: usize) {
7387 if !text.is_empty() {
7388 nodes.push(Inline::Text(Text {
7389 meta: NodeMeta::new(Some(Span::new(text_start, end))),
7390 value: core::mem::take(text),
7391 }));
7392 }
7393}
7394
7395fn next_char(input: &str, index: usize) -> Option<(usize, char)> {
7396 let char = input[index..].chars().next()?;
7397 Some((index + char.len_utf8(), char))
7398}
7399
7400fn is_flanking_punctuation(value: char) -> bool {
7405 value.is_ascii_punctuation() || crate::unicode_punctuation::is_unicode_punctuation(value)
7406}
7407
7408pub(crate) fn normalize_label(label: &str) -> String {
7417 label
7418 .replace('ẞ', "ss")
7424 .replace('\0', "\u{FFFD}")
7425 .split_whitespace()
7426 .collect::<Vec<_>>()
7427 .join(" ")
7428 .to_uppercase()
7429 .to_lowercase()
7430}
7431
7432fn definition_exists(definitions: &[String], label: &str) -> bool {
7433 if label.is_empty() || !reference_label_is_within_limit(label) {
7434 return false;
7435 }
7436
7437 definitions.binary_search(&normalize_label(label)).is_ok()
7438}
7439
7440fn reference_label_is_within_limit(label: &str) -> bool {
7441 label.chars().take(REFERENCE_LABEL_MAX_CHARS + 1).count() <= REFERENCE_LABEL_MAX_CHARS
7442}
7443
7444fn trim_up_to_three_spaces(input: &str) -> Option<&str> {
7445 let (columns, bytes) = leading_indent(input);
7446 if columns <= 3 {
7447 Some(&input[bytes..])
7448 } else {
7449 None
7450 }
7451}
7452
7453fn fence_start(input: &str) -> Option<(FenceMarker, usize)> {
7454 let marker = match input.as_bytes().first()? {
7455 b'`' => FenceMarker::Backtick,
7456 b'~' => FenceMarker::Tilde,
7457 _ => return None,
7458 };
7459 let byte = match marker {
7460 FenceMarker::Backtick => b'`',
7461 FenceMarker::Tilde => b'~',
7462 };
7463 let length = input
7464 .as_bytes()
7465 .iter()
7466 .take_while(|item| **item == byte)
7467 .count();
7468 if length >= 3 {
7469 Some((marker, length))
7470 } else {
7471 None
7472 }
7473}
7474
7475fn fence_close(input: &str, marker: FenceMarker, length: usize) -> bool {
7476 let byte = match marker {
7477 FenceMarker::Backtick => b'`',
7478 FenceMarker::Tilde => b'~',
7479 };
7480 let count = input
7481 .as_bytes()
7482 .iter()
7483 .take_while(|item| **item == byte)
7484 .count();
7485 count >= length && input[count..].trim().is_empty()
7486}
7487
7488fn trim_closing_hashes(input: &str) -> &str {
7489 let input = input.trim_end();
7490 let hash_start = input.trim_end_matches('#').len();
7491 if hash_start == input.len() {
7492 return input;
7493 }
7494 if hash_start == 0 {
7495 return "";
7496 }
7497
7498 let before = &input[..hash_start];
7499 if before.ends_with(' ') || before.ends_with('\t') {
7500 before.trim_end()
7501 } else {
7502 input
7503 }
7504}
7505
7506fn list_marker_info(input: &str) -> Option<ListMarkerInfo<'_>> {
7507 let trimmed = trim_up_to_three_spaces(input)?;
7508 let indent = input.len() - trimmed.len();
7509 let bytes = trimmed.as_bytes();
7510 match bytes.first()? {
7511 b'-' | b'*' | b'+' if is_list_padding_byte(bytes.get(1).copied()) => {
7512 let delimiter = match bytes[0] {
7513 b'-' => ListDelimiter::Dash,
7514 b'*' => ListDelimiter::Asterisk,
7515 _ => ListDelimiter::Plus,
7516 };
7517 let (content_offset, content_indent) = list_content_offset(trimmed, 1, indent);
7518 Some(ListMarkerInfo {
7519 ordered: false,
7520 start: None,
7521 delimiter,
7522 indent,
7523 marker_len: 1,
7524 content_indent,
7525 content: &trimmed[content_offset..],
7526 })
7527 }
7528 byte if byte.is_ascii_digit() => {
7529 let mut end = 0;
7530 while bytes.get(end).is_some_and(|byte| byte.is_ascii_digit()) {
7531 end += 1;
7532 }
7533 if end > 9 {
7534 return None;
7535 }
7536 let delimiter = match bytes.get(end)? {
7537 b'.' => ListDelimiter::Period,
7538 b')' => ListDelimiter::Paren,
7539 _ => return None,
7540 };
7541 if !is_list_padding_byte(bytes.get(end + 1).copied()) {
7542 return None;
7543 }
7544 let start = trimmed[..end].parse().ok()?;
7545 let marker_len = end + 1;
7546 let (content_offset, content_indent) = list_content_offset(trimmed, marker_len, indent);
7547 Some(ListMarkerInfo {
7548 ordered: true,
7549 start: Some(start),
7550 delimiter,
7551 indent,
7552 marker_len,
7553 content_indent,
7554 content: &trimmed[content_offset..],
7555 })
7556 }
7557 _ => None,
7558 }
7559}
7560
7561fn list_content_offset(input: &str, marker_len: usize, indent: usize) -> (usize, usize) {
7562 let bytes = input.as_bytes();
7563 if bytes.get(marker_len).is_none() {
7564 return (marker_len, indent + marker_len + 1);
7565 }
7566 let mut cursor = marker_len;
7567 let mut column = indent + marker_len;
7568 let marker_end_column = column;
7569 while let Some(byte) = bytes.get(cursor) {
7570 match *byte {
7571 b' ' => column += 1,
7572 b'\t' => column += 4 - (column % 4),
7573 _ => break,
7574 }
7575 cursor += 1;
7576 }
7577 if cursor >= bytes.len() {
7582 return (cursor, marker_end_column + 1);
7583 }
7584 let padding_columns = column.saturating_sub(marker_end_column);
7585 if padding_columns > 0 && padding_columns <= 4 {
7586 (cursor, column)
7587 } else {
7588 (marker_len + 1, marker_end_column + 1)
7589 }
7590}
7591
7592fn list_marker_first_content<'a>(input: &'a str, marker: ListMarkerInfo<'a>) -> Cow<'a, str> {
7593 let Some(trimmed) = trim_up_to_three_spaces(input) else {
7594 return Cow::Borrowed(marker.content);
7595 };
7596 let after_marker = &trimmed[marker.marker_len..];
7597 if after_marker.starts_with('\t') {
7598 strip_leading_indent_columns_from(after_marker, 1, marker.indent + marker.marker_len)
7599 } else {
7600 Cow::Borrowed(marker.content)
7601 }
7602}
7603
7604fn is_list_padding_byte(byte: Option<u8>) -> bool {
7605 matches!(byte, None | Some(b' ' | b'\t'))
7606}
7607
7608fn same_list_marker(left: ListMarkerInfo<'_>, right: ListMarkerInfo<'_>) -> bool {
7609 left.ordered == right.ordered && left.delimiter == right.delimiter
7613}
7614
7615fn sibling_list_marker_at_line(
7622 input: &str,
7623 first_marker: ListMarkerInfo<'_>,
7624 content_indent: usize,
7625) -> bool {
7626 list_marker_info(input).is_some_and(|candidate| {
7627 same_list_marker(first_marker, candidate) && candidate.indent < content_indent
7628 })
7629}
7630
7631fn same_list_marker_line(input: &str, first_marker: ListMarkerInfo<'_>) -> bool {
7636 list_marker_info(input).is_some_and(|candidate| same_list_marker(first_marker, candidate))
7637}
7638
7639fn next_nonblank_line(lines: &[Line<'_>], mut index: usize) -> usize {
7640 while index < lines.len() && lines[index].text.trim().is_empty() {
7641 index += 1;
7642 }
7643 index
7644}
7645
7646fn leading_indent(input: &str) -> (usize, usize) {
7647 let mut column = 0usize;
7648 let mut bytes = 0usize;
7649 for byte in input.as_bytes() {
7650 match *byte {
7651 b' ' => column += 1,
7652 b'\t' => column += 4 - (column % 4),
7653 _ => break,
7654 }
7655 bytes += 1;
7656 }
7657 (column, bytes)
7658}
7659
7660fn leading_indent_columns(input: &str) -> usize {
7661 leading_indent(input).0
7662}
7663
7664fn strip_leading_indent_columns(input: &str, max_columns: usize) -> Cow<'_, str> {
7671 strip_leading_indent_columns_from(input, max_columns, 0)
7672}
7673
7674fn strip_leading_indent_columns_from(
7675 input: &str,
7676 max_columns: usize,
7677 start_column: usize,
7678) -> Cow<'_, str> {
7679 let mut column = start_column;
7680 let target_column = start_column + max_columns;
7681 for (index, byte) in input.as_bytes().iter().enumerate() {
7682 let next = match *byte {
7683 b' ' => column + 1,
7684 b'\t' => column + (4 - (column % 4)),
7685 _ => return Cow::Borrowed(&input[index..]),
7686 };
7687 if next > target_column {
7688 if *byte == b'\t' && column < target_column {
7691 let residual = next - target_column;
7692 let mut owned = String::with_capacity(residual + input.len() - (index + 1));
7693 for _ in 0..residual {
7694 owned.push(' ');
7695 }
7696 let mut rest_column = next;
7697 let mut rest_index = index + 1;
7698 while let Some(rest_byte) = input.as_bytes().get(rest_index) {
7699 match *rest_byte {
7700 b' ' => {
7701 owned.push(' ');
7702 rest_column += 1;
7703 rest_index += 1;
7704 }
7705 b'\t' => {
7706 let width = 4 - (rest_column % 4);
7707 for _ in 0..width {
7708 owned.push(' ');
7709 }
7710 rest_column += width;
7711 rest_index += 1;
7712 }
7713 _ => break,
7714 }
7715 }
7716 owned.push_str(&input[rest_index..]);
7717 return Cow::Owned(owned);
7718 }
7719 return Cow::Borrowed(&input[index..]);
7720 }
7721 column = next;
7722 }
7723 Cow::Borrowed("")
7724}
7725
7726fn strip_list_continuation(input: &str, content_indent: usize, list_indent: usize) -> Cow<'_, str> {
7727 let (indent_columns, indent_bytes) = leading_indent(input);
7728 if indent_columns >= content_indent {
7729 strip_leading_indent_columns(input, content_indent)
7734 } else if indent_columns > list_indent {
7735 Cow::Borrowed(&input[indent_bytes..])
7736 } else {
7737 Cow::Borrowed(trim_ascii_start(input))
7738 }
7739}
7740
7741fn take_task_marker_from_children(children: &mut [Block]) -> Option<bool> {
7742 let Some(Block::Paragraph(paragraph)) = children.first_mut() else {
7743 return None;
7744 };
7745 take_task_marker_from_inlines(&mut paragraph.children)
7746}
7747
7748fn take_task_marker_from_inlines(inlines: &mut Vec<Inline>) -> Option<bool> {
7749 let Some(Inline::Text(text)) = inlines.first() else {
7750 return None;
7751 };
7752 let first = text.value.clone();
7753
7754 if let Some((checked, consumed)) = task_marker_inline_prefix(&first) {
7755 if !first[consumed..].is_empty() || inlines_have_content_after(inlines, 1) {
7756 remove_text_prefix(inlines, consumed);
7757 return Some(checked);
7758 }
7759 }
7760
7761 if let Some(checked) = task_marker_at_text_end(&first) {
7762 if inlines
7763 .get(1)
7764 .is_some_and(|inline| matches!(inline, Inline::SoftBreak(_)))
7765 && inlines_have_content_after(inlines, 2)
7766 {
7767 inlines.remove(1);
7768 inlines.remove(0);
7769 return Some(checked);
7770 }
7771 }
7772
7773 if task_marker_split_open(&first)
7774 && inlines
7775 .get(1)
7776 .is_some_and(|inline| matches!(inline, Inline::SoftBreak(_)))
7777 {
7778 let Some(Inline::Text(next)) = inlines.get(2) else {
7779 return None;
7780 };
7781 if let Some((checked, consumed)) = task_marker_split_close_prefix(&next.value) {
7782 if !next.value[consumed..].is_empty() || inlines_have_content_after(inlines, 3) {
7783 inlines.remove(1);
7784 inlines.remove(0);
7785 remove_text_prefix(inlines, consumed);
7786 return Some(checked);
7787 }
7788 }
7789 }
7790
7791 None
7792}
7793
7794fn task_marker_inline_prefix(input: &str) -> Option<(bool, usize)> {
7795 let start = leading_trim_bytes(input);
7796 let rest = &input[start..];
7797 let checked = task_marker_checked(rest)?;
7798 let after_marker = start + 3;
7799 match input.as_bytes().get(after_marker) {
7800 Some(b' ' | b'\t') => Some((checked, after_marker + 1)),
7801 _ => None,
7802 }
7803}
7804
7805fn task_marker_at_text_end(input: &str) -> Option<bool> {
7806 let start = leading_trim_bytes(input);
7807 let rest = &input[start..];
7808 let checked = task_marker_checked(rest)?;
7809 if rest.len() == 3 {
7810 Some(checked)
7811 } else {
7812 None
7813 }
7814}
7815
7816fn task_marker_split_open(input: &str) -> bool {
7817 let start = leading_trim_bytes(input);
7818 input[start..] == *"["
7819}
7820
7821fn task_marker_split_close_prefix(input: &str) -> Option<(bool, usize)> {
7822 match input.as_bytes().get(..2)? {
7823 b"] " => Some((false, 2)),
7824 b"]\t" => Some((false, 2)),
7825 b"x]" | b"X]" if matches!(input.as_bytes().get(2), Some(b' ' | b'\t')) => Some((true, 3)),
7826 _ => None,
7827 }
7828}
7829
7830fn task_marker_checked(input: &str) -> Option<bool> {
7831 if input.starts_with("[ ]") {
7832 Some(false)
7833 } else if input.starts_with("[x]") || input.starts_with("[X]") {
7834 Some(true)
7835 } else {
7836 None
7837 }
7838}
7839
7840fn remove_text_prefix(inlines: &mut Vec<Inline>, consumed: usize) {
7841 if let Some(Inline::Text(text)) = inlines.first_mut() {
7842 text.value = text.value[consumed..].into();
7843 if text.value.is_empty() {
7844 inlines.remove(0);
7845 }
7846 }
7847}
7848
7849fn inlines_have_content_after(inlines: &[Inline], start: usize) -> bool {
7850 inlines.iter().skip(start).any(|inline| match inline {
7851 Inline::Text(text) => !text.value.is_empty(),
7852 Inline::SoftBreak(_) | Inline::LineBreak(_) => false,
7853 _ => true,
7854 })
7855}
7856
7857fn update_list_item_fence(line: &str, open_fence: &mut Option<(FenceMarker, usize)>) {
7858 let Some(trimmed) = trim_up_to_three_spaces(line) else {
7859 return;
7860 };
7861 if let Some((marker, length)) = *open_fence {
7862 if fence_close(trimmed, marker, length) {
7863 *open_fence = None;
7864 }
7865 return;
7866 }
7867 if let Some((marker, length)) = fence_start(trimmed) {
7868 *open_fence = Some((marker, length));
7869 }
7870}
7871
7872fn trim_ascii_start(input: &str) -> &str {
7873 input.trim_start_matches(|char| matches!(char, ' ' | '\t'))
7874}
7875
7876fn leading_trim_bytes(input: &str) -> usize {
7877 input.len() - trim_ascii_start(input).len()
7878}
7879
7880fn parse_table_delimiter(input: &str, spoiler: bool) -> Option<Vec<TableAlignment>> {
7881 let cells = split_table_row(input, spoiler);
7882 if cells.is_empty() {
7883 return None;
7884 }
7885 let mut alignments = Vec::new();
7886 for cell in cells {
7887 alignments.push(table_delimiter_alignment(cell.trim())?);
7888 }
7889 Some(alignments)
7890}
7891
7892fn table_delimiter_alignment(cell: &str) -> Option<TableAlignment> {
7895 let bytes = cell.as_bytes();
7896 let mut cursor = 0;
7897 let left = bytes.first() == Some(&b':');
7898 if left {
7899 cursor += 1;
7900 }
7901 let dash_start = cursor;
7902 while bytes.get(cursor) == Some(&b'-') {
7903 cursor += 1;
7904 }
7905 if cursor == dash_start {
7906 return None;
7907 }
7908 let right = bytes.get(cursor) == Some(&b':');
7909 if right {
7910 cursor += 1;
7911 }
7912 if cursor != bytes.len() {
7913 return None;
7914 }
7915 Some(match (left, right) {
7916 (true, true) => TableAlignment::Center,
7917 (true, false) => TableAlignment::Left,
7918 (false, true) => TableAlignment::Right,
7919 (false, false) => TableAlignment::None,
7920 })
7921}
7922
7923fn table_indent_line(input: &str, indented_code: bool) -> Option<&str> {
7927 if indented_code {
7928 trim_up_to_three_spaces(input)
7929 } else {
7930 Some(input)
7931 }
7932}
7933
7934pub(crate) fn table_row_delimiters(row: &str, spoiler: bool) -> Vec<usize> {
7952 scan_table_row(row, spoiler).0
7953}
7954
7955fn scan_table_row(row: &str, spoiler: bool) -> (Vec<usize>, Vec<(usize, usize)>) {
7958 let bytes = row.as_bytes();
7959
7960 let mut bars: Vec<TableBar> = Vec::new();
7962 let mut runs: Vec<(usize, usize)> = Vec::new();
7963 let mut backticks = false;
7964 let mut cursor = 0;
7965 while cursor < bytes.len() {
7966 match bytes[cursor] {
7967 b'\\' => {
7968 let backslashes = delimiter_byte_run_len(row, cursor, b'\\');
7969 let pipe = cursor + backslashes;
7970 if bytes.get(pipe) == Some(&b'|') && backslashes % 2 == 1 {
7971 let text_start = if backslashes == 1 { cursor } else { pipe };
7974 push_table_bar(&mut bars, &mut runs, pipe, text_start, true);
7975 cursor = pipe + 1;
7976 } else {
7977 cursor = pipe;
7978 }
7979 }
7980 b'|' => {
7981 push_table_bar(&mut bars, &mut runs, cursor, cursor, false);
7982 cursor += 1;
7983 }
7984 byte => {
7985 backticks |= byte == b'`';
7986 cursor += 1;
7987 }
7988 }
7989 }
7990 if !spoiler || (!backticks && runs.iter().all(|&(_, length)| length == 1)) {
7991 let delimiters = bars
7993 .iter()
7994 .filter(|bar| !bar.escaped)
7995 .map(|bar| bar.position)
7996 .collect();
7997 return (delimiters, Vec::new());
7998 }
7999
8000 let mut code_delimits = vec![false; bars.len()];
8003 for &(first, length) in &runs {
8004 let mut bar = first;
8005 while bar < first + length {
8006 if bar + 1 < first + length && !bars[bar].escaped && !bars[bar + 1].escaped {
8007 bar += 2;
8008 } else {
8009 code_delimits[bar] = !bars[bar].escaped;
8010 bar += 1;
8011 }
8012 }
8013 }
8014
8015 let mut breaker = 0;
8018 let code_spans = if backticks {
8019 inline_code_spans(row, |open, close| {
8020 while breaker < bars.len() && (bars[breaker].position < open || !code_delimits[breaker])
8021 {
8022 breaker += 1;
8023 }
8024 bars.get(breaker).is_none_or(|bar| close < bar.position)
8025 })
8026 } else {
8027 Vec::new()
8028 };
8029
8030 let mut delimits = vec![false; bars.len()];
8033 let mut held = Vec::new();
8034 let mut opener: Option<usize> = None;
8035 let mut blocked = false;
8036 let mut code = code_spans.iter().peekable();
8037 for (first, length) in runs {
8038 let end = first + length;
8039 let position = bars[first].position;
8040 while code.peek().is_some_and(|&&(_, close)| close < position) {
8041 code.next();
8042 }
8043 if code.peek().is_some_and(|&&(open, _)| open < position) {
8044 delimits[first..end].copy_from_slice(&code_delimits[first..end]);
8045 blocked |= code_delimits[first..end].contains(&true);
8046 continue;
8047 }
8048 if length == 1 {
8049 delimits[first] = true;
8050 blocked |= !bars[first].escaped;
8051 continue;
8052 }
8053 let mut rest = first;
8054 if let Some(opened) = opener.take() {
8055 let escaped = bars[opened..opened + 2]
8056 .iter()
8057 .chain(&bars[first..first + 2])
8058 .any(|bar| bar.escaped);
8059 if escaped && blocked {
8060 delimits[opened..opened + 2].fill(true);
8061 } else {
8062 held.push((bars[opened].position, bars[first + 1].position));
8063 rest = first + 2;
8064 }
8065 }
8066 let mut open_at = end;
8067 if end - rest >= 2 {
8068 open_at = end - 2;
8069 opener = Some(open_at);
8070 blocked = false;
8071 }
8072 delimits[rest..open_at].fill(true);
8073 }
8074 if let Some(opened) = opener {
8075 delimits[opened..opened + 2].fill(true);
8076 }
8077
8078 let mut spoilers = held.iter().copied().peekable();
8079 let mut delimiters = Vec::new();
8080 for (bar, delimit) in bars.iter().zip(delimits) {
8081 while spoilers.peek().is_some_and(|&(_, end)| end < bar.position) {
8082 spoilers.next();
8083 }
8084 let in_spoiler = spoilers
8085 .peek()
8086 .is_some_and(|&(start, end)| start <= bar.position && bar.position <= end);
8087 if delimit && !bar.escaped && !in_spoiler {
8088 delimiters.push(bar.position);
8089 }
8090 }
8091 (delimiters, held)
8092}
8093
8094fn inline_code_spans(
8101 text: &str,
8102 mut may_close: impl FnMut(usize, usize) -> bool,
8103) -> Vec<(usize, usize)> {
8104 let bytes = text.as_bytes();
8105 let mut runs = Vec::new();
8108 let mut cursor = 0;
8109 while cursor < bytes.len() {
8110 match bytes[cursor] {
8111 b'\\' => {
8112 let backslashes = delimiter_byte_run_len(text, cursor, b'\\');
8113 cursor += backslashes;
8114 if bytes.get(cursor) == Some(&b'`') {
8115 let length = delimiter_byte_run_len(text, cursor, b'`');
8116 runs.push((cursor, length, backslashes % 2 == 1));
8117 cursor += length;
8118 }
8119 }
8120 b'`' => {
8121 let length = delimiter_byte_run_len(text, cursor, b'`');
8122 runs.push((cursor, length, false));
8123 cursor += length;
8124 }
8125 _ => cursor += 1,
8126 }
8127 }
8128 let longest = runs.iter().map(|&(_, length, _)| length).max().unwrap_or(0);
8129 let mut by_length = vec![Vec::new(); longest + 1];
8130 for (index, &(_, length, _)) in runs.iter().enumerate() {
8131 by_length[length].push(index);
8132 }
8133
8134 let mut spans = Vec::new();
8135 let mut index = 0;
8136 while index < runs.len() {
8137 let (position, length, escaped) = runs[index];
8138 let (open, length) = if escaped {
8139 (position + 1, length - 1)
8140 } else {
8141 (position, length)
8142 };
8143 let close = by_length[length]
8144 .get(by_length[length].partition_point(|&later| later <= index))
8145 .copied()
8146 .filter(|_| length > 0);
8147 match close {
8148 Some(close) if may_close(open, runs[close].0) => {
8149 spans.push((open, runs[close].0));
8150 index = close + 1;
8151 }
8152 _ => index += 1,
8153 }
8154 }
8155 spans
8156}
8157
8158struct TableBar {
8160 position: usize,
8161 escaped: bool,
8162}
8163
8164fn push_table_bar(
8167 bars: &mut Vec<TableBar>,
8168 runs: &mut Vec<(usize, usize)>,
8169 position: usize,
8170 text_start: usize,
8171 escaped: bool,
8172) {
8173 let joins = bars
8174 .last()
8175 .is_some_and(|previous| previous.position + 1 == text_start);
8176 match runs.last_mut() {
8177 Some(run) if joins => run.1 += 1,
8178 _ => runs.push((bars.len(), 1)),
8179 }
8180 bars.push(TableBar { position, escaped });
8181}
8182
8183fn split_table_row(input: &str, spoiler: bool) -> Vec<String> {
8184 let trimmed = input.trim();
8185 let delimiters = table_row_delimiters(trimmed, spoiler);
8186 let mut cells = Vec::new();
8187 let mut start = 0;
8188 for &pipe in &delimiters {
8189 cells.push(table_cell_text(&trimmed[start..pipe]));
8190 start = pipe + 1;
8191 }
8192 cells.push(table_cell_text(&trimmed[start..]));
8193
8194 if delimiters.first() == Some(&0) {
8197 cells.remove(0);
8198 }
8199 if delimiters
8200 .last()
8201 .is_some_and(|&pipe| trimmed[pipe + 1..].trim().is_empty())
8202 {
8203 cells.pop();
8204 }
8205 cells
8206}
8207
8208fn table_cell_text(source: &str) -> String {
8212 let bytes = source.as_bytes();
8213 let mut cell = String::with_capacity(source.len());
8214 let mut copied = 0;
8215 let mut cursor = 0;
8216 while cursor < bytes.len() {
8217 if bytes[cursor] == b'\\' {
8218 let pipe = cursor + delimiter_byte_run_len(source, cursor, b'\\');
8219 if bytes.get(pipe) == Some(&b'|') && (pipe - cursor) % 2 == 1 {
8220 cell.push_str(&source[copied..pipe - 1]);
8221 copied = pipe;
8222 }
8223 cursor = pipe;
8224 } else {
8225 cursor += 1;
8226 }
8227 }
8228 cell.push_str(&source[copied..]);
8229 cell
8230}
8231
8232fn table_can_start(lines: &[Line<'_>], index: usize, options: &SyntaxOptions) -> bool {
8233 if !options.constructs.gfm_table || index + 1 >= lines.len() {
8234 return false;
8235 }
8236 table_can_start_source(
8237 lines[index].text,
8238 lines[index + 1].text,
8239 options.constructs.indented_code,
8240 options.constructs.spoiler,
8241 )
8242}
8243
8244pub(crate) fn gfm_table_can_start_source(header: &str, delimiter: &str) -> bool {
8245 table_can_start_source(header, delimiter, true, false)
8246}
8247
8248fn table_can_start_source(
8249 header: &str,
8250 delimiter: &str,
8251 indented_code: bool,
8252 spoiler: bool,
8253) -> bool {
8254 let Some(delimiter) = table_indent_line(delimiter, indented_code) else {
8255 return false;
8256 };
8257 if list_marker_info(delimiter).is_some() {
8258 return false;
8259 }
8260 if !table_has_separator(header, delimiter, spoiler) {
8261 return false;
8262 }
8263 let Some(alignments) = parse_table_delimiter(delimiter, spoiler) else {
8264 return false;
8265 };
8266 split_table_row(header, spoiler).len() == alignments.len()
8267}
8268
8269fn table_has_separator(header: &str, delimiter: &str, spoiler: bool) -> bool {
8270 let Some(alignments) = parse_table_delimiter(delimiter, spoiler) else {
8278 return true;
8279 };
8280 if alignments.len() == 1 {
8281 return contains_unescaped_pipe(header, spoiler)
8282 || contains_unescaped_pipe(delimiter, spoiler)
8283 || delimiter.contains(':');
8284 }
8285 true
8286}
8287
8288fn contains_unescaped_pipe(input: &str, spoiler: bool) -> bool {
8291 !table_row_delimiters(input, spoiler).is_empty()
8292}
8293
8294fn likely_block_start(input: &str, options: &SyntaxOptions) -> bool {
8295 let Some(trimmed) = trim_up_to_three_spaces(input) else {
8300 return false;
8301 };
8302 trimmed.starts_with('#')
8303 || trimmed.starts_with('>')
8304 || trimmed.starts_with("```")
8305 || trimmed.starts_with("~~~")
8306 || list_marker_can_interrupt_paragraph(input)
8307 || parse_thematic_break(Line {
8308 text: input,
8309 eol: "",
8310 start: 0,
8311 end: input.len(),
8312 end_with_eol: input.len(),
8313 lazy: false,
8314 })
8315 .is_some()
8316 || (options.constructs.html_container && line_starts_html_container(input))
8317 || (options.constructs.html_block && line_starts_interrupting_html_block(input))
8318 || (options.constructs.math_block && math_block_fence_length(trimmed).is_some())
8319 || (options.constructs.directive_container && trimmed.starts_with(":::"))
8320 || (options.constructs.directive_leaf && trimmed.starts_with("::"))
8321 || (options.constructs.footnote_definition && line_starts_footnote_definition(trimmed))
8322}
8323
8324fn line_starts_footnote_definition(trimmed: &str) -> bool {
8327 trimmed.starts_with("[^")
8328 && find_footnote_definition_label_end(trimmed)
8329 .is_some_and(|close| is_footnote_label(&trimmed[2..close]))
8330}
8331
8332fn list_marker_can_interrupt_paragraph(input: &str) -> bool {
8333 list_marker_info(input).is_some_and(|marker| {
8334 !marker.content.trim().is_empty() && (!marker.ordered || marker.start == Some(1))
8337 })
8338}
8339
8340fn table_body_line_ends_table(line: &str, options: &SyntaxOptions) -> bool {
8345 likely_block_start(line, options)
8346 || list_marker_info(line).is_some()
8347 || (options.constructs.html_container && line_starts_html_container(line))
8348 || (options.constructs.html_block && line_starts_html_block(line))
8349}
8350
8351fn line_starts_interrupting_html_block(input: &str) -> bool {
8352 match trim_up_to_three_spaces(input).and_then(html_block_start) {
8353 Some(HtmlBlockKind::UntilBlank) | None => false,
8354 Some(_) => true,
8355 }
8356}
8357
8358fn autolink_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
8360 let end = lookups.find(">", index)? + 1;
8361 is_autolink(&input[index..end]).then_some(end)
8362}
8363
8364fn html_inline_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
8367 let rest = &input[index..];
8368 if rest.starts_with("<!--") {
8369 return Some(lookups.find("-->", index)? + 3);
8370 }
8371 if rest.starts_with("<?") {
8372 return Some(lookups.find("?>", index)? + 2);
8373 }
8374 if rest.starts_with("<![CDATA[") {
8375 return Some(lookups.find("]]>", index)? + 3);
8376 }
8377 if is_declaration_start(rest) {
8378 return Some(lookups.find(">", index)? + 1);
8379 }
8380 parse_html_tag_with(lookups, input, index).map(|(end, _)| end)
8381}
8382
8383fn parse_html_tag(input: &str, index: usize) -> Option<(usize, &str)> {
8384 parse_html_tag_with(&mut DirectLookups { input }, input, index)
8385}
8386
8387fn parse_html_tag_with<'a>(
8388 lookups: &mut impl Lookups,
8389 input: &'a str,
8390 index: usize,
8391) -> Option<(usize, &'a str)> {
8392 let bytes = input.as_bytes();
8393 if bytes.get(index) != Some(&b'<') {
8394 return None;
8395 }
8396
8397 let closing = bytes.get(index + 1) == Some(&b'/');
8398 let name_start = index + if closing { 2 } else { 1 };
8399 let first = *bytes.get(name_start)?;
8400 if !first.is_ascii_alphabetic() {
8401 return None;
8402 }
8403
8404 let mut cursor = name_start + 1;
8405 while bytes.get(cursor).is_some_and(|byte| html_name_byte(*byte)) {
8406 cursor += 1;
8407 }
8408 let name = &input[name_start..cursor];
8409
8410 if closing {
8411 cursor = skip_spaces(input, cursor);
8412 if bytes.get(cursor) == Some(&b'>') {
8413 return Some((cursor + 1, name));
8414 }
8415 return None;
8416 }
8417
8418 let mut needs_space = false;
8419 loop {
8420 let before_spaces = cursor;
8421 cursor = skip_spaces(input, cursor);
8422 let had_space = cursor > before_spaces;
8423 match bytes.get(cursor) {
8424 Some(b'>') => return Some((cursor + 1, name)),
8425 Some(b'/') if bytes.get(cursor + 1) == Some(&b'>') => return Some((cursor + 2, name)),
8426 Some(byte) if had_space && html_attribute_name_start(*byte) => {
8427 cursor += 1;
8428 while bytes
8429 .get(cursor)
8430 .is_some_and(|byte| html_attribute_name_byte(*byte))
8431 {
8432 cursor += 1;
8433 }
8434 let after_name = cursor;
8435 let after_spaces = skip_spaces(input, cursor);
8436 if bytes.get(after_spaces) == Some(&b'=') {
8437 cursor = skip_spaces(input, after_spaces + 1);
8438 cursor = parse_html_attribute_value(lookups, input, cursor)?;
8439 } else {
8440 cursor = after_name;
8441 }
8442 needs_space = true;
8443 }
8444 Some(_) if needs_space => return None,
8445 _ => return None,
8446 }
8447 }
8448}
8449
8450fn parse_html_attribute_value(
8451 lookups: &mut impl Lookups,
8452 input: &str,
8453 index: usize,
8454) -> Option<usize> {
8455 let bytes = input.as_bytes();
8456 match bytes.get(index)? {
8457 b'"' => Some(lookups.find("\"", index + 1)? + 1),
8458 b'\'' => Some(lookups.find("'", index + 1)? + 1),
8459 b'=' | b'<' | b'>' | b'`' => None,
8460 _ => {
8461 let mut cursor = index;
8462 while bytes.get(cursor).is_some_and(|byte| {
8463 !byte.is_ascii_whitespace()
8464 && !matches!(*byte, b'"' | b'\'' | b'=' | b'<' | b'>' | b'`')
8465 }) {
8466 cursor += 1;
8467 }
8468 if cursor == index {
8469 None
8470 } else {
8471 Some(cursor)
8472 }
8473 }
8474 }
8475}
8476
8477fn html_name_byte(byte: u8) -> bool {
8478 byte.is_ascii_alphanumeric() || byte == b'-'
8479}
8480
8481fn html_attribute_name_start(byte: u8) -> bool {
8482 byte.is_ascii_alphabetic() || byte == b'_' || byte == b':'
8483}
8484
8485fn html_attribute_name_byte(byte: u8) -> bool {
8486 byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b':' | b'.' | b'-')
8487}
8488
8489fn skip_spaces(input: &str, mut index: usize) -> usize {
8490 while input
8491 .as_bytes()
8492 .get(index)
8493 .is_some_and(|byte| matches!(*byte, b' ' | b'\t' | b'\n' | b'\r'))
8494 {
8495 index += 1;
8496 }
8497 index
8498}
8499
8500fn is_autolink(input: &str) -> bool {
8501 let inner = &input[1..input.len() - 1];
8502 is_uri_autolink(inner) || is_email_autolink(inner)
8503}
8504
8505fn is_uri_autolink(input: &str) -> bool {
8506 let Some(colon) = input
8508 .as_bytes()
8509 .iter()
8510 .take(33)
8511 .position(|byte| *byte == b':')
8512 else {
8513 return false;
8514 };
8515 let scheme = &input[..colon];
8516 if scheme.len() < 2 || scheme.len() > 32 {
8517 return false;
8518 }
8519 let mut bytes = scheme.bytes();
8520 if !bytes.next().is_some_and(|byte| byte.is_ascii_alphabetic()) {
8521 return false;
8522 }
8523 if !bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'.' | b'-')) {
8524 return false;
8525 }
8526 input[colon + 1..]
8527 .chars()
8528 .map(source_char)
8529 .all(|char| !matches!(char, '<' | '>') && !char.is_control() && !char.is_whitespace())
8530}
8531
8532fn is_email_autolink(input: &str) -> bool {
8533 if input
8536 .chars()
8537 .any(|char| char.is_whitespace() || char == '<')
8538 {
8539 return false;
8540 }
8541 let Some(at) = input.find('@') else {
8542 return false;
8543 };
8544 if at == 0 || at + 1 >= input.len() {
8545 return false;
8546 }
8547 is_email_local_part(&input[..at]) && is_email_domain(&input[at + 1..], 1)
8550}
8551
8552fn parse_literal_autolink(
8560 input: &str,
8561 index: usize,
8562 gfm: bool,
8563 relaxed: bool,
8564 scan: &mut LiteralAutolinkScan,
8565) -> Option<(usize, String)> {
8566 let rest = &input[index..];
8567
8568 if gfm {
8569 if let Some(scheme_len) = rest
8572 .starts_with("http://")
8573 .then_some(7)
8574 .or_else(|| rest.starts_with("https://").then_some(8))
8575 {
8576 if !literal_scheme_prefix_ok(input, index) {
8577 return None;
8578 }
8579 let host = &input[index + scheme_len..];
8580 if !http_literal_host_ok(host) {
8583 if relaxed {
8584 } else {
8587 return None;
8588 }
8589 } else {
8590 let end = autolink_url_end(input, index + scheme_len, index + scheme_len, relaxed);
8594 if end <= index + scheme_len {
8595 return None;
8596 }
8597 if literal_autolink_suppressed_by_link_label(
8598 input,
8599 index,
8600 end,
8601 relaxed,
8602 gfm,
8603 &mut scan.label_openers,
8604 ) {
8605 return None;
8606 }
8607 return Some((end, input[index..end].into()));
8608 }
8609 }
8610
8611 if rest
8614 .as_bytes()
8615 .get(..4)
8616 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"www."))
8617 {
8618 if !literal_www_prefix_ok(input, index) {
8619 return None;
8620 }
8621 check_domain(rest, false)?;
8622 let end = autolink_url_end(input, index, index, relaxed);
8623 if end <= index || (!relaxed && end <= index + 3 && !literal_starts_line(input, index))
8624 {
8625 return None;
8626 }
8627 if literal_autolink_suppressed_by_link_label(
8628 input,
8629 index,
8630 end,
8631 relaxed,
8632 gfm,
8633 &mut scan.label_openers,
8634 ) {
8635 return None;
8636 }
8637 let mut destination = String::from("http://");
8638 destination.push_str(&input[index..end]);
8639 return Some((end, destination));
8640 }
8641
8642 if let Some(email) = parse_literal_email(input, index, &mut scan.email_local) {
8643 return Some(email);
8644 }
8645 }
8646
8647 if relaxed {
8648 if literal_scheme_prefix_ok(input, index) {
8655 if let Some(after_slashes) =
8656 relaxed_scheme_after_slashes(input, index, &mut scan.scheme)
8657 {
8658 let body_start = index + after_slashes;
8659 let next = input[body_start..].chars().next();
8660 if next.is_none_or(|char| char.is_whitespace()) && after_slashes == 3 {
8661 return None;
8662 }
8663 let end = autolink_url_end(input, body_start, body_start, true);
8664 if end > index {
8665 if literal_autolink_suppressed_by_link_label(
8666 input,
8667 index,
8668 end,
8669 relaxed,
8670 gfm,
8671 &mut scan.label_openers,
8672 ) {
8673 return None;
8674 }
8675 return Some((end, input[index..end].into()));
8676 }
8677 }
8678 }
8679 }
8680
8681 None
8682}
8683
8684fn relaxed_scheme_after_slashes(
8690 input: &str,
8691 index: usize,
8692 scheme_run: &mut ByteRun,
8693) -> Option<usize> {
8694 let bytes = input[index..].as_bytes();
8695 if bytes.starts_with(b"://") {
8696 return Some(3);
8697 }
8698 let first = bytes.first()?;
8699 if !first.is_ascii_alphabetic() {
8700 return None;
8701 }
8702 let i = scheme_run.end(input, index, is_relaxed_scheme_byte) - index;
8705 if bytes.get(i..i + 3) == Some(b"://") {
8706 Some(i + 3)
8707 } else {
8708 None
8709 }
8710}
8711
8712fn literal_scheme_prefix_ok(input: &str, index: usize) -> bool {
8716 if index == 0 {
8717 return true;
8718 }
8719 let Some(previous) = input[..index].chars().next_back() else {
8720 return true;
8721 };
8722 !previous.is_ascii_alphabetic()
8723}
8724
8725fn literal_www_prefix_ok(input: &str, index: usize) -> bool {
8729 if index == 0 {
8730 return true;
8731 }
8732 let Some(previous) = input[..index].chars().next_back() else {
8733 return true;
8734 };
8735 if matches!(previous, '*' | '_' | '~' | '(' | '[' | ']') {
8736 return true;
8737 }
8738 matches!(previous, ' ' | '\t' | '\n' | '\r')
8739}
8740
8741fn literal_starts_line(input: &str, index: usize) -> bool {
8742 index == 0
8743 || input
8744 .as_bytes()
8745 .get(index - 1)
8746 .is_some_and(|byte| matches!(byte, b'\n' | b'\r'))
8747}
8748
8749fn literal_autolink_suppressed_by_link_label(
8750 input: &str,
8751 index: usize,
8752 end: usize,
8753 relaxed: bool,
8754 gfm_autolink_literal: bool,
8755 label_openers: &mut LabelOpenerScan,
8756) -> bool {
8757 if !label_openers.has_unclosed_opener(input, index) {
8758 return false;
8759 }
8760 if input[end..].starts_with("](") && !link_resource_tail_has_close(input, end + 2) {
8761 return true;
8762 }
8763 !relaxed && !gfm_autolink_literal && input.as_bytes().get(end).is_some_and(|byte| *byte == b']')
8764}
8765
8766#[derive(Default)]
8772struct LabelOpenerScan {
8773 queried: usize,
8776 cursor: usize,
8777 depth: usize,
8778}
8779
8780impl LabelOpenerScan {
8781 fn has_unclosed_opener(&mut self, input: &str, index: usize) -> bool {
8782 if index < self.queried {
8783 *self = Self::default();
8784 }
8785 while self.cursor < index {
8786 let Some((next, char)) = next_char(input, self.cursor) else {
8787 break;
8788 };
8789 match char {
8790 '\\' => {
8791 self.cursor = match next_char(input, next) {
8794 Some((_, '\n' | '\r')) | None => next,
8795 Some((after_escape, _)) => after_escape,
8796 };
8797 continue;
8798 }
8799 '\n' | '\r' => self.depth = 0,
8800 '[' => self.depth += 1,
8801 ']' => self.depth = self.depth.saturating_sub(1),
8802 _ => {}
8803 }
8804 self.cursor = next;
8805 }
8806 self.queried = index;
8807 self.depth > 0
8808 }
8809}
8810
8811fn link_resource_tail_has_close(input: &str, start: usize) -> bool {
8812 let mut cursor = start;
8813 while cursor < input.len() {
8814 let Some((next, char)) = next_char(input, cursor) else {
8815 break;
8816 };
8817 match char {
8818 '\\' => {
8819 cursor = next_char(input, next)
8820 .map(|(after_escape, _)| after_escape)
8821 .unwrap_or(next);
8822 continue;
8823 }
8824 '\n' | '\r' => return false,
8825 ')' => return true,
8826 _ => {}
8827 }
8828 cursor = next;
8829 }
8830 false
8831}
8832
8833fn http_literal_host_ok(host: &str) -> bool {
8834 if host.starts_with('[') {
8835 return bracketed_ipv6_host_end(host).is_some();
8836 }
8837 match host.chars().next() {
8838 Some(char) if char.is_ascii() && char.is_ascii_alphanumeric() => {
8839 check_domain(host, true).is_some()
8840 }
8841 Some(char) if !char.is_ascii() && is_valid_hostchar(source_char(char)) => {
8842 check_domain(host, true).is_some()
8843 }
8844 _ => false,
8845 }
8846}
8847
8848fn bracketed_ipv6_host_end(host: &str) -> Option<usize> {
8849 let close = host.find(']')?;
8850 (close > 1).then_some(close + 1)
8851}
8852
8853fn is_valid_hostchar(char: char) -> bool {
8856 !char.is_whitespace() && !crate::unicode_punctuation::is_unicode_punctuation(char)
8857}
8858
8859fn check_domain(data: &str, allow_short: bool) -> Option<usize> {
8870 let mut np = 0usize;
8871 let mut uscore1 = 0usize;
8872 let mut uscore2 = 0usize;
8873 let mut host_len = 0usize;
8874
8875 let mut chars = data.char_indices().peekable();
8876 while let Some((offset, char)) = chars.next() {
8877 let account = offset != 0 && chars.peek().is_some();
8882 match char {
8883 '\\' => {
8884 host_len = offset + char.len_utf8();
8886 if let Some((next_off, next)) = chars.next() {
8887 host_len = next_off + next.len_utf8();
8888 }
8889 }
8890 '_' if account => {
8891 uscore2 += 1;
8892 host_len = offset + char.len_utf8();
8893 }
8894 '.' if account => {
8895 uscore1 = uscore2;
8896 uscore2 = 0;
8897 np += 1;
8898 host_len = offset + char.len_utf8();
8899 }
8900 '_' | '.' | '-' => {
8901 host_len = offset + char.len_utf8();
8902 }
8903 _ => {
8904 if !is_valid_hostchar(source_char(char)) {
8905 break;
8906 }
8907 host_len = offset + char.len_utf8();
8908 }
8909 }
8910 }
8911
8912 if (uscore1 > 0 || uscore2 > 0) && np <= 10 {
8913 return None;
8914 }
8915
8916 if allow_short || np > 0 {
8917 Some(host_len)
8918 } else {
8919 None
8920 }
8921}
8922
8923fn autolink_url_end(input: &str, start: usize, trim_from: usize, balanced: bool) -> usize {
8929 let bytes = input.as_bytes();
8930 let mut end = start;
8931 let mut bracket_depth = 0i32;
8938 let mut curly_depth = 0i32;
8939 let mut strict_has_open_bracket = false;
8940 let mut strict_inside_backticks = false;
8941 for (offset, char) in input[start..].char_indices() {
8942 if char.is_whitespace() || char == '<' || is_autolink_terminating_control(char) {
8943 break;
8944 }
8945 if balanced {
8946 match char {
8947 '[' => bracket_depth += 1,
8948 ']' => {
8949 if bracket_depth > 0 {
8950 bracket_depth -= 1;
8951 } else {
8952 break;
8953 }
8954 }
8955 '{' => curly_depth += 1,
8956 '}' => {
8957 if curly_depth > 0 {
8958 curly_depth -= 1;
8959 } else {
8960 break;
8961 }
8962 }
8963 _ => {}
8964 }
8965 } else {
8966 match char {
8967 '[' => strict_has_open_bracket = true,
8968 '`' => strict_inside_backticks = !strict_inside_backticks,
8969 ']' if !strict_has_open_bracket && !strict_inside_backticks => break,
8970 _ => {}
8971 }
8972 }
8973 if char == '\\' {
8982 if let Some(&next) = bytes.get(start + offset + 1) {
8983 let next_is_escapable_punct = next.is_ascii_punctuation() && next != b'.';
8984 if next_is_escapable_punct {
8985 break;
8986 }
8987 }
8988 }
8989 end = start + offset + char.len_utf8();
8990 }
8991 autolink_delim(input, trim_from, end)
8992}
8993
8994fn is_autolink_terminating_control(char: char) -> bool {
8995 matches!(char, '\u{2066}'..='\u{2069}')
8996}
8997
8998fn autolink_delim(input: &str, start: usize, mut end: usize) -> usize {
9003 let bytes = input.as_bytes();
9004 let mut opening = 0usize;
9005 let mut closing = 0usize;
9006 for &byte in &bytes[start..end] {
9007 match byte {
9008 b'(' => opening += 1,
9009 b')' => closing += 1,
9010 _ => {}
9011 }
9012 }
9013
9014 while end > start {
9015 match bytes[end - 1] {
9016 b')' => {
9017 if closing <= opening {
9018 break;
9019 }
9020 closing -= 1;
9021 end -= 1;
9022 }
9023 b'?' | b'!' | b'.' | b',' | b':' | b'*' | b'_' | b'~' | b'\'' | b'"' => {
9024 end -= 1;
9025 }
9026 b';' => {
9027 if let Some(amp) = trailing_hex_entity_run_start(bytes, start, end) {
9034 end = amp;
9035 } else {
9036 let mut new_end = end - 1;
9039 while new_end > start && bytes[new_end - 1].is_ascii_alphanumeric() {
9040 new_end -= 1;
9041 }
9042 if new_end > start && new_end < end - 1 && bytes[new_end - 1] == b'&' {
9043 end = new_end - 1;
9044 } else {
9045 end -= 1;
9046 }
9047 }
9048 }
9049 _ => break,
9050 }
9051 }
9052 end
9053}
9054
9055fn trailing_hex_entity_run_start(bytes: &[u8], start: usize, end: usize) -> Option<usize> {
9061 if end <= start || bytes[end - 1] != b';' {
9062 return None;
9063 }
9064 let mut cursor = end - 1;
9065 while cursor > start && bytes[cursor - 1].is_ascii_hexdigit() {
9066 cursor -= 1;
9067 }
9068 if cursor == end - 1 || cursor < start + 3 {
9070 return None;
9071 }
9072 let x = bytes[cursor - 1];
9073 if (x == b'x' || x == b'X') && bytes[cursor - 2] == b'#' && bytes[cursor - 3] == b'&' {
9074 Some(cursor - 3)
9075 } else {
9076 None
9077 }
9078}
9079
9080#[derive(Default)]
9089struct LiteralAutolinkScan {
9090 email_local: ByteRun,
9092 scheme: ByteRun,
9094 label_openers: LabelOpenerScan,
9095}
9096
9097#[derive(Default)]
9101struct ByteRun {
9102 start: usize,
9103 end: Option<usize>,
9104}
9105
9106impl ByteRun {
9107 fn end(&mut self, input: &str, index: usize, in_run: fn(u8) -> bool) -> usize {
9110 if let Some(end) = self.end {
9111 if self.start <= index && index <= end {
9112 return end;
9113 }
9114 }
9115 let end = input.as_bytes()[index..]
9116 .iter()
9117 .position(|byte| !in_run(*byte))
9118 .map_or(input.len(), |offset| index + offset);
9119 self.start = index;
9120 self.end = Some(end);
9121 end
9122 }
9123}
9124
9125fn is_email_local_or_scheme_byte(byte: u8) -> bool {
9126 is_gfm_email_local_byte(byte) || byte == b':'
9127}
9128
9129fn is_relaxed_scheme_byte(byte: u8) -> bool {
9130 byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'.' | b'-')
9131}
9132
9133fn parse_literal_email(
9134 input: &str,
9135 index: usize,
9136 local_run: &mut ByteRun,
9137) -> Option<(usize, String)> {
9138 let rest = &input[index..];
9139 let at_index = local_run.end(input, index, is_email_local_or_scheme_byte);
9142 if input.as_bytes().get(at_index) != Some(&b'@') {
9143 return None;
9144 }
9145 let at = at_index - index;
9146 if at == 0 {
9147 return None;
9148 }
9149 let local = &rest[..at];
9150
9151 let (auto_mailto, is_xmpp) = classify_email_local(local);
9155
9156 if !email_left_boundary_ok(input, index, auto_mailto) {
9161 return None;
9162 }
9163
9164 if !email_local_is_valid(local, auto_mailto) {
9165 return None;
9166 }
9167
9168 let domain_start = index + at + 1;
9169 let domain_end = literal_email_domain_end(input, domain_start, is_xmpp)?;
9170 let trimmed = autolink_delim(input, domain_start, domain_end);
9171 if trimmed <= domain_start {
9172 return None;
9173 }
9174
9175 let domain = &input[domain_start..trimmed];
9176 if !is_gfm_email_domain(domain, is_xmpp) {
9177 return None;
9178 }
9179
9180 let mut destination = String::new();
9181 if auto_mailto {
9182 destination.push_str("mailto:");
9183 }
9184 destination.push_str(&input[index..trimmed]);
9185 Some((trimmed, destination))
9186}
9187
9188fn classify_email_local(local: &str) -> (bool, bool) {
9193 if let Some(rest) = strip_ci_prefix(local, "mailto:") {
9194 if !rest.is_empty() {
9195 return (false, false);
9196 }
9197 }
9198 if let Some(rest) = strip_ci_prefix(local, "xmpp:") {
9199 if !rest.is_empty() {
9200 return (false, true);
9201 }
9202 }
9203 (true, false)
9204}
9205
9206fn strip_ci_prefix<'a>(input: &'a str, prefix: &str) -> Option<&'a str> {
9207 let bytes = input.as_bytes();
9208 let plen = prefix.len();
9209 if bytes.len() >= plen && bytes[..plen].eq_ignore_ascii_case(prefix.as_bytes()) {
9210 Some(&input[plen..])
9211 } else {
9212 None
9213 }
9214}
9215
9216fn email_left_boundary_ok(input: &str, index: usize, auto_mailto: bool) -> bool {
9223 if index == 0 {
9224 return true;
9225 }
9226 let Some(previous) = input[..index].chars().next_back() else {
9227 return true;
9228 };
9229 if previous.is_ascii_alphanumeric() {
9230 if auto_mailto
9231 && input[index..].starts_with('+')
9232 && prefix_ends_with_gfm_email(input, index)
9233 {
9234 return true;
9235 }
9236 return false;
9237 }
9238 if auto_mailto && previous == '/' {
9239 return false;
9240 }
9241 true
9242}
9243
9244fn prefix_ends_with_gfm_email(input: &str, end: usize) -> bool {
9245 let start = input[..end]
9246 .rfind(char::is_whitespace)
9247 .map_or(0, |offset| offset + 1);
9248 let candidate = &input[start..end];
9249 let Some(at) = candidate.rfind('@') else {
9250 return false;
9251 };
9252 email_local_is_valid(&candidate[..at], true) && is_gfm_email_domain(&candidate[at + 1..], false)
9253}
9254
9255fn email_local_is_valid(local: &str, auto_mailto: bool) -> bool {
9259 let body = if auto_mailto {
9260 local
9261 } else if let Some(rest) = strip_ci_prefix(local, "mailto:") {
9262 rest
9263 } else if let Some(rest) = strip_ci_prefix(local, "xmpp:") {
9264 rest
9265 } else {
9266 local
9267 };
9268 !body.is_empty() && body.bytes().all(is_gfm_email_local_byte)
9269}
9270
9271fn is_gfm_email_local_byte(byte: u8) -> bool {
9274 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'+' | b'_' | b'-')
9275}
9276
9277fn is_email_local_part(input: &str) -> bool {
9278 !input.is_empty()
9279 && input
9280 .split('.')
9281 .all(|segment| !segment.is_empty() && segment.bytes().all(is_email_atext))
9282}
9283
9284fn is_email_atext(byte: u8) -> bool {
9285 byte.is_ascii_alphanumeric()
9286 || matches!(
9287 byte,
9288 b'!' | b'#'
9289 | b'$'
9290 | b'%'
9291 | b'&'
9292 | b'\''
9293 | b'*'
9294 | b'+'
9295 | b'/'
9296 | b'='
9297 | b'?'
9298 | b'^'
9299 | b'_'
9300 | b'`'
9301 | b'{'
9302 | b'|'
9303 | b'}'
9304 | b'~'
9305 | b'-'
9306 )
9307}
9308
9309fn literal_email_domain_end(input: &str, index: usize, is_xmpp: bool) -> Option<usize> {
9317 let bytes = input.as_bytes();
9318 let mut end = index;
9319 let mut np = 0usize;
9320 while end < bytes.len() {
9321 let byte = bytes[end];
9322 if byte.is_ascii_alphanumeric() {
9323 end += 1;
9324 } else if byte == b'.' && end + 1 < bytes.len() && bytes[end + 1].is_ascii_alphanumeric() {
9325 np += 1;
9326 end += 1;
9327 } else if byte == b'-' || byte == b'_' || (byte == b'/' && is_xmpp) {
9328 end += 1;
9331 } else {
9332 break;
9333 }
9334 }
9335 if end <= index {
9336 return None;
9337 }
9338 let len = end - index;
9339 let last = bytes[end - 1];
9340 if len < 1 || np == 0 || !(last.is_ascii_alphabetic() || last == b'.') {
9341 return None;
9342 }
9343 Some(end)
9344}
9345
9346fn is_gfm_email_domain(input: &str, is_xmpp: bool) -> bool {
9351 if input.is_empty() {
9352 return false;
9353 }
9354 let host = if is_xmpp {
9357 input.split('/').next().unwrap_or(input)
9358 } else {
9359 input
9360 };
9361 if !host.contains('.') {
9362 return false;
9363 }
9364 let last = host.as_bytes()[host.len() - 1];
9365 if matches!(last, b'-' | b'_') {
9368 return false;
9369 }
9370 host.split('.').all(|label| {
9371 !label.is_empty()
9372 && label
9373 .bytes()
9374 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
9375 })
9376}
9377
9378fn is_email_domain(input: &str, min_labels: usize) -> bool {
9379 let mut label_count = 0usize;
9380 for label in input.split('.') {
9381 label_count += 1;
9382 let bytes = label.as_bytes();
9383 if bytes.is_empty()
9384 || bytes.len() > 63
9385 || !bytes
9386 .first()
9387 .is_some_and(|byte| byte.is_ascii_alphanumeric())
9388 || !bytes
9389 .last()
9390 .is_some_and(|byte| byte.is_ascii_alphanumeric())
9391 || !bytes
9392 .iter()
9393 .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-')
9394 {
9395 return false;
9396 }
9397 }
9398 label_count >= min_labels
9399}
9400
9401fn is_footnote_label(label: &str) -> bool {
9402 !label.is_empty()
9403 && reference_label_is_within_limit(label)
9404 && !label.chars().any(char::is_whitespace)
9405}
9406
9407fn find_footnote_definition_label_end(input: &str) -> Option<usize> {
9408 let close = find_footnote_reference_label_end(input, 2)?;
9409 if input.as_bytes().get(close + 1) == Some(&b':') {
9410 Some(close)
9411 } else {
9412 None
9413 }
9414}
9415
9416fn find_footnote_reference_label_end(input: &str, mut cursor: usize) -> Option<usize> {
9417 while cursor < input.len() {
9418 let (next, char) = next_char(input, cursor)?;
9419 if char == ']' && !is_escaped_at(input, cursor) {
9420 return Some(cursor);
9421 }
9422 cursor = next;
9423 }
9424 None
9425}