1use alloc::{borrow::Cow, string::String, vec, vec::Vec};
7
8use crate::{
9 ast::*,
10 diagnostic::{Diagnostic, DiagnosticCode, DiagnosticSeverity},
11 entities::named_character_reference,
12 options::{SyntaxConfigError, SyntaxOptions},
13 span::Span,
14 validate::is_directive_name,
15};
16
17#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct ParseOutput {
21 pub document: Document,
23 pub diagnostics: Vec<Diagnostic>,
25}
26
27#[derive(Clone, Debug, Eq, PartialEq)]
29pub enum ParseStrictError {
30 Config(SyntaxConfigError),
32 Diagnostic(Diagnostic),
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
37struct ParsedLinkResource {
38 destination: String,
39 destination_kind: LinkDestinationKind,
40 title: Option<String>,
41 title_kind: Option<LinkTitleKind>,
42}
43
44const REFERENCE_LABEL_MAX_CHARS: usize = 999;
45const WIKILINK_MAX_BYTES: usize = 999;
46
47#[derive(Clone, Copy, Debug)]
48struct Line<'a> {
49 text: &'a str,
50 eol: &'a str,
51 start: usize,
52 end: usize,
53 end_with_eol: usize,
54 lazy: bool,
59}
60
61#[derive(Clone, Copy, Debug)]
62struct ListMarkerInfo<'a> {
63 ordered: bool,
64 start: Option<u64>,
65 delimiter: ListDelimiter,
66 indent: usize,
67 marker_len: usize,
68 content_indent: usize,
69 content: &'a str,
70}
71
72#[derive(Clone, Copy, Debug)]
73struct DescriptionMarker<'a> {
74 content_offset: usize,
75 content: &'a str,
76}
77
78#[derive(Clone, Debug)]
79struct DescriptionTerm {
80 marker_index: usize,
81 term_end: usize,
82 blank_after_term: bool,
83 source: String,
84 source_offset: usize,
85}
86
87#[derive(Clone, Copy, Debug, Eq, PartialEq)]
88enum HtmlBlockKind {
89 RawTag,
90 BlockTag,
91 Until(&'static str),
92 UntilBlank,
93}
94
95pub fn parse(input: &str) -> ParseOutput {
98 SyntaxOptions::default().parse(input)
99}
100
101impl SyntaxOptions {
102 pub fn parse(&self, input: &str) -> ParseOutput {
107 match parse_checked(input, self) {
108 Ok(output) => output,
109 Err(error) => ParseOutput {
110 document: Document::default(),
111 diagnostics: vec![Diagnostic::new(
112 DiagnosticSeverity::Error,
113 DiagnosticCode::StrictParse,
114 Span::new(0, input.len()),
115 error.message(),
116 )],
117 },
118 }
119 }
120
121 pub fn parse_strict(&self, input: &str) -> Result<ParseOutput, ParseStrictError> {
124 let output = parse_checked(input, self).map_err(ParseStrictError::Config)?;
125 if let Some(diagnostic) = output
126 .diagnostics
127 .iter()
128 .find(|diagnostic| diagnostic.severity == DiagnosticSeverity::Error)
129 {
130 return Err(ParseStrictError::Diagnostic(diagnostic.clone()));
131 }
132 Ok(output)
133 }
134}
135
136fn parse_checked(input: &str, options: &SyntaxOptions) -> Result<ParseOutput, SyntaxConfigError> {
137 options.validate()?;
138 let mut diagnostics = Vec::new();
139 let definitions = collect_definitions(input, options);
140 let children = parse_blocks(input, 0, true, options, &definitions, &mut diagnostics);
141
142 Ok(ParseOutput {
143 document: Document {
144 meta: NodeMeta::new(Some(Span::new(0, input.len()))),
145 children,
146 },
147 diagnostics,
148 })
149}
150
151fn parse_blocks(
152 input: &str,
153 base_offset: usize,
154 allow_frontmatter: bool,
155 options: &SyntaxOptions,
156 definitions: &[String],
157 diagnostics: &mut Vec<Diagnostic>,
158) -> Vec<Block> {
159 let lines = collect_lines(input, base_offset);
160 parse_blocks_from_lines(&lines, allow_frontmatter, options, definitions, diagnostics)
161}
162
163fn parse_blocks_from_lines(
164 lines: &[Line<'_>],
165 allow_frontmatter: bool,
166 options: &SyntaxOptions,
167 definitions: &[String],
168 diagnostics: &mut Vec<Diagnostic>,
169) -> Vec<Block> {
170 let mut blocks = Vec::new();
171 let mut index = 0;
172
173 while index < lines.len() {
174 let line = lines[index];
175 if line.text.trim().is_empty() {
176 index += 1;
177 continue;
178 }
179 let after_definition_unbroken = index > 0
180 && !lines[index - 1].text.trim().is_empty()
181 && matches!(blocks.last(), Some(Block::Definition(_)));
182
183 if allow_frontmatter && index == 0 {
184 if let Some((block, next)) = parse_frontmatter(lines, index, options) {
185 blocks.push(block);
186 index = next;
187 continue;
188 }
189 }
190
191 if let Some((block, next)) =
192 parse_container_directive(lines, index, options, definitions, diagnostics)
193 {
194 blocks.push(block);
195 index = next;
196 continue;
197 }
198
199 if let Some((block, next)) = parse_math_block(lines, index, options) {
200 blocks.push(block);
201 index = next;
202 continue;
203 }
204
205 if let Some((block, next)) = parse_fenced_code(lines, index, options) {
206 blocks.push(block);
207 index = next;
208 continue;
209 }
210
211 if let Some((block, next)) =
212 parse_block_quote(lines, index, options, definitions, diagnostics)
213 {
214 blocks.push(block);
215 index = next;
216 continue;
217 }
218
219 if let Some(block) = parse_atx_heading(line, options, definitions) {
220 blocks.push(block);
221 index += 1;
222 continue;
223 }
224
225 if let Some(block) = parse_thematic_break(line) {
226 blocks.push(block);
227 index += 1;
228 continue;
229 }
230
231 if let Some((block, next)) = parse_list(lines, index, options, definitions, diagnostics) {
232 blocks.push(block);
233 index = next;
234 continue;
235 }
236
237 if let Some((block, next)) =
238 parse_footnote_definition(lines, index, options, definitions, diagnostics)
239 {
240 blocks.push(block);
241 index = next;
242 continue;
243 }
244
245 if let Some((block, next)) =
246 parse_definition(lines, index, options, after_definition_unbroken)
247 {
248 blocks.push(block);
249 index = next;
250 continue;
251 }
252
253 if let Some(block) = parse_leaf_directive(line, options, definitions, diagnostics) {
254 blocks.push(block);
255 index += 1;
256 continue;
257 }
258
259 if let Some((block, next)) =
260 parse_html_container(lines, index, options, definitions, diagnostics)
261 {
262 blocks.push(block);
263 index = next;
264 continue;
265 }
266
267 if let Some((block, next)) = parse_html_block(lines, index, options) {
268 blocks.push(block);
269 index = next;
270 continue;
271 }
272
273 if let Some((block, next)) = parse_mdx_flow(lines, index, options, diagnostics) {
274 blocks.push(block);
275 index = next;
276 continue;
277 }
278
279 if !after_definition_unbroken {
280 if let Some((block, next)) = parse_indented_code(lines, index, options) {
281 blocks.push(block);
282 index = next;
283 continue;
284 }
285 }
286
287 if let Some((block, next)) = parse_table(lines, index, options, definitions, diagnostics) {
288 blocks.push(block);
289 index = next;
290 continue;
291 }
292
293 if let Some((block, next)) = parse_setext_heading(lines, index, options, definitions) {
294 blocks.push(block);
295 index = next;
296 continue;
297 }
298
299 if let Some((block, next)) =
300 parse_description_list(lines, index, options, definitions, diagnostics)
301 {
302 blocks.push(block);
303 index = next;
304 continue;
305 }
306
307 let (block, next) = parse_paragraph(lines, index, options, definitions, diagnostics);
308 blocks.push(block);
309 index = next;
310 }
311
312 blocks
313}
314
315fn collect_lines(input: &str, base_offset: usize) -> Vec<Line<'_>> {
316 let bytes = input.as_bytes();
317 let mut lines = Vec::new();
318 let mut start = 0;
319 let mut index = 0;
320
321 while index < bytes.len() {
322 match bytes[index] {
323 b'\n' => {
324 let end = index;
325 lines.push(Line {
326 text: &input[start..end],
327 eol: &input[index..index + 1],
328 start: base_offset + start,
329 end: base_offset + end,
330 end_with_eol: base_offset + index + 1,
331 lazy: false,
332 });
333 index += 1;
334 start = index;
335 }
336 b'\r' => {
337 let end = index;
338 let eol_end = if index + 1 < bytes.len() && bytes[index + 1] == b'\n' {
339 index + 2
340 } else {
341 index + 1
342 };
343 lines.push(Line {
344 text: &input[start..end],
345 eol: &input[index..eol_end],
346 start: base_offset + start,
347 end: base_offset + end,
348 end_with_eol: base_offset + eol_end,
349 lazy: false,
350 });
351 index = eol_end;
352 start = index;
353 }
354 _ => index += 1,
355 }
356 }
357
358 if start < bytes.len() || input.is_empty() {
359 lines.push(Line {
360 text: &input[start..],
361 eol: "",
362 start: base_offset + start,
363 end: base_offset + bytes.len(),
364 end_with_eol: base_offset + bytes.len(),
365 lazy: false,
366 });
367 }
368
369 lines
370}
371
372fn collect_definitions(input: &str, options: &SyntaxOptions) -> Vec<String> {
373 let mut diagnostics = Vec::new();
374 let blocks = parse_blocks(input, 0, true, options, &[], &mut diagnostics);
375 let mut definitions = Vec::new();
376 collect_definition_refs_from_blocks(&blocks, &mut definitions);
377 definitions
378}
379
380fn collect_definition_refs_from_blocks(blocks: &[Block], definitions: &mut Vec<String>) {
381 for block in blocks {
382 match block {
383 Block::Definition(definition) => {
384 if definitions
385 .iter()
386 .all(|identifier| identifier != &definition.identifier)
387 {
388 definitions.push(definition.identifier.clone());
389 }
390 }
391 Block::BlockQuote(node) => {
392 collect_definition_refs_from_blocks(&node.children, definitions);
393 }
394 Block::Alert(node) => {
395 collect_definition_refs_from_blocks(&node.children, definitions);
396 }
397 Block::List(node) => {
398 for item in &node.children {
399 collect_definition_refs_from_blocks(&item.children, definitions);
400 }
401 }
402 Block::DescriptionList(node) => {
403 for item in &node.children {
404 for details in &item.details {
405 collect_definition_refs_from_blocks(&details.children, definitions);
406 }
407 }
408 }
409 Block::FootnoteDefinition(node) => {
410 collect_definition_refs_from_blocks(&node.children, definitions);
411 }
412 Block::HtmlContainer(node) => {
413 if let HtmlContainerContent::Blocks(children) = &node.content {
414 collect_definition_refs_from_blocks(children, definitions);
415 }
416 }
417 Block::ContainerDirective(node) => {
418 collect_definition_refs_from_blocks(&node.children, definitions);
419 }
420 _ => {}
421 }
422 }
423}
424
425fn parse_frontmatter(
426 lines: &[Line<'_>],
427 index: usize,
428 options: &SyntaxOptions,
429) -> Option<(Block, usize)> {
430 if !options.constructs.frontmatter {
431 return None;
432 }
433 let kind = frontmatter_fence_kind(lines[index].text)?;
434
435 let mut value = String::new();
436 let mut cursor = index + 1;
437 while cursor < lines.len() {
438 if frontmatter_fence_kind(lines[cursor].text) == Some(kind) {
439 let span = Span::new(lines[index].start, lines[cursor].end_with_eol);
440 return Some((
441 Block::Frontmatter(Frontmatter {
442 meta: NodeMeta::new(Some(span)),
443 kind,
444 value,
445 }),
446 cursor + 1,
447 ));
448 }
449 push_line(&mut value, lines[cursor].text);
450 cursor += 1;
451 }
452
453 None
454}
455
456fn frontmatter_fence_kind(line: &str) -> Option<FrontmatterKind> {
457 match line.trim_end_matches([' ', '\t']) {
458 "---" => Some(FrontmatterKind::Yaml),
459 "+++" => Some(FrontmatterKind::Toml),
460 _ => None,
461 }
462}
463
464fn parse_container_directive(
465 lines: &[Line<'_>],
466 index: usize,
467 options: &SyntaxOptions,
468 definitions: &[String],
469 diagnostics: &mut Vec<Diagnostic>,
470) -> Option<(Block, usize)> {
471 if !options.constructs.directive_container {
472 return None;
473 }
474 let trimmed = trim_up_to_three_spaces(lines[index].text)?;
475 let Some((fence_len, opener_rest)) = directive_container_opener_prefix(trimmed) else {
476 return None;
477 };
478 let opener_base = lines[index].start + (lines[index].text.len() - trimmed.len()) + fence_len;
479
480 let Some((name, label_source, attributes, _consumed)) = parse_directive_opener(opener_rest)
481 else {
482 diagnostics.push(Diagnostic::new(
483 DiagnosticSeverity::Error,
484 DiagnosticCode::InvalidDirectiveName,
485 Span::new(lines[index].start, lines[index].end),
486 "container directive must have a valid name",
487 ));
488 return None;
489 };
490 let label_base = opener_base + name.len() + 1;
491
492 let mut content = String::new();
493 let mut cursor = index + 1;
494 let mut nested_fences = Vec::new();
495 while cursor < lines.len() {
496 let line = lines[cursor].text;
497 let trimmed = trim_up_to_three_spaces(line);
498 if let Some(trimmed) = trimmed {
499 if let Some(nested_len) = nested_fences.last().copied() {
500 if directive_container_closing_fence(trimmed, nested_len).is_some() {
501 nested_fences.pop();
502 push_line(&mut content, line);
503 cursor += 1;
504 continue;
505 }
506 } else if directive_container_closing_fence(trimmed, fence_len).is_some() {
507 let label = label_source
508 .map(|source| {
509 parse_inlines(source, label_base, options, definitions, diagnostics)
510 })
511 .unwrap_or_default();
512 let children = parse_blocks(
513 &content,
514 lines[index + 1].start,
515 false,
516 options,
517 definitions,
518 diagnostics,
519 );
520 return Some((
521 Block::ContainerDirective(ContainerDirective {
522 meta: NodeMeta::new(Some(Span::new(
523 lines[index].start,
524 lines[cursor].end_with_eol,
525 ))),
526 name,
527 label,
528 attributes,
529 children,
530 }),
531 cursor + 1,
532 ));
533 }
534
535 if let Some((nested_len, nested_rest)) = directive_container_opener_prefix(trimmed) {
536 if parse_directive_opener(nested_rest).is_some() {
537 nested_fences.push(nested_len);
538 }
539 }
540 }
541
542 push_line(&mut content, line);
543 cursor += 1;
544 }
545
546 diagnostics.push(Diagnostic::new(
547 DiagnosticSeverity::Error,
548 DiagnosticCode::UnclosedDirectiveContainer,
549 Span::new(lines[index].start, lines[index].end),
550 "container directive is missing a closing fence",
551 ));
552 Some((
553 Block::ContainerDirective(ContainerDirective {
554 meta: NodeMeta::new(Some(Span::new(
555 lines[index].start,
556 lines.last()?.end_with_eol,
557 ))),
558 name,
559 label: label_source
560 .map(|source| parse_inlines(source, label_base, options, definitions, diagnostics))
561 .unwrap_or_default(),
562 attributes,
563 children: parse_blocks(
564 &content,
565 lines
566 .get(index + 1)
567 .map(|line| line.start)
568 .unwrap_or(lines[index].end),
569 false,
570 options,
571 definitions,
572 diagnostics,
573 ),
574 }),
575 lines.len(),
576 ))
577}
578
579fn directive_container_opener_prefix(input: &str) -> Option<(usize, &str)> {
580 let fence_len = input
581 .as_bytes()
582 .iter()
583 .take_while(|byte| **byte == b':')
584 .count();
585 if fence_len >= 3 {
586 Some((fence_len, &input[fence_len..]))
587 } else {
588 None
589 }
590}
591
592fn directive_container_closing_fence(input: &str, min_len: usize) -> Option<usize> {
593 let fence_len = input
594 .as_bytes()
595 .iter()
596 .take_while(|byte| **byte == b':')
597 .count();
598 if fence_len >= min_len && input[fence_len..].trim().is_empty() {
599 Some(fence_len)
600 } else {
601 None
602 }
603}
604
605fn parse_math_block(
606 lines: &[Line<'_>],
607 index: usize,
608 options: &SyntaxOptions,
609) -> Option<(Block, usize)> {
610 if !options.constructs.math_block {
611 return None;
612 }
613 let opener = trim_up_to_three_spaces(lines[index].text)?;
619 let fence_length = math_block_fence_length(opener)?;
620 let opening_indent = leading_indent_columns(lines[index].text);
621
622 let mut value = String::new();
623 let mut content_lines = 0usize;
624 let mut cursor = index + 1;
625 while cursor < lines.len() {
626 if let Some(close_line) = trim_up_to_three_spaces(lines[cursor].text) {
627 if math_block_fence_closes(close_line, fence_length) {
628 return Some((
629 Block::MathBlock(MathBlock {
630 meta: NodeMeta::new(Some(Span::new(
631 lines[index].start,
632 lines[cursor].end_with_eol,
633 ))),
634 value,
635 }),
636 cursor + 1,
637 ));
638 }
639 }
640 if content_lines > 0 {
641 ensure_line_separator(&mut value);
645 }
646 let stripped = strip_leading_indent_columns(lines[cursor].text, opening_indent);
647 value.push_str(&stripped);
648 value.push_str(lines[cursor].eol);
649 content_lines += 1;
650 cursor += 1;
651 }
652
653 Some((
656 Block::MathBlock(MathBlock {
657 meta: NodeMeta::new(Some(Span::new(
658 lines[index].start,
659 lines.last()?.end_with_eol,
660 ))),
661 value,
662 }),
663 lines.len(),
664 ))
665}
666
667fn math_block_fence_length(input: &str) -> Option<usize> {
670 let length = input
671 .as_bytes()
672 .iter()
673 .take_while(|byte| **byte == b'$')
674 .count();
675 if length < 2 || input[length..].contains('$') {
676 return None;
677 }
678 Some(length)
679}
680
681fn math_block_fence_closes(input: &str, length: usize) -> bool {
684 let count = input
685 .as_bytes()
686 .iter()
687 .take_while(|byte| **byte == b'$')
688 .count();
689 count >= length && input[count..].trim().is_empty()
690}
691
692fn parse_fenced_code(
693 lines: &[Line<'_>],
694 index: usize,
695 options: &SyntaxOptions,
696) -> Option<(Block, usize)> {
697 let line = fence_line(lines[index].text, options)?;
698 let (marker, length) = fence_start(line)?;
699 let opening_indent = leading_indent_columns(lines[index].text);
702 let info = line[length..].trim();
703 if marker == FenceMarker::Backtick && info.contains('`') {
704 return None;
705 }
706 let info = if info.is_empty() {
707 None
708 } else {
709 Some(unescape_string(info))
710 };
711
712 let mut value = String::new();
713 let mut content_lines = 0usize;
718 let mut cursor = index + 1;
719 while cursor < lines.len() {
720 if let Some(close_line) = fence_line(lines[cursor].text, options) {
721 if fence_close(close_line, marker, length) {
722 return Some((
723 Block::CodeBlock(CodeBlock {
724 meta: NodeMeta::new(Some(Span::new(
725 lines[index].start,
726 lines[cursor].end_with_eol,
727 ))),
728 kind: CodeBlockKind::Fenced { marker, length },
729 info,
730 value,
731 }),
732 cursor + 1,
733 ));
734 }
735 }
736 if content_lines > 0 {
737 ensure_line_separator(&mut value);
741 }
742 let stripped = strip_leading_indent_columns(lines[cursor].text, opening_indent);
743 value.push_str(&stripped);
744 value.push_str(lines[cursor].eol);
745 content_lines += 1;
746 cursor += 1;
747 }
748 Some((
749 Block::CodeBlock(CodeBlock {
750 meta: NodeMeta::new(Some(Span::new(
751 lines[index].start,
752 lines.last()?.end_with_eol,
753 ))),
754 kind: CodeBlockKind::Fenced { marker, length },
755 info,
756 value,
757 }),
758 lines.len(),
759 ))
760}
761
762fn fence_line<'a>(line: &'a str, options: &SyntaxOptions) -> Option<&'a str> {
763 if options.constructs.indented_code {
764 trim_up_to_three_spaces(line)
765 } else {
766 Some(trim_ascii_start(line))
767 }
768}
769
770fn container_closed_after_unclosed_fence(
771 lines: &[Line<'_>],
772 cursor: usize,
773 last_content_index: usize,
774 content: &str,
775 options: &SyntaxOptions,
776) -> bool {
777 !lines[last_content_index].eol.is_empty()
778 && (cursor >= lines.len() || lines[cursor].text.trim().is_empty())
779 && content_has_unclosed_fenced_code(content, options)
780}
781
782fn content_has_unclosed_fenced_code(content: &str, options: &SyntaxOptions) -> bool {
783 let lines = collect_lines(content, 0);
784 let mut open_fence = None;
785 for line in lines {
786 let Some(trimmed) = fence_line(line.text, options) else {
787 continue;
788 };
789 if let Some((marker, length, has_nonblank_content)) = open_fence {
790 if fence_close(trimmed, marker, length) {
791 open_fence = None;
792 } else {
793 open_fence = Some((
794 marker,
795 length,
796 has_nonblank_content || !trimmed.trim().is_empty(),
797 ));
798 }
799 continue;
800 }
801 let Some((marker, length)) = fence_start(trimmed) else {
802 continue;
803 };
804 let info = trimmed[length..].trim();
805 if marker != FenceMarker::Backtick || !info.contains('`') {
806 open_fence = Some((marker, length, false));
807 }
808 }
809 open_fence.is_some_and(|(_, _, has_nonblank_content)| !has_nonblank_content)
810}
811
812fn block_quote_content_paragraph_open(content: &str, options: &SyntaxOptions) -> bool {
822 let Some(trimmed) = trim_up_to_three_spaces(content) else {
823 return false;
825 };
826 if trimmed.is_empty() {
827 return false;
828 }
829 if let Some(rest) = trimmed.strip_prefix('>') {
830 let rest = rest.strip_prefix(' ').unwrap_or(rest);
831 return block_quote_content_paragraph_open(rest, options);
832 }
833 if let Some(marker) = list_marker_info(trimmed) {
834 let first_content = list_marker_first_content(trimmed, marker);
835 return block_quote_content_paragraph_open(&first_content, options);
836 }
837 !lazy_line_starts_block(trimmed, options)
838}
839
840fn lazy_line_starts_block(input: &str, options: &SyntaxOptions) -> bool {
846 likely_block_start(input, options)
847 || (options.constructs.html_block && line_starts_html_block(input))
848 || trim_up_to_three_spaces(input).is_some_and(|t| t.starts_with('`') || t.starts_with('~'))
853}
854
855fn parse_block_quote(
856 lines: &[Line<'_>],
857 index: usize,
858 options: &SyntaxOptions,
859 definitions: &[String],
860 diagnostics: &mut Vec<Diagnostic>,
861) -> Option<(Block, usize)> {
862 if !trim_up_to_three_spaces(lines[index].text)?.starts_with('>') {
863 return None;
864 }
865
866 let mut content = String::new();
867 let mut lazy_flags: Vec<bool> = Vec::new();
871 let mut cursor = index;
872 let mut paragraph_open = false;
873 let mut in_table = false;
874 let mut last_content_line: Option<String> = None;
875 let mut content_base_offset = None;
876 while cursor < lines.len() {
877 let raw = lines[cursor].text;
878 let trimmed_opt = trim_up_to_three_spaces(raw);
879 let marked = trimmed_opt.is_some_and(|trimmed| trimmed.starts_with('>'));
880 let quote_rest_owned: String;
881 if let Some(trimmed) = trimmed_opt {
882 if trimmed.is_empty() {
883 break;
884 }
885 }
886 let (line, line_start) = if marked {
887 let trimmed = trimmed_opt.expect("marked implies a trimmed line");
888 let trimmed_start = lines[cursor].start + (raw.len() - trimmed.len());
889 let mut rest_start = 1;
890 let mut rest = &trimmed[rest_start..];
891 if rest.starts_with(' ') {
892 rest_start += 1;
893 rest = &rest[1..];
894 } else if rest.starts_with('\t') {
895 let marker_end_column = leading_indent_columns(raw) + 1;
896 match strip_leading_indent_columns_from(rest, 1, marker_end_column) {
897 Cow::Borrowed(stripped) => rest = stripped,
898 Cow::Owned(stripped) => {
899 quote_rest_owned = stripped;
900 rest = "e_rest_owned;
901 }
902 }
903 }
904 (rest, trimmed_start + rest_start)
905 } else if in_table {
906 break;
909 } else if paragraph_open && !lazy_line_starts_block(raw, options) {
910 (raw, lines[cursor].start)
914 } else {
915 break;
916 };
917
918 let mut escaped_lazy = String::new();
919 let line = if !marked
920 && last_content_line.as_deref().is_some_and(|previous| {
921 table_can_start_source(
922 previous,
923 line,
924 options.constructs.indented_code,
925 options.constructs.spoiler,
926 )
927 }) {
928 escaped_lazy.push_str(line);
929 if let Some(offset) = escaped_lazy.find('-') {
930 escaped_lazy.insert(offset, '\\');
931 }
932 &escaped_lazy
933 } else {
934 line
935 };
936
937 let starts_table = last_content_line.as_deref().is_some_and(|previous| {
938 table_can_start_source(
939 previous,
940 line,
941 options.constructs.indented_code,
942 options.constructs.spoiler,
943 )
944 });
945 if marked && starts_table {
946 paragraph_open = false;
947 in_table = true;
948 } else if marked && in_table && block_quote_table_body_row(line, options) {
949 paragraph_open = false;
950 } else {
951 in_table = false;
952 paragraph_open = block_quote_content_paragraph_open(line, options);
955 }
956 last_content_line = Some(line.into());
957 if content_base_offset.is_none() {
958 content_base_offset = Some(line_start);
959 }
960 push_line(&mut content, line);
961 lazy_flags.push(!marked);
962 cursor += 1;
963 }
964
965 let span = Span::new(lines[index].start, lines[cursor - 1].end_with_eol);
966 let child_base_offset = content_base_offset.unwrap_or(lines[index].start);
967 if !lines[cursor - 1].eol.is_empty() && !ends_with_line_ending(&content) {
968 content.push_str(lines[cursor - 1].eol);
969 }
970 if container_closed_after_unclosed_fence(lines, cursor, cursor - 1, &content, options) {
971 content.push('\n');
972 }
973 if let Some(alert) = parse_alert_from_block_quote(
974 &content,
975 child_base_offset,
976 span,
977 options,
978 definitions,
979 diagnostics,
980 ) {
981 return Some((alert, cursor));
982 }
983
984 let mut child_lines = collect_lines(&content, child_base_offset);
985 for (child, &lazy) in child_lines.iter_mut().zip(lazy_flags.iter()) {
986 child.lazy = lazy;
987 }
988 let children = parse_blocks_from_lines(&child_lines, false, options, definitions, diagnostics);
989 Some((
990 Block::BlockQuote(BlockQuote {
991 meta: NodeMeta::new(Some(span)),
992 children,
993 }),
994 cursor,
995 ))
996}
997
998fn parse_alert_from_block_quote(
999 content: &str,
1000 base_offset: usize,
1001 span: Span,
1002 options: &SyntaxOptions,
1003 definitions: &[String],
1004 diagnostics: &mut Vec<Diagnostic>,
1005) -> Option<Block> {
1006 if !options.constructs.gfm_alert {
1007 return None;
1008 }
1009 let (first_line, rest) = content.split_once('\n').unwrap_or((content, ""));
1010 let (kind, title) = parse_alert_marker(first_line)?;
1011 let rest_base_offset = base_offset + first_line.len() + usize::from(!rest.is_empty());
1012 let children = if rest.is_empty() {
1013 Vec::new()
1014 } else {
1015 parse_blocks(
1016 rest,
1017 rest_base_offset,
1018 false,
1019 options,
1020 definitions,
1021 diagnostics,
1022 )
1023 };
1024 Some(Block::Alert(Alert {
1025 meta: NodeMeta::new(Some(span)),
1026 kind,
1027 title,
1028 children,
1029 }))
1030}
1031
1032fn parse_alert_marker(line: &str) -> Option<(AlertKind, Option<String>)> {
1033 let close = line.find(']')?;
1034 let marker = line.get(0..close + 1)?;
1035 if !marker.starts_with("[!") {
1036 return None;
1037 }
1038 let kind = match &marker[2..close].to_ascii_lowercase()[..] {
1039 "note" => AlertKind::Note,
1040 "tip" => AlertKind::Tip,
1041 "important" => AlertKind::Important,
1042 "warning" => AlertKind::Warning,
1043 "caution" => AlertKind::Caution,
1044 _ => return None,
1045 };
1046 let title = line[close + 1..].trim();
1047 Some((
1048 kind,
1049 if title.is_empty() {
1050 None
1051 } else {
1052 Some(title.into())
1053 },
1054 ))
1055}
1056
1057fn block_quote_table_body_row(line: &str, options: &SyntaxOptions) -> bool {
1058 table_indent_line(line, options.constructs.indented_code).is_some_and(|row| {
1059 !row.trim().is_empty() && contains_unescaped_pipe(row, options.constructs.spoiler)
1060 })
1061}
1062
1063fn parse_list(
1064 lines: &[Line<'_>],
1065 index: usize,
1066 options: &SyntaxOptions,
1067 definitions: &[String],
1068 diagnostics: &mut Vec<Diagnostic>,
1069) -> Option<(Block, usize)> {
1070 let first_marker = list_marker_info(lines[index].text)?;
1071 let mut items = Vec::new();
1072 let mut cursor = index;
1073 let mut tight = true;
1074
1075 while cursor < lines.len() {
1076 if parse_thematic_break(lines[cursor]).is_some() {
1081 break;
1082 }
1083 let Some(marker) = list_marker_info(lines[cursor].text) else {
1084 break;
1085 };
1086 if !same_list_marker(first_marker, marker) {
1087 break;
1088 }
1089
1090 let item_start = cursor;
1091 let mut item_end = cursor;
1092 let mut item_tight = true;
1093 let mut item_blank_offsets: Vec<usize> = Vec::new();
1099 let mut content = String::new();
1100 let mut lazy_flags: Vec<bool> = Vec::new();
1107 let mut open_fence = None;
1108 let first_content = list_marker_first_content(lines[cursor].text, marker);
1109 let mut last_content_line: Option<String> = Some(first_content.as_ref().into());
1110 let mut paragraph_open = list_item_paragraph_stays_open(None, &first_content, options);
1111 let mut item_started_blank = first_content.trim().is_empty();
1116 push_line(&mut content, &first_content);
1117 lazy_flags.push(false);
1118 update_list_item_fence(&first_content, &mut open_fence);
1119 cursor += 1;
1120
1121 while cursor < lines.len() {
1122 if lines[cursor].text.trim().is_empty() {
1123 if open_fence.is_some() {
1126 let stripped = strip_list_continuation(
1127 lines[cursor].text,
1128 marker.content_indent,
1129 first_marker.indent,
1130 );
1131 push_line(&mut content, &stripped);
1132 lazy_flags.push(false);
1133 update_list_item_fence(&stripped, &mut open_fence);
1134 item_end = cursor;
1135 cursor += 1;
1136 continue;
1137 }
1138 let next = next_nonblank_line(lines, cursor + 1);
1139 if item_started_blank
1140 || next >= lines.len()
1141 || sibling_list_marker_at_line(
1142 lines[next].text,
1143 first_marker,
1144 marker.content_indent,
1145 )
1146 || leading_indent_columns(lines[next].text) < marker.content_indent
1147 {
1148 if next < lines.len()
1149 && sibling_list_marker_at_line(
1150 lines[next].text,
1151 first_marker,
1152 marker.content_indent,
1153 )
1154 {
1155 item_tight = false;
1156 }
1157 cursor = next;
1158 break;
1159 }
1160 item_blank_offsets.push(content.len() + usize::from(!content.is_empty()));
1168 paragraph_open = false;
1169 push_line(&mut content, "");
1170 lazy_flags.push(false);
1171 item_end = cursor;
1172 cursor += 1;
1173 continue;
1174 }
1175
1176 item_started_blank = false;
1177
1178 if sibling_list_marker_at_line(lines[cursor].text, first_marker, marker.content_indent)
1179 {
1180 break;
1181 }
1182
1183 if leading_indent_columns(lines[cursor].text) < marker.content_indent
1188 && !same_list_marker_line(lines[cursor].text, first_marker)
1189 && list_marker_info(lines[cursor].text).is_some()
1190 {
1191 break;
1192 }
1193
1194 if leading_indent_columns(lines[cursor].text) < marker.content_indent {
1195 if likely_block_start(lines[cursor].text, options) || !paragraph_open {
1196 break;
1197 }
1198 }
1199
1200 let lazy = paragraph_open
1206 && leading_indent_columns(lines[cursor].text) < marker.content_indent;
1207 let stripped = strip_list_continuation(
1208 lines[cursor].text,
1209 marker.content_indent,
1210 first_marker.indent,
1211 );
1212 let starts_table = last_content_line.as_deref().is_some_and(|previous| {
1213 table_can_start_source(
1214 previous,
1215 &stripped,
1216 options.constructs.indented_code,
1217 options.constructs.spoiler,
1218 )
1219 });
1220 paragraph_open = if starts_table {
1221 false
1222 } else {
1223 list_item_paragraph_stays_open(Some(paragraph_open), &stripped, options)
1224 };
1225 push_line(&mut content, &stripped);
1226 lazy_flags.push(lazy);
1227 update_list_item_fence(&stripped, &mut open_fence);
1228 last_content_line = Some(stripped.into_owned());
1229 item_end = cursor;
1230 cursor += 1;
1231 }
1232
1233 let child_base = lines[item_start].start + marker.content_indent;
1234 if !lines[item_end].eol.is_empty() && !ends_with_line_ending(&content) {
1235 content.push_str(lines[item_end].eol);
1236 }
1237 if container_closed_after_unclosed_fence(lines, cursor, item_end, &content, options) {
1238 content.push('\n');
1239 }
1240 let mut child_lines = collect_lines(&content, child_base);
1241 for (child, &lazy) in child_lines.iter_mut().zip(lazy_flags.iter()) {
1242 child.lazy = lazy;
1243 }
1244 let mut children =
1245 parse_blocks_from_lines(&child_lines, false, options, definitions, diagnostics);
1246 let checked = if options.constructs.gfm_task_list_item {
1247 take_task_marker_from_children(&mut children)
1248 } else {
1249 None
1250 };
1251
1252 if item_tight
1253 && blank_separates_top_level_blocks(&item_blank_offsets, &children, child_base)
1254 {
1255 item_tight = false;
1256 }
1257 tight = tight && item_tight;
1258 items.push(ListItem {
1259 meta: NodeMeta::new(Some(Span::new(
1260 lines[item_start].start,
1261 lines[item_end].end_with_eol,
1262 ))),
1263 checked,
1264 children,
1265 });
1266 }
1267
1268 Some((
1269 Block::List(List {
1270 meta: NodeMeta::new(Some(Span::new(
1271 lines[index].start,
1272 lines[cursor - 1].end_with_eol,
1273 ))),
1274 ordered: first_marker.ordered,
1275 start: first_marker.start,
1276 delimiter: first_marker.delimiter,
1277 tight,
1278 children: items,
1279 }),
1280 cursor,
1281 ))
1282}
1283
1284fn blank_separates_top_level_blocks(
1297 blank_offsets: &[usize],
1298 children: &[Block],
1299 child_base: usize,
1300) -> bool {
1301 if blank_offsets.is_empty() || children.len() < 2 {
1302 return false;
1303 }
1304 let Some(&first_blank) = blank_offsets.iter().min() else {
1305 return false;
1306 };
1307 children.iter().any(|child| {
1308 block_span(child).is_some_and(|span| span.start.saturating_sub(child_base) > first_blank)
1309 })
1310}
1311
1312fn block_span(block: &Block) -> Option<Span> {
1313 let meta = match block {
1314 Block::Paragraph(node) => &node.meta,
1315 Block::Heading(node) => &node.meta,
1316 Block::ThematicBreak(node) => &node.meta,
1317 Block::BlockQuote(node) => &node.meta,
1318 Block::Alert(node) => &node.meta,
1319 Block::List(node) => &node.meta,
1320 Block::DescriptionList(node) => &node.meta,
1321 Block::CodeBlock(node) => &node.meta,
1322 Block::HtmlBlock(node) => &node.meta,
1323 Block::HtmlContainer(node) => &node.meta,
1324 Block::Definition(node) => &node.meta,
1325 Block::FootnoteDefinition(node) => &node.meta,
1326 Block::Table(node) => &node.meta,
1327 Block::MathBlock(node) => &node.meta,
1328 Block::Frontmatter(node) => &node.meta,
1329 Block::MdxEsm(node) => &node.meta,
1330 Block::MdxExpression(node) => &node.meta,
1331 Block::MdxJsx(node) => &node.meta,
1332 Block::LeafDirective(node) => &node.meta,
1333 Block::ContainerDirective(node) => &node.meta,
1334 };
1335 meta.span
1336}
1337
1338fn list_item_paragraph_stays_open(
1339 previous_open: Option<bool>,
1340 line: &str,
1341 options: &SyntaxOptions,
1342) -> bool {
1343 if line.trim().is_empty() {
1344 return false;
1345 }
1346 if previous_open == Some(false) {
1347 return false;
1348 }
1349 block_quote_content_paragraph_open(line, options)
1350}
1351
1352fn parse_description_list(
1353 lines: &[Line<'_>],
1354 index: usize,
1355 options: &SyntaxOptions,
1356 definitions: &[String],
1357 diagnostics: &mut Vec<Diagnostic>,
1358) -> Option<(Block, usize)> {
1359 if !options.constructs.description_list || !is_description_term_line(lines[index].text, options)
1360 {
1361 return None;
1362 }
1363
1364 let mut cursor = index;
1365 let mut items = Vec::new();
1366 let mut tight = true;
1367 let mut list_end = lines[index].end_with_eol;
1368
1369 while cursor < lines.len() {
1370 if !is_description_term_line(lines[cursor].text, options) {
1371 break;
1372 }
1373 let Some(term) = description_term(lines, cursor, options) else {
1374 break;
1375 };
1376 let term_line = lines[cursor];
1377 let mut details = Vec::new();
1378 let item_start = term_line.start;
1379 let mut item_end = lines[term.term_end].end_with_eol;
1380 tight = tight && !term.blank_after_term;
1381 cursor = term.marker_index;
1382
1383 loop {
1384 let Some(marker) = description_marker(lines[cursor].text) else {
1385 break;
1386 };
1387 let (detail, next, detail_tight) = parse_description_details(
1388 lines,
1389 cursor,
1390 marker,
1391 options,
1392 definitions,
1393 diagnostics,
1394 )?;
1395 tight = tight && detail_tight;
1396 item_end = detail
1397 .meta
1398 .span
1399 .map(|span| span.end)
1400 .unwrap_or(lines[cursor].end_with_eol);
1401 details.push(detail);
1402 cursor = next;
1403
1404 let next_nonblank = next_nonblank_line(lines, cursor);
1405 if next_nonblank < lines.len()
1406 && description_marker(lines[next_nonblank].text).is_some()
1407 {
1408 if next_nonblank != cursor {
1409 tight = false;
1410 }
1411 cursor = next_nonblank;
1412 continue;
1413 }
1414 break;
1415 }
1416
1417 if details.is_empty() {
1418 return None;
1419 }
1420 list_end = item_end;
1421 items.push(DescriptionItem {
1422 meta: NodeMeta::new(Some(Span::new(item_start, item_end))),
1423 term: parse_inlines(
1424 &term.source,
1425 term.source_offset,
1426 options,
1427 definitions,
1428 diagnostics,
1429 ),
1430 details,
1431 });
1432
1433 let next_item = next_nonblank_line(lines, cursor);
1434 if next_item >= lines.len() {
1435 cursor = next_item;
1436 break;
1437 }
1438 if description_term(lines, next_item, options).is_some() {
1439 if next_item != cursor {
1440 tight = false;
1441 }
1442 cursor = next_item;
1443 continue;
1444 }
1445 cursor = next_item;
1446 break;
1447 }
1448
1449 (!items.is_empty()).then_some((
1450 Block::DescriptionList(DescriptionList {
1451 meta: NodeMeta::new(Some(Span::new(lines[index].start, list_end))),
1452 tight,
1453 children: items,
1454 }),
1455 cursor,
1456 ))
1457}
1458
1459fn parse_description_details(
1460 lines: &[Line<'_>],
1461 index: usize,
1462 marker: DescriptionMarker<'_>,
1463 options: &SyntaxOptions,
1464 definitions: &[String],
1465 diagnostics: &mut Vec<Diagnostic>,
1466) -> Option<(DescriptionDetails, usize, bool)> {
1467 let mut content = String::new();
1468 push_line(&mut content, marker.content);
1469 let mut cursor = index + 1;
1470 let mut end = lines[index].end_with_eol;
1471 let mut tight = true;
1472 let mut paragraph_open = paragraph_stays_open(marker.content, options);
1473
1474 while cursor < lines.len() {
1475 if lines[cursor].text.trim().is_empty() {
1476 let next = next_nonblank_line(lines, cursor + 1);
1477 if next >= lines.len() || description_term(lines, next, options).is_some() {
1484 cursor = next;
1485 break;
1486 }
1487 if description_marker(lines[next].text).is_some() {
1488 tight = false;
1489 cursor = next;
1490 break;
1491 }
1492 if strip_indent_continuation(lines[next].text).is_none() {
1493 break;
1494 }
1495 push_line(&mut content, "");
1496 paragraph_open = false;
1497 tight = false;
1498 end = lines[cursor].end_with_eol;
1499 cursor += 1;
1500 continue;
1501 }
1502
1503 if description_marker(lines[cursor].text).is_some()
1504 || description_term(lines, cursor, options).is_some()
1505 {
1506 break;
1507 }
1508
1509 let continuation = if let Some(continuation) = strip_indent_continuation(lines[cursor].text)
1510 {
1511 continuation
1512 } else if paragraph_open && !likely_block_start(lines[cursor].text, options) {
1513 trim_ascii_start(lines[cursor].text)
1514 } else {
1515 break;
1516 };
1517 paragraph_open = paragraph_stays_open(continuation, options);
1518 push_line(&mut content, continuation);
1519 end = lines[cursor].end_with_eol;
1520 cursor += 1;
1521 }
1522
1523 if content.trim().is_empty() {
1524 return None;
1525 }
1526
1527 Some((
1528 DescriptionDetails {
1529 meta: NodeMeta::new(Some(Span::new(lines[index].start, end))),
1530 children: parse_blocks(
1531 &content,
1532 lines[index].start + marker.content_offset,
1533 false,
1534 options,
1535 definitions,
1536 diagnostics,
1537 ),
1538 },
1539 cursor,
1540 tight,
1541 ))
1542}
1543
1544fn description_term(
1545 lines: &[Line<'_>],
1546 term_index: usize,
1547 options: &SyntaxOptions,
1548) -> Option<DescriptionTerm> {
1549 if term_index >= lines.len() || !is_description_term_line(lines[term_index].text, options) {
1550 return None;
1551 }
1552 let mut source = String::new();
1553 let mut term_end = term_index;
1554 let mut cursor = term_index;
1555 while cursor < lines.len() && is_description_term_line(lines[cursor].text, options) {
1556 if !source.is_empty() {
1557 source.push('\n');
1558 }
1559 source.push_str(trim_ascii_start(lines[cursor].text).trim_end());
1560 term_end = cursor;
1561 cursor += 1;
1562 }
1563
1564 let mut marker_index = cursor;
1565 let mut blank_after_term = false;
1566 while marker_index < lines.len() && lines[marker_index].text.trim().is_empty() {
1567 blank_after_term = true;
1568 marker_index += 1;
1569 }
1570 (marker_index < lines.len() && description_marker(lines[marker_index].text).is_some()).then(
1571 || DescriptionTerm {
1572 marker_index,
1573 term_end,
1574 blank_after_term,
1575 source,
1576 source_offset: lines[term_index].start + leading_trim_bytes(lines[term_index].text),
1577 },
1578 )
1579}
1580
1581fn is_description_term_line(line: &str, options: &SyntaxOptions) -> bool {
1582 leading_indent_columns(line) <= 3
1583 && !line.trim().is_empty()
1584 && description_marker(line).is_none()
1585 && !likely_block_start(line, options)
1586}
1587
1588fn description_marker(line: &str) -> Option<DescriptionMarker<'_>> {
1589 let (columns, bytes) = leading_indent(line);
1590 if columns > 2 || !matches!(line.as_bytes().get(bytes), Some(b':' | b'~')) {
1591 return None;
1592 }
1593 if line
1594 .as_bytes()
1595 .get(bytes + 1)
1596 .is_some_and(|byte| !matches!(*byte, b' ' | b'\t'))
1597 {
1598 return None;
1599 }
1600 let mut content_offset = bytes + 1;
1601 while line
1602 .as_bytes()
1603 .get(content_offset)
1604 .is_some_and(|byte| matches!(*byte, b' ' | b'\t'))
1605 {
1606 content_offset += 1;
1607 }
1608 Some(DescriptionMarker {
1609 content_offset,
1610 content: &line[content_offset..],
1611 })
1612}
1613
1614fn paragraph_stays_open(line: &str, options: &SyntaxOptions) -> bool {
1618 !line.trim().is_empty() && !likely_block_start(line, options)
1619}
1620
1621fn strip_indent_continuation(input: &str) -> Option<&str> {
1623 input
1624 .strip_prefix(" ")
1625 .or_else(|| input.strip_prefix('\t'))
1626}
1627
1628fn parse_atx_heading(
1629 line: Line<'_>,
1630 options: &SyntaxOptions,
1631 definitions: &[String],
1632) -> Option<Block> {
1633 let text = trim_up_to_three_spaces(line.text)?;
1634 let depth = text
1635 .as_bytes()
1636 .iter()
1637 .take_while(|byte| **byte == b'#')
1638 .count();
1639 if depth == 0 || depth > 6 {
1640 return None;
1641 }
1642 if text
1643 .as_bytes()
1644 .get(depth)
1645 .is_some_and(|byte| !matches!(*byte, b' ' | b'\t'))
1646 && text.len() != depth
1647 {
1648 return None;
1649 }
1650 let after_opening = &text[depth..];
1651 let content_start_in_text = depth + leading_trim_bytes(after_opening);
1652 let content = trim_closing_hashes(after_opening.trim_start());
1653 let content_start = line.start + (line.text.len() - text.len()) + content_start_in_text;
1654 Some(Block::Heading(Heading {
1655 meta: NodeMeta::new(Some(Span::new(line.start, line.end))),
1656 depth: depth as u8,
1657 kind: HeadingKind::Atx,
1658 children: parse_inlines(
1659 content,
1660 content_start,
1661 options,
1662 definitions,
1663 &mut Vec::new(),
1664 ),
1665 }))
1666}
1667
1668fn parse_thematic_break(line: Line<'_>) -> Option<Block> {
1669 let text = trim_up_to_three_spaces(line.text)?.trim();
1670 let mut marker = None;
1671 let mut count = 0;
1672 for char in text.chars() {
1673 if char == ' ' || char == '\t' {
1674 continue;
1675 }
1676 let current = match char {
1677 '-' => ThematicBreakMarker::Dash,
1678 '*' => ThematicBreakMarker::Asterisk,
1679 '_' => ThematicBreakMarker::Underscore,
1680 _ => return None,
1681 };
1682 if marker.is_some_and(|marker| marker != current) {
1683 return None;
1684 }
1685 marker = Some(current);
1686 count += 1;
1687 }
1688 if count >= 3 {
1689 Some(Block::ThematicBreak(ThematicBreak {
1690 meta: NodeMeta::new(Some(Span::new(line.start, line.end))),
1691 marker: marker?,
1692 }))
1693 } else {
1694 None
1695 }
1696}
1697
1698fn parse_definition(
1699 lines: &[Line<'_>],
1700 index: usize,
1701 options: &SyntaxOptions,
1702 allow_subsequent_indent: bool,
1703) -> Option<(Block, usize)> {
1704 let line = lines[index];
1705 let text = trim_definition_start(line.text, allow_subsequent_indent)?;
1706 if !text.starts_with('[') {
1707 return None;
1708 }
1709
1710 let mut accumulated = String::from(text);
1718 let mut label_end_line = index;
1719 let close = loop {
1720 if let Some(close) = find_reference_label_end(&accumulated, 0) {
1721 if accumulated.as_bytes().get(close + 1) == Some(&b':') {
1722 break close;
1723 }
1724 return None;
1726 }
1727 let next = label_end_line + 1;
1728 if next >= lines.len() || lines[next].text.trim().is_empty() {
1729 return None;
1730 }
1731 if likely_block_start(lines[next].text, options)
1738 || setext_underline_depth(lines[next].text).is_some()
1739 || table_can_start(lines, next, options)
1740 {
1741 return None;
1742 }
1743 accumulated.push('\n');
1744 accumulated.push_str(lines[next].text);
1745 label_end_line = next;
1746 };
1747 let label = String::from(&accumulated[1..close]);
1748 if normalize_label(&label).is_empty() {
1749 return None;
1750 }
1751 let label = label.as_str();
1752 let mut source = String::from(&accumulated[close + 2..]);
1753 let mut cursor = label_end_line;
1754 let mut best_without_title = None;
1755
1756 loop {
1757 if let Some(resource) = parse_definition_destination_title(&source) {
1758 if resource.title.is_some() {
1759 return Some((
1760 Block::Definition(Definition {
1761 meta: NodeMeta::new(Some(Span::new(
1762 line.start,
1763 lines[cursor].end_with_eol,
1764 ))),
1765 label: label.into(),
1766 identifier: normalize_label(label),
1767 destination: resource.destination,
1768 destination_kind: resource.destination_kind,
1769 title: resource.title,
1770 title_kind: resource.title_kind,
1771 }),
1772 cursor + 1,
1773 ));
1774 }
1775
1776 best_without_title = Some((resource, cursor + 1));
1777 let next = cursor + 1;
1778 if next >= lines.len()
1779 || lines[next].text.trim().is_empty()
1780 || !line_can_start_definition_title(lines[next].text)
1781 {
1782 break;
1783 }
1784 }
1785
1786 let next = cursor + 1;
1787 if next >= lines.len() || lines[next].text.trim().is_empty() {
1788 break;
1789 }
1790 if likely_block_start(lines[next].text, options)
1796 || setext_underline_depth(lines[next].text).is_some()
1797 {
1798 break;
1799 }
1800 source.push('\n');
1801 source.push_str(lines[next].text);
1802 cursor = next;
1803 }
1804
1805 let (resource, next) = best_without_title?;
1806 let end = lines[next - 1].end_with_eol;
1807 Some((
1808 Block::Definition(Definition {
1809 meta: NodeMeta::new(Some(Span::new(line.start, end))),
1810 label: label.into(),
1811 identifier: normalize_label(label),
1812 destination: resource.destination,
1813 destination_kind: resource.destination_kind,
1814 title: resource.title,
1815 title_kind: resource.title_kind,
1816 }),
1817 next,
1818 ))
1819}
1820
1821fn trim_definition_start(input: &str, allow_subsequent_indent: bool) -> Option<&str> {
1822 if let Some(trimmed) = trim_up_to_three_spaces(input) {
1823 return Some(trimmed);
1824 }
1825 if allow_subsequent_indent {
1826 let (columns, bytes) = leading_indent(input);
1827 if columns == 4 {
1828 return Some(&input[bytes..]);
1829 }
1830 }
1831 None
1832}
1833
1834fn parse_footnote_definition(
1835 lines: &[Line<'_>],
1836 index: usize,
1837 options: &SyntaxOptions,
1838 definitions: &[String],
1839 diagnostics: &mut Vec<Diagnostic>,
1840) -> Option<(Block, usize)> {
1841 if !options.constructs.footnote_definition {
1842 return None;
1843 }
1844 let line = lines[index];
1845 let text = line.text.trim();
1846 if !text.starts_with("[^") {
1847 return None;
1848 }
1849 let close = find_footnote_definition_label_end(text)?;
1850 let label = &text[2..close];
1851 if !is_footnote_label(label) {
1852 return None;
1853 }
1854 let rest = text[close + 2..].trim();
1855 let mut content = String::new();
1856 push_line(&mut content, rest);
1857 let mut cursor = index + 1;
1858 let mut end = line.end_with_eol;
1859 let mut paragraph_open = paragraph_stays_open(rest, options);
1860
1861 while cursor < lines.len() {
1862 if lines[cursor].text.trim().is_empty() {
1863 let next = next_nonblank_line(lines, cursor + 1);
1864 if next >= lines.len() || !is_footnote_continuation(lines[next].text) {
1865 break;
1866 }
1867 push_line(&mut content, "");
1868 paragraph_open = false;
1869 end = lines[cursor].end_with_eol;
1870 cursor += 1;
1871 continue;
1872 }
1873
1874 let continuation = if let Some(continuation) = strip_indent_continuation(lines[cursor].text)
1875 {
1876 continuation
1877 } else if paragraph_open && !likely_block_start(lines[cursor].text, options) {
1878 trim_ascii_start(lines[cursor].text)
1879 } else {
1880 break;
1881 };
1882 paragraph_open = paragraph_stays_open(continuation, options);
1883 push_line(&mut content, continuation);
1884 end = lines[cursor].end_with_eol;
1885 cursor += 1;
1886 }
1887
1888 Some((
1889 Block::FootnoteDefinition(FootnoteDefinition {
1890 meta: NodeMeta::new(Some(Span::new(line.start, end))),
1891 label: label.into(),
1892 identifier: normalize_label(label),
1893 children: parse_blocks(
1894 &content,
1895 line.end.saturating_sub(rest.len()),
1896 false,
1897 options,
1898 definitions,
1899 diagnostics,
1900 ),
1901 }),
1902 cursor,
1903 ))
1904}
1905
1906fn is_footnote_continuation(input: &str) -> bool {
1907 strip_indent_continuation(input).is_some()
1908}
1909
1910fn parse_leaf_directive(
1911 line: Line<'_>,
1912 options: &SyntaxOptions,
1913 definitions: &[String],
1914 diagnostics: &mut Vec<Diagnostic>,
1915) -> Option<Block> {
1916 if !options.constructs.directive_leaf {
1917 return None;
1918 }
1919 let trimmed = line.text.trim_start();
1920 if trimmed.starts_with(":::") || !trimmed.starts_with("::") {
1921 return None;
1922 }
1923 let opener_base = line.start + (line.text.len() - trimmed.len()) + 2;
1924 let Some((name, label_source, attributes, _)) = parse_directive_opener(&trimmed[2..]) else {
1925 diagnostics.push(Diagnostic::new(
1926 DiagnosticSeverity::Error,
1927 DiagnosticCode::InvalidDirectiveName,
1928 Span::new(line.start, line.end),
1929 "leaf directive must have a valid name",
1930 ));
1931 return None;
1932 };
1933 let label = label_source
1934 .map(|source| {
1935 parse_inlines(
1936 source,
1937 opener_base + name.len() + 1,
1938 options,
1939 definitions,
1940 diagnostics,
1941 )
1942 })
1943 .unwrap_or_default();
1944 Some(Block::LeafDirective(LeafDirective {
1945 meta: NodeMeta::new(Some(Span::new(line.start, line.end))),
1946 name,
1947 label,
1948 attributes,
1949 }))
1950}
1951
1952fn parse_html_container(
1953 lines: &[Line<'_>],
1954 index: usize,
1955 options: &SyntaxOptions,
1956 definitions: &[String],
1957 diagnostics: &mut Vec<Diagnostic>,
1958) -> Option<(Block, usize)> {
1959 if !options.constructs.html_container {
1960 return None;
1961 }
1962
1963 let (opening, leading_summary) =
1964 parse_details_container_opening_line(lines[index], options, definitions, diagnostics)?;
1965 let close_index = find_html_container_close(lines, index + 1, "details")?;
1966 let closing =
1967 parse_html_container_tag_line(lines[close_index], "details", HtmlContainerTag::Closing)?;
1968 let children = parse_details_container_children(
1969 &lines[index + 1..close_index],
1970 leading_summary,
1971 options,
1972 definitions,
1973 diagnostics,
1974 );
1975
1976 Some((
1977 Block::HtmlContainer(HtmlContainer {
1978 meta: NodeMeta::new(Some(Span::new(
1979 opening
1980 .meta
1981 .span
1982 .map(|span| span.start)
1983 .unwrap_or(lines[index].start),
1984 lines[close_index].end_with_eol,
1985 ))),
1986 opening,
1987 content: HtmlContainerContent::Blocks(children),
1988 closing,
1989 }),
1990 close_index + 1,
1991 ))
1992}
1993
1994#[derive(Clone, Copy, Eq, PartialEq)]
1995enum HtmlContainerTag {
1996 Opening,
1997 Closing,
1998}
1999
2000#[derive(Clone, Copy)]
2001struct HtmlContainerFence {
2002 marker: FenceMarker,
2003 length: usize,
2004}
2005
2006fn parse_details_container_children(
2007 lines: &[Line<'_>],
2008 leading_summary: Option<Block>,
2009 options: &SyntaxOptions,
2010 definitions: &[String],
2011 diagnostics: &mut Vec<Diagnostic>,
2012) -> Vec<Block> {
2013 let mut children = Vec::new();
2014 if let Some(summary) = leading_summary {
2015 children.push(summary);
2016 children.extend(parse_blocks_from_lines(
2017 lines,
2018 false,
2019 options,
2020 definitions,
2021 diagnostics,
2022 ));
2023 return children;
2024 }
2025
2026 let Some(summary_index) = lines.iter().position(|line| !line.text.trim().is_empty()) else {
2027 return children;
2028 };
2029
2030 if let Some((summary, next)) =
2031 parse_summary_container(lines, summary_index, options, definitions, diagnostics)
2032 {
2033 children.push(summary);
2034 children.extend(parse_blocks_from_lines(
2035 &lines[next..],
2036 false,
2037 options,
2038 definitions,
2039 diagnostics,
2040 ));
2041 return children;
2042 }
2043
2044 parse_blocks_from_lines(lines, false, options, definitions, diagnostics)
2045}
2046
2047fn parse_summary_container(
2048 lines: &[Line<'_>],
2049 index: usize,
2050 options: &SyntaxOptions,
2051 definitions: &[String],
2052 diagnostics: &mut Vec<Diagnostic>,
2053) -> Option<(Block, usize)> {
2054 let line = lines[index];
2055 let (trimmed, indent_bytes) = trim_html_container_line(line.text)?;
2056 parse_summary_container_source(
2057 trimmed,
2058 line.start + indent_bytes,
2059 options,
2060 definitions,
2061 diagnostics,
2062 )
2063 .map(|block| (block, index + 1))
2064}
2065
2066fn parse_details_container_opening_line(
2067 line: Line<'_>,
2068 options: &SyntaxOptions,
2069 definitions: &[String],
2070 diagnostics: &mut Vec<Diagnostic>,
2071) -> Option<(HtmlTag, Option<Block>)> {
2072 let (trimmed, indent_bytes) = trim_html_container_line(line.text)?;
2073 let (open_end, name) = parse_html_tag(trimmed, 0)?;
2074 if !name.eq_ignore_ascii_case("details")
2075 || html_tag_is_closing(trimmed, 0)
2076 || html_tag_is_self_closing(&trimmed[..open_end])
2077 {
2078 return None;
2079 }
2080
2081 let tag_start = line.start + indent_bytes;
2082 let rest_start = open_end + leading_ascii_whitespace_len(&trimmed[open_end..]);
2083 let summary = if trimmed[rest_start..].is_empty() {
2084 None
2085 } else {
2086 Some(parse_summary_container_source(
2087 &trimmed[rest_start..],
2088 tag_start + rest_start,
2089 options,
2090 definitions,
2091 diagnostics,
2092 )?)
2093 };
2094
2095 Some((
2096 HtmlTag {
2097 meta: NodeMeta::new(Some(Span::new(tag_start, tag_start + open_end))),
2098 name: "details".into(),
2099 raw: trimmed[..open_end].into(),
2100 },
2101 summary,
2102 ))
2103}
2104
2105fn parse_summary_container_source(
2106 source: &str,
2107 base_offset: usize,
2108 options: &SyntaxOptions,
2109 definitions: &[String],
2110 diagnostics: &mut Vec<Diagnostic>,
2111) -> Option<Block> {
2112 let (open_end, open_name) = parse_html_tag(source, 0)?;
2113 if !open_name.eq_ignore_ascii_case("summary")
2114 || html_tag_is_closing(source, 0)
2115 || html_tag_is_self_closing(&source[..open_end])
2116 {
2117 return None;
2118 }
2119
2120 let close_start = source[open_end..]
2121 .find("</")
2122 .map(|offset| open_end + offset)?;
2123 let (close_end, close_name) = parse_html_tag(source, close_start)?;
2124 if !close_name.eq_ignore_ascii_case("summary")
2125 || !html_tag_is_closing(source, close_start)
2126 || !source[close_end..].trim().is_empty()
2127 {
2128 return None;
2129 }
2130
2131 let content = &source[open_end..close_start];
2132 let content_base = base_offset + open_end;
2133 let opening = HtmlTag {
2134 meta: NodeMeta::new(Some(Span::new(base_offset, base_offset + open_end))),
2135 name: "summary".into(),
2136 raw: source[..open_end].into(),
2137 };
2138 let closing = HtmlTag {
2139 meta: NodeMeta::new(Some(Span::new(
2140 base_offset + close_start,
2141 base_offset + close_end,
2142 ))),
2143 name: "summary".into(),
2144 raw: source[close_start..close_end].into(),
2145 };
2146
2147 Some(Block::HtmlContainer(HtmlContainer {
2148 meta: NodeMeta::new(Some(Span::new(base_offset, base_offset + source.len()))),
2149 opening,
2150 content: HtmlContainerContent::Inlines(parse_inlines(
2151 content,
2152 content_base,
2153 options,
2154 definitions,
2155 diagnostics,
2156 )),
2157 closing,
2158 }))
2159}
2160
2161fn find_html_container_close(lines: &[Line<'_>], mut cursor: usize, tag: &str) -> Option<usize> {
2162 let mut depth = 1usize;
2163 let mut fence = None;
2164
2165 while cursor < lines.len() {
2166 let line = lines[cursor].text;
2167 if let Some(open_fence) = fence {
2168 if html_container_fence_closes(line, open_fence) {
2169 fence = None;
2170 }
2171 cursor += 1;
2172 continue;
2173 }
2174
2175 if let Some(open_fence) = html_container_fence_opens(line) {
2176 fence = Some(open_fence);
2177 cursor += 1;
2178 continue;
2179 }
2180
2181 if parse_html_container_tag_line(lines[cursor], tag, HtmlContainerTag::Closing).is_some() {
2182 depth -= 1;
2183 if depth == 0 {
2184 return Some(cursor);
2185 }
2186 } else if parse_html_container_opening_line(lines[cursor], tag).is_some() {
2187 depth += 1;
2188 }
2189
2190 cursor += 1;
2191 }
2192
2193 None
2194}
2195
2196fn parse_html_container_tag_line(
2197 line: Line<'_>,
2198 tag: &str,
2199 kind: HtmlContainerTag,
2200) -> Option<HtmlTag> {
2201 let (trimmed, indent_bytes) = trim_html_container_line(line.text)?;
2202 let (end, name) = parse_html_tag(trimmed, 0)?;
2203 if !name.eq_ignore_ascii_case(tag) || !trimmed[end..].trim().is_empty() {
2204 return None;
2205 }
2206
2207 let closing = html_tag_is_closing(trimmed, 0);
2208 if (kind == HtmlContainerTag::Opening && closing)
2209 || (kind == HtmlContainerTag::Closing && !closing)
2210 || html_tag_is_self_closing(&trimmed[..end])
2211 {
2212 return None;
2213 }
2214
2215 let start = line.start + indent_bytes;
2216 Some(HtmlTag {
2217 meta: NodeMeta::new(Some(Span::new(start, start + end))),
2218 name: tag.into(),
2219 raw: trimmed[..end].into(),
2220 })
2221}
2222
2223fn parse_html_container_opening_line(line: Line<'_>, tag: &str) -> Option<HtmlTag> {
2224 let (trimmed, indent_bytes) = trim_html_container_line(line.text)?;
2225 let (end, name) = parse_html_tag(trimmed, 0)?;
2226 if !name.eq_ignore_ascii_case(tag)
2227 || html_tag_is_closing(trimmed, 0)
2228 || html_tag_is_self_closing(&trimmed[..end])
2229 {
2230 return None;
2231 }
2232
2233 let rest_start = end + leading_ascii_whitespace_len(&trimmed[end..]);
2234 if !trimmed[rest_start..].is_empty() && !html_container_line_has_summary(&trimmed[rest_start..])
2235 {
2236 return None;
2237 }
2238
2239 let start = line.start + indent_bytes;
2240 Some(HtmlTag {
2241 meta: NodeMeta::new(Some(Span::new(start, start + end))),
2242 name: tag.into(),
2243 raw: trimmed[..end].into(),
2244 })
2245}
2246
2247fn html_container_line_has_summary(input: &str) -> bool {
2248 let Some((open_end, name)) = parse_html_tag(input, 0) else {
2249 return false;
2250 };
2251 if !name.eq_ignore_ascii_case("summary")
2252 || html_tag_is_closing(input, 0)
2253 || html_tag_is_self_closing(&input[..open_end])
2254 {
2255 return false;
2256 }
2257 let Some(close_start) = input[open_end..].find("</").map(|offset| open_end + offset) else {
2258 return false;
2259 };
2260 let Some((close_end, close_name)) = parse_html_tag(input, close_start) else {
2261 return false;
2262 };
2263 close_name.eq_ignore_ascii_case("summary")
2264 && html_tag_is_closing(input, close_start)
2265 && input[close_end..].trim().is_empty()
2266}
2267
2268fn trim_html_container_line(input: &str) -> Option<(&str, usize)> {
2269 let trimmed = trim_up_to_three_spaces(input)?;
2270 Some((trimmed, input.len() - trimmed.len()))
2271}
2272
2273fn leading_ascii_whitespace_len(input: &str) -> usize {
2274 input
2275 .as_bytes()
2276 .iter()
2277 .take_while(|byte| byte.is_ascii_whitespace())
2278 .count()
2279}
2280
2281fn html_tag_is_closing(input: &str, index: usize) -> bool {
2282 input.as_bytes().get(index + 1) == Some(&b'/')
2283}
2284
2285fn html_tag_is_self_closing(input: &str) -> bool {
2286 input.trim_end().ends_with("/>")
2287}
2288
2289fn html_container_fence_opens(line: &str) -> Option<HtmlContainerFence> {
2290 let trimmed = trim_up_to_three_spaces(line)?;
2291 let (marker, length) = fence_start(trimmed)?;
2292 Some(HtmlContainerFence { marker, length })
2293}
2294
2295fn html_container_fence_closes(line: &str, fence: HtmlContainerFence) -> bool {
2296 trim_up_to_three_spaces(line)
2297 .is_some_and(|trimmed| fence_close(trimmed, fence.marker, fence.length))
2298}
2299
2300fn parse_html_block(
2301 lines: &[Line<'_>],
2302 index: usize,
2303 options: &SyntaxOptions,
2304) -> Option<(Block, usize)> {
2305 if !options.constructs.html_block {
2306 return None;
2307 }
2308
2309 let trimmed = trim_up_to_three_spaces(lines[index].text)?;
2310 let kind = html_block_start(trimmed)?;
2311 let mut value = String::new();
2312 let mut cursor = index;
2313 match kind {
2314 HtmlBlockKind::RawTag => {
2315 while cursor < lines.len() {
2319 push_line(&mut value, lines[cursor].text);
2320 if ["script", "pre", "style", "textarea"]
2321 .iter()
2322 .any(|tag| line_contains_raw_closing_tag(lines[cursor].text, tag))
2323 {
2324 cursor += 1;
2325 break;
2326 }
2327 cursor += 1;
2328 }
2329 }
2330 HtmlBlockKind::BlockTag => {
2331 while cursor < lines.len() && !lines[cursor].text.trim().is_empty() {
2332 push_line(&mut value, lines[cursor].text);
2333 cursor += 1;
2334 }
2335 }
2336 HtmlBlockKind::Until(end) => {
2337 while cursor < lines.len() {
2338 push_line(&mut value, lines[cursor].text);
2339 if lines[cursor].text.contains(end) {
2340 cursor += 1;
2341 break;
2342 }
2343 cursor += 1;
2344 }
2345 }
2346 HtmlBlockKind::UntilBlank => {
2347 while cursor < lines.len() && !lines[cursor].text.trim().is_empty() {
2348 push_line(&mut value, lines[cursor].text);
2349 cursor += 1;
2350 }
2351 }
2352 }
2353 Some((
2354 Block::HtmlBlock(HtmlBlock {
2355 meta: NodeMeta::new(Some(Span::new(
2356 lines[index].start,
2357 lines[cursor - 1].end_with_eol,
2358 ))),
2359 value,
2360 }),
2361 cursor,
2362 ))
2363}
2364
2365fn html_block_start(input: &str) -> Option<HtmlBlockKind> {
2366 let trimmed = input.trim_end();
2367 if !trimmed.starts_with('<') {
2368 return None;
2369 }
2370
2371 if raw_html_tag_start(trimmed) {
2372 return Some(HtmlBlockKind::RawTag);
2373 }
2374 if trimmed.starts_with("<!--") {
2375 return Some(HtmlBlockKind::Until("-->"));
2376 }
2377 if trimmed.starts_with("<?") {
2378 return Some(HtmlBlockKind::Until("?>"));
2379 }
2380 if is_declaration_start(trimmed) {
2381 return Some(HtmlBlockKind::Until(">"));
2382 }
2383 if trimmed.starts_with("<![CDATA[") {
2384 return Some(HtmlBlockKind::Until("]]>"));
2385 }
2386
2387 if html_block_tag_start(trimmed) {
2388 return Some(HtmlBlockKind::BlockTag);
2389 }
2390
2391 let Some((end, _tag_name)) = parse_html_tag(trimmed, 0) else {
2392 return None;
2393 };
2394 let rest = trimmed[end..].trim();
2395 if rest.is_empty() {
2396 Some(HtmlBlockKind::UntilBlank)
2397 } else {
2398 None
2399 }
2400}
2401
2402pub(crate) fn line_starts_html_block(input: &str) -> bool {
2403 trim_up_to_three_spaces(input)
2404 .and_then(html_block_start)
2405 .is_some()
2406}
2407
2408fn line_starts_html_container(input: &str) -> bool {
2409 let line = Line {
2410 text: input,
2411 eol: "",
2412 start: 0,
2413 end: input.len(),
2414 end_with_eol: input.len(),
2415 lazy: false,
2416 };
2417 parse_html_container_opening_line(line, "details").is_some()
2418}
2419
2420fn raw_html_tag_start(input: &str) -> bool {
2421 for tag in ["script", "pre", "style", "textarea"] {
2422 if html_raw_open_tag_prefix(input, tag) {
2423 return true;
2424 }
2425 }
2426 false
2427}
2428
2429fn html_raw_open_tag_prefix(input: &str, tag: &str) -> bool {
2430 let Some(rest) = input.strip_prefix('<') else {
2431 return false;
2432 };
2433 if rest.starts_with('/') || rest.len() < tag.len() {
2434 return false;
2435 }
2436 let rest_bytes = rest.as_bytes();
2437 let tag_bytes = tag.as_bytes();
2438 if !rest_bytes
2439 .get(..tag_bytes.len())
2440 .is_some_and(|name| name.eq_ignore_ascii_case(tag_bytes))
2441 {
2442 return false;
2443 }
2444 match rest_bytes.get(tag.len()) {
2445 None => true,
2446 Some(b' ' | b'\t' | b'\n' | b'\r' | b'>') => true,
2447 Some(b'/') => {
2448 rest_bytes.get(tag.len() + 1) == Some(&b'>') && rest_bytes.get(tag.len() + 2).is_none()
2449 }
2450 _ => false,
2451 }
2452}
2453
2454fn line_contains_raw_closing_tag(input: &str, tag: &str) -> bool {
2455 let bytes = input.as_bytes();
2456 let tag_bytes = tag.as_bytes();
2457 let mut cursor = 0;
2458
2459 while cursor + 2 + tag_bytes.len() <= bytes.len() {
2460 let tag_start = cursor + 2;
2461 let tag_end = tag_start + tag_bytes.len();
2462 if bytes.get(cursor) == Some(&b'<')
2463 && bytes.get(cursor + 1) == Some(&b'/')
2464 && bytes
2465 .get(tag_start..tag_end)
2466 .is_some_and(|name| name.eq_ignore_ascii_case(tag_bytes))
2467 {
2468 match bytes.get(tag_end) {
2469 Some(b'>') => return true,
2470 Some(byte) if byte.is_ascii_whitespace() => {
2471 let mut after_space = tag_end;
2472 while bytes
2473 .get(after_space)
2474 .is_some_and(|byte| byte.is_ascii_whitespace())
2475 {
2476 after_space += 1;
2477 }
2478 if bytes.get(after_space) == Some(&b'>') {
2479 return true;
2480 }
2481 }
2482 _ => {}
2483 }
2484 }
2485 cursor += 1;
2486 }
2487
2488 false
2489}
2490
2491fn html_block_tag_start(input: &str) -> bool {
2492 let bytes = input.as_bytes();
2493 if bytes.first() != Some(&b'<') {
2494 return false;
2495 }
2496
2497 let mut cursor = 1;
2498 if bytes.get(cursor) == Some(&b'/') {
2499 cursor += 1;
2500 }
2501
2502 let name_start = cursor;
2503 if !bytes
2504 .get(cursor)
2505 .is_some_and(|byte| byte.is_ascii_alphabetic())
2506 {
2507 return false;
2508 }
2509 cursor += 1;
2510 while bytes.get(cursor).is_some_and(|byte| html_name_byte(*byte)) {
2511 cursor += 1;
2512 }
2513
2514 let name = &input[name_start..cursor];
2515 if !html_block_tag(name) {
2516 return false;
2517 }
2518
2519 match bytes.get(cursor) {
2520 None | Some(b' ' | b'\t' | b'\n' | b'\r' | b'>') => true,
2521 Some(b'/') if bytes.get(cursor + 1) == Some(&b'>') => true,
2522 _ => false,
2523 }
2524}
2525
2526fn html_block_tag(tag: &str) -> bool {
2527 matches!(
2528 tag.to_ascii_lowercase().as_str(),
2529 "address"
2530 | "article"
2531 | "aside"
2532 | "base"
2533 | "basefont"
2534 | "blockquote"
2535 | "body"
2536 | "caption"
2537 | "center"
2538 | "col"
2539 | "colgroup"
2540 | "dd"
2541 | "details"
2542 | "dialog"
2543 | "dir"
2544 | "div"
2545 | "dl"
2546 | "dt"
2547 | "fieldset"
2548 | "figcaption"
2549 | "figure"
2550 | "footer"
2551 | "form"
2552 | "frame"
2553 | "frameset"
2554 | "h1"
2555 | "h2"
2556 | "h3"
2557 | "h4"
2558 | "h5"
2559 | "h6"
2560 | "head"
2561 | "header"
2562 | "hr"
2563 | "html"
2564 | "iframe"
2565 | "legend"
2566 | "li"
2567 | "link"
2568 | "main"
2569 | "menu"
2570 | "menuitem"
2571 | "nav"
2572 | "noframes"
2573 | "ol"
2574 | "optgroup"
2575 | "option"
2576 | "p"
2577 | "param"
2578 | "search"
2579 | "section"
2580 | "summary"
2581 | "table"
2582 | "tbody"
2583 | "td"
2584 | "tfoot"
2585 | "th"
2586 | "thead"
2587 | "title"
2588 | "tr"
2589 | "track"
2590 | "ul"
2591 )
2592}
2593
2594fn is_declaration_start(input: &str) -> bool {
2595 input
2596 .as_bytes()
2597 .get(2)
2598 .is_some_and(|byte| input.starts_with("<!") && byte.is_ascii_alphabetic())
2599}
2600
2601fn parse_mdx_flow(
2602 lines: &[Line<'_>],
2603 index: usize,
2604 options: &SyntaxOptions,
2605 diagnostics: &mut Vec<Diagnostic>,
2606) -> Option<(Block, usize)> {
2607 if options.constructs.mdx_esm {
2608 if let Some((block, next)) = parse_mdx_esm_flow(lines, index, diagnostics) {
2609 return Some((block, next));
2610 }
2611 }
2612
2613 let line = lines[index];
2614 let trimmed = line.text.trim_start();
2615 if options.constructs.mdx_expression_block && trimmed.starts_with('{') {
2616 let open_byte = line.text.len() - trimmed.len();
2617 if let Some((close_line, close_byte)) = find_mdx_expression_close(lines, index, open_byte) {
2618 return Some((
2619 Block::MdxExpression(MdxExpression {
2620 meta: NodeMeta::new(Some(Span::new(line.start, lines[close_line].end))),
2621 value: collect_mdx_expression_value(
2622 lines, index, open_byte, close_line, close_byte,
2623 ),
2624 }),
2625 close_line + 1,
2626 ));
2627 }
2628 diagnostics.push(Diagnostic::new(
2629 DiagnosticSeverity::Error,
2630 DiagnosticCode::InvalidMdx,
2631 Span::new(line.start + open_byte, lines.last()?.end_with_eol),
2632 "MDX expression block is missing a closing brace",
2633 ));
2634 }
2635 if options.constructs.mdx_jsx_block && trimmed.starts_with('<') {
2636 if let Some(close_line) = find_mdx_jsx_close(lines, index) {
2637 return Some((
2638 Block::MdxJsx(MdxJsx {
2639 meta: NodeMeta::new(Some(Span::new(line.start, lines[close_line].end))),
2640 value: collect_line_range(lines, index, close_line),
2641 }),
2642 close_line + 1,
2643 ));
2644 }
2645 let start_byte = line.text.len() - trimmed.len();
2646 if let Some(root) = mdx_jsx_tag_start(line.text, start_byte) {
2647 if !root.closing {
2648 if let Some((_tag_end_line, _tag_end_byte, self_closing)) =
2649 find_mdx_jsx_tag_end(lines, index, start_byte)
2650 {
2651 if !self_closing {
2652 diagnostics.push(Diagnostic::new(
2653 DiagnosticSeverity::Error,
2654 DiagnosticCode::InvalidMdx,
2655 Span::new(line.start + start_byte, lines.last()?.end_with_eol),
2656 "MDX JSX block is missing a closing tag",
2657 ));
2658 }
2659 }
2660 }
2661 }
2662 }
2663 None
2664}
2665
2666#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
2667struct MdxEsmState {
2668 brace_depth: usize,
2669 bracket_depth: usize,
2670 paren_depth: usize,
2671 block_comment: bool,
2672 quote: Option<u8>,
2673 escaped: bool,
2674}
2675
2676#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2677enum MdxBraceState {
2678 Normal,
2679 SingleQuoted,
2680 DoubleQuoted,
2681 Template,
2682 LineComment,
2683 BlockComment,
2684}
2685
2686#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2687enum MdxJsxTag<'a> {
2688 Fragment,
2689 Named(&'a str),
2690}
2691
2692#[derive(Clone, Copy, Debug, Eq, PartialEq)]
2693struct MdxJsxTagStart<'a> {
2694 tag: MdxJsxTag<'a>,
2695 closing: bool,
2696}
2697
2698fn parse_mdx_esm_flow(
2699 lines: &[Line<'_>],
2700 index: usize,
2701 diagnostics: &mut Vec<Diagnostic>,
2702) -> Option<(Block, usize)> {
2703 if !is_mdx_esm_start(lines[index].text) {
2704 return None;
2705 }
2706
2707 let mut value = String::new();
2708 let mut state = MdxEsmState::default();
2709 let mut cursor = index;
2710 while cursor < lines.len() {
2711 let line = lines[cursor].text;
2712 if cursor > index && !is_mdx_esm_continuation(line, &state) {
2713 break;
2714 }
2715 if cursor > index {
2716 value.push('\n');
2717 }
2718 value.push_str(line);
2719 update_mdx_esm_state(line, &mut state);
2720 cursor += 1;
2721 }
2722 if cursor >= lines.len() && state_has_open_mdx_esm_construct(&state) {
2723 diagnostics.push(Diagnostic::new(
2724 DiagnosticSeverity::Error,
2725 DiagnosticCode::InvalidMdx,
2726 Span::new(lines[index].start, lines[cursor - 1].end_with_eol),
2727 "MDX ESM block is missing a closing delimiter",
2728 ));
2729 }
2730
2731 Some((
2732 Block::MdxEsm(MdxEsm {
2733 meta: NodeMeta::new(Some(Span::new(lines[index].start, lines[cursor - 1].end))),
2734 value,
2735 }),
2736 cursor,
2737 ))
2738}
2739
2740fn is_mdx_esm_start(line: &str) -> bool {
2741 line.starts_with("import ") || line.starts_with("export ")
2742}
2743
2744fn is_mdx_esm_continuation(line: &str, state: &MdxEsmState) -> bool {
2745 if state_has_open_mdx_esm_construct(state) {
2746 return true;
2747 }
2748 let trimmed = line.trim_start();
2749 if trimmed.is_empty() {
2750 return false;
2751 }
2752 is_mdx_esm_start(line) || trimmed.starts_with("//") || trimmed.starts_with("/*")
2753}
2754
2755fn state_has_open_mdx_esm_construct(state: &MdxEsmState) -> bool {
2756 state.brace_depth > 0
2757 || state.bracket_depth > 0
2758 || state.paren_depth > 0
2759 || state.block_comment
2760 || state.quote == Some(b'`')
2761}
2762
2763fn update_mdx_esm_state(line: &str, state: &mut MdxEsmState) {
2764 let bytes = line.as_bytes();
2765 let mut index = 0;
2766 while index < bytes.len() {
2767 let byte = bytes[index];
2768 if state.block_comment {
2769 if byte == b'*' && bytes.get(index + 1) == Some(&b'/') {
2770 state.block_comment = false;
2771 index += 1;
2772 }
2773 index += 1;
2774 continue;
2775 }
2776
2777 if let Some(delimiter) = state.quote {
2778 if state.escaped {
2779 state.escaped = false;
2780 } else if byte == b'\\' {
2781 state.escaped = true;
2782 } else if byte == delimiter {
2783 state.quote = None;
2784 }
2785 index += 1;
2786 continue;
2787 }
2788
2789 match byte {
2790 b'\'' | b'"' | b'`' => state.quote = Some(byte),
2791 b'/' if bytes.get(index + 1) == Some(&b'/') => break,
2792 b'/' if bytes.get(index + 1) == Some(&b'*') => {
2793 state.block_comment = true;
2794 index += 1;
2795 }
2796 b'{' => state.brace_depth += 1,
2797 b'}' => state.brace_depth = state.brace_depth.saturating_sub(1),
2798 b'[' => state.bracket_depth += 1,
2799 b']' => state.bracket_depth = state.bracket_depth.saturating_sub(1),
2800 b'(' => state.paren_depth += 1,
2801 b')' => state.paren_depth = state.paren_depth.saturating_sub(1),
2802 _ => {}
2803 }
2804 index += 1;
2805 }
2806}
2807
2808fn find_mdx_expression_close(
2809 lines: &[Line<'_>],
2810 index: usize,
2811 open_byte: usize,
2812) -> Option<(usize, usize)> {
2813 let mut depth = 0usize;
2814 let mut state = MdxBraceState::Normal;
2815 let mut escaped = false;
2816 let mut cursor = index;
2817
2818 while cursor < lines.len() {
2819 let bytes = lines[cursor].text.as_bytes();
2820 let mut byte_index = if cursor == index { open_byte } else { 0 };
2821 while byte_index < bytes.len() {
2822 let byte = bytes[byte_index];
2823 match state {
2824 MdxBraceState::Normal => match byte {
2825 b'\'' => state = MdxBraceState::SingleQuoted,
2826 b'"' => state = MdxBraceState::DoubleQuoted,
2827 b'`' => state = MdxBraceState::Template,
2828 b'/' if bytes.get(byte_index + 1) == Some(&b'/') => {
2829 state = MdxBraceState::LineComment;
2830 break;
2831 }
2832 b'/' if bytes.get(byte_index + 1) == Some(&b'*') => {
2833 state = MdxBraceState::BlockComment;
2834 byte_index += 1;
2835 }
2836 b'{' => depth += 1,
2837 b'}' => {
2838 depth = depth.checked_sub(1)?;
2839 if depth == 0 {
2840 return lines[cursor].text[byte_index + 1..]
2841 .trim()
2842 .is_empty()
2843 .then_some((cursor, byte_index));
2844 }
2845 }
2846 _ => {}
2847 },
2848 MdxBraceState::SingleQuoted => {
2849 update_mdx_quote_state(byte, b'\'', &mut state, &mut escaped);
2850 }
2851 MdxBraceState::DoubleQuoted => {
2852 update_mdx_quote_state(byte, b'"', &mut state, &mut escaped);
2853 }
2854 MdxBraceState::Template => {
2855 update_mdx_quote_state(byte, b'`', &mut state, &mut escaped);
2856 }
2857 MdxBraceState::LineComment => break,
2858 MdxBraceState::BlockComment => {
2859 if byte == b'*' && bytes.get(byte_index + 1) == Some(&b'/') {
2860 state = MdxBraceState::Normal;
2861 byte_index += 1;
2862 }
2863 }
2864 }
2865 byte_index += 1;
2866 }
2867 if state == MdxBraceState::LineComment {
2868 state = MdxBraceState::Normal;
2869 }
2870 cursor += 1;
2871 }
2872
2873 None
2874}
2875
2876fn update_mdx_quote_state(byte: u8, delimiter: u8, state: &mut MdxBraceState, escaped: &mut bool) {
2877 if *escaped {
2878 *escaped = false;
2879 return;
2880 }
2881 if byte == b'\\' {
2882 *escaped = true;
2883 return;
2884 }
2885 if byte == delimiter {
2886 *state = MdxBraceState::Normal;
2887 }
2888}
2889
2890fn find_mdx_expression_inline_close(input: &str, open_byte: usize) -> Option<usize> {
2891 let bytes = input.as_bytes();
2892 if bytes.get(open_byte) != Some(&b'{') {
2893 return None;
2894 }
2895
2896 let mut depth = 0usize;
2897 let mut state = MdxBraceState::Normal;
2898 let mut escaped = false;
2899 let mut cursor = open_byte;
2900 while cursor < bytes.len() {
2901 let byte = bytes[cursor];
2902 match state {
2903 MdxBraceState::Normal => match byte {
2904 b'\'' => state = MdxBraceState::SingleQuoted,
2905 b'"' => state = MdxBraceState::DoubleQuoted,
2906 b'`' => state = MdxBraceState::Template,
2907 b'/' if bytes.get(cursor + 1) == Some(&b'/') => {
2908 state = MdxBraceState::LineComment;
2909 cursor += 1;
2910 }
2911 b'/' if bytes.get(cursor + 1) == Some(&b'*') => {
2912 state = MdxBraceState::BlockComment;
2913 cursor += 1;
2914 }
2915 b'{' => depth += 1,
2916 b'}' => {
2917 depth = depth.checked_sub(1)?;
2918 if depth == 0 {
2919 return Some(cursor);
2920 }
2921 }
2922 _ => {}
2923 },
2924 MdxBraceState::SingleQuoted => {
2925 update_mdx_quote_state(byte, b'\'', &mut state, &mut escaped);
2926 }
2927 MdxBraceState::DoubleQuoted => {
2928 update_mdx_quote_state(byte, b'"', &mut state, &mut escaped);
2929 }
2930 MdxBraceState::Template => {
2931 update_mdx_quote_state(byte, b'`', &mut state, &mut escaped);
2932 }
2933 MdxBraceState::LineComment => {
2934 if byte == b'\n' {
2935 state = MdxBraceState::Normal;
2936 }
2937 }
2938 MdxBraceState::BlockComment => {
2939 if byte == b'*' && bytes.get(cursor + 1) == Some(&b'/') {
2940 state = MdxBraceState::Normal;
2941 cursor += 1;
2942 }
2943 }
2944 }
2945 cursor += 1;
2946 }
2947 None
2948}
2949
2950fn collect_mdx_expression_value(
2951 lines: &[Line<'_>],
2952 start_line: usize,
2953 open_byte: usize,
2954 close_line: usize,
2955 close_byte: usize,
2956) -> String {
2957 let mut value = String::new();
2958 let mut cursor = start_line;
2959 while cursor <= close_line {
2960 if cursor > start_line {
2961 value.push('\n');
2962 }
2963 let line = lines[cursor].text;
2964 let segment = if cursor == start_line && cursor == close_line {
2965 &line[open_byte + 1..close_byte]
2966 } else if cursor == start_line {
2967 &line[open_byte + 1..]
2968 } else if cursor == close_line {
2969 &line[..close_byte]
2970 } else {
2971 line
2972 };
2973 value.push_str(segment);
2974 cursor += 1;
2975 }
2976 value
2977}
2978
2979fn find_mdx_jsx_close<'a>(lines: &'a [Line<'a>], index: usize) -> Option<usize> {
2980 let line = lines[index];
2981 let trimmed = line.text.trim_start();
2982 let start_byte = line.text.len() - trimmed.len();
2983 let root = mdx_jsx_tag_start(line.text, start_byte)?;
2984 if root.closing {
2985 return None;
2986 }
2987
2988 let (mut cursor_line, mut cursor_byte, self_closing) =
2989 find_mdx_jsx_tag_end(lines, index, start_byte)?;
2990 if self_closing {
2991 return Some(cursor_line);
2992 }
2993
2994 let mut depth = 1usize;
2995 cursor_byte += 1;
2996 'scan: while cursor_line < lines.len() {
2997 let line = lines[cursor_line].text;
2998 while cursor_byte < line.len() {
2999 let Some(relative_start) = line[cursor_byte..].find('<') else {
3000 break;
3001 };
3002 let tag_start_byte = cursor_byte + relative_start;
3003 let Some(candidate) = mdx_jsx_tag_start(line, tag_start_byte) else {
3004 cursor_byte = tag_start_byte + 1;
3005 continue;
3006 };
3007 let Some((tag_end_line, tag_end_byte, candidate_self_closing)) =
3008 find_mdx_jsx_tag_end(lines, cursor_line, tag_start_byte)
3009 else {
3010 return None;
3011 };
3012
3013 if mdx_jsx_tag_matches(root.tag, candidate.tag) {
3014 if candidate.closing {
3015 depth = depth.saturating_sub(1);
3016 if depth == 0 {
3017 return Some(tag_end_line);
3018 }
3019 } else if !candidate_self_closing {
3020 depth += 1;
3021 }
3022 }
3023
3024 cursor_byte = tag_end_byte + 1;
3025 if tag_end_line != cursor_line {
3026 cursor_line = tag_end_line;
3027 continue 'scan;
3028 }
3029 }
3030 cursor_line += 1;
3031 cursor_byte = 0;
3032 }
3033 None
3034}
3035
3036fn parse_mdx_jsx_inline(input: &str, index: usize) -> Option<(usize, String)> {
3037 let root = mdx_jsx_tag_start(input, index)?;
3038 if root.closing {
3039 return None;
3040 }
3041
3042 let (mut cursor, self_closing) = find_mdx_jsx_tag_end_in_text(input, index)?;
3043 if self_closing {
3044 let end = cursor + 1;
3045 return Some((end, input[index..end].into()));
3046 }
3047
3048 let mut depth = 1usize;
3049 cursor += 1;
3050 while cursor < input.len() {
3051 let Some(relative_start) = input[cursor..].find('<') else {
3052 return None;
3053 };
3054 let tag_start_byte = cursor + relative_start;
3055 let Some(candidate) = mdx_jsx_tag_start(input, tag_start_byte) else {
3056 cursor = tag_start_byte + 1;
3057 continue;
3058 };
3059 let Some((tag_end, candidate_self_closing)) =
3060 find_mdx_jsx_tag_end_in_text(input, tag_start_byte)
3061 else {
3062 return None;
3063 };
3064
3065 if mdx_jsx_tag_matches(root.tag, candidate.tag) {
3066 if candidate.closing {
3067 depth = depth.saturating_sub(1);
3068 if depth == 0 {
3069 let end = tag_end + 1;
3070 return Some((end, input[index..end].into()));
3071 }
3072 } else if !candidate_self_closing {
3073 depth += 1;
3074 }
3075 }
3076 cursor = tag_end + 1;
3077 }
3078 None
3079}
3080
3081fn mdx_jsx_tag_start(input: &str, start: usize) -> Option<MdxJsxTagStart<'_>> {
3082 let bytes = input.as_bytes();
3083 if bytes.get(start) != Some(&b'<') {
3084 return None;
3085 }
3086
3087 match bytes.get(start + 1) {
3088 Some(b'>') => {
3089 return Some(MdxJsxTagStart {
3090 tag: MdxJsxTag::Fragment,
3091 closing: false,
3092 });
3093 }
3094 Some(b'/') if bytes.get(start + 2) == Some(&b'>') => {
3095 return Some(MdxJsxTagStart {
3096 tag: MdxJsxTag::Fragment,
3097 closing: true,
3098 });
3099 }
3100 Some(b'!' | b'?') | None => return None,
3101 _ => {}
3102 }
3103
3104 let closing = bytes.get(start + 1) == Some(&b'/');
3105 let name_start = start + if closing { 2 } else { 1 };
3106 if !bytes
3107 .get(name_start)
3108 .is_some_and(|byte| is_mdx_jsx_name_start_byte(*byte))
3109 {
3110 return None;
3111 }
3112
3113 let mut name_end = name_start + 1;
3114 while bytes
3115 .get(name_end)
3116 .is_some_and(|byte| is_mdx_jsx_name_byte(*byte))
3117 {
3118 name_end += 1;
3119 }
3120 if name_end == name_start {
3121 return None;
3122 }
3123 if bytes
3124 .get(name_end)
3125 .is_some_and(|byte| !is_mdx_jsx_name_delimiter(*byte))
3126 {
3127 return None;
3128 }
3129 Some(MdxJsxTagStart {
3130 tag: MdxJsxTag::Named(&input[name_start..name_end]),
3131 closing,
3132 })
3133}
3134
3135fn mdx_jsx_tag_matches(left: MdxJsxTag<'_>, right: MdxJsxTag<'_>) -> bool {
3136 match (left, right) {
3137 (MdxJsxTag::Fragment, MdxJsxTag::Fragment) => true,
3138 (MdxJsxTag::Named(left), MdxJsxTag::Named(right)) => left == right,
3139 _ => false,
3140 }
3141}
3142
3143fn find_mdx_jsx_tag_end(
3144 lines: &[Line<'_>],
3145 start_line: usize,
3146 start_byte: usize,
3147) -> Option<(usize, usize, bool)> {
3148 let mut line_index = start_line;
3149 let mut byte_index = start_byte + 1;
3150 let mut quote = None;
3151 let mut escaped = false;
3152 let mut expression_depth = 0usize;
3153 let mut expression_state = MdxBraceState::Normal;
3154 let mut expression_escaped = false;
3155
3156 while line_index < lines.len() {
3157 let bytes = lines[line_index].text.as_bytes();
3158 while byte_index < bytes.len() {
3159 let byte = bytes[byte_index];
3160 if expression_depth > 0 {
3161 if update_mdx_jsx_expression_state(
3162 byte,
3163 bytes.get(byte_index + 1).copied(),
3164 &mut expression_depth,
3165 &mut expression_state,
3166 &mut expression_escaped,
3167 ) {
3168 byte_index += 1;
3169 }
3170 byte_index += 1;
3171 continue;
3172 }
3173
3174 if let Some(delimiter) = quote {
3175 if escaped {
3176 escaped = false;
3177 } else if byte == b'\\' {
3178 escaped = true;
3179 } else if byte == delimiter {
3180 quote = None;
3181 }
3182 byte_index += 1;
3183 continue;
3184 }
3185
3186 match byte {
3187 b'\'' | b'"' => quote = Some(byte),
3188 b'{' => {
3189 expression_depth = 1;
3190 expression_state = MdxBraceState::Normal;
3191 expression_escaped = false;
3192 }
3193 b'>' if expression_depth == 0 => {
3194 let self_closing =
3195 previous_nonspace_before(lines, line_index, byte_index) == Some(b'/');
3196 return Some((line_index, byte_index, self_closing));
3197 }
3198 _ => {}
3199 }
3200 byte_index += 1;
3201 }
3202 if expression_state == MdxBraceState::LineComment {
3203 expression_state = MdxBraceState::Normal;
3204 }
3205 line_index += 1;
3206 byte_index = 0;
3207 }
3208 None
3209}
3210
3211fn previous_nonspace_before(
3212 lines: &[Line<'_>],
3213 line_index: usize,
3214 byte_index: usize,
3215) -> Option<u8> {
3216 let mut cursor_line = line_index;
3217 let mut cursor_byte = byte_index;
3218
3219 loop {
3220 if let Some(byte) = lines[cursor_line].text.as_bytes()[..cursor_byte]
3221 .iter()
3222 .rev()
3223 .copied()
3224 .find(|byte| !byte.is_ascii_whitespace())
3225 {
3226 return Some(byte);
3227 }
3228 if cursor_line == 0 {
3229 return None;
3230 }
3231 cursor_line -= 1;
3232 cursor_byte = lines[cursor_line].text.len();
3233 }
3234}
3235
3236fn find_mdx_jsx_tag_end_in_text(input: &str, start_byte: usize) -> Option<(usize, bool)> {
3237 let bytes = input.as_bytes();
3238 let mut byte_index = start_byte + 1;
3239 let mut quote = None;
3240 let mut escaped = false;
3241 let mut expression_depth = 0usize;
3242 let mut expression_state = MdxBraceState::Normal;
3243 let mut expression_escaped = false;
3244
3245 while byte_index < bytes.len() {
3246 let byte = bytes[byte_index];
3247 if expression_depth > 0 {
3248 if update_mdx_jsx_expression_state(
3249 byte,
3250 bytes.get(byte_index + 1).copied(),
3251 &mut expression_depth,
3252 &mut expression_state,
3253 &mut expression_escaped,
3254 ) {
3255 byte_index += 1;
3256 }
3257 byte_index += 1;
3258 continue;
3259 }
3260
3261 if let Some(delimiter) = quote {
3262 if escaped {
3263 escaped = false;
3264 } else if byte == b'\\' {
3265 escaped = true;
3266 } else if byte == delimiter {
3267 quote = None;
3268 }
3269 byte_index += 1;
3270 continue;
3271 }
3272
3273 match byte {
3274 b'\'' | b'"' => quote = Some(byte),
3275 b'{' => {
3276 expression_depth = 1;
3277 expression_state = MdxBraceState::Normal;
3278 expression_escaped = false;
3279 }
3280 b'>' if expression_depth == 0 => {
3281 let self_closing = previous_nonspace_before_text(input, byte_index) == Some(b'/');
3282 return Some((byte_index, self_closing));
3283 }
3284 _ => {}
3285 }
3286 byte_index += 1;
3287 }
3288 None
3289}
3290
3291fn previous_nonspace_before_text(input: &str, byte_index: usize) -> Option<u8> {
3292 input.as_bytes()[..byte_index]
3293 .iter()
3294 .rev()
3295 .copied()
3296 .find(|byte| !byte.is_ascii_whitespace())
3297}
3298
3299fn update_mdx_jsx_expression_state(
3300 byte: u8,
3301 next: Option<u8>,
3302 depth: &mut usize,
3303 state: &mut MdxBraceState,
3304 escaped: &mut bool,
3305) -> bool {
3306 match *state {
3307 MdxBraceState::Normal => match byte {
3308 b'\'' => *state = MdxBraceState::SingleQuoted,
3309 b'"' => *state = MdxBraceState::DoubleQuoted,
3310 b'`' => *state = MdxBraceState::Template,
3311 b'/' if next == Some(b'/') => {
3312 *state = MdxBraceState::LineComment;
3313 return true;
3314 }
3315 b'/' if next == Some(b'*') => {
3316 *state = MdxBraceState::BlockComment;
3317 return true;
3318 }
3319 b'{' => *depth += 1,
3320 b'}' => {
3321 *depth = (*depth).saturating_sub(1);
3322 if *depth == 0 {
3323 *state = MdxBraceState::Normal;
3324 *escaped = false;
3325 }
3326 }
3327 _ => {}
3328 },
3329 MdxBraceState::SingleQuoted => {
3330 update_mdx_quote_state(byte, b'\'', state, escaped);
3331 }
3332 MdxBraceState::DoubleQuoted => {
3333 update_mdx_quote_state(byte, b'"', state, escaped);
3334 }
3335 MdxBraceState::Template => {
3336 update_mdx_quote_state(byte, b'`', state, escaped);
3337 }
3338 MdxBraceState::LineComment => {
3339 if byte == b'\n' {
3340 *state = MdxBraceState::Normal;
3341 }
3342 }
3343 MdxBraceState::BlockComment => {
3344 if byte == b'*' && next == Some(b'/') {
3345 *state = MdxBraceState::Normal;
3346 return true;
3347 }
3348 }
3349 }
3350 false
3351}
3352
3353fn is_mdx_jsx_name_start_byte(byte: u8) -> bool {
3354 byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$')
3355}
3356
3357fn is_mdx_jsx_name_byte(byte: u8) -> bool {
3358 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b':' | b'_' | b'-' | b'$')
3359}
3360
3361fn is_mdx_jsx_name_delimiter(byte: u8) -> bool {
3362 byte.is_ascii_whitespace() || matches!(byte, b'/' | b'>' | b'{' | b'}')
3363}
3364
3365fn collect_line_range(lines: &[Line<'_>], start: usize, end: usize) -> String {
3366 let mut value = String::new();
3367 let mut cursor = start;
3368 while cursor <= end {
3369 if cursor > start {
3370 value.push('\n');
3371 }
3372 value.push_str(lines[cursor].text);
3373 cursor += 1;
3374 }
3375 value
3376}
3377
3378fn parse_indented_code(
3379 lines: &[Line<'_>],
3380 index: usize,
3381 options: &SyntaxOptions,
3382) -> Option<(Block, usize)> {
3383 if !options.constructs.indented_code || strip_indented_code_prefix(lines[index].text).is_none()
3384 {
3385 return None;
3386 }
3387 let mut value = String::new();
3388 let mut cursor = index;
3389 let mut content_end = index;
3392 let mut content_end_len = 0usize;
3393 while cursor < lines.len() {
3394 if let Some(text) = strip_indented_code_prefix(lines[cursor].text) {
3395 ensure_line_separator(&mut value);
3396 value.push_str(text);
3397 value.push_str(lines[cursor].eol);
3398 if !text.trim().is_empty() {
3399 content_end = cursor;
3400 content_end_len = value.len();
3401 }
3402 cursor += 1;
3403 continue;
3404 }
3405
3406 if !lines[cursor].text.trim().is_empty() {
3407 break;
3408 }
3409 ensure_line_separator(&mut value);
3410 value.push_str(lines[cursor].eol);
3411 cursor += 1;
3412 }
3413 value.truncate(content_end_len);
3415 Some((
3416 Block::CodeBlock(CodeBlock {
3417 meta: NodeMeta::new(Some(Span::new(
3418 lines[index].start,
3419 lines[content_end].end_with_eol,
3420 ))),
3421 kind: CodeBlockKind::Indented,
3422 info: None,
3423 value,
3424 }),
3425 cursor,
3426 ))
3427}
3428
3429fn strip_indented_code_prefix(input: &str) -> Option<&str> {
3430 let mut column = 0usize;
3431 for (index, byte) in input.as_bytes().iter().enumerate() {
3432 match *byte {
3433 b' ' => {
3434 column += 1;
3435 if column == 4 {
3436 return Some(&input[index + 1..]);
3437 }
3438 }
3439 b'\t' => {
3440 column += 4 - (column % 4);
3441 if column >= 4 {
3442 return Some(&input[index + 1..]);
3443 }
3444 }
3445 _ => return None,
3446 }
3447 }
3448 None
3449}
3450
3451fn parse_table(
3452 lines: &[Line<'_>],
3453 index: usize,
3454 options: &SyntaxOptions,
3455 definitions: &[String],
3456 diagnostics: &mut Vec<Diagnostic>,
3457) -> Option<(Block, usize)> {
3458 if !options.constructs.gfm_table || index + 1 >= lines.len() {
3459 return None;
3460 }
3461 let delimiter = table_indent_line(lines[index + 1].text, options.constructs.indented_code)?;
3462 if list_marker_info(delimiter).is_some() {
3463 return None;
3464 }
3465 if !table_has_separator(lines[index].text, delimiter, options.constructs.spoiler) {
3466 return None;
3467 }
3468 let alignments = parse_table_delimiter(delimiter, options.constructs.spoiler)?;
3469 let headers = split_table_row(lines[index].text, options.constructs.spoiler);
3470 if headers.len() != alignments.len() {
3471 return None;
3472 }
3473
3474 let mut rows = Vec::new();
3475 rows.push(TableRow {
3476 meta: NodeMeta::new(Some(Span::new(lines[index].start, lines[index].end))),
3477 cells: headers
3478 .iter()
3479 .map(|cell| TableCell {
3480 meta: NodeMeta::default(),
3481 children: parse_inlines(
3482 cell.trim(),
3483 lines[index].start,
3484 options,
3485 definitions,
3486 diagnostics,
3487 ),
3488 })
3489 .collect(),
3490 });
3491
3492 let mut cursor = index + 2;
3493 while cursor < lines.len() {
3494 let Some(row) = table_indent_line(lines[cursor].text, options.constructs.indented_code)
3495 else {
3496 break;
3497 };
3498 if row.trim().is_empty() || table_body_line_ends_table(lines[cursor].text, options) {
3502 break;
3503 }
3504 let cells = split_table_row(row, options.constructs.spoiler);
3505 rows.push(TableRow {
3506 meta: NodeMeta::new(Some(Span::new(lines[cursor].start, lines[cursor].end))),
3507 cells: alignments
3508 .iter()
3509 .enumerate()
3510 .map(|(cell_index, _)| {
3511 let value = cells.get(cell_index).map(String::as_str).unwrap_or("");
3512 TableCell {
3513 meta: NodeMeta::default(),
3514 children: parse_inlines(
3515 value.trim(),
3516 lines[cursor].start,
3517 options,
3518 definitions,
3519 diagnostics,
3520 ),
3521 }
3522 })
3523 .collect(),
3524 });
3525 cursor += 1;
3526 }
3527
3528 Some((
3529 Block::Table(Table {
3530 meta: NodeMeta::new(Some(Span::new(
3531 lines[index].start,
3532 lines[cursor - 1].end_with_eol,
3533 ))),
3534 alignments,
3535 rows,
3536 }),
3537 cursor,
3538 ))
3539}
3540
3541fn parse_setext_heading(
3542 lines: &[Line<'_>],
3543 index: usize,
3544 options: &SyntaxOptions,
3545 definitions: &[String],
3546) -> Option<(Block, usize)> {
3547 if index + 1 >= lines.len() || lines[index].text.trim().is_empty() {
3548 return None;
3549 }
3550
3551 let mut underline_index = index + 1;
3556 loop {
3557 let underline_depth = if lines[underline_index].lazy {
3563 None
3564 } else {
3565 setext_underline_depth(lines[underline_index].text)
3566 };
3567 if let Some(depth) = underline_depth {
3568 let mut value = String::new();
3569 for line in &lines[index..underline_index] {
3570 push_line(&mut value, trim_ascii_start(line.text));
3573 }
3574 return Some((
3575 Block::Heading(Heading {
3576 meta: NodeMeta::new(Some(Span::new(
3577 lines[index].start,
3578 lines[underline_index].end,
3579 ))),
3580 depth,
3581 kind: HeadingKind::Setext,
3582 children: parse_inlines(
3583 &value,
3584 lines[index].start,
3585 options,
3586 definitions,
3587 &mut Vec::new(),
3588 ),
3589 }),
3590 underline_index + 1,
3591 ));
3592 }
3593
3594 let line = lines[underline_index].text;
3597 if line.trim().is_empty()
3598 || table_can_start(lines, underline_index, options)
3599 || likely_block_start(line, options)
3600 {
3601 return None;
3602 }
3603 underline_index += 1;
3604 if underline_index >= lines.len() {
3605 return None;
3606 }
3607 }
3608}
3609
3610fn setext_underline_depth(input: &str) -> Option<u8> {
3611 let underline = trim_up_to_three_spaces(input)?.trim();
3612 match underline {
3613 text if !text.is_empty() && text.chars().all(|char| char == '=') => Some(1),
3614 text if !text.is_empty() && text.chars().all(|char| char == '-') => Some(2),
3615 _ => None,
3616 }
3617}
3618
3619fn parse_paragraph(
3620 lines: &[Line<'_>],
3621 index: usize,
3622 options: &SyntaxOptions,
3623 definitions: &[String],
3624 diagnostics: &mut Vec<Diagnostic>,
3625) -> (Block, usize) {
3626 let mut value = String::new();
3627 let start = lines[index].start;
3628 let mut cursor = index;
3629 while cursor < lines.len() {
3630 if lines[cursor].text.trim().is_empty() {
3631 break;
3632 }
3633 if cursor > index && !lines[cursor].lazy {
3637 if table_can_start(lines, cursor, options) {
3638 break;
3639 }
3640 if likely_block_start(lines[cursor].text, options) {
3641 break;
3642 }
3643 }
3644 if !value.is_empty() {
3645 value.push('\n');
3646 }
3647 value.push_str(trim_ascii_start(lines[cursor].text));
3648 cursor += 1;
3649 }
3650
3651 let end = lines[cursor - 1].end;
3652 (
3653 Block::Paragraph(Paragraph {
3654 meta: NodeMeta::new(Some(Span::new(start, end))),
3655 children: parse_inlines(&value, start, options, definitions, diagnostics),
3656 }),
3657 cursor,
3658 )
3659}
3660
3661#[derive(Clone, Copy)]
3664struct DelimMarker {
3665 node_index: usize,
3669 marker: u8,
3670 length: usize,
3672 can_open: bool,
3673 can_close: bool,
3674 span_start: usize,
3676 inactive: bool,
3678}
3679
3680fn record_emphasis_delimiter(
3694 nodes: &mut Vec<Inline>,
3695 delimiters: &mut Vec<DelimMarker>,
3696 input: &str,
3697 index: usize,
3698 base_offset: usize,
3699 marker: u8,
3700 strikethrough: bool,
3701) {
3702 let length = delimiter_byte_run_len(input, index, marker);
3703 let (mut can_open, mut can_close) = if marker == b'_' {
3704 (
3705 can_open_underscore(input, index, length),
3706 can_close_underscore(input, index, length),
3707 )
3708 } else {
3709 (
3710 can_open_delimited(input, index, length),
3711 can_close_delimited(input, index, length),
3712 )
3713 };
3714
3715 if strikethrough && marker != b'~' {
3719 let before = input[..index].chars().next_back();
3720 let after = input[index + length..].chars().next();
3721 if after == Some('~') {
3722 can_open = true;
3723 }
3724 if before == Some('~') {
3725 can_close = true;
3726 }
3727 }
3728
3729 let value = String::from(marker as char).repeat(length);
3730
3731 let node_index = nodes.len();
3732 nodes.push(Inline::Text(Text {
3733 meta: NodeMeta::new(Some(Span::new(
3734 base_offset + index,
3735 base_offset + index + length,
3736 ))),
3737 value,
3738 }));
3739
3740 delimiters.push(DelimMarker {
3741 node_index,
3742 marker,
3743 length,
3744 can_open,
3745 can_close,
3746 span_start: base_offset + index,
3747 inactive: false,
3748 });
3749}
3750
3751fn process_emphasis(mut nodes: Vec<Inline>, mut delimiters: Vec<DelimMarker>) -> Vec<Inline> {
3754 if delimiters.is_empty() {
3755 return nodes;
3756 }
3757
3758 let mut openers_bottom: [Option<usize>; 18] = [None; 18];
3763 let mut closer_idx = 0;
3764
3765 while closer_idx < delimiters.len() {
3766 let closer = delimiters[closer_idx];
3767 if closer.inactive || !closer.can_close {
3768 closer_idx += 1;
3769 continue;
3770 }
3771
3772 let key = openers_bottom_key(&closer);
3773 let bottom = openers_bottom[key];
3774
3775 let mut opener_idx = None;
3777 let mut search = closer_idx;
3778 while search > 0 {
3779 search -= 1;
3780 if let Some(bottom) = bottom {
3781 if search < bottom {
3782 break;
3783 }
3784 }
3785 let candidate = delimiters[search];
3786 if candidate.inactive || candidate.marker != closer.marker || !candidate.can_open {
3787 continue;
3788 }
3789 if emphasis_delimiters_match(&candidate, &closer) {
3790 opener_idx = Some(search);
3791 break;
3792 }
3793 }
3794
3795 let Some(opener_idx) = opener_idx else {
3796 openers_bottom[key] = Some(closer_idx);
3800 if !closer.can_open {
3801 delimiters[closer_idx].inactive = true;
3802 }
3803 closer_idx += 1;
3804 continue;
3805 };
3806
3807 let (used, wrap) = if closer.marker == b'~' {
3808 let length = delimiters[closer_idx].length;
3811 let marker = if length >= 2 {
3812 DeleteMarker::DoubleTilde
3813 } else {
3814 DeleteMarker::SingleTilde
3815 };
3816 (length, EmphasisWrap::Delete(marker))
3817 } else {
3818 let strong = delimiters[opener_idx].length >= 2 && delimiters[closer_idx].length >= 2;
3819 let used = if strong { 2 } else { 1 };
3820 let wrap = if strong {
3821 EmphasisWrap::Strong
3822 } else {
3823 EmphasisWrap::Emphasis
3824 };
3825 (used, wrap)
3826 };
3827
3828 apply_emphasis(
3829 &mut nodes,
3830 &mut delimiters,
3831 opener_idx,
3832 closer_idx,
3833 used,
3834 wrap,
3835 );
3836
3837 let mut inner = opener_idx + 1;
3840 while inner < closer_idx {
3841 delimiters[inner].inactive = true;
3842 inner += 1;
3843 }
3844
3845 if delimiters[opener_idx].length == 0 {
3846 delimiters[opener_idx].inactive = true;
3847 }
3848 if delimiters[closer_idx].length == 0 {
3849 delimiters[closer_idx].inactive = true;
3850 closer_idx += 1;
3851 }
3852 }
3855
3856 merge_adjacent_text(&mut nodes);
3860 nodes
3861}
3862
3863fn merge_adjacent_text(nodes: &mut Vec<Inline>) {
3867 let mut write = 0;
3868 for read in 0..nodes.len() {
3869 if read != write {
3870 nodes.swap(read, write);
3871 }
3872 if write > 0 {
3873 let (head, tail) = nodes.split_at_mut(write);
3874 if let (Inline::Text(previous), Inline::Text(current)) =
3875 (&mut head[write - 1], &tail[0])
3876 {
3877 previous.value.push_str(¤t.value);
3878 if let (Some(previous_span), Some(current_span)) =
3879 (previous.meta.span.as_mut(), current.meta.span)
3880 {
3881 previous_span.end = current_span.end;
3882 }
3883 continue;
3884 }
3885 }
3886 write += 1;
3887 }
3888 nodes.truncate(write);
3889
3890 for node in nodes.iter_mut() {
3891 match node {
3892 Inline::Emphasis(emphasis) => merge_adjacent_text(&mut emphasis.children),
3893 Inline::Strong(strong) => merge_adjacent_text(&mut strong.children),
3894 Inline::Delete(delete) => merge_adjacent_text(&mut delete.children),
3895 _ => {}
3896 }
3897 }
3898}
3899
3900fn openers_bottom_key(closer: &DelimMarker) -> usize {
3902 let marker = match closer.marker {
3903 b'_' => 1,
3904 b'~' => 2,
3905 _ => 0,
3906 };
3907 let both = usize::from(closer.can_open && closer.can_close);
3908 let modulo = closer.length % 3;
3909 ((marker * 2) + both) * 3 + modulo
3910}
3911
3912fn emphasis_delimiters_match(opener: &DelimMarker, closer: &DelimMarker) -> bool {
3914 if opener.marker == b'~' {
3917 return opener.length == closer.length;
3918 }
3919
3920 let opener_both = opener.can_open && opener.can_close;
3924 let closer_both = closer.can_open && closer.can_close;
3925 if opener_both || closer_both {
3926 let sum = opener.length + closer.length;
3927 if sum % 3 == 0 && !(opener.length % 3 == 0 && closer.length % 3 == 0) {
3928 return false;
3929 }
3930 }
3931 true
3932}
3933
3934#[derive(Clone, Copy)]
3936enum EmphasisWrap {
3937 Emphasis,
3938 Strong,
3939 Delete(DeleteMarker),
3940}
3941
3942fn apply_emphasis(
3946 nodes: &mut Vec<Inline>,
3947 delimiters: &mut [DelimMarker],
3948 opener_idx: usize,
3949 closer_idx: usize,
3950 used: usize,
3951 wrap: EmphasisWrap,
3952) {
3953 let opener_node = delimiters[opener_idx].node_index;
3954 let closer_node = delimiters[closer_idx].node_index;
3955
3956 trim_delimiter_text_tail(&mut nodes[opener_node], used);
3959 delimiters[opener_idx].length -= used;
3960 delimiters[opener_idx].span_start += used;
3961
3962 trim_delimiter_text_head(&mut nodes[closer_node], used);
3963 delimiters[closer_idx].length -= used;
3964
3965 let span_start = delimiters[opener_idx].span_start - used;
3968 let span_end = delimiters[closer_idx].span_start + delimiters[closer_idx].length + used;
3969
3970 let children_start = opener_node + 1;
3973 let children_end = closer_node; let children: Vec<Inline> = nodes.drain(children_start..children_end).collect();
3975 let removed = children.len();
3976
3977 let meta = NodeMeta::new(Some(Span::new(span_start, span_end)));
3978 let wrapped = match wrap {
3979 EmphasisWrap::Strong => Inline::Strong(Strong { meta, children }),
3980 EmphasisWrap::Emphasis => Inline::Emphasis(Emphasis { meta, children }),
3981 EmphasisWrap::Delete(marker) => Inline::Delete(Delete {
3982 meta,
3983 marker,
3984 children,
3985 }),
3986 };
3987 nodes.insert(children_start, wrapped);
3988
3989 reindex_delimiters(delimiters, children_end, 1 - removed as isize);
3993
3994 if delimiters[closer_idx].length == 0 {
3998 let pos = delimiters[closer_idx].node_index;
3999 nodes.remove(pos);
4000 reindex_delimiters(delimiters, pos, -1);
4001 }
4002 if delimiters[opener_idx].length == 0 {
4003 let pos = delimiters[opener_idx].node_index;
4004 nodes.remove(pos);
4005 reindex_delimiters(delimiters, pos, -1);
4006 }
4007}
4008
4009fn reindex_delimiters(delimiters: &mut [DelimMarker], from: usize, delta: isize) {
4011 if delta == 0 {
4012 return;
4013 }
4014 for delimiter in delimiters.iter_mut() {
4015 if delimiter.node_index >= from {
4016 delimiter.node_index = (delimiter.node_index as isize + delta) as usize;
4017 }
4018 }
4019}
4020
4021fn trim_delimiter_text_tail(node: &mut Inline, count: usize) {
4023 if let Inline::Text(text) = node {
4024 let new_len = text.value.len().saturating_sub(count);
4025 text.value.truncate(new_len);
4026 if let Some(span) = text.meta.span.as_mut() {
4027 span.end = span.end.saturating_sub(count);
4028 }
4029 }
4030}
4031
4032fn trim_delimiter_text_head(node: &mut Inline, count: usize) {
4034 if let Inline::Text(text) = node {
4035 let count = count.min(text.value.len());
4036 text.value.drain(..count);
4037 if let Some(span) = text.meta.span.as_mut() {
4038 span.start += count;
4039 }
4040 }
4041}
4042
4043fn parse_inlines(
4044 input: &str,
4045 base_offset: usize,
4046 options: &SyntaxOptions,
4047 definitions: &[String],
4048 diagnostics: &mut Vec<Diagnostic>,
4049) -> Vec<Inline> {
4050 parse_inlines_with_context(
4051 input,
4052 base_offset,
4053 options,
4054 definitions,
4055 diagnostics,
4056 InlineContext::default(),
4057 )
4058}
4059
4060#[derive(Clone, Copy)]
4061struct InlineContext {
4062 allow_links: bool,
4063}
4064
4065impl Default for InlineContext {
4066 fn default() -> Self {
4067 Self { allow_links: true }
4068 }
4069}
4070
4071fn parse_inlines_with_context(
4072 input: &str,
4073 base_offset: usize,
4074 options: &SyntaxOptions,
4075 definitions: &[String],
4076 diagnostics: &mut Vec<Diagnostic>,
4077 context: InlineContext,
4078) -> Vec<Inline> {
4079 let bytes = input.as_bytes();
4080 let mut nodes = Vec::new();
4081 let mut text_start = 0;
4082 let mut text = String::new();
4083 let mut index = 0;
4084 let mut delimiters: Vec<DelimMarker> = Vec::new();
4089
4090 while index < bytes.len() {
4091 if bytes[index] == b'\\' {
4092 if let Some((next_index, char)) = next_char(input, index + 1) {
4093 if char.is_ascii_punctuation() {
4094 if options.parse.preserve_character_escapes {
4095 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4096 nodes.push(Inline::Escape(Escape {
4097 meta: NodeMeta::new(Some(Span::new(
4098 base_offset + index,
4099 base_offset + next_index,
4100 ))),
4101 value: char,
4102 }));
4103 index = next_index;
4104 text_start = index;
4105 continue;
4106 }
4107 if text.is_empty() {
4108 text_start = base_offset + index;
4109 }
4110 if gfm_link_label_preserves_url_dot_escape(&text, char, options, context) {
4111 text.push('\\');
4112 }
4113 text.push(char);
4114 index = next_index;
4115 continue;
4116 }
4117 }
4118 }
4119
4120 if bytes[index] == b'&' {
4121 if let Some((end, value)) = parse_character_reference(input, index) {
4122 if options.parse.preserve_character_references {
4123 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4124 nodes.push(Inline::CharacterReference(CharacterReference {
4125 meta: NodeMeta::new(Some(Span::new(
4126 base_offset + index,
4127 base_offset + end,
4128 ))),
4129 reference: input[index..end].into(),
4130 value,
4131 }));
4132 index = end;
4133 text_start = index;
4134 continue;
4135 }
4136 if text.is_empty() {
4137 text_start = base_offset + index;
4138 }
4139 text.push_str(&value);
4140 index = end;
4141 continue;
4142 }
4143 }
4144
4145 if bytes[index] == b'\n' {
4146 if text.ends_with('\\') {
4147 text.pop();
4148 flush_text(
4149 &mut nodes,
4150 &mut text,
4151 text_start,
4152 base_offset + index.saturating_sub(1),
4153 );
4154 nodes.push(Inline::LineBreak(LineBreak {
4155 meta: NodeMeta::new(Some(Span::new(
4156 base_offset + index.saturating_sub(1),
4157 base_offset + index + 1,
4158 ))),
4159 kind: LineBreakKind::Backslash,
4160 }));
4161 index += 1;
4162 text_start = index;
4163 continue;
4164 }
4165 let trailing_spaces = trailing_space_count(&text);
4166 if is_hard_break_suffix(&text, trailing_spaces) {
4167 text.truncate(text.len() - trailing_spaces);
4168 flush_text(
4169 &mut nodes,
4170 &mut text,
4171 text_start,
4172 base_offset + index.saturating_sub(trailing_spaces),
4173 );
4174 nodes.push(Inline::LineBreak(LineBreak {
4175 meta: NodeMeta::new(Some(Span::new(
4176 base_offset + index.saturating_sub(trailing_spaces),
4177 base_offset + index + 1,
4178 ))),
4179 kind: LineBreakKind::Spaces,
4180 }));
4181 index += 1;
4182 text_start = index;
4183 continue;
4184 }
4185 if trailing_spaces > 0 {
4186 text.truncate(text.len() - trailing_spaces);
4187 }
4188 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4189 nodes.push(Inline::SoftBreak(SoftBreak {
4190 meta: NodeMeta::new(Some(Span::new(
4191 base_offset + index,
4192 base_offset + index + 1,
4193 ))),
4194 }));
4195 index += 1;
4196 text_start = index;
4197 continue;
4198 }
4199
4200 if bytes[index] == b'`' {
4201 if let Some((end, code_span)) = parse_code_span(input, index) {
4202 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4203 nodes.push(Inline::Code(CodeInline {
4204 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
4205 value: code_span.value,
4206 raw: code_span.raw,
4207 fence_length: code_span.fence_length,
4208 }));
4209 index = end;
4210 text_start = index;
4211 continue;
4212 } else {
4213 let run = bytes[index..]
4219 .iter()
4220 .take_while(|byte| **byte == b'`')
4221 .count();
4222 if text.is_empty() {
4223 text_start = base_offset + index;
4224 }
4225 for _ in 0..run {
4226 text.push('`');
4227 }
4228 index += run;
4229 continue;
4230 }
4231 }
4232
4233 if options.constructs.spoiler
4234 && bytes.get(index) == Some(&b'|')
4235 && bytes.get(index + 1) == Some(&b'|')
4236 && bytes.get(index + 2) != Some(&b'|')
4237 {
4238 if let Some(end) = find_spoiler_close(input, index + 2) {
4239 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4240 let inner = &input[index + 2..end];
4241 nodes.push(Inline::Spoiler(Spoiler {
4242 meta: NodeMeta::new(Some(Span::new(
4243 base_offset + index,
4244 base_offset + end + 2,
4245 ))),
4246 children: parse_inlines_with_context(
4247 inner,
4248 base_offset + index + 2,
4249 options,
4250 definitions,
4251 diagnostics,
4252 context,
4253 ),
4254 }));
4255 index = end + 2;
4256 text_start = index;
4257 continue;
4258 }
4259 }
4260
4261 if bytes[index] == b'*' && delimiter_byte_run_start(input, index, b'*') == index {
4262 let run_len = delimiter_byte_run_len(input, index, b'*');
4263 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4264 record_emphasis_delimiter(
4265 &mut nodes,
4266 &mut delimiters,
4267 input,
4268 index,
4269 base_offset,
4270 b'*',
4271 options.constructs.gfm_strikethrough,
4272 );
4273 index += run_len;
4274 text_start = index;
4275 continue;
4276 }
4277
4278 if options.constructs.underline
4279 && bytes.get(index) == Some(&b'_')
4280 && bytes.get(index + 1) == Some(&b'_')
4281 && bytes.get(index + 2) == Some(&b'_')
4282 && can_open_underscore(input, index, 1)
4283 {
4284 if let Some(end) = find_closing_delimiter(input, index + 3, "___", true) {
4285 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4286 let inner = &input[index + 3..end];
4287 let underline = Inline::Underline(Underline {
4288 meta: NodeMeta::new(Some(Span::new(
4289 base_offset + index + 1,
4290 base_offset + end + 2,
4291 ))),
4292 children: parse_inlines_with_context(
4293 inner,
4294 base_offset + index + 3,
4295 options,
4296 definitions,
4297 diagnostics,
4298 context,
4299 ),
4300 });
4301 nodes.push(Inline::Emphasis(Emphasis {
4302 meta: NodeMeta::new(Some(Span::new(
4303 base_offset + index,
4304 base_offset + end + 3,
4305 ))),
4306 children: vec![underline],
4307 }));
4308 index = end + 3;
4309 text_start = index;
4310 continue;
4311 }
4312 }
4313
4314 if options.constructs.underline
4315 && bytes.get(index) == Some(&b'_')
4316 && bytes.get(index + 1) == Some(&b'_')
4317 && can_open_underscore(input, index, 2)
4318 {
4319 if let Some(end) = find_closing_delimiter(input, index + 2, "__", true) {
4320 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4321 let inner = &input[index + 2..end];
4322 nodes.push(Inline::Underline(Underline {
4323 meta: NodeMeta::new(Some(Span::new(
4324 base_offset + index,
4325 base_offset + end + 2,
4326 ))),
4327 children: parse_inlines_with_context(
4328 inner,
4329 base_offset + index + 2,
4330 options,
4331 definitions,
4332 diagnostics,
4333 context,
4334 ),
4335 }));
4336 index = end + 2;
4337 text_start = index;
4338 continue;
4339 }
4340 }
4341
4342 if bytes[index] == b'_' && delimiter_byte_run_start(input, index, b'_') == index {
4348 if (options.constructs.gfm_autolink_literal || options.constructs.relaxed_autolinks)
4353 && context.allow_links
4354 {
4355 if let Some((end, destination)) = parse_literal_autolink(
4356 input,
4357 index,
4358 options.constructs.gfm_autolink_literal,
4359 options.constructs.relaxed_autolinks,
4360 ) {
4361 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4362 nodes.push(Inline::Autolink(Autolink {
4363 meta: NodeMeta::new(Some(Span::new(
4364 base_offset + index,
4365 base_offset + end,
4366 ))),
4367 destination,
4368 kind: AutolinkKind::GfmLiteral {
4369 original: input[index..end].into(),
4370 },
4371 }));
4372 index = end;
4373 text_start = index;
4374 continue;
4375 }
4376 }
4377 let run_len = delimiter_byte_run_len(input, index, b'_');
4378 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4379 record_emphasis_delimiter(
4380 &mut nodes,
4381 &mut delimiters,
4382 input,
4383 index,
4384 base_offset,
4385 b'_',
4386 options.constructs.gfm_strikethrough,
4387 );
4388 index += run_len;
4389 text_start = index;
4390 continue;
4391 }
4392
4393 if options.constructs.insert
4394 && bytes.get(index) == Some(&b'+')
4395 && bytes.get(index + 1) == Some(&b'+')
4396 && bytes.get(index + 2) != Some(&b'+')
4397 && can_open_delimited(input, index, 2)
4398 {
4399 if let Some(end) = find_closing_delimiter(input, index + 2, "++", false) {
4400 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4401 let inner = &input[index + 2..end];
4402 nodes.push(Inline::Insert(Insert {
4403 meta: NodeMeta::new(Some(Span::new(
4404 base_offset + index,
4405 base_offset + end + 2,
4406 ))),
4407 children: parse_inlines_with_context(
4408 inner,
4409 base_offset + index + 2,
4410 options,
4411 definitions,
4412 diagnostics,
4413 context,
4414 ),
4415 }));
4416 index = end + 2;
4417 text_start = index;
4418 continue;
4419 }
4420 }
4421
4422 if options.constructs.highlight
4423 && bytes.get(index) == Some(&b'=')
4424 && bytes.get(index + 1) == Some(&b'=')
4425 && bytes.get(index + 2) != Some(&b'=')
4426 && can_open_delimited(input, index, 2)
4427 {
4428 if let Some(end) = find_closing_delimiter(input, index + 2, "==", false) {
4429 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4430 let inner = &input[index + 2..end];
4431 nodes.push(Inline::Mark(Mark {
4432 meta: NodeMeta::new(Some(Span::new(
4433 base_offset + index,
4434 base_offset + end + 2,
4435 ))),
4436 children: parse_inlines_with_context(
4437 inner,
4438 base_offset + index + 2,
4439 options,
4440 definitions,
4441 diagnostics,
4442 context,
4443 ),
4444 }));
4445 index = end + 2;
4446 text_start = index;
4447 continue;
4448 }
4449 }
4450
4451 if options.constructs.subscript
4452 && starts_exact_byte_run(input, index, b'~', 1)
4453 && !single_tilde_delete_takes_precedence(options, input, index)
4454 {
4455 if let Some(end) = find_simple_inline_close(input, index + 1, b'~') {
4456 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4457 let inner = &input[index + 1..end];
4458 nodes.push(Inline::Subscript(Subscript {
4459 meta: NodeMeta::new(Some(Span::new(
4460 base_offset + index,
4461 base_offset + end + 1,
4462 ))),
4463 children: parse_inlines_with_context(
4464 inner,
4465 base_offset + index + 1,
4466 options,
4467 definitions,
4468 diagnostics,
4469 context,
4470 ),
4471 }));
4472 index = end + 1;
4473 text_start = index;
4474 continue;
4475 }
4476 }
4477
4478 if options.constructs.inline_footnote
4479 && options.constructs.footnote_reference
4480 && bytes.get(index) == Some(&b'^')
4481 && bytes.get(index + 1) == Some(&b'[')
4482 {
4483 if let Some(close) = find_inline_footnote_end(input, index + 2) {
4484 let inner = &input[index + 2..close];
4485 if !inner.trim().is_empty() {
4486 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4487 nodes.push(Inline::InlineFootnote(InlineFootnote {
4488 meta: NodeMeta::new(Some(Span::new(
4489 base_offset + index,
4490 base_offset + close + 1,
4491 ))),
4492 children: parse_inlines_with_context(
4493 inner,
4494 base_offset + index + 2,
4495 options,
4496 definitions,
4497 diagnostics,
4498 context,
4499 ),
4500 }));
4501 index = close + 1;
4502 text_start = index;
4503 continue;
4504 }
4505 }
4506 }
4507
4508 if options.constructs.superscript
4509 && bytes.get(index) == Some(&b'^')
4510 && !(options.constructs.inline_footnote && bytes.get(index + 1) == Some(&b'['))
4511 {
4512 if let Some(end) = find_simple_inline_close(input, index + 1, b'^') {
4513 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4514 let inner = &input[index + 1..end];
4515 nodes.push(Inline::Superscript(Superscript {
4516 meta: NodeMeta::new(Some(Span::new(
4517 base_offset + index,
4518 base_offset + end + 1,
4519 ))),
4520 children: parse_inlines_with_context(
4521 inner,
4522 base_offset + index + 1,
4523 options,
4524 definitions,
4525 diagnostics,
4526 context,
4527 ),
4528 }));
4529 index = end + 1;
4530 text_start = index;
4531 continue;
4532 }
4533 }
4534
4535 if options.constructs.gfm_strikethrough
4542 && bytes[index] == b'~'
4543 && delimiter_byte_run_start(input, index, b'~') == index
4544 {
4545 let run_len = delimiter_byte_run_len(input, index, b'~');
4546 let recordable =
4547 run_len == 2 || (run_len == 1 && options.parse.single_tilde_strikethrough);
4548 if recordable {
4549 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4550 record_emphasis_delimiter(
4551 &mut nodes,
4552 &mut delimiters,
4553 input,
4554 index,
4555 base_offset,
4556 b'~',
4557 true,
4558 );
4559 index += run_len;
4560 text_start = index;
4561 continue;
4562 }
4563 }
4564
4565 if bytes[index] == b'!' && index + 1 < bytes.len() && bytes[index + 1] == b'[' {
4566 if let Some((end, image)) =
4567 parse_image(input, index, base_offset, options, definitions, diagnostics)
4568 {
4569 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4570 nodes.push(image);
4571 index = end;
4572 text_start = index;
4573 continue;
4574 }
4575 }
4576
4577 if bytes[index] == b'[' {
4578 if let Some((end, wikilink)) = parse_wikilink(input, index, base_offset, options) {
4579 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4580 nodes.push(wikilink);
4581 index = end;
4582 text_start = index;
4583 continue;
4584 }
4585 if let Some((end, link)) = parse_link(
4586 input,
4587 index,
4588 base_offset,
4589 options,
4590 definitions,
4591 diagnostics,
4592 context,
4593 ) {
4594 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4595 nodes.push(link);
4596 index = end;
4597 text_start = index;
4598 continue;
4599 }
4600 if options.constructs.footnote_reference
4601 && bytes.get(index) == Some(&b'[')
4602 && bytes.get(index + 1) == Some(&b'^')
4603 {
4604 if let Some(close) = find_footnote_reference_label_end(input, index + 2) {
4605 let label = &input[index + 2..close];
4606 if is_footnote_label(label) {
4607 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4608 nodes.push(Inline::FootnoteReference(FootnoteReference {
4609 meta: NodeMeta::new(Some(Span::new(
4610 base_offset + index,
4611 base_offset + close + 1,
4612 ))),
4613 label: label.into(),
4614 identifier: normalize_label(label),
4615 }));
4616 index = close + 1;
4617 text_start = index;
4618 continue;
4619 }
4620 }
4621 }
4622 }
4623
4624 if bytes[index] == b'$' && options.constructs.math_inline {
4625 if let Some((end, value, kind)) = parse_math_inline(input, index) {
4626 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4627 nodes.push(Inline::Math(MathInline {
4628 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
4629 value,
4630 kind,
4631 }));
4632 index = end;
4633 text_start = index;
4634 continue;
4635 }
4636 let run = bytes[index..]
4643 .iter()
4644 .take_while(|byte| **byte == b'$')
4645 .count();
4646 if run > 1 {
4647 if text.is_empty() {
4648 text_start = base_offset + index;
4649 }
4650 text.push_str(&input[index..index + run]);
4651 index += run;
4652 continue;
4653 }
4654 }
4655
4656 if (options.constructs.gfm_autolink_literal || options.constructs.relaxed_autolinks)
4659 && context.allow_links
4660 {
4661 if let Some((end, destination)) = parse_literal_autolink(
4662 input,
4663 index,
4664 options.constructs.gfm_autolink_literal,
4665 options.constructs.relaxed_autolinks,
4666 ) {
4667 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4668 nodes.push(Inline::Autolink(Autolink {
4669 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
4670 destination,
4671 kind: AutolinkKind::GfmLiteral {
4672 original: input[index..end].into(),
4673 },
4674 }));
4675 index = end;
4676 text_start = index;
4677 continue;
4678 }
4679 }
4680
4681 if bytes[index] == b'<' {
4682 if let Some(end) = parse_autolink_end(input, index) {
4683 let raw = &input[index..end];
4684 if is_autolink(raw) {
4685 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4686 if context.allow_links {
4687 nodes.push(Inline::Autolink(Autolink {
4688 meta: NodeMeta::new(Some(Span::new(
4689 base_offset + index,
4690 base_offset + end,
4691 ))),
4692 destination: raw[1..raw.len() - 1].into(),
4693 kind: AutolinkKind::Angle,
4694 }));
4695 } else {
4696 nodes.push(Inline::Text(Text {
4697 meta: NodeMeta::new(Some(Span::new(
4698 base_offset + index,
4699 base_offset + end,
4700 ))),
4701 value: raw[1..raw.len() - 1].into(),
4702 }));
4703 }
4704 index = end;
4705 text_start = index;
4706 continue;
4707 }
4708 }
4709 if options.constructs.mdx_jsx_inline {
4710 if let Some((end, raw)) = parse_mdx_jsx_inline(input, index) {
4711 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4712 nodes.push(Inline::MdxJsx(MdxJsxInline {
4713 meta: NodeMeta::new(Some(Span::new(
4714 base_offset + index,
4715 base_offset + end,
4716 ))),
4717 value: raw,
4718 }));
4719 index = end;
4720 text_start = index;
4721 continue;
4722 }
4723 }
4724 if let Some((end, raw)) = parse_html_inline(input, index) {
4725 if options.constructs.html_inline {
4726 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4727 nodes.push(Inline::Html(HtmlInline {
4728 meta: NodeMeta::new(Some(Span::new(
4729 base_offset + index,
4730 base_offset + end,
4731 ))),
4732 value: raw,
4733 }));
4734 index = end;
4735 text_start = index;
4736 continue;
4737 }
4738 }
4739 }
4740
4741 if bytes[index] == b'{' && options.constructs.mdx_expression_inline {
4742 if let Some(end) = find_mdx_expression_inline_close(input, index) {
4743 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4744 nodes.push(Inline::MdxExpression(MdxExpressionInline {
4745 meta: NodeMeta::new(Some(Span::new(
4746 base_offset + index,
4747 base_offset + end + 1,
4748 ))),
4749 value: input[index + 1..end].into(),
4750 }));
4751 index = end + 1;
4752 text_start = index;
4753 continue;
4754 } else {
4755 diagnostics.push(Diagnostic::new(
4756 DiagnosticSeverity::Error,
4757 DiagnosticCode::InvalidMdx,
4758 Span::new(base_offset + index, base_offset + input.len()),
4759 "MDX expression is missing a closing brace",
4760 ));
4761 }
4762 }
4763
4764 if bytes[index] == b':' && options.constructs.shortcode {
4765 if let Some((end, name)) = parse_shortcode(input, index) {
4766 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4767 nodes.push(Inline::Shortcode(Shortcode {
4768 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
4769 name,
4770 }));
4771 index = end;
4772 text_start = index;
4773 continue;
4774 }
4775 }
4776
4777 if bytes[index] == b':' && options.constructs.directive_text {
4778 if let Some((end, directive)) =
4779 parse_text_directive(input, index, base_offset, options, definitions, diagnostics)
4780 {
4781 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
4782 nodes.push(directive);
4783 index = end;
4784 text_start = index;
4785 continue;
4786 }
4787 }
4788
4789 let (next_index, char) = next_char(input, index).expect("valid UTF-8 byte index");
4790 if text.is_empty() {
4791 text_start = base_offset + index;
4792 }
4793 text.push(if char == '\0' { '\u{FFFD}' } else { char });
4794 index = next_index;
4795 }
4796
4797 flush_text(&mut nodes, &mut text, text_start, base_offset + input.len());
4798 process_emphasis(nodes, delimiters)
4799}
4800
4801fn parse_shortcode(input: &str, index: usize) -> Option<(usize, String)> {
4802 if input[index..].starts_with("::") {
4803 return None;
4804 }
4805
4806 let mut cursor = index + 1;
4807 while let Some((next, char)) = next_char(input, cursor) {
4808 if char == ':' {
4809 if cursor == index + 1 {
4810 return None;
4811 }
4812 return Some((next, input[index + 1..cursor].into()));
4813 }
4814 if !(char.is_ascii_alphanumeric() || matches!(char, '_' | '-' | '+')) {
4815 return None;
4816 }
4817 cursor = next;
4818 }
4819 None
4820}
4821
4822fn parse_wikilink(
4823 input: &str,
4824 index: usize,
4825 base_offset: usize,
4826 options: &SyntaxOptions,
4827) -> Option<(usize, Inline)> {
4828 let configured_order = if options.constructs.wikilink_title_after_pipe {
4829 WikiLinkLabelOrder::AfterPipe
4830 } else if options.constructs.wikilink_title_before_pipe {
4831 WikiLinkLabelOrder::BeforePipe
4832 } else {
4833 return None;
4834 };
4835 if input.as_bytes().get(index) != Some(&b'[') || input.as_bytes().get(index + 1) != Some(&b'[')
4836 {
4837 return None;
4838 }
4839
4840 let close = find_wikilink_close(input, index + 2)?;
4841 let source = &input[index + 2..close];
4842 if source.is_empty() || source.len() > WIKILINK_MAX_BYTES {
4843 return None;
4844 }
4845
4846 let (target_source, label_source, label_order) =
4847 if let Some(separator) = find_wikilink_separator(source) {
4848 match configured_order {
4849 WikiLinkLabelOrder::AfterPipe => (
4850 &source[..separator],
4851 &source[separator + 1..],
4852 WikiLinkLabelOrder::AfterPipe,
4853 ),
4854 WikiLinkLabelOrder::BeforePipe => (
4855 &source[separator + 1..],
4856 &source[..separator],
4857 WikiLinkLabelOrder::BeforePipe,
4858 ),
4859 }
4860 } else {
4861 (source, source, configured_order)
4862 };
4863
4864 let target = unescape_string(target_source);
4865 if target.is_empty() {
4866 return None;
4867 }
4868 let label = unescape_string(label_source);
4869 let end = close + 2;
4870 Some((
4871 end,
4872 Inline::WikiLink(WikiLink {
4873 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
4874 target,
4875 label,
4876 label_order,
4877 }),
4878 ))
4879}
4880
4881fn find_wikilink_close(input: &str, start: usize) -> Option<usize> {
4882 let bytes = input.as_bytes();
4883 let mut cursor = start;
4884 while cursor < input.len() {
4885 match bytes[cursor] {
4886 b'\\' => {
4887 cursor += 1;
4888 if cursor < input.len() {
4889 cursor = next_char(input, cursor)?.0;
4890 }
4891 }
4892 b'\n' | b'\r' => return None,
4893 b']' if bytes.get(cursor + 1) == Some(&b']') => return Some(cursor),
4894 _ => cursor = next_char(input, cursor)?.0,
4895 }
4896 }
4897 None
4898}
4899
4900fn find_wikilink_separator(input: &str) -> Option<usize> {
4901 let bytes = input.as_bytes();
4902 let mut cursor = 0;
4903 while cursor < input.len() {
4904 match bytes[cursor] {
4905 b'\\' => {
4906 cursor += 1;
4907 if cursor < input.len() {
4908 cursor = next_char(input, cursor)?.0;
4909 }
4910 }
4911 b'|' => return Some(cursor),
4912 _ => cursor = next_char(input, cursor)?.0,
4913 }
4914 }
4915 None
4916}
4917
4918fn trailing_space_count(input: &str) -> usize {
4919 input
4920 .as_bytes()
4921 .iter()
4922 .rev()
4923 .take_while(|byte| matches!(**byte, b' ' | b'\t'))
4924 .count()
4925}
4926
4927fn is_hard_break_suffix(input: &str, trailing: usize) -> bool {
4928 let bytes = input.as_bytes();
4932 trailing >= 2
4933 && bytes[bytes.len() - trailing..]
4934 .iter()
4935 .all(|byte| *byte == b' ')
4936}
4937
4938fn parse_image(
4939 input: &str,
4940 index: usize,
4941 base_offset: usize,
4942 options: &SyntaxOptions,
4943 definitions: &[String],
4944 diagnostics: &mut Vec<Diagnostic>,
4945) -> Option<(usize, Inline)> {
4946 let label_start = index + 2;
4947 let label_end = find_link_label_end(input, index + 1)?;
4948 let alt_source = &input[label_start..label_end];
4949 let after_label = label_end + 1;
4950 if input.as_bytes().get(after_label) == Some(&b'(') {
4951 let (close, resource) = parse_link_resource(input, after_label)?;
4952 return Some((
4953 close,
4954 Inline::Image(Image {
4955 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + close))),
4956 destination: resource.destination,
4957 destination_kind: resource.destination_kind,
4958 title: resource.title,
4959 title_kind: resource.title_kind,
4960 alt: parse_inlines(
4961 alt_source,
4962 base_offset + label_start,
4963 options,
4964 definitions,
4965 diagnostics,
4966 ),
4967 }),
4968 ));
4969 }
4970 if input.as_bytes().get(after_label) == Some(&b'[') {
4971 let close = find_reference_label_end(input, after_label)?;
4972 let label = &input[after_label + 1..close];
4973 let identifier = if label.is_empty() { alt_source } else { label };
4974 if definition_exists(definitions, identifier) {
4975 return Some((
4976 close + 1,
4977 Inline::ImageReference(ImageReference {
4978 meta: NodeMeta::new(Some(Span::new(
4979 base_offset + index,
4980 base_offset + close + 1,
4981 ))),
4982 identifier: normalize_label(identifier),
4983 label: identifier.into(),
4984 kind: if label.is_empty() {
4985 ReferenceKind::Collapsed
4986 } else {
4987 ReferenceKind::Full
4988 },
4989 alt: parse_inlines(
4990 alt_source,
4991 base_offset + label_start,
4992 options,
4993 definitions,
4994 diagnostics,
4995 ),
4996 }),
4997 ));
4998 }
4999 return None;
5002 }
5003 if definition_exists(definitions, alt_source) {
5006 return Some((
5007 after_label,
5008 Inline::ImageReference(ImageReference {
5009 meta: NodeMeta::new(Some(Span::new(
5010 base_offset + index,
5011 base_offset + after_label,
5012 ))),
5013 identifier: normalize_label(alt_source),
5014 label: alt_source.into(),
5015 kind: ReferenceKind::Shortcut,
5016 alt: parse_inlines(
5017 alt_source,
5018 base_offset + label_start,
5019 options,
5020 definitions,
5021 diagnostics,
5022 ),
5023 }),
5024 ));
5025 }
5026 None
5027}
5028
5029fn parse_link(
5030 input: &str,
5031 index: usize,
5032 base_offset: usize,
5033 options: &SyntaxOptions,
5034 definitions: &[String],
5035 diagnostics: &mut Vec<Diagnostic>,
5036 context: InlineContext,
5037) -> Option<(usize, Inline)> {
5038 if !context.allow_links {
5039 return None;
5040 }
5041 let label_end = find_link_label_end(input, index)?;
5042 let label_source = &input[index + 1..label_end];
5043 if label_contains_link(label_source, base_offset + index + 1, options, definitions) {
5044 return None;
5045 }
5046 let after_label = label_end + 1;
5047 if input.as_bytes().get(after_label) == Some(&b'(') {
5048 if let Some((close, resource)) = parse_link_resource(input, after_label) {
5053 return Some((
5054 close,
5055 Inline::Link(Link {
5056 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + close))),
5057 destination: resource.destination,
5058 destination_kind: resource.destination_kind,
5059 title: resource.title,
5060 title_kind: resource.title_kind,
5061 children: parse_inlines_with_context(
5062 label_source,
5063 base_offset + index + 1,
5064 options,
5065 definitions,
5066 diagnostics,
5067 InlineContext { allow_links: false },
5068 ),
5069 }),
5070 ));
5071 }
5072 }
5073 if input.as_bytes().get(after_label) == Some(&b'[') {
5074 let close = find_reference_label_end(input, after_label)?;
5075 let label = &input[after_label + 1..close];
5076 let identifier = if label.is_empty() {
5077 label_source
5078 } else {
5079 label
5080 };
5081 if definition_exists(definitions, identifier) {
5082 return Some((
5083 close + 1,
5084 Inline::LinkReference(LinkReference {
5085 meta: NodeMeta::new(Some(Span::new(
5086 base_offset + index,
5087 base_offset + close + 1,
5088 ))),
5089 identifier: normalize_label(identifier),
5090 label: identifier.into(),
5091 kind: if label.is_empty() {
5092 ReferenceKind::Collapsed
5093 } else {
5094 ReferenceKind::Full
5095 },
5096 children: parse_inlines_with_context(
5097 label_source,
5098 base_offset + index + 1,
5099 options,
5100 definitions,
5101 diagnostics,
5102 InlineContext { allow_links: false },
5103 ),
5104 }),
5105 ));
5106 }
5107 return None;
5112 }
5113 if definition_exists(definitions, label_source) {
5114 return Some((
5115 after_label,
5116 Inline::LinkReference(LinkReference {
5117 meta: NodeMeta::new(Some(Span::new(
5118 base_offset + index,
5119 base_offset + after_label,
5120 ))),
5121 identifier: normalize_label(label_source),
5122 label: label_source.into(),
5123 kind: ReferenceKind::Shortcut,
5124 children: parse_inlines_with_context(
5125 label_source,
5126 base_offset + index + 1,
5127 options,
5128 definitions,
5129 diagnostics,
5130 InlineContext { allow_links: false },
5131 ),
5132 }),
5133 ));
5134 }
5135 None
5136}
5137
5138fn find_reference_label_end(input: &str, open: usize) -> Option<usize> {
5139 if input.as_bytes().get(open) != Some(&b'[') {
5142 return None;
5143 }
5144
5145 let mut cursor = open + 1;
5146 while cursor < input.len() {
5147 let (next, char) = next_char(input, cursor)?;
5148 match char {
5149 '\\' => {
5150 cursor = next_char(input, next)
5151 .map(|(after_escape, _)| after_escape)
5152 .unwrap_or(next);
5153 continue;
5154 }
5155 '[' => return None,
5156 ']' => {
5157 return reference_label_is_within_limit(&input[open + 1..cursor]).then_some(cursor);
5158 }
5159 _ => {}
5160 }
5161 cursor = next;
5162 }
5163 None
5164}
5165
5166fn label_contains_link(
5167 label_source: &str,
5168 base_offset: usize,
5169 options: &SyntaxOptions,
5170 definitions: &[String],
5171) -> bool {
5172 let mut diagnostics = Vec::new();
5173 let inlines = parse_inlines_with_context(
5174 label_source,
5175 base_offset,
5176 options,
5177 definitions,
5178 &mut diagnostics,
5179 InlineContext::default(),
5180 );
5181 contains_link_inline(&inlines)
5182}
5183
5184fn contains_link_inline(inlines: &[Inline]) -> bool {
5185 inlines.iter().any(|inline| match inline {
5186 Inline::Link(_) | Inline::LinkReference(_) => true,
5187 Inline::Emphasis(node) => contains_link_inline(&node.children),
5188 Inline::Strong(node) => contains_link_inline(&node.children),
5189 Inline::Delete(node) => contains_link_inline(&node.children),
5190 Inline::TextDirective(node) => contains_link_inline(&node.label),
5191 _ => false,
5192 })
5193}
5194
5195fn find_link_label_end(input: &str, open: usize) -> Option<usize> {
5196 if input.as_bytes().get(open) != Some(&b'[') {
5197 return None;
5198 }
5199
5200 let mut depth = 1usize;
5201 let mut cursor = open + 1;
5202 while cursor < input.len() {
5203 let (next, char) = next_char(input, cursor)?;
5204 match char {
5205 '\\' => {
5206 cursor = next_char(input, next)
5207 .map(|(after_escape, _)| after_escape)
5208 .unwrap_or(next);
5209 continue;
5210 }
5211 '`' => {
5212 if let Some((end, _)) = parse_code_span(input, cursor) {
5213 cursor = end;
5214 continue;
5215 }
5216 }
5217 '<' => {
5218 if let Some(end) = parse_autolink_end(input, cursor) {
5219 let raw = &input[cursor..end];
5220 if is_autolink(raw) {
5221 cursor = end;
5222 continue;
5223 }
5224 }
5225 if let Some((end, _)) = parse_html_inline(input, cursor) {
5226 cursor = end;
5227 continue;
5228 }
5229 }
5230 '[' => depth += 1,
5231 ']' => {
5232 depth = depth.checked_sub(1)?;
5233 if depth == 0 {
5234 return Some(cursor);
5235 }
5236 }
5237 _ => {}
5238 }
5239 cursor = next;
5240 }
5241 None
5242}
5243
5244fn parse_text_directive(
5245 input: &str,
5246 index: usize,
5247 base_offset: usize,
5248 options: &SyntaxOptions,
5249 definitions: &[String],
5250 diagnostics: &mut Vec<Diagnostic>,
5251) -> Option<(usize, Inline)> {
5252 if input[index..].starts_with("::") {
5253 return None;
5254 }
5255 if index > 0 {
5256 let previous = input[..index].chars().next_back()?;
5257 if !previous.is_whitespace() && !matches!(previous, '(' | '[' | '{') {
5258 return None;
5259 }
5260 }
5261 let opener_source = &input[index + 1..];
5262 let (name, label_source, attributes, consumed) = match parse_directive_opener(opener_source) {
5263 Some(opener) => opener,
5264 None => {
5265 if directive_opener_looks_malformed(opener_source) {
5266 diagnostics.push(Diagnostic::new(
5267 DiagnosticSeverity::Error,
5268 DiagnosticCode::InvalidDirectiveName,
5269 Span::new(base_offset + index, base_offset + input.len()),
5270 "text directive opener is malformed",
5271 ));
5272 }
5273 return None;
5274 }
5275 };
5276 let label = label_source
5277 .map(|source| {
5278 parse_inlines(
5279 source,
5280 base_offset + index + 1 + name.len() + 1,
5281 options,
5282 definitions,
5283 diagnostics,
5284 )
5285 })
5286 .unwrap_or_default();
5287 Some((
5288 index + 1 + consumed,
5289 Inline::TextDirective(TextDirective {
5290 meta: NodeMeta::new(Some(Span::new(
5291 base_offset + index,
5292 base_offset + index + 1 + consumed,
5293 ))),
5294 name,
5295 label,
5296 attributes,
5297 }),
5298 ))
5299}
5300
5301fn parse_directive_opener(
5302 input: &str,
5303) -> Option<(String, Option<&str>, Vec<DirectiveAttribute>, usize)> {
5304 let mut index = 0;
5305 while let Some((next, char)) = next_char(input, index) {
5306 if char.is_ascii_alphanumeric() || char == '_' || char == '-' {
5307 index = next;
5308 } else {
5309 break;
5310 }
5311 }
5312 let name = &input[..index];
5313 if !is_directive_name(name) {
5314 return None;
5315 }
5316
5317 let mut label = None;
5318 let mut attributes = Vec::new();
5319 let mut consumed = index;
5320 if input.as_bytes().get(consumed) == Some(&b'[') {
5321 let close = find_link_label_end(input, consumed)?;
5322 label = Some(&input[consumed + 1..close]);
5323 consumed = close + 1;
5324 }
5325 if input.as_bytes().get(consumed) == Some(&b'{') {
5326 let close = find_directive_attributes_close(input, consumed)?;
5327 attributes = parse_attributes(&input[consumed + 1..close]);
5328 consumed = close + 1;
5329 }
5330
5331 Some((name.into(), label, attributes, consumed))
5332}
5333
5334fn directive_opener_looks_malformed(input: &str) -> bool {
5335 let mut index = 0;
5336 while let Some((next, char)) = next_char(input, index) {
5337 if char.is_ascii_alphanumeric() || char == '_' || char == '-' {
5338 index = next;
5339 } else {
5340 break;
5341 }
5342 }
5343 index > 0
5344 && is_directive_name(&input[..index])
5345 && matches!(input.as_bytes().get(index), Some(b'[' | b'{'))
5346}
5347
5348fn find_directive_attributes_close(input: &str, open: usize) -> Option<usize> {
5349 if input.as_bytes().get(open) != Some(&b'{') {
5350 return None;
5351 }
5352
5353 let bytes = input.as_bytes();
5354 let mut cursor = open + 1;
5355 let mut quote = None;
5356 let mut escaped = false;
5357 while cursor < input.len() {
5358 let byte = bytes[cursor];
5359 if escaped {
5360 escaped = false;
5361 cursor += 1;
5362 continue;
5363 }
5364 if byte == b'\\' {
5365 escaped = true;
5366 cursor += 1;
5367 continue;
5368 }
5369 if let Some(delimiter) = quote {
5370 if byte == delimiter {
5371 quote = None;
5372 }
5373 cursor += 1;
5374 continue;
5375 }
5376 match byte {
5377 b'"' | b'\'' => quote = Some(byte),
5378 b'}' => return Some(cursor),
5379 _ => {}
5380 }
5381 cursor += 1;
5382 }
5383 None
5384}
5385
5386fn parse_attributes(input: &str) -> Vec<DirectiveAttribute> {
5387 let mut attributes = Vec::new();
5388 let mut cursor = 0;
5389 while cursor < input.len() {
5390 cursor = skip_spaces(input, cursor);
5391 if cursor >= input.len() {
5392 break;
5393 }
5394
5395 if input.as_bytes().get(cursor) == Some(&b'#') {
5396 let (id, next) = parse_attribute_token(input, cursor + 1);
5397 if !id.is_empty() {
5398 attributes.push(DirectiveAttribute {
5399 name: "id".into(),
5400 value: Some(id.into()),
5401 });
5402 }
5403 cursor = next;
5404 continue;
5405 }
5406
5407 if input.as_bytes().get(cursor) == Some(&b'.') {
5408 let (class, next) = parse_attribute_token(input, cursor + 1);
5409 if !class.is_empty() {
5410 attributes.push(DirectiveAttribute {
5411 name: "class".into(),
5412 value: Some(class.into()),
5413 });
5414 }
5415 cursor = next;
5416 continue;
5417 }
5418
5419 let (name, next) = parse_attribute_name(input, cursor);
5420 if name.is_empty() {
5421 break;
5422 }
5423 cursor = skip_spaces(input, next);
5424 if input.as_bytes().get(cursor) == Some(&b'=') {
5425 cursor = skip_spaces(input, cursor + 1);
5426 if let Some((value, next)) = parse_attribute_value(input, cursor) {
5427 attributes.push(DirectiveAttribute {
5428 name: name.into(),
5429 value: Some(value),
5430 });
5431 cursor = next;
5432 } else {
5433 attributes.push(DirectiveAttribute {
5434 name: name.into(),
5435 value: Some(String::new()),
5436 });
5437 }
5438 } else {
5439 attributes.push(DirectiveAttribute {
5440 name: name.into(),
5441 value: None,
5442 });
5443 }
5444 }
5445 attributes
5446}
5447
5448fn parse_attribute_token(input: &str, index: usize) -> (&str, usize) {
5449 let mut cursor = index;
5450 while let Some((next, char)) = next_char(input, cursor) {
5451 if char.is_whitespace() {
5452 break;
5453 }
5454 cursor = next;
5455 }
5456 (&input[index..cursor], cursor)
5457}
5458
5459fn parse_attribute_name(input: &str, index: usize) -> (&str, usize) {
5460 let mut cursor = index;
5461 while let Some((next, char)) = next_char(input, cursor) {
5462 if char.is_whitespace() || char == '=' {
5463 break;
5464 }
5465 cursor = next;
5466 }
5467 (&input[index..cursor], cursor)
5468}
5469
5470fn parse_attribute_value(input: &str, index: usize) -> Option<(String, usize)> {
5471 let quote = input.as_bytes().get(index).copied();
5472 if matches!(quote, Some(b'"' | b'\'')) {
5473 let quote = quote?;
5474 let mut cursor = index + 1;
5475 while cursor < input.len() {
5476 let (next, char) = next_char(input, cursor)?;
5477 if char as u8 == quote && !is_escaped_at(input, cursor) {
5478 return Some((unescape_ascii_punctuation(&input[index + 1..cursor]), next));
5479 }
5480 cursor = next;
5481 }
5482 return None;
5483 }
5484
5485 let (value, next) = parse_attribute_token(input, index);
5486 Some((
5487 unescape_selected(value, |char| matches!(char, '\\' | '&')),
5488 next,
5489 ))
5490}
5491
5492struct CodeSpanSource {
5493 value: String,
5494 raw: String,
5495 fence_length: usize,
5496}
5497
5498fn parse_code_span(input: &str, index: usize) -> Option<(usize, CodeSpanSource)> {
5499 let len = input[index..]
5500 .as_bytes()
5501 .iter()
5502 .take_while(|byte| **byte == b'`')
5503 .count();
5504 let search_start = index + len;
5505 let close = find_code_span_close(input, search_start, len)?;
5506 let raw = &input[search_start..close];
5507 Some((
5508 close + len,
5509 CodeSpanSource {
5510 value: normalize_code_span(raw),
5511 raw: raw.into(),
5512 fence_length: len,
5513 },
5514 ))
5515}
5516
5517fn find_code_span_close(input: &str, start: usize, marker_len: usize) -> Option<usize> {
5518 let bytes = input.as_bytes();
5519 let mut cursor = start;
5520 while cursor < bytes.len() {
5521 if bytes[cursor] != b'`' {
5522 cursor = next_char(input, cursor)
5523 .map(|(next, _)| next)
5524 .unwrap_or(bytes.len());
5525 continue;
5526 }
5527 let run_len = bytes[cursor..]
5528 .iter()
5529 .take_while(|byte| **byte == b'`')
5530 .count();
5531 if run_len == marker_len {
5532 return Some(cursor);
5533 }
5534 cursor += run_len;
5535 }
5536 None
5537}
5538
5539fn normalize_code_span(input: &str) -> String {
5540 let mut normalized = String::new();
5541 let mut cursor = 0;
5542 while cursor < input.len() {
5543 let (next, char) = next_char(input, cursor).expect("valid UTF-8 byte index");
5544 if char == '\r' {
5545 if input.as_bytes().get(next) == Some(&b'\n') {
5546 cursor = next + 1;
5547 } else {
5548 cursor = next;
5549 }
5550 normalized.push(' ');
5551 continue;
5552 }
5553 if char == '\n' {
5554 normalized.push(' ');
5555 cursor = next;
5556 continue;
5557 }
5558 normalized.push(char);
5559 cursor = next;
5560 }
5561
5562 if normalized.starts_with(' ')
5563 && normalized.ends_with(' ')
5564 && normalized.chars().any(|char| char != ' ')
5565 {
5566 normalized[1..normalized.len() - 1].into()
5567 } else {
5568 normalized
5569 }
5570}
5571
5572fn can_open_delimited(input: &str, index: usize, marker_len: usize) -> bool {
5573 delimiter_flanking(input, index, marker_len).left
5574}
5575
5576fn can_close_delimited(input: &str, index: usize, marker_len: usize) -> bool {
5577 delimiter_flanking(input, index, marker_len).right
5578}
5579
5580fn find_closing_delimiter(
5581 input: &str,
5582 start: usize,
5583 marker: &str,
5584 underscore: bool,
5585) -> Option<usize> {
5586 let marker_len = marker.len();
5587 let mut cursor = start;
5588 let mut nested = 0usize;
5589 while cursor <= input.len() {
5590 let candidate = input[cursor..].find(marker).map(|offset| cursor + offset)?;
5591 if is_escaped_at(input, candidate) {
5592 cursor = candidate + marker_len;
5593 continue;
5594 }
5595 if delimiter_candidate_precedes_link_close(input, start, candidate, marker_len) {
5596 cursor = candidate + marker_len;
5597 continue;
5598 }
5599 if marker_len == 1 && nested == 0 && starts_longer_delimiter_run(input, candidate, marker) {
5600 cursor = candidate + delimiter_run_len(input, candidate, marker);
5601 continue;
5602 }
5603
5604 let can_open = if underscore {
5605 can_open_underscore(input, candidate, marker_len)
5606 } else {
5607 can_open_delimited(input, candidate, marker_len)
5608 };
5609 let can_close = if underscore {
5610 can_close_underscore(input, candidate, marker_len)
5611 } else {
5612 can_close_delimited(input, candidate, marker_len)
5613 };
5614
5615 if can_close {
5616 if nested == 0 {
5617 return Some(candidate);
5618 }
5619 nested -= 1;
5620 cursor = candidate + marker_len;
5621 continue;
5622 }
5623 if can_open {
5624 nested += 1;
5625 }
5626 cursor = candidate + marker_len;
5627 }
5628 None
5629}
5630
5631fn find_single_tilde_delete_close(input: &str, start: usize) -> Option<usize> {
5632 let mut cursor = start;
5633 while cursor < input.len() {
5634 let Some(candidate) = input[cursor..].find('~').map(|index| cursor + index) else {
5635 break;
5636 };
5637 if !is_escaped_at(input, candidate) && single_tilde_can_close_delete(input, candidate) {
5638 return Some(candidate);
5639 }
5640 cursor = candidate + 1;
5641 }
5642 None
5643}
5644
5645fn single_tilde_can_open_delete(input: &str, index: usize) -> bool {
5646 starts_exact_byte_run(input, index, b'~', 1)
5647 && can_open_delimited(input, index, 1)
5648 && !tilde_is_alphanumeric_interior(input, index)
5649}
5650
5651fn single_tilde_can_close_delete(input: &str, index: usize) -> bool {
5652 starts_exact_byte_run(input, index, b'~', 1)
5653 && can_close_delimited(input, index, 1)
5654 && !tilde_is_alphanumeric_interior(input, index)
5655}
5656
5657fn single_tilde_delete_takes_precedence(
5658 options: &SyntaxOptions,
5659 input: &str,
5660 index: usize,
5661) -> bool {
5662 options.constructs.gfm_strikethrough
5663 && options.parse.single_tilde_strikethrough
5664 && single_tilde_can_open_delete(input, index)
5665 && find_single_tilde_delete_close(input, index + 1).is_some()
5666}
5667
5668fn tilde_is_alphanumeric_interior(input: &str, index: usize) -> bool {
5669 let previous = input[..index].chars().next_back();
5670 let next = input[index + 1..].chars().next();
5671 previous.is_some_and(|char| char.is_alphanumeric())
5672 && next.is_some_and(|char| char.is_alphanumeric())
5673}
5674
5675fn starts_exact_byte_run(input: &str, index: usize, marker: u8, len: usize) -> bool {
5676 input.as_bytes().get(index) == Some(&marker)
5677 && delimiter_byte_run_start(input, index, marker) == index
5678 && delimiter_byte_run_len(input, index, marker) == len
5679}
5680
5681fn delimiter_byte_run_start(input: &str, index: usize, marker: u8) -> usize {
5682 let bytes = input.as_bytes();
5683 let mut start = index;
5684 while start > 0 && bytes[start - 1] == marker && !is_escaped_at(input, start - 1) {
5685 start -= 1;
5686 }
5687 start
5688}
5689
5690fn delimiter_byte_run_len(input: &str, index: usize, marker: u8) -> usize {
5691 let bytes = input.as_bytes();
5692 let mut cursor = index;
5693 while bytes.get(cursor) == Some(&marker) {
5694 cursor += 1;
5695 }
5696 cursor - index
5697}
5698
5699fn find_simple_inline_close(input: &str, start: usize, marker: u8) -> Option<usize> {
5700 let bytes = input.as_bytes();
5701 let mut cursor = start;
5702 while cursor < input.len() {
5703 match bytes[cursor] {
5704 b'\\' => {
5705 cursor += 1;
5706 if cursor < input.len() {
5707 cursor = next_char(input, cursor)?.0;
5708 }
5709 }
5710 b'\n' | b'\r' => return None,
5711 byte if byte == marker => return (cursor > start).then_some(cursor),
5712 _ => cursor = next_char(input, cursor)?.0,
5713 }
5714 }
5715 None
5716}
5717
5718fn find_spoiler_close(input: &str, start: usize) -> Option<usize> {
5719 let bytes = input.as_bytes();
5720 let mut cursor = start;
5721 while cursor + 1 < input.len() {
5722 match bytes[cursor] {
5723 b'\\' => {
5724 cursor += 1;
5725 if cursor < input.len() {
5726 cursor = next_char(input, cursor)?.0;
5727 }
5728 }
5729 b'\n' | b'\r' => return None,
5730 b'|' if bytes.get(cursor + 1) == Some(&b'|')
5731 && cursor > start
5732 && bytes.get(cursor.wrapping_sub(1)) != Some(&b'|') =>
5733 {
5734 return Some(cursor);
5735 }
5736 _ => cursor = next_char(input, cursor)?.0,
5737 }
5738 }
5739 None
5740}
5741
5742fn starts_longer_delimiter_run(input: &str, index: usize, marker: &str) -> bool {
5743 input[index..].starts_with(marker)
5744 && !input[..index].ends_with(marker)
5745 && input[index + marker.len()..].starts_with(marker)
5746}
5747
5748fn delimiter_run_len(input: &str, index: usize, marker: &str) -> usize {
5749 let mut cursor = index;
5750 while input[cursor..].starts_with(marker) {
5751 cursor += marker.len();
5752 }
5753 cursor - index
5754}
5755
5756fn delimiter_candidate_precedes_link_close(
5757 input: &str,
5758 start: usize,
5759 candidate: usize,
5760 marker_len: usize,
5761) -> bool {
5762 let bytes = input.as_bytes();
5763 if bytes.get(candidate + marker_len) != Some(&b']') {
5764 return false;
5765 }
5766 if !matches!(bytes.get(candidate + marker_len + 1), Some(b'(' | b'[')) {
5767 return false;
5768 }
5769
5770 let mut depth = 0usize;
5771 let mut cursor = start;
5772 while cursor < candidate {
5773 let Some((next, char)) = next_char(input, cursor) else {
5774 break;
5775 };
5776 match char {
5777 '\\' => {
5778 cursor = next_char(input, next)
5779 .map(|(after_escape, _)| after_escape)
5780 .unwrap_or(next);
5781 continue;
5782 }
5783 '`' => {
5784 if let Some((end, _)) = parse_code_span(input, cursor) {
5785 cursor = end;
5786 continue;
5787 }
5788 }
5789 '[' => depth += 1,
5790 ']' => depth = depth.saturating_sub(1),
5791 _ => {}
5792 }
5793 cursor = next;
5794 }
5795 depth > 0
5796}
5797
5798fn can_open_underscore(input: &str, index: usize, marker_len: usize) -> bool {
5799 let flanking = delimiter_flanking(input, index, marker_len);
5800 flanking.left
5801 && (!flanking.right || flanking.previous.is_some_and(|c| c.is_ascii_punctuation()))
5802}
5803
5804fn can_close_underscore(input: &str, index: usize, marker_len: usize) -> bool {
5805 let flanking = delimiter_flanking(input, index, marker_len);
5806 flanking.right && (!flanking.left || flanking.next.is_some_and(|c| c.is_ascii_punctuation()))
5807}
5808
5809#[derive(Clone, Copy)]
5810struct DelimiterFlanking {
5811 left: bool,
5812 right: bool,
5813 previous: Option<char>,
5814 next: Option<char>,
5815}
5816
5817fn delimiter_flanking(input: &str, index: usize, marker_len: usize) -> DelimiterFlanking {
5818 let previous = input[..index].chars().next_back();
5819 let next = input[index + marker_len..].chars().next();
5820
5821 let previous_whitespace = previous.is_none_or(char::is_whitespace);
5822 let next_whitespace = next.is_none_or(char::is_whitespace);
5823 let previous_punctuation = previous.is_some_and(is_flanking_punctuation);
5824 let next_punctuation = next.is_some_and(is_flanking_punctuation);
5825
5826 let left = next.is_some()
5827 && !next_whitespace
5828 && !(next_punctuation && !previous_whitespace && !previous_punctuation);
5829 let right = previous.is_some()
5830 && !previous_whitespace
5831 && !(previous_punctuation && !next_whitespace && !next_punctuation);
5832
5833 DelimiterFlanking {
5834 left,
5835 right,
5836 previous,
5837 next,
5838 }
5839}
5840
5841fn parse_math_inline(input: &str, index: usize) -> Option<(usize, String, MathInlineKind)> {
5862 if let Some((end, value)) = parse_math_code_inline(input, index) {
5863 return Some((end, value, MathInlineKind::Code));
5864 }
5865
5866 let bytes = input.as_bytes();
5867 let open_dollars = bytes[index..]
5868 .iter()
5869 .take_while(|byte| **byte == b'$')
5870 .count();
5871 if open_dollars == 0 || open_dollars > 2 {
5874 return None;
5875 }
5876
5877 let content_start = index + open_dollars;
5878 let close = scan_to_closing_dollar(input, content_start, open_dollars)?;
5879 let content_end = close - open_dollars;
5880 if content_end <= content_start {
5883 return None;
5884 }
5885
5886 let raw = &input[content_start..content_end];
5887 let value = if open_dollars == 1 {
5888 normalize_math_text(raw)
5889 } else {
5890 raw.into()
5891 };
5892 let dollars = u8::try_from(open_dollars).unwrap_or(u8::MAX);
5893 Some((close, value, MathInlineKind::Dollar { dollars }))
5894}
5895
5896fn scan_to_closing_dollar(input: &str, start: usize, open_dollars: usize) -> Option<usize> {
5900 let bytes = input.as_bytes();
5901 if open_dollars == 1 && bytes.get(start).is_some_and(|byte| is_math_space(*byte)) {
5903 return None;
5904 }
5905
5906 let mut cursor = start;
5907 loop {
5908 while cursor < bytes.len() && bytes[cursor] != b'$' {
5909 cursor += 1;
5910 }
5911 if cursor >= bytes.len() {
5912 return None;
5913 }
5914 let prev = bytes[cursor - 1];
5917 if open_dollars == 1 && is_math_space(prev) {
5918 return None;
5919 }
5920 if open_dollars == 1 && prev == b'\\' {
5921 cursor += 1;
5924 continue;
5925 }
5926 let run = bytes[cursor..]
5927 .iter()
5928 .take(open_dollars)
5929 .take_while(|byte| **byte == b'$')
5930 .count();
5931 if open_dollars == 1 && bytes.get(cursor + run).is_some_and(u8::is_ascii_digit) {
5933 return None;
5934 }
5935 if run == open_dollars {
5936 return Some(cursor + run);
5937 }
5938 cursor += run;
5939 }
5940}
5941
5942fn is_math_space(byte: u8) -> bool {
5944 matches!(byte, b'\t' | b'\n' | b'\r' | b' ')
5945}
5946
5947fn normalize_math_text(input: &str) -> String {
5951 let mut normalized = String::new();
5952 let mut cursor = 0;
5953 while cursor < input.len() {
5954 let (next, char) = next_char(input, cursor).expect("valid UTF-8 byte index");
5955 if char == '\r' {
5956 if input.as_bytes().get(next) == Some(&b'\n') {
5957 cursor = next + 1;
5958 } else {
5959 cursor = next;
5960 }
5961 normalized.push(' ');
5962 continue;
5963 }
5964 if char == '\n' {
5965 normalized.push(' ');
5966 cursor = next;
5967 continue;
5968 }
5969 normalized.push(char);
5970 cursor = next;
5971 }
5972
5973 if normalized.starts_with(' ')
5974 && normalized.ends_with(' ')
5975 && normalized.chars().any(|char| char != ' ')
5976 {
5977 normalized[1..normalized.len() - 1].into()
5978 } else {
5979 normalized
5980 }
5981}
5982
5983fn parse_math_code_inline(input: &str, index: usize) -> Option<(usize, String)> {
5984 if !input[index..].starts_with("$`") {
5985 return None;
5986 }
5987
5988 let search_start = index + 2;
5989 let close = input[search_start..]
5990 .find("`$")
5991 .map(|offset| search_start + offset)?;
5992 if close == search_start {
5993 return None;
5994 }
5995
5996 Some((close + 2, input[search_start..close].into()))
5997}
5998
5999fn parse_link_resource(input: &str, open: usize) -> Option<(usize, ParsedLinkResource)> {
6000 let bytes = input.as_bytes();
6001 if bytes.get(open) != Some(&b'(') {
6002 return None;
6003 }
6004 let (mut cursor, initial_space) = skip_link_resource_space_with_info(input, open + 1)?;
6005 if bytes.get(cursor) == Some(&b')') {
6006 return Some((
6007 cursor + 1,
6008 ParsedLinkResource {
6009 destination: String::new(),
6010 destination_kind: LinkDestinationKind::Omitted,
6011 title: None,
6012 title_kind: None,
6013 },
6014 ));
6015 }
6016 if initial_space && matches!(bytes.get(cursor), Some(b'"' | b'\'' | b'(')) {
6017 let (title, title_kind, next) = parse_link_title(input, cursor)?;
6018 cursor = skip_link_resource_space(input, next)?;
6019 if bytes.get(cursor) == Some(&b')') {
6020 return Some((
6021 cursor + 1,
6022 ParsedLinkResource {
6023 destination: String::new(),
6024 destination_kind: LinkDestinationKind::Omitted,
6025 title: Some(title),
6026 title_kind: Some(title_kind),
6027 },
6028 ));
6029 }
6030 return None;
6031 }
6032 let (destination, destination_kind, next) = parse_link_destination(input, cursor)?;
6033 let (after_destination, had_space) = skip_link_resource_space_with_info(input, next)?;
6034 cursor = after_destination;
6035 if bytes.get(cursor) == Some(&b')') {
6036 return Some((
6037 cursor + 1,
6038 ParsedLinkResource {
6039 destination,
6040 destination_kind,
6041 title: None,
6042 title_kind: None,
6043 },
6044 ));
6045 }
6046 if !had_space {
6047 return None;
6048 }
6049
6050 let (title, title_kind, next) = parse_link_title(input, cursor)?;
6051 cursor = skip_link_resource_space(input, next)?;
6052 if bytes.get(cursor) == Some(&b')') {
6053 Some((
6054 cursor + 1,
6055 ParsedLinkResource {
6056 destination,
6057 destination_kind,
6058 title: Some(title),
6059 title_kind: Some(title_kind),
6060 },
6061 ))
6062 } else {
6063 None
6064 }
6065}
6066
6067fn parse_link_destination(
6068 input: &str,
6069 index: usize,
6070) -> Option<(String, LinkDestinationKind, usize)> {
6071 if input.as_bytes().get(index) == Some(&b'<') {
6072 let mut cursor = index + 1;
6073 while cursor < input.len() {
6074 let (next, char) = next_char(input, cursor)?;
6075 if char == '>' && !is_escaped_at(input, cursor) {
6076 return Some((
6077 unescape_ascii_punctuation(&input[index + 1..cursor]),
6078 LinkDestinationKind::Angle,
6079 next,
6080 ));
6081 }
6082 if (char == '<' && !is_escaped_at(input, cursor)) || char == '\n' || char == '\r' {
6083 return None;
6084 }
6085 cursor = next;
6086 }
6087 return None;
6088 }
6089
6090 let mut cursor = index;
6091 let mut depth = 0usize;
6092 while cursor < input.len() {
6093 let (next, char) = next_char(input, cursor)?;
6094 if (char == ' ' || char.is_ascii_control()) && depth == 0 {
6099 break;
6100 }
6101 if char == '(' && !is_escaped_at(input, cursor) {
6102 depth += 1;
6103 if depth > 32 {
6105 return None;
6106 }
6107 } else if char == ')' && !is_escaped_at(input, cursor) {
6108 if depth == 0 {
6109 break;
6110 }
6111 depth -= 1;
6112 }
6113 cursor = next;
6114 }
6115
6116 if cursor == index || depth > 0 {
6117 None
6118 } else {
6119 Some((
6120 unescape_ascii_punctuation(&input[index..cursor]),
6121 LinkDestinationKind::Bare,
6122 cursor,
6123 ))
6124 }
6125}
6126
6127fn parse_link_title(input: &str, index: usize) -> Option<(String, LinkTitleKind, usize)> {
6128 let opener = input.as_bytes().get(index).copied()?;
6129 let (closer, title_kind) = match opener {
6130 b'"' => ('"', LinkTitleKind::DoubleQuote),
6131 b'\'' => ('\'', LinkTitleKind::SingleQuote),
6132 b'(' => (')', LinkTitleKind::Paren),
6133 _ => return None,
6134 };
6135 let mut cursor = index + 1;
6136 while cursor < input.len() {
6137 let (next, char) = next_char(input, cursor)?;
6138 if char == closer && !is_escaped_at(input, cursor) {
6139 if contains_blank_line(&input[index + 1..cursor]) {
6140 return None;
6141 }
6142 return Some((
6143 unescape_ascii_punctuation(&input[index + 1..cursor]),
6144 title_kind,
6145 next,
6146 ));
6147 }
6148 if opener == b'(' && char == '(' && !is_escaped_at(input, cursor) {
6149 return None;
6150 }
6151 cursor = next;
6152 }
6153 None
6154}
6155
6156fn contains_blank_line(input: &str) -> bool {
6157 if !input.bytes().any(|byte| matches!(byte, b'\n' | b'\r')) {
6158 return false;
6159 }
6160 let lines = collect_lines(input, 0);
6165 let interior = lines.len().saturating_sub(1);
6166 lines
6167 .iter()
6168 .take(interior)
6169 .skip(1)
6170 .any(|line| line.text.trim().is_empty())
6171}
6172
6173fn skip_link_resource_space(input: &str, index: usize) -> Option<usize> {
6174 skip_link_resource_space_with_info(input, index).map(|(index, _)| index)
6175}
6176
6177fn skip_link_resource_space_with_info(input: &str, mut index: usize) -> Option<(usize, bool)> {
6178 let mut line_breaks = 0usize;
6179 let mut had_space = false;
6180 while input
6181 .as_bytes()
6182 .get(index)
6183 .is_some_and(|byte| matches!(*byte, b' ' | b'\t' | b'\n' | b'\r'))
6184 {
6185 had_space = true;
6186 match input.as_bytes()[index] {
6187 b'\n' => {
6188 line_breaks += 1;
6189 if line_breaks > 1 {
6190 return None;
6191 }
6192 index += 1;
6193 }
6194 b'\r' => {
6195 line_breaks += 1;
6196 if line_breaks > 1 {
6197 return None;
6198 }
6199 if input.as_bytes().get(index + 1) == Some(&b'\n') {
6200 index += 2;
6201 } else {
6202 index += 1;
6203 }
6204 }
6205 _ => index += 1,
6206 }
6207 }
6208 Some((index, had_space))
6209}
6210
6211pub(crate) fn parse_character_reference(input: &str, index: usize) -> Option<(usize, String)> {
6212 let rest = input.get(index..)?;
6213 if let Some(rest) = rest
6214 .strip_prefix("&#x")
6215 .or_else(|| rest.strip_prefix("&#X"))
6216 {
6217 let digits = rest.find(';')?;
6218 if digits == 0 || digits > 6 || !rest[..digits].bytes().all(|byte| byte.is_ascii_hexdigit())
6219 {
6220 return None;
6221 }
6222 let value = u32::from_str_radix(&rest[..digits], 16).ok()?;
6223 return Some((
6224 index + 3 + digits + 1,
6225 character_reference_value(value).into(),
6226 ));
6227 }
6228 if let Some(rest) = rest.strip_prefix("&#") {
6229 let digits = rest.find(';')?;
6230 if digits == 0 || digits > 7 || !rest[..digits].bytes().all(|byte| byte.is_ascii_digit()) {
6231 return None;
6232 }
6233 let value = rest[..digits].parse::<u32>().ok()?;
6234 return Some((
6235 index + 2 + digits + 1,
6236 character_reference_value(value).into(),
6237 ));
6238 }
6239
6240 let name_end = rest.find(';')?;
6241 if name_end == 0 || name_end > 32 {
6242 return None;
6243 }
6244 let name = &rest[1..name_end];
6245 named_character_reference(name).map(|value| (index + name_end + 1, value.into()))
6246}
6247
6248pub(crate) fn character_reference_value(value: u32) -> char {
6265 if value == 0 {
6266 '\u{FFFD}'
6267 } else {
6268 char::from_u32(value).unwrap_or('\u{FFFD}')
6269 }
6270}
6271
6272pub(crate) fn is_escaped_at(input: &str, index: usize) -> bool {
6273 let bytes = input.as_bytes();
6274 let mut cursor = index;
6275 let mut count = 0;
6276 while cursor > 0 && bytes[cursor - 1] == b'\\' {
6277 count += 1;
6278 cursor -= 1;
6279 }
6280 count % 2 == 1
6281}
6282
6283fn parse_definition_destination_title(input: &str) -> Option<ParsedLinkResource> {
6284 let (mut cursor, _) = skip_link_resource_space_with_info(input, 0)?;
6285 let (destination, destination_kind, next) = parse_link_destination(input, cursor)?;
6286 cursor = next;
6287
6288 let (next, had_space) = skip_link_resource_space_with_info(input, cursor)?;
6289 cursor = next;
6290 if cursor >= input.len() {
6291 return Some(ParsedLinkResource {
6292 destination,
6293 destination_kind,
6294 title: None,
6295 title_kind: None,
6296 });
6297 }
6298 if !had_space {
6299 return None;
6300 }
6301
6302 let (title, title_kind, next) = parse_link_title(input, cursor)?;
6303 let after_title = skip_link_resource_space(input, next)?;
6304 (after_title == input.len()).then_some(ParsedLinkResource {
6305 destination,
6306 destination_kind,
6307 title: Some(title),
6308 title_kind: Some(title_kind),
6309 })
6310}
6311
6312fn line_can_start_definition_title(input: &str) -> bool {
6313 let trimmed = input.trim_start();
6314 matches!(trimmed.as_bytes().first(), Some(b'"' | b'\'' | b'('))
6315}
6316
6317fn unescape_ascii_punctuation(input: &str) -> String {
6318 unescape_selected(input, |char| char.is_ascii_punctuation())
6320}
6321
6322fn unescape_string(input: &str) -> String {
6323 unescape_selected(input, |char| char.is_ascii_punctuation() || char == '&')
6324}
6325
6326fn unescape_selected(input: &str, should_unescape: impl Fn(char) -> bool) -> String {
6327 let mut output = String::new();
6328 let mut cursor = 0;
6329 while cursor < input.len() {
6330 if input.as_bytes().get(cursor) == Some(&b'&') {
6331 if let Some((end, value)) = parse_character_reference(input, cursor) {
6332 output.push_str(&value);
6333 cursor = end;
6334 continue;
6335 }
6336 }
6337 let (next, char) = next_char(input, cursor).expect("valid UTF-8 byte index");
6338 if char == '\\' {
6339 if let Some((after_escape, escaped)) = next_char(input, next) {
6340 if should_unescape(escaped) {
6341 output.push(escaped);
6342 } else {
6343 output.push(char);
6344 output.push(escaped);
6345 }
6346 cursor = after_escape;
6347 } else {
6348 output.push(char);
6349 cursor = next;
6350 }
6351 } else {
6352 output.push(if char == '\0' { '\u{FFFD}' } else { char });
6353 cursor = next;
6354 }
6355 }
6356 output
6357}
6358
6359fn push_line(output: &mut String, line: &str) {
6360 if !output.is_empty() {
6361 output.push('\n');
6362 }
6363 output.push_str(line);
6364}
6365
6366fn ensure_line_separator(output: &mut String) {
6367 if !output.is_empty() && !ends_with_line_ending(output) {
6368 output.push('\n');
6369 }
6370}
6371
6372fn ends_with_line_ending(input: &str) -> bool {
6373 input.ends_with('\n') || input.ends_with('\r')
6374}
6375
6376fn flush_text(nodes: &mut Vec<Inline>, text: &mut String, text_start: usize, end: usize) {
6377 if !text.is_empty() {
6378 nodes.push(Inline::Text(Text {
6379 meta: NodeMeta::new(Some(Span::new(text_start, end))),
6380 value: core::mem::take(text),
6381 }));
6382 }
6383}
6384
6385fn gfm_link_label_preserves_url_dot_escape(
6386 text: &str,
6387 escaped: char,
6388 options: &SyntaxOptions,
6389 context: InlineContext,
6390) -> bool {
6391 escaped == '.'
6392 && !context.allow_links
6393 && options.constructs.gfm_autolink_literal
6394 && (text.starts_with("www.") || text.starts_with("http://") || text.starts_with("https://"))
6395}
6396
6397fn next_char(input: &str, index: usize) -> Option<(usize, char)> {
6398 let char = input[index..].chars().next()?;
6399 Some((index + char.len_utf8(), char))
6400}
6401
6402fn is_flanking_punctuation(value: char) -> bool {
6407 value.is_ascii_punctuation() || crate::unicode_punctuation::is_unicode_punctuation(value)
6408}
6409
6410pub(crate) fn normalize_label(label: &str) -> String {
6419 label
6420 .replace('ẞ', "ss")
6426 .split_whitespace()
6427 .collect::<Vec<_>>()
6428 .join(" ")
6429 .to_uppercase()
6430 .to_lowercase()
6431}
6432
6433fn definition_exists(definitions: &[String], label: &str) -> bool {
6434 if label.is_empty() || !reference_label_is_within_limit(label) {
6435 return false;
6436 }
6437
6438 let identifier = normalize_label(label);
6439 definitions
6440 .iter()
6441 .any(|definition| definition == &identifier)
6442}
6443
6444fn reference_label_is_within_limit(label: &str) -> bool {
6445 label.chars().take(REFERENCE_LABEL_MAX_CHARS + 1).count() <= REFERENCE_LABEL_MAX_CHARS
6446}
6447
6448fn trim_up_to_three_spaces(input: &str) -> Option<&str> {
6449 let (columns, bytes) = leading_indent(input);
6450 if columns <= 3 {
6451 Some(&input[bytes..])
6452 } else {
6453 None
6454 }
6455}
6456
6457fn fence_start(input: &str) -> Option<(FenceMarker, usize)> {
6458 let marker = match input.as_bytes().first()? {
6459 b'`' => FenceMarker::Backtick,
6460 b'~' => FenceMarker::Tilde,
6461 _ => return None,
6462 };
6463 let byte = match marker {
6464 FenceMarker::Backtick => b'`',
6465 FenceMarker::Tilde => b'~',
6466 };
6467 let length = input
6468 .as_bytes()
6469 .iter()
6470 .take_while(|item| **item == byte)
6471 .count();
6472 if length >= 3 {
6473 Some((marker, length))
6474 } else {
6475 None
6476 }
6477}
6478
6479fn fence_close(input: &str, marker: FenceMarker, length: usize) -> bool {
6480 let byte = match marker {
6481 FenceMarker::Backtick => b'`',
6482 FenceMarker::Tilde => b'~',
6483 };
6484 let count = input
6485 .as_bytes()
6486 .iter()
6487 .take_while(|item| **item == byte)
6488 .count();
6489 count >= length && input[count..].trim().is_empty()
6490}
6491
6492fn trim_closing_hashes(input: &str) -> &str {
6493 let input = input.trim_end();
6494 let hash_start = input.trim_end_matches('#').len();
6495 if hash_start == input.len() {
6496 return input;
6497 }
6498 if hash_start == 0 {
6499 return "";
6500 }
6501
6502 let before = &input[..hash_start];
6503 if before.ends_with(' ') || before.ends_with('\t') {
6504 before.trim_end()
6505 } else {
6506 input
6507 }
6508}
6509
6510fn list_marker_info(input: &str) -> Option<ListMarkerInfo<'_>> {
6511 let trimmed = trim_up_to_three_spaces(input)?;
6512 let indent = input.len() - trimmed.len();
6513 let bytes = trimmed.as_bytes();
6514 match bytes.first()? {
6515 b'-' | b'*' | b'+' if is_list_padding_byte(bytes.get(1).copied()) => {
6516 let delimiter = match bytes[0] {
6517 b'-' => ListDelimiter::Dash,
6518 b'*' => ListDelimiter::Asterisk,
6519 _ => ListDelimiter::Plus,
6520 };
6521 let (content_offset, content_indent) = list_content_offset(trimmed, 1, indent);
6522 Some(ListMarkerInfo {
6523 ordered: false,
6524 start: None,
6525 delimiter,
6526 indent,
6527 marker_len: 1,
6528 content_indent,
6529 content: &trimmed[content_offset..],
6530 })
6531 }
6532 byte if byte.is_ascii_digit() => {
6533 let mut end = 0;
6534 while bytes.get(end).is_some_and(|byte| byte.is_ascii_digit()) {
6535 end += 1;
6536 }
6537 if end > 9 {
6538 return None;
6539 }
6540 let delimiter = match bytes.get(end)? {
6541 b'.' => ListDelimiter::Period,
6542 b')' => ListDelimiter::Paren,
6543 _ => return None,
6544 };
6545 if !is_list_padding_byte(bytes.get(end + 1).copied()) {
6546 return None;
6547 }
6548 let start = trimmed[..end].parse().ok()?;
6549 let marker_len = end + 1;
6550 let (content_offset, content_indent) = list_content_offset(trimmed, marker_len, indent);
6551 Some(ListMarkerInfo {
6552 ordered: true,
6553 start: Some(start),
6554 delimiter,
6555 indent,
6556 marker_len,
6557 content_indent,
6558 content: &trimmed[content_offset..],
6559 })
6560 }
6561 _ => None,
6562 }
6563}
6564
6565fn list_content_offset(input: &str, marker_len: usize, indent: usize) -> (usize, usize) {
6566 let bytes = input.as_bytes();
6567 if bytes.get(marker_len).is_none() {
6568 return (marker_len, indent + marker_len + 1);
6569 }
6570 let mut cursor = marker_len;
6571 let mut column = indent + marker_len;
6572 let marker_end_column = column;
6573 while let Some(byte) = bytes.get(cursor) {
6574 match *byte {
6575 b' ' => column += 1,
6576 b'\t' => column += 4 - (column % 4),
6577 _ => break,
6578 }
6579 cursor += 1;
6580 }
6581 if cursor >= bytes.len() {
6586 return (cursor, marker_end_column + 1);
6587 }
6588 let padding_columns = column.saturating_sub(marker_end_column);
6589 if padding_columns > 0 && padding_columns <= 4 {
6590 (cursor, column)
6591 } else {
6592 (marker_len + 1, marker_end_column + 1)
6593 }
6594}
6595
6596fn list_marker_first_content<'a>(input: &'a str, marker: ListMarkerInfo<'a>) -> Cow<'a, str> {
6597 let Some(trimmed) = trim_up_to_three_spaces(input) else {
6598 return Cow::Borrowed(marker.content);
6599 };
6600 let after_marker = &trimmed[marker.marker_len..];
6601 if after_marker.starts_with('\t') {
6602 strip_leading_indent_columns_from(after_marker, 1, marker.indent + marker.marker_len)
6603 } else {
6604 Cow::Borrowed(marker.content)
6605 }
6606}
6607
6608fn is_list_padding_byte(byte: Option<u8>) -> bool {
6609 matches!(byte, None | Some(b' ' | b'\t'))
6610}
6611
6612fn same_list_marker(left: ListMarkerInfo<'_>, right: ListMarkerInfo<'_>) -> bool {
6613 left.ordered == right.ordered && left.delimiter == right.delimiter
6617}
6618
6619fn sibling_list_marker_at_line(
6626 input: &str,
6627 first_marker: ListMarkerInfo<'_>,
6628 content_indent: usize,
6629) -> bool {
6630 list_marker_info(input).is_some_and(|candidate| {
6631 same_list_marker(first_marker, candidate) && candidate.indent < content_indent
6632 })
6633}
6634
6635fn same_list_marker_line(input: &str, first_marker: ListMarkerInfo<'_>) -> bool {
6640 list_marker_info(input).is_some_and(|candidate| same_list_marker(first_marker, candidate))
6641}
6642
6643fn next_nonblank_line(lines: &[Line<'_>], mut index: usize) -> usize {
6644 while index < lines.len() && lines[index].text.trim().is_empty() {
6645 index += 1;
6646 }
6647 index
6648}
6649
6650fn leading_indent(input: &str) -> (usize, usize) {
6651 let mut column = 0usize;
6652 let mut bytes = 0usize;
6653 for byte in input.as_bytes() {
6654 match *byte {
6655 b' ' => column += 1,
6656 b'\t' => column += 4 - (column % 4),
6657 _ => break,
6658 }
6659 bytes += 1;
6660 }
6661 (column, bytes)
6662}
6663
6664fn leading_indent_columns(input: &str) -> usize {
6665 leading_indent(input).0
6666}
6667
6668fn strip_leading_indent_columns(input: &str, max_columns: usize) -> Cow<'_, str> {
6675 strip_leading_indent_columns_from(input, max_columns, 0)
6676}
6677
6678fn strip_leading_indent_columns_from(
6679 input: &str,
6680 max_columns: usize,
6681 start_column: usize,
6682) -> Cow<'_, str> {
6683 let mut column = start_column;
6684 let target_column = start_column + max_columns;
6685 for (index, byte) in input.as_bytes().iter().enumerate() {
6686 let next = match *byte {
6687 b' ' => column + 1,
6688 b'\t' => column + (4 - (column % 4)),
6689 _ => return Cow::Borrowed(&input[index..]),
6690 };
6691 if next > target_column {
6692 if *byte == b'\t' && column < target_column {
6695 let residual = next - target_column;
6696 let mut owned = String::with_capacity(residual + input.len() - (index + 1));
6697 for _ in 0..residual {
6698 owned.push(' ');
6699 }
6700 let mut rest_column = next;
6701 let mut rest_index = index + 1;
6702 while let Some(rest_byte) = input.as_bytes().get(rest_index) {
6703 match *rest_byte {
6704 b' ' => {
6705 owned.push(' ');
6706 rest_column += 1;
6707 rest_index += 1;
6708 }
6709 b'\t' => {
6710 let width = 4 - (rest_column % 4);
6711 for _ in 0..width {
6712 owned.push(' ');
6713 }
6714 rest_column += width;
6715 rest_index += 1;
6716 }
6717 _ => break,
6718 }
6719 }
6720 owned.push_str(&input[rest_index..]);
6721 return Cow::Owned(owned);
6722 }
6723 return Cow::Borrowed(&input[index..]);
6724 }
6725 column = next;
6726 }
6727 Cow::Borrowed("")
6728}
6729
6730fn strip_list_continuation(input: &str, content_indent: usize, list_indent: usize) -> Cow<'_, str> {
6731 let (indent_columns, indent_bytes) = leading_indent(input);
6732 if indent_columns >= content_indent {
6733 strip_leading_indent_columns(input, content_indent)
6738 } else if indent_columns > list_indent {
6739 Cow::Borrowed(&input[indent_bytes..])
6740 } else {
6741 Cow::Borrowed(trim_ascii_start(input))
6742 }
6743}
6744
6745fn take_task_marker_from_children(children: &mut [Block]) -> Option<bool> {
6746 let Some(Block::Paragraph(paragraph)) = children.first_mut() else {
6747 return None;
6748 };
6749 take_task_marker_from_inlines(&mut paragraph.children)
6750}
6751
6752fn take_task_marker_from_inlines(inlines: &mut Vec<Inline>) -> Option<bool> {
6753 let Some(Inline::Text(text)) = inlines.first() else {
6754 return None;
6755 };
6756 let first = text.value.clone();
6757
6758 if let Some((checked, consumed)) = task_marker_inline_prefix(&first) {
6759 if !first[consumed..].is_empty() || inlines_have_content_after(inlines, 1) {
6760 remove_text_prefix(inlines, consumed);
6761 return Some(checked);
6762 }
6763 }
6764
6765 if let Some(checked) = task_marker_at_text_end(&first) {
6766 if inlines
6767 .get(1)
6768 .is_some_and(|inline| matches!(inline, Inline::SoftBreak(_)))
6769 && inlines_have_content_after(inlines, 2)
6770 {
6771 inlines.remove(1);
6772 inlines.remove(0);
6773 return Some(checked);
6774 }
6775 }
6776
6777 if task_marker_split_open(&first)
6778 && inlines
6779 .get(1)
6780 .is_some_and(|inline| matches!(inline, Inline::SoftBreak(_)))
6781 {
6782 let Some(Inline::Text(next)) = inlines.get(2) else {
6783 return None;
6784 };
6785 if let Some((checked, consumed)) = task_marker_split_close_prefix(&next.value) {
6786 if !next.value[consumed..].is_empty() || inlines_have_content_after(inlines, 3) {
6787 inlines.remove(1);
6788 inlines.remove(0);
6789 remove_text_prefix(inlines, consumed);
6790 return Some(checked);
6791 }
6792 }
6793 }
6794
6795 None
6796}
6797
6798fn task_marker_inline_prefix(input: &str) -> Option<(bool, usize)> {
6799 let start = leading_trim_bytes(input);
6800 let rest = &input[start..];
6801 let checked = task_marker_checked(rest)?;
6802 let after_marker = start + 3;
6803 match input.as_bytes().get(after_marker) {
6804 Some(b' ' | b'\t') => Some((checked, after_marker + 1)),
6805 _ => None,
6806 }
6807}
6808
6809fn task_marker_at_text_end(input: &str) -> Option<bool> {
6810 let start = leading_trim_bytes(input);
6811 let rest = &input[start..];
6812 let checked = task_marker_checked(rest)?;
6813 if rest.len() == 3 {
6814 Some(checked)
6815 } else {
6816 None
6817 }
6818}
6819
6820fn task_marker_split_open(input: &str) -> bool {
6821 let start = leading_trim_bytes(input);
6822 input[start..] == *"["
6823}
6824
6825fn task_marker_split_close_prefix(input: &str) -> Option<(bool, usize)> {
6826 match input.as_bytes().get(..2)? {
6827 b"] " => Some((false, 2)),
6828 b"]\t" => Some((false, 2)),
6829 b"x]" | b"X]" if matches!(input.as_bytes().get(2), Some(b' ' | b'\t')) => Some((true, 3)),
6830 _ => None,
6831 }
6832}
6833
6834fn task_marker_checked(input: &str) -> Option<bool> {
6835 if input.starts_with("[ ]") {
6836 Some(false)
6837 } else if input.starts_with("[x]") || input.starts_with("[X]") {
6838 Some(true)
6839 } else {
6840 None
6841 }
6842}
6843
6844fn remove_text_prefix(inlines: &mut Vec<Inline>, consumed: usize) {
6845 if let Some(Inline::Text(text)) = inlines.first_mut() {
6846 text.value = text.value[consumed..].into();
6847 if text.value.is_empty() {
6848 inlines.remove(0);
6849 }
6850 }
6851}
6852
6853fn inlines_have_content_after(inlines: &[Inline], start: usize) -> bool {
6854 inlines.iter().skip(start).any(|inline| match inline {
6855 Inline::Text(text) => !text.value.is_empty(),
6856 Inline::SoftBreak(_) | Inline::LineBreak(_) => false,
6857 _ => true,
6858 })
6859}
6860
6861fn update_list_item_fence(line: &str, open_fence: &mut Option<(FenceMarker, usize)>) {
6862 let Some(trimmed) = trim_up_to_three_spaces(line) else {
6863 return;
6864 };
6865 if let Some((marker, length)) = *open_fence {
6866 if fence_close(trimmed, marker, length) {
6867 *open_fence = None;
6868 }
6869 return;
6870 }
6871 if let Some((marker, length)) = fence_start(trimmed) {
6872 *open_fence = Some((marker, length));
6873 }
6874}
6875
6876fn trim_ascii_start(input: &str) -> &str {
6877 input.trim_start_matches(|char| matches!(char, ' ' | '\t'))
6878}
6879
6880fn leading_trim_bytes(input: &str) -> usize {
6881 input.len() - trim_ascii_start(input).len()
6882}
6883
6884fn parse_table_delimiter(input: &str, spoiler: bool) -> Option<Vec<TableAlignment>> {
6885 let cells = split_table_row(input, spoiler);
6886 if cells.is_empty() {
6887 return None;
6888 }
6889 let mut alignments = Vec::new();
6890 for cell in cells {
6891 alignments.push(table_delimiter_alignment(cell.trim())?);
6892 }
6893 Some(alignments)
6894}
6895
6896fn table_delimiter_alignment(cell: &str) -> Option<TableAlignment> {
6899 let bytes = cell.as_bytes();
6900 let mut cursor = 0;
6901 let left = bytes.first() == Some(&b':');
6902 if left {
6903 cursor += 1;
6904 }
6905 let dash_start = cursor;
6906 while bytes.get(cursor) == Some(&b'-') {
6907 cursor += 1;
6908 }
6909 if cursor == dash_start {
6910 return None;
6911 }
6912 let right = bytes.get(cursor) == Some(&b':');
6913 if right {
6914 cursor += 1;
6915 }
6916 if cursor != bytes.len() {
6917 return None;
6918 }
6919 Some(match (left, right) {
6920 (true, true) => TableAlignment::Center,
6921 (true, false) => TableAlignment::Left,
6922 (false, true) => TableAlignment::Right,
6923 (false, false) => TableAlignment::None,
6924 })
6925}
6926
6927fn table_indent_line(input: &str, indented_code: bool) -> Option<&str> {
6931 if indented_code {
6932 trim_up_to_three_spaces(input)
6933 } else {
6934 Some(input)
6935 }
6936}
6937
6938fn backtick_run_has_close(input: &str, start: usize, length: usize) -> bool {
6943 let bytes = input.as_bytes();
6944 let mut i = start + length;
6945 while i < input.len() {
6946 if bytes[i] == b'`' {
6947 let run = input[i..]
6948 .as_bytes()
6949 .iter()
6950 .take_while(|byte| **byte == b'`')
6951 .count();
6952 if run == length {
6953 return true;
6954 }
6955 i += run;
6956 } else {
6957 i += 1;
6958 }
6959 }
6960 false
6961}
6962
6963fn table_backslash_pipe_run(input: &str, cursor: usize) -> Option<(usize, bool)> {
6964 let bytes = input.as_bytes();
6965 if bytes.get(cursor) != Some(&b'\\') {
6966 return None;
6967 }
6968 let mut pipe = cursor;
6969 while bytes.get(pipe) == Some(&b'\\') {
6970 pipe += 1;
6971 }
6972 (bytes.get(pipe) == Some(&b'|')).then_some((pipe, (pipe - cursor) % 2 == 1))
6973}
6974
6975fn split_table_row(input: &str, spoiler: bool) -> Vec<String> {
6976 let trimmed = input.trim();
6977 let mut cells = Vec::new();
6978 let mut cell = String::new();
6979 let mut cursor = 0;
6980 let mut code_fence = None;
6981 let mut spoiler_open = false;
6982 let mut trailing_delimiter_end = None;
6988
6989 while cursor < trimmed.len() {
6990 let (next, char) = next_char(trimmed, cursor).expect("valid UTF-8 byte index");
6991 if char == '\\' {
6996 if let Some((pipe, escaped)) = table_backslash_pipe_run(trimmed, cursor) {
6997 if escaped {
6998 for _ in 0..pipe - cursor - 1 {
6999 cell.push('\\');
7000 }
7001 cell.push('|');
7002 cursor = pipe + 1;
7003 } else {
7004 for _ in 0..pipe - cursor {
7005 cell.push('\\');
7006 }
7007 cursor = pipe;
7008 }
7009 continue;
7010 }
7011 }
7012 if char == '`' {
7016 let length = trimmed[cursor..]
7017 .as_bytes()
7018 .iter()
7019 .take_while(|byte| **byte == b'`')
7020 .count();
7021 if code_fence == Some(length) {
7022 code_fence = None;
7023 } else if code_fence.is_none() && backtick_run_has_close(trimmed, cursor, length) {
7024 code_fence = Some(length);
7025 }
7026 cell.push_str(&trimmed[cursor..cursor + length]);
7027 cursor += length;
7028 continue;
7029 }
7030
7031 if spoiler
7032 && char == '|'
7033 && trimmed.as_bytes().get(cursor + 1) == Some(&b'|')
7034 && code_fence.is_some()
7035 {
7036 cell.push_str("||");
7037 cursor += 2;
7038 continue;
7039 }
7040
7041 if spoiler
7042 && char == '|'
7043 && trimmed.as_bytes().get(cursor + 1) == Some(&b'|')
7044 && code_fence.is_none()
7045 && !is_escaped_at(trimmed, cursor)
7046 {
7047 let closes_spoiler =
7048 spoiler_open && trimmed.as_bytes().get(cursor.wrapping_sub(1)) != Some(&b'|');
7049 let opens_spoiler = !spoiler_open
7050 && trimmed.as_bytes().get(cursor + 2) != Some(&b'|')
7051 && find_spoiler_close(trimmed, cursor + 2).is_some();
7052 if closes_spoiler || opens_spoiler {
7053 spoiler_open = opens_spoiler;
7054 cell.push_str("||");
7055 cursor += 2;
7056 continue;
7057 }
7058 }
7059
7060 if char == '|' && !spoiler_open && !is_escaped_at(trimmed, cursor) {
7061 cells.push(core::mem::take(&mut cell));
7062 spoiler_open = false;
7064 trailing_delimiter_end = Some(next);
7065 } else {
7066 cell.push(char);
7067 }
7068 cursor = next;
7069 }
7070 cells.push(cell);
7071
7072 if trimmed.starts_with('|') {
7073 cells.remove(0);
7074 }
7075 if let Some(end) = trailing_delimiter_end {
7078 if trimmed[end..].trim().is_empty() {
7079 cells.pop();
7080 }
7081 }
7082 cells
7083}
7084
7085fn table_can_start(lines: &[Line<'_>], index: usize, options: &SyntaxOptions) -> bool {
7086 if !options.constructs.gfm_table || index + 1 >= lines.len() {
7087 return false;
7088 }
7089 table_can_start_source(
7090 lines[index].text,
7091 lines[index + 1].text,
7092 options.constructs.indented_code,
7093 options.constructs.spoiler,
7094 )
7095}
7096
7097pub(crate) fn gfm_table_can_start_source(header: &str, delimiter: &str) -> bool {
7098 table_can_start_source(header, delimiter, true, false)
7099}
7100
7101fn table_can_start_source(
7102 header: &str,
7103 delimiter: &str,
7104 indented_code: bool,
7105 spoiler: bool,
7106) -> bool {
7107 let Some(delimiter) = table_indent_line(delimiter, indented_code) else {
7108 return false;
7109 };
7110 if list_marker_info(delimiter).is_some() {
7111 return false;
7112 }
7113 if !table_has_separator(header, delimiter, spoiler) {
7114 return false;
7115 }
7116 let Some(alignments) = parse_table_delimiter(delimiter, spoiler) else {
7117 return false;
7118 };
7119 split_table_row(header, spoiler).len() == alignments.len()
7120}
7121
7122fn table_has_separator(header: &str, delimiter: &str, spoiler: bool) -> bool {
7123 let Some(alignments) = parse_table_delimiter(delimiter, spoiler) else {
7131 return true;
7132 };
7133 if alignments.len() == 1 {
7134 return contains_unescaped_pipe(header, spoiler)
7135 || contains_unescaped_pipe(delimiter, spoiler)
7136 || delimiter.contains(':');
7137 }
7138 true
7139}
7140
7141fn contains_unescaped_pipe(input: &str, spoiler: bool) -> bool {
7144 let mut cursor = 0;
7145 let mut code_fence = None;
7146 let mut spoiler_open = false;
7147 while cursor < input.len() {
7148 let (next, char) = next_char(input, cursor).expect("valid UTF-8 byte index");
7149 if char == '\\' {
7150 if let Some((pipe, escaped)) = table_backslash_pipe_run(input, cursor) {
7151 cursor = if escaped { pipe + 1 } else { pipe };
7152 continue;
7153 }
7154 }
7155 if char == '`' {
7157 let length = input[cursor..]
7158 .as_bytes()
7159 .iter()
7160 .take_while(|byte| **byte == b'`')
7161 .count();
7162 if code_fence == Some(length) {
7163 code_fence = None;
7164 } else if code_fence.is_none() {
7165 code_fence = Some(length);
7166 }
7167 cursor += length;
7168 continue;
7169 }
7170 if spoiler
7171 && char == '|'
7172 && input.as_bytes().get(cursor + 1) == Some(&b'|')
7173 && code_fence.is_some()
7174 {
7175 cursor += 2;
7176 continue;
7177 }
7178 if spoiler
7179 && char == '|'
7180 && input.as_bytes().get(cursor + 1) == Some(&b'|')
7181 && code_fence.is_none()
7182 && !is_escaped_at(input, cursor)
7183 {
7184 let closes_spoiler =
7185 spoiler_open && input.as_bytes().get(cursor.wrapping_sub(1)) != Some(&b'|');
7186 let opens_spoiler = !spoiler_open
7187 && input.as_bytes().get(cursor + 2) != Some(&b'|')
7188 && find_spoiler_close(input, cursor + 2).is_some();
7189 if closes_spoiler || opens_spoiler {
7190 spoiler_open = opens_spoiler;
7191 cursor += 2;
7192 continue;
7193 }
7194 }
7195 if char == '|' && !spoiler_open && !is_escaped_at(input, cursor) {
7196 return true;
7197 }
7198 cursor = next;
7199 }
7200 false
7201}
7202
7203fn likely_block_start(input: &str, options: &SyntaxOptions) -> bool {
7204 let Some(trimmed) = trim_up_to_three_spaces(input) else {
7209 return false;
7210 };
7211 trimmed.starts_with('#')
7212 || trimmed.starts_with('>')
7213 || trimmed.starts_with("```")
7214 || trimmed.starts_with("~~~")
7215 || list_marker_can_interrupt_paragraph(input)
7216 || parse_thematic_break(Line {
7217 text: input,
7218 eol: "",
7219 start: 0,
7220 end: input.len(),
7221 end_with_eol: input.len(),
7222 lazy: false,
7223 })
7224 .is_some()
7225 || (options.constructs.html_container && line_starts_html_container(input))
7226 || (options.constructs.html_block && line_starts_interrupting_html_block(input))
7227 || (options.constructs.math_block && math_block_fence_length(trimmed).is_some())
7228 || (options.constructs.directive_container && trimmed.starts_with(":::"))
7229 || (options.constructs.directive_leaf && trimmed.starts_with("::"))
7230 || (options.constructs.footnote_definition && line_starts_footnote_definition(trimmed))
7231}
7232
7233fn line_starts_footnote_definition(trimmed: &str) -> bool {
7236 trimmed.starts_with("[^")
7237 && find_footnote_definition_label_end(trimmed)
7238 .is_some_and(|close| is_footnote_label(&trimmed[2..close]))
7239}
7240
7241fn list_marker_can_interrupt_paragraph(input: &str) -> bool {
7242 list_marker_info(input).is_some_and(|marker| {
7243 !marker.content.trim().is_empty() && (!marker.ordered || marker.start == Some(1))
7246 })
7247}
7248
7249fn table_body_line_ends_table(line: &str, options: &SyntaxOptions) -> bool {
7254 likely_block_start(line, options)
7255 || list_marker_info(line).is_some()
7256 || (options.constructs.html_container && line_starts_html_container(line))
7257 || (options.constructs.html_block && line_starts_html_block(line))
7258}
7259
7260fn line_starts_interrupting_html_block(input: &str) -> bool {
7261 match trim_up_to_three_spaces(input).and_then(html_block_start) {
7262 Some(HtmlBlockKind::UntilBlank) | None => false,
7263 Some(_) => true,
7264 }
7265}
7266
7267fn parse_autolink_end(input: &str, index: usize) -> Option<usize> {
7268 input[index..].find('>').map(|end| index + end + 1)
7269}
7270
7271fn parse_html_inline(input: &str, index: usize) -> Option<(usize, String)> {
7272 let rest = &input[index..];
7273 if rest.starts_with("<!--") {
7274 let end = rest.find("-->")? + 3;
7275 return Some((index + end, rest[..end].into()));
7276 }
7277 if rest.starts_with("<?") {
7278 let end = rest.find("?>")? + 2;
7279 return Some((index + end, rest[..end].into()));
7280 }
7281 if rest.starts_with("<![CDATA[") {
7282 let end = rest.find("]]>")? + 3;
7283 return Some((index + end, rest[..end].into()));
7284 }
7285 if is_declaration_start(rest) {
7286 let end = rest.find('>')? + 1;
7287 return Some((index + end, rest[..end].into()));
7288 }
7289
7290 let (end, _) = parse_html_tag(input, index)?;
7291 Some((end, input[index..end].into()))
7292}
7293
7294fn parse_html_tag(input: &str, index: usize) -> Option<(usize, &str)> {
7295 let bytes = input.as_bytes();
7296 if bytes.get(index) != Some(&b'<') {
7297 return None;
7298 }
7299
7300 let closing = bytes.get(index + 1) == Some(&b'/');
7301 let name_start = index + if closing { 2 } else { 1 };
7302 let first = *bytes.get(name_start)?;
7303 if !first.is_ascii_alphabetic() {
7304 return None;
7305 }
7306
7307 let mut cursor = name_start + 1;
7308 while bytes.get(cursor).is_some_and(|byte| html_name_byte(*byte)) {
7309 cursor += 1;
7310 }
7311 let name = &input[name_start..cursor];
7312
7313 if closing {
7314 cursor = skip_spaces(input, cursor);
7315 if bytes.get(cursor) == Some(&b'>') {
7316 return Some((cursor + 1, name));
7317 }
7318 return None;
7319 }
7320
7321 let mut needs_space = false;
7322 loop {
7323 let before_spaces = cursor;
7324 cursor = skip_spaces(input, cursor);
7325 let had_space = cursor > before_spaces;
7326 match bytes.get(cursor) {
7327 Some(b'>') => return Some((cursor + 1, name)),
7328 Some(b'/') if bytes.get(cursor + 1) == Some(&b'>') => return Some((cursor + 2, name)),
7329 Some(byte) if had_space && html_attribute_name_start(*byte) => {
7330 cursor += 1;
7331 while bytes
7332 .get(cursor)
7333 .is_some_and(|byte| html_attribute_name_byte(*byte))
7334 {
7335 cursor += 1;
7336 }
7337 let after_name = cursor;
7338 let after_spaces = skip_spaces(input, cursor);
7339 if bytes.get(after_spaces) == Some(&b'=') {
7340 cursor = skip_spaces(input, after_spaces + 1);
7341 cursor = parse_html_attribute_value(input, cursor)?;
7342 } else {
7343 cursor = after_name;
7344 }
7345 needs_space = true;
7346 }
7347 Some(_) if needs_space => return None,
7348 _ => return None,
7349 }
7350 }
7351}
7352
7353fn parse_html_attribute_value(input: &str, index: usize) -> Option<usize> {
7354 let bytes = input.as_bytes();
7355 match bytes.get(index)? {
7356 b'"' | b'\'' => {
7357 let quote = bytes[index];
7358 let mut cursor = index + 1;
7359 while cursor < bytes.len() {
7360 if bytes[cursor] == quote {
7361 return Some(cursor + 1);
7362 }
7363 cursor += 1;
7364 }
7365 None
7366 }
7367 b'=' | b'<' | b'>' | b'`' => None,
7368 _ => {
7369 let mut cursor = index;
7370 while bytes.get(cursor).is_some_and(|byte| {
7371 !byte.is_ascii_whitespace()
7372 && !matches!(*byte, b'"' | b'\'' | b'=' | b'<' | b'>' | b'`')
7373 }) {
7374 cursor += 1;
7375 }
7376 if cursor == index {
7377 None
7378 } else {
7379 Some(cursor)
7380 }
7381 }
7382 }
7383}
7384
7385fn html_name_byte(byte: u8) -> bool {
7386 byte.is_ascii_alphanumeric() || byte == b'-'
7387}
7388
7389fn html_attribute_name_start(byte: u8) -> bool {
7390 byte.is_ascii_alphabetic() || byte == b'_' || byte == b':'
7391}
7392
7393fn html_attribute_name_byte(byte: u8) -> bool {
7394 byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b':' | b'.' | b'-')
7395}
7396
7397fn skip_spaces(input: &str, mut index: usize) -> usize {
7398 while input
7399 .as_bytes()
7400 .get(index)
7401 .is_some_and(|byte| matches!(*byte, b' ' | b'\t' | b'\n' | b'\r'))
7402 {
7403 index += 1;
7404 }
7405 index
7406}
7407
7408fn is_autolink(input: &str) -> bool {
7409 let inner = &input[1..input.len() - 1];
7410 is_uri_autolink(inner) || is_email_autolink(inner)
7411}
7412
7413fn is_uri_autolink(input: &str) -> bool {
7414 let Some(colon) = input.find(':') else {
7415 return false;
7416 };
7417 let scheme = &input[..colon];
7418 if scheme.len() < 2 || scheme.len() > 32 {
7419 return false;
7420 }
7421 let mut bytes = scheme.bytes();
7422 if !bytes.next().is_some_and(|byte| byte.is_ascii_alphabetic()) {
7423 return false;
7424 }
7425 if !bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'.' | b'-')) {
7426 return false;
7427 }
7428 input[colon + 1..]
7429 .chars()
7430 .all(|char| !matches!(char, '<' | '>') && !char.is_control() && !char.is_whitespace())
7431}
7432
7433fn is_email_autolink(input: &str) -> bool {
7434 if input.chars().any(char::is_whitespace) {
7435 return false;
7436 }
7437 let Some(at) = input.find('@') else {
7438 return false;
7439 };
7440 if at == 0 || at + 1 >= input.len() {
7441 return false;
7442 }
7443 is_email_local_part(&input[..at]) && is_email_domain(&input[at + 1..], 1)
7446}
7447
7448fn parse_literal_autolink(
7456 input: &str,
7457 index: usize,
7458 gfm: bool,
7459 relaxed: bool,
7460) -> Option<(usize, String)> {
7461 let rest = &input[index..];
7462
7463 if gfm {
7464 if let Some(scheme_len) = rest
7467 .starts_with("http://")
7468 .then_some(7)
7469 .or_else(|| rest.starts_with("https://").then_some(8))
7470 {
7471 if !literal_scheme_prefix_ok(input, index) {
7472 return None;
7473 }
7474 let host = &input[index + scheme_len..];
7475 if !http_literal_host_ok(host) {
7478 if relaxed {
7479 } else {
7482 return None;
7483 }
7484 } else {
7485 let end = autolink_url_end(input, index + scheme_len, index + scheme_len, relaxed);
7489 if end <= index + scheme_len {
7490 return None;
7491 }
7492 if literal_autolink_suppressed_by_link_label(input, index, end, relaxed, gfm) {
7493 return None;
7494 }
7495 return Some((end, input[index..end].into()));
7496 }
7497 }
7498
7499 if rest
7502 .as_bytes()
7503 .get(..4)
7504 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"www."))
7505 {
7506 if !literal_www_prefix_ok(input, index) {
7507 return None;
7508 }
7509 check_domain(rest, false)?;
7510 let end = autolink_url_end(input, index, index, relaxed);
7511 if end <= index || (!relaxed && end <= index + 3 && !literal_starts_line(input, index))
7512 {
7513 return None;
7514 }
7515 if literal_autolink_suppressed_by_link_label(input, index, end, relaxed, gfm) {
7516 return None;
7517 }
7518 let mut destination = String::from("http://");
7519 destination.push_str(&input[index..end]);
7520 return Some((end, destination));
7521 }
7522
7523 if let Some(email) = parse_literal_email(input, index) {
7524 return Some(email);
7525 }
7526 }
7527
7528 if relaxed {
7529 if literal_scheme_prefix_ok(input, index) {
7536 if let Some(after_slashes) = relaxed_scheme_after_slashes(rest) {
7537 let body_start = index + after_slashes;
7538 let next = input[body_start..].chars().next();
7539 if next.is_none_or(|char| char.is_whitespace()) && after_slashes == 3 {
7540 return None;
7541 }
7542 let end = autolink_url_end(input, body_start, body_start, true);
7543 if end > index {
7544 if literal_autolink_suppressed_by_link_label(input, index, end, relaxed, gfm) {
7545 return None;
7546 }
7547 return Some((end, input[index..end].into()));
7548 }
7549 }
7550 }
7551 }
7552
7553 None
7554}
7555
7556fn relaxed_scheme_after_slashes(rest: &str) -> Option<usize> {
7562 let bytes = rest.as_bytes();
7563 if bytes.starts_with(b"://") {
7564 return Some(3);
7565 }
7566 let first = bytes.first()?;
7567 if !first.is_ascii_alphabetic() {
7568 return None;
7569 }
7570 let mut i = 1;
7571 while i < bytes.len() {
7572 match bytes[i] {
7573 b':' => break,
7574 byte if byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'.' | b'-') => i += 1,
7575 _ => return None,
7576 }
7577 }
7578 if bytes.get(i..i + 3) == Some(b"://") {
7579 Some(i + 3)
7580 } else {
7581 None
7582 }
7583}
7584
7585fn literal_scheme_prefix_ok(input: &str, index: usize) -> bool {
7589 if index == 0 {
7590 return true;
7591 }
7592 let Some(previous) = input[..index].chars().next_back() else {
7593 return true;
7594 };
7595 !previous.is_ascii_alphabetic()
7596}
7597
7598fn literal_www_prefix_ok(input: &str, index: usize) -> bool {
7602 if index == 0 {
7603 return true;
7604 }
7605 let Some(previous) = input[..index].chars().next_back() else {
7606 return true;
7607 };
7608 if matches!(previous, '*' | '_' | '~' | '(' | '[' | ']') {
7609 return true;
7610 }
7611 matches!(previous, ' ' | '\t' | '\n' | '\r')
7612}
7613
7614fn literal_starts_line(input: &str, index: usize) -> bool {
7615 index == 0
7616 || input
7617 .as_bytes()
7618 .get(index - 1)
7619 .is_some_and(|byte| matches!(byte, b'\n' | b'\r'))
7620}
7621
7622fn literal_autolink_suppressed_by_link_label(
7623 input: &str,
7624 index: usize,
7625 end: usize,
7626 relaxed: bool,
7627 gfm_autolink_literal: bool,
7628) -> bool {
7629 if !has_unclosed_link_label_opener(input, index) {
7630 return false;
7631 }
7632 if input[end..].starts_with("](") && !link_resource_tail_has_close(input, end + 2) {
7633 return true;
7634 }
7635 !relaxed && !gfm_autolink_literal && input.as_bytes().get(end).is_some_and(|byte| *byte == b']')
7636}
7637
7638fn has_unclosed_link_label_opener(input: &str, index: usize) -> bool {
7639 let line_start = input[..index]
7640 .rfind(['\n', '\r'])
7641 .map_or(0, |offset| offset + 1);
7642 let mut depth = 0usize;
7643 let mut cursor = line_start;
7644 while cursor < index {
7645 let Some((next, char)) = next_char(input, cursor) else {
7646 break;
7647 };
7648 match char {
7649 '\\' => {
7650 cursor = next_char(input, next)
7651 .map(|(after_escape, _)| after_escape)
7652 .unwrap_or(next);
7653 continue;
7654 }
7655 '[' => depth += 1,
7656 ']' => {
7657 depth = depth.saturating_sub(1);
7658 }
7659 _ => {}
7660 }
7661 cursor = next;
7662 }
7663 depth > 0
7664}
7665
7666fn link_resource_tail_has_close(input: &str, start: usize) -> bool {
7667 let mut cursor = start;
7668 while cursor < input.len() {
7669 let Some((next, char)) = next_char(input, cursor) else {
7670 break;
7671 };
7672 match char {
7673 '\\' => {
7674 cursor = next_char(input, next)
7675 .map(|(after_escape, _)| after_escape)
7676 .unwrap_or(next);
7677 continue;
7678 }
7679 '\n' | '\r' => return false,
7680 ')' => return true,
7681 _ => {}
7682 }
7683 cursor = next;
7684 }
7685 false
7686}
7687
7688fn http_literal_host_ok(host: &str) -> bool {
7689 if host.starts_with('[') {
7690 return bracketed_ipv6_host_end(host).is_some();
7691 }
7692 match host.chars().next() {
7693 Some(char) if char.is_ascii() && char.is_ascii_alphanumeric() => {
7694 check_domain(host, true).is_some()
7695 }
7696 Some(char) if !char.is_ascii() && is_valid_hostchar(char) => {
7697 check_domain(host, true).is_some()
7698 }
7699 _ => false,
7700 }
7701}
7702
7703fn bracketed_ipv6_host_end(host: &str) -> Option<usize> {
7704 let close = host.find(']')?;
7705 (close > 1).then_some(close + 1)
7706}
7707
7708fn is_valid_hostchar(char: char) -> bool {
7711 !char.is_whitespace() && !crate::unicode_punctuation::is_unicode_punctuation(char)
7712}
7713
7714fn check_domain(data: &str, allow_short: bool) -> Option<usize> {
7725 let mut np = 0usize;
7726 let mut uscore1 = 0usize;
7727 let mut uscore2 = 0usize;
7728 let mut host_len = 0usize;
7729
7730 let mut chars = data.char_indices().peekable();
7731 while let Some((offset, char)) = chars.next() {
7732 let account = offset != 0 && chars.peek().is_some();
7737 match char {
7738 '\\' => {
7739 host_len = offset + char.len_utf8();
7741 if let Some((next_off, next)) = chars.next() {
7742 host_len = next_off + next.len_utf8();
7743 }
7744 }
7745 '_' if account => {
7746 uscore2 += 1;
7747 host_len = offset + char.len_utf8();
7748 }
7749 '.' if account => {
7750 uscore1 = uscore2;
7751 uscore2 = 0;
7752 np += 1;
7753 host_len = offset + char.len_utf8();
7754 }
7755 '_' | '.' | '-' => {
7756 host_len = offset + char.len_utf8();
7757 }
7758 _ => {
7759 if !is_valid_hostchar(char) {
7760 break;
7761 }
7762 host_len = offset + char.len_utf8();
7763 }
7764 }
7765 }
7766
7767 if (uscore1 > 0 || uscore2 > 0) && np <= 10 {
7768 return None;
7769 }
7770
7771 if allow_short || np > 0 {
7772 Some(host_len)
7773 } else {
7774 None
7775 }
7776}
7777
7778fn autolink_url_end(input: &str, start: usize, trim_from: usize, balanced: bool) -> usize {
7784 let bytes = input.as_bytes();
7785 let mut end = start;
7786 let mut bracket_depth = 0i32;
7793 let mut curly_depth = 0i32;
7794 let mut strict_has_open_bracket = false;
7795 let mut strict_inside_backticks = false;
7796 for (offset, char) in input[start..].char_indices() {
7797 if char.is_whitespace() || char == '<' || is_autolink_terminating_control(char) {
7798 break;
7799 }
7800 if balanced {
7801 match char {
7802 '[' => bracket_depth += 1,
7803 ']' => {
7804 if bracket_depth > 0 {
7805 bracket_depth -= 1;
7806 } else {
7807 break;
7808 }
7809 }
7810 '{' => curly_depth += 1,
7811 '}' => {
7812 if curly_depth > 0 {
7813 curly_depth -= 1;
7814 } else {
7815 break;
7816 }
7817 }
7818 _ => {}
7819 }
7820 } else {
7821 match char {
7822 '[' => strict_has_open_bracket = true,
7823 '`' => strict_inside_backticks = !strict_inside_backticks,
7824 ']' if !strict_has_open_bracket && !strict_inside_backticks => break,
7825 _ => {}
7826 }
7827 }
7828 if char == '\\' {
7837 if let Some(&next) = bytes.get(start + offset + 1) {
7838 let next_is_escapable_punct = next.is_ascii_punctuation() && next != b'.';
7839 if next_is_escapable_punct {
7840 break;
7841 }
7842 }
7843 }
7844 end = start + offset + char.len_utf8();
7845 }
7846 autolink_delim(input, trim_from, end)
7847}
7848
7849fn is_autolink_terminating_control(char: char) -> bool {
7850 matches!(char, '\u{2066}'..='\u{2069}')
7851}
7852
7853fn autolink_delim(input: &str, start: usize, mut end: usize) -> usize {
7858 let bytes = input.as_bytes();
7859 let mut opening = 0usize;
7860 let mut closing = 0usize;
7861 for &byte in &bytes[start..end] {
7862 match byte {
7863 b'(' => opening += 1,
7864 b')' => closing += 1,
7865 _ => {}
7866 }
7867 }
7868
7869 while end > start {
7870 match bytes[end - 1] {
7871 b')' => {
7872 if closing <= opening {
7873 break;
7874 }
7875 closing -= 1;
7876 end -= 1;
7877 }
7878 b'?' | b'!' | b'.' | b',' | b':' | b'*' | b'_' | b'~' | b'\'' | b'"' => {
7879 end -= 1;
7880 }
7881 b';' => {
7882 if let Some(amp) = trailing_hex_entity_run_start(bytes, start, end) {
7889 end = amp;
7890 } else {
7891 let mut new_end = end - 1;
7894 while new_end > start && bytes[new_end - 1].is_ascii_alphanumeric() {
7895 new_end -= 1;
7896 }
7897 if new_end > start && new_end < end - 1 && bytes[new_end - 1] == b'&' {
7898 end = new_end - 1;
7899 } else {
7900 end -= 1;
7901 }
7902 }
7903 }
7904 _ => break,
7905 }
7906 }
7907 end
7908}
7909
7910fn trailing_hex_entity_run_start(bytes: &[u8], start: usize, end: usize) -> Option<usize> {
7916 if end <= start || bytes[end - 1] != b';' {
7917 return None;
7918 }
7919 let mut cursor = end - 1;
7920 while cursor > start && bytes[cursor - 1].is_ascii_hexdigit() {
7921 cursor -= 1;
7922 }
7923 if cursor == end - 1 || cursor < start + 3 {
7925 return None;
7926 }
7927 let x = bytes[cursor - 1];
7928 if (x == b'x' || x == b'X') && bytes[cursor - 2] == b'#' && bytes[cursor - 3] == b'&' {
7929 Some(cursor - 3)
7930 } else {
7931 None
7932 }
7933}
7934
7935fn parse_literal_email(input: &str, index: usize) -> Option<(usize, String)> {
7941 let rest = &input[index..];
7942 let at = rest.find('@')?;
7943 if at == 0 {
7944 return None;
7945 }
7946 let local = &rest[..at];
7947
7948 let (auto_mailto, is_xmpp) = classify_email_local(local);
7952
7953 if !email_left_boundary_ok(input, index, auto_mailto) {
7958 return None;
7959 }
7960
7961 if !email_local_is_valid(local, auto_mailto) {
7962 return None;
7963 }
7964
7965 let domain_start = index + at + 1;
7966 let domain_end = literal_email_domain_end(input, domain_start, is_xmpp)?;
7967 let trimmed = autolink_delim(input, domain_start, domain_end);
7968 if trimmed <= domain_start {
7969 return None;
7970 }
7971
7972 let domain = &input[domain_start..trimmed];
7973 if !is_gfm_email_domain(domain, is_xmpp) {
7974 return None;
7975 }
7976
7977 let mut destination = String::new();
7978 if auto_mailto {
7979 destination.push_str("mailto:");
7980 }
7981 destination.push_str(&input[index..trimmed]);
7982 Some((trimmed, destination))
7983}
7984
7985fn classify_email_local(local: &str) -> (bool, bool) {
7990 if let Some(rest) = strip_ci_prefix(local, "mailto:") {
7991 if !rest.is_empty() {
7992 return (false, false);
7993 }
7994 }
7995 if let Some(rest) = strip_ci_prefix(local, "xmpp:") {
7996 if !rest.is_empty() {
7997 return (false, true);
7998 }
7999 }
8000 (true, false)
8001}
8002
8003fn strip_ci_prefix<'a>(input: &'a str, prefix: &str) -> Option<&'a str> {
8004 let bytes = input.as_bytes();
8005 let plen = prefix.len();
8006 if bytes.len() >= plen && bytes[..plen].eq_ignore_ascii_case(prefix.as_bytes()) {
8007 Some(&input[plen..])
8008 } else {
8009 None
8010 }
8011}
8012
8013fn email_left_boundary_ok(input: &str, index: usize, auto_mailto: bool) -> bool {
8020 if index == 0 {
8021 return true;
8022 }
8023 let Some(previous) = input[..index].chars().next_back() else {
8024 return true;
8025 };
8026 if previous.is_ascii_alphanumeric() {
8027 if auto_mailto
8028 && input[index..].starts_with('+')
8029 && prefix_ends_with_gfm_email(input, index)
8030 {
8031 return true;
8032 }
8033 return false;
8034 }
8035 if auto_mailto && previous == '/' {
8036 return false;
8037 }
8038 true
8039}
8040
8041fn prefix_ends_with_gfm_email(input: &str, end: usize) -> bool {
8042 let start = input[..end]
8043 .rfind(char::is_whitespace)
8044 .map_or(0, |offset| offset + 1);
8045 let candidate = &input[start..end];
8046 let Some(at) = candidate.rfind('@') else {
8047 return false;
8048 };
8049 email_local_is_valid(&candidate[..at], true) && is_gfm_email_domain(&candidate[at + 1..], false)
8050}
8051
8052fn email_local_is_valid(local: &str, auto_mailto: bool) -> bool {
8056 let body = if auto_mailto {
8057 local
8058 } else if let Some(rest) = strip_ci_prefix(local, "mailto:") {
8059 rest
8060 } else if let Some(rest) = strip_ci_prefix(local, "xmpp:") {
8061 rest
8062 } else {
8063 local
8064 };
8065 !body.is_empty() && body.bytes().all(is_gfm_email_local_byte)
8066}
8067
8068fn is_gfm_email_local_byte(byte: u8) -> bool {
8071 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'+' | b'_' | b'-')
8072}
8073
8074fn is_email_local_part(input: &str) -> bool {
8075 !input.is_empty()
8076 && input
8077 .split('.')
8078 .all(|segment| !segment.is_empty() && segment.bytes().all(is_email_atext))
8079}
8080
8081fn is_email_atext(byte: u8) -> bool {
8082 byte.is_ascii_alphanumeric()
8083 || matches!(
8084 byte,
8085 b'!' | b'#'
8086 | b'$'
8087 | b'%'
8088 | b'&'
8089 | b'\''
8090 | b'*'
8091 | b'+'
8092 | b'/'
8093 | b'='
8094 | b'?'
8095 | b'^'
8096 | b'_'
8097 | b'`'
8098 | b'{'
8099 | b'|'
8100 | b'}'
8101 | b'~'
8102 | b'-'
8103 )
8104}
8105
8106fn literal_email_domain_end(input: &str, index: usize, is_xmpp: bool) -> Option<usize> {
8114 let bytes = input.as_bytes();
8115 let mut end = index;
8116 let mut np = 0usize;
8117 while end < bytes.len() {
8118 let byte = bytes[end];
8119 if byte.is_ascii_alphanumeric() {
8120 end += 1;
8121 } else if byte == b'.' && end + 1 < bytes.len() && bytes[end + 1].is_ascii_alphanumeric() {
8122 np += 1;
8123 end += 1;
8124 } else if byte == b'-' || byte == b'_' || (byte == b'/' && is_xmpp) {
8125 end += 1;
8128 } else {
8129 break;
8130 }
8131 }
8132 if end <= index {
8133 return None;
8134 }
8135 let len = end - index;
8136 let last = bytes[end - 1];
8137 if len < 1 || np == 0 || !(last.is_ascii_alphabetic() || last == b'.') {
8138 return None;
8139 }
8140 Some(end)
8141}
8142
8143fn is_gfm_email_domain(input: &str, is_xmpp: bool) -> bool {
8148 if input.is_empty() {
8149 return false;
8150 }
8151 let host = if is_xmpp {
8154 input.split('/').next().unwrap_or(input)
8155 } else {
8156 input
8157 };
8158 if !host.contains('.') {
8159 return false;
8160 }
8161 let last = host.as_bytes()[host.len() - 1];
8162 if matches!(last, b'-' | b'_') {
8165 return false;
8166 }
8167 host.split('.').all(|label| {
8168 !label.is_empty()
8169 && label
8170 .bytes()
8171 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
8172 })
8173}
8174
8175fn is_email_domain(input: &str, min_labels: usize) -> bool {
8176 let mut label_count = 0usize;
8177 for label in input.split('.') {
8178 label_count += 1;
8179 let bytes = label.as_bytes();
8180 if bytes.is_empty()
8181 || bytes.len() > 63
8182 || !bytes
8183 .first()
8184 .is_some_and(|byte| byte.is_ascii_alphanumeric())
8185 || !bytes
8186 .last()
8187 .is_some_and(|byte| byte.is_ascii_alphanumeric())
8188 || !bytes
8189 .iter()
8190 .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-')
8191 {
8192 return false;
8193 }
8194 }
8195 label_count >= min_labels
8196}
8197
8198fn is_footnote_label(label: &str) -> bool {
8199 !label.is_empty()
8200 && reference_label_is_within_limit(label)
8201 && !label.chars().any(char::is_whitespace)
8202}
8203
8204fn find_footnote_definition_label_end(input: &str) -> Option<usize> {
8205 let close = find_footnote_reference_label_end(input, 2)?;
8206 if input.as_bytes().get(close + 1) == Some(&b':') {
8207 Some(close)
8208 } else {
8209 None
8210 }
8211}
8212
8213fn find_footnote_reference_label_end(input: &str, mut cursor: usize) -> Option<usize> {
8214 while cursor < input.len() {
8215 let (next, char) = next_char(input, cursor)?;
8216 if char == ']' && !is_escaped_at(input, cursor) {
8217 return Some(cursor);
8218 }
8219 cursor = next;
8220 }
8221 None
8222}
8223
8224fn find_inline_footnote_end(input: &str, mut cursor: usize) -> Option<usize> {
8225 let mut depth = 0usize;
8226 while cursor < input.len() {
8227 let (next, char) = next_char(input, cursor)?;
8228 if !is_escaped_at(input, cursor) {
8229 match char {
8230 '[' => depth += 1,
8231 ']' if depth == 0 => return Some(cursor),
8232 ']' => depth = depth.saturating_sub(1),
8233 _ => {}
8234 }
8235 }
8236 cursor = next;
8237 }
8238 None
8239}