1use alloc::{borrow::Cow, collections::BTreeMap, string::String, vec, vec::Vec};
6
7use crate::{
8 ast::*,
9 decode::{
10 decode_escapes_and_references, decode_selected_escapes_and_references,
11 parse_character_reference,
12 },
13 diagnostic::{Diagnostic, DiagnosticCode, DiagnosticSeverity},
14 memo::{
15 bracket_walk, path_walk, pattern_starts, BracketMemo, BracketStep, PathMemo, Positions,
16 Step,
17 },
18 span::Span,
19 validate::is_directive_name,
20};
21
22mod blank_lines;
23mod blocks;
24mod nul_replacement;
25#[cfg(test)]
26mod scan_tests;
27mod source_map;
28mod tags;
29mod tokens;
30
31pub(crate) use blocks::interrupts_paragraph;
32use source_map::{DerivedText, Segment, SourceMap};
33
34pub(crate) const MAX_ORDERED_NUMBER: u64 = 999_999_999;
36
37#[derive(Clone, Debug, Eq, PartialEq)]
40pub struct ParseOutput {
41 pub document: Document,
43 pub diagnostics: Vec<Diagnostic>,
45}
46
47#[derive(Clone, Debug, Eq, PartialEq)]
48struct ParsedLinkResource {
49 destination: String,
50 destination_kind: LinkDestinationKind,
51 title: Option<Title>,
52}
53
54#[derive(Clone, Copy)]
58struct LinkTailRanges {
59 destination: (usize, usize),
60 title: Option<(usize, usize)>,
61}
62
63const REFERENCE_LABEL_MAX_CHARS: usize = 999;
64const WIKILINK_MAX_BYTES: usize = 999;
65
66#[derive(Clone, Copy, Debug)]
71struct Line<'a> {
72 text: &'a str,
73 eol: &'a str,
74 start: usize,
75 end: usize,
76 end_with_eol: usize,
77 segments: &'a [Segment],
80 text_offset: usize,
81 column: usize,
84}
85
86impl<'a> Line<'a> {
87 fn detached(text: &'a str) -> Self {
89 Line {
90 text,
91 eol: "",
92 start: 0,
93 end: text.len(),
94 end_with_eol: text.len(),
95 segments: &[],
96 text_offset: 0,
97 column: 0,
98 }
99 }
100
101 fn column_at(&self, offset: usize) -> usize {
103 advance_columns(self.column, &self.text[..offset])
104 }
105
106 fn source_start(&self, offset: usize) -> usize {
109 source_map::start_of(self.segments, self.text_offset + offset)
110 }
111
112 fn source_end(&self, offset: usize) -> usize {
114 source_map::end_of(self.segments, self.text_offset + offset)
115 }
116
117 fn eol_source(&self) -> (usize, usize) {
119 (self.end, self.end_with_eol)
120 }
121
122 fn copy_into(&self, map: &mut SourceMap, at: usize, from: usize, to: usize) {
125 map.copy(
126 at,
127 self.segments,
128 self.text_offset + from,
129 self.text_offset + to,
130 );
131 }
132}
133
134#[derive(Clone, Copy, Debug, Eq, PartialEq)]
135enum HtmlBlockKind {
136 RawTag,
137 BlockTag,
138 Until(&'static str),
139 UntilBlank,
140}
141
142pub fn parse(input: &str) -> ParseOutput {
145 let mut diagnostics = Vec::new();
146 let start = if input.starts_with('\u{feff}') {
149 '\u{feff}'.len_utf8()
150 } else {
151 0
152 };
153 let source = &input[start..];
154 let map = SourceMap::verbatim(source.len(), start);
155 let lines = collect_lines(source, &map);
156 let mut children = blocks::parse_document(&lines, &mut diagnostics);
157 blank_lines::add_blank_lines(input, &mut children);
158 let mut document = Document::parsed(
159 NodeMeta::new(Some(Span::new(0, input.len()))),
160 children,
161 input,
162 );
163 if source.contains('\0') {
164 nul_replacement::replace_in_document(&mut document);
165 }
166
167 ParseOutput {
168 document,
169 diagnostics,
170 }
171}
172
173#[derive(Clone, Debug, Default, Eq, PartialEq)]
177pub struct Definitions {
178 identifiers: Vec<String>,
180}
181
182impl Definitions {
183 pub fn of(document: &Document) -> Self {
186 fn walk(blocks: &[Block], identifiers: &mut Vec<String>) {
187 for block in blocks {
188 match block {
189 Block::Definition(node) => identifiers.push(node.identifier.clone()),
190 Block::BlockQuote(node) => walk(&node.children, identifiers),
191 Block::Alert(node) => walk(&node.children, identifiers),
192 Block::List(node) => {
193 for item in &node.children {
194 walk(&item.children, identifiers);
195 }
196 }
197 Block::FootnoteDefinition(node) => walk(&node.children, identifiers),
198 Block::HtmlContainer(node) => {
199 if let HtmlContainerContent::Blocks(children) = &node.content {
200 walk(children, identifiers);
201 }
202 }
203 Block::ContainerDirective(node) => walk(&node.children, identifiers),
204 _ => {}
205 }
206 }
207 }
208 let mut identifiers = Vec::new();
209 walk(&document.children, &mut identifiers);
210 Self::sorted(identifiers)
211 }
212
213 pub fn new<I, S>(labels: I) -> Self
216 where
217 I: IntoIterator<Item = S>,
218 S: AsRef<str>,
219 {
220 Self::sorted(
221 labels
222 .into_iter()
223 .map(|label| identifier(label.as_ref()))
224 .collect(),
225 )
226 }
227
228 fn sorted(mut identifiers: Vec<String>) -> Self {
229 identifiers.sort_unstable();
230 identifiers.dedup();
231 Self { identifiers }
232 }
233
234 fn labels(&self) -> Labels<'_> {
235 Labels {
236 own: &self.identifiers,
237 }
238 }
239}
240
241#[derive(Clone, Debug, Eq, PartialEq)]
244#[non_exhaustive]
245pub struct InlineOutput {
246 pub inlines: Vec<Inline>,
248 pub tokens: Vec<InlineToken>,
251 pub diagnostics: Vec<Diagnostic>,
253}
254
255impl InlineOutput {
256 pub fn nodes(&self) -> Vec<&Inline> {
260 let mut nodes = Vec::new();
261 let mut stack: Vec<&Inline> = self.inlines.iter().rev().collect();
262 while let Some(inline) = stack.pop() {
263 nodes.push(inline);
264 stack.extend(inline.children().iter().rev());
265 }
266 nodes
267 }
268}
269
270#[derive(Clone, Copy, Debug, Eq, PartialEq)]
274pub struct InlineToken {
275 pub span: Span,
277 pub node: usize,
280 pub role: TokenRole,
282}
283
284#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
286#[non_exhaustive]
287pub enum TokenRole {
288 Text,
293 Syntax,
295 Destination,
297 Title,
299 Label,
302 Target,
304 Name,
306 Attributes,
308}
309
310pub fn parse_inline(text: &str, definitions: &Definitions) -> InlineOutput {
315 read_inline(text, definitions, true)
316}
317
318pub(crate) fn parse_inline_tree(text: &str, definitions: &Definitions) -> InlineOutput {
321 read_inline(text, definitions, false)
322}
323
324fn read_inline(text: &str, definitions: &Definitions, with_tokens: bool) -> InlineOutput {
325 let mut diagnostics = Vec::new();
326 let (content, map) = if text.contains('\r') {
329 let mut content = String::with_capacity(text.len());
330 let mut map = SourceMap::default();
331 for line in collect_lines(text, &SourceMap::verbatim(text.len(), 0)) {
332 map.push(content.len(), line.text.len(), line.start, line.end);
333 content.push_str(line.text);
334 if !line.eol.is_empty() {
335 map.push(content.len(), 1, line.end, line.end_with_eol);
336 content.push('\n');
337 }
338 }
339 (Cow::Owned(content), map)
340 } else {
341 (Cow::Borrowed(text), SourceMap::verbatim(text.len(), 0))
342 };
343 let mut tokens = Vec::new();
344 let mut inlines = parse_inlines(
345 &content,
346 &map,
347 Some(definitions.labels()),
348 &mut diagnostics,
349 with_tokens.then_some(&mut tokens),
350 );
351 if text.contains('\0') {
352 nul_replacement::replace_in_inlines(&mut inlines);
353 }
354 diagnostics.sort_by_key(|diagnostic| diagnostic.span.map(|span| span.start));
355 InlineOutput {
356 inlines,
357 tokens,
358 diagnostics,
359 }
360}
361
362#[derive(Clone, Debug)]
366pub struct InlineSource {
367 pub text: String,
371 map: SourceMap,
372}
373
374impl InlineSource {
375 pub fn to_source(&self, offset: usize) -> usize {
378 self.span_to_source(Span::new(offset, offset)).start
379 }
380
381 pub fn span_to_source(&self, span: Span) -> Span {
384 source_map::translate_span(&self.map, span)
385 }
386}
387
388#[doc(hidden)]
392pub fn inline_spans_to_source(source: &InlineSource, inlines: &mut [Inline]) {
393 source_map::translate_inlines(&source.map, inlines, &mut []);
394}
395
396impl Document {
397 pub fn inline_source(&self, path: &NodePath) -> Option<InlineSource> {
406 let source = self.source()?;
407 let (&first, rest) = path.0.split_first()?;
408 let top = self.children.get(first)?;
409 let mut block = top;
410 let mut rest = rest.iter();
411 let leaf = loop {
412 let Some(&index) = rest.next() else {
413 break match block {
414 Block::Paragraph(node) => node.meta.span?,
415 Block::Heading(node) => node.meta.span?,
416 Block::LeafDirective(node) => node.meta.span?,
417 Block::ContainerDirective(node) => node.meta.span?,
418 _ => return None,
419 };
420 };
421 block = match block {
422 Block::BlockQuote(node) => node.children.get(index)?,
423 Block::Alert(node) => node.children.get(index)?,
424 Block::FootnoteDefinition(node) => node.children.get(index)?,
425 Block::ContainerDirective(node) => node.children.get(index)?,
426 Block::HtmlContainer(node) => match &node.content {
427 HtmlContainerContent::Blocks(children) => children.get(index)?,
428 _ => return None,
429 },
430 Block::List(node) => {
431 let item = node.children.get(index)?;
432 item.children.get(*rest.next()?)?
433 }
434 Block::Table(node) => {
435 let cell = node.rows.get(index)?.cells.get(*rest.next()?)?;
436 if rest.next().is_some() {
437 return None;
438 }
439 break cell.meta.span?;
440 }
441 _ => return None,
442 };
443 };
444 let end = top.span()?.end;
445 let opening = self.children[..first]
450 .iter()
451 .rev()
452 .take_while(|block| matches!(block, Block::Definition(_)))
453 .last()
454 .unwrap_or(top);
455 let start = opening.span()?.start;
456 if end > source.len() || start > end || !source.is_char_boundary(start) {
457 return None;
458 }
459 let bytes = source.as_bytes();
460 let mut start = start;
461 while start > 0 && !matches!(bytes[start - 1], b'\n' | b'\r') {
462 start -= 1;
463 }
464 if start == 0 && source.starts_with('\u{feff}') {
465 start = '\u{feff}'.len_utf8().min(end);
466 }
467 let slice = source.get(start..end)?;
468 let map = SourceMap::verbatim(slice.len(), start);
469 let lines = collect_lines(slice, &map);
470 let (_, text) = blocks::leaf_contents(&lines)
471 .into_iter()
472 .find(|(span, _)| *span == leaf)?;
473 let (text, map) = text.into_parts();
474 Some(InlineSource { text, map })
475 }
476
477 pub fn leaf_sources(&self) -> Vec<(NodePath, InlineSource)> {
481 let mut found = Vec::new();
482 self.each_leaf_source(|path, _, source| found.push((NodePath(path.to_vec()), source)));
483 found
484 }
485
486 pub(crate) fn each_leaf_source(&self, mut found: impl FnMut(&[usize], Span, InlineSource)) {
489 let Some(source) = self.source() else {
490 return;
491 };
492 let start = if source.starts_with('\u{feff}') {
493 '\u{feff}'.len_utf8()
494 } else {
495 0
496 };
497 let map = SourceMap::verbatim(source.len() - start, start);
498 let lines = collect_lines(&source[start..], &map);
499 let mut contents: Vec<(Span, Option<DerivedText>)> = blocks::leaf_contents(&lines)
500 .into_iter()
501 .map(|(span, text)| (span, Some(text)))
502 .collect();
503 let mut take = |path: &[usize], span: Option<Span>, _: Option<&str>| {
505 let Some(span) = span else { return };
506 let first = contents.partition_point(|(other, _)| other.start < span.start);
507 let entry = contents[first..]
508 .iter_mut()
509 .take_while(|(other, _)| other.start == span.start)
510 .find(|(other, text)| *other == span && text.is_some());
511 if let Some((_, text)) = entry {
512 let (text, map) = text.take().expect("found unused").into_parts();
513 found(path, span, InlineSource { text, map });
514 }
515 };
516 crate::leaves::visit(&self.children, &mut Vec::new(), &mut take);
517 }
518}
519
520fn source_char(char: char) -> char {
524 if char == '\0' {
525 '\u{FFFD}'
526 } else {
527 char
528 }
529}
530
531pub(crate) const MAX_BLOCK_NESTING: usize = 32;
535
536fn advance_columns(column: usize, text: &str) -> usize {
539 text.chars().fold(column, |column, char| {
540 if char == '\t' {
541 column + 4 - column % 4
542 } else {
543 column + 1
544 }
545 })
546}
547
548fn collect_lines<'a>(input: &'a str, map: &'a SourceMap) -> Vec<Line<'a>> {
551 let bytes = input.as_bytes();
552 let mut lines = Vec::new();
553 let mut segments = LineSegments {
554 segments: map.segments(),
555 first: 0,
556 };
557 let mut start = 0;
558 let mut index = 0;
559
560 while index < bytes.len() {
561 let eol_end = match bytes[index] {
562 b'\n' => index + 1,
563 b'\r' if bytes.get(index + 1) == Some(&b'\n') => index + 2,
564 b'\r' => index + 1,
565 _ => {
566 index += 1;
567 continue;
568 }
569 };
570 lines.push(segments.line(input, start, index, eol_end));
571 index = eol_end;
572 start = index;
573 }
574
575 if start < bytes.len() || input.is_empty() {
576 lines.push(segments.line(input, start, bytes.len(), bytes.len()));
577 }
578
579 lines
580}
581
582struct LineSegments<'a> {
585 segments: &'a [Segment],
586 first: usize,
587}
588
589impl<'a> LineSegments<'a> {
590 fn line(&mut self, input: &'a str, start: usize, end: usize, eol_end: usize) -> Line<'a> {
591 let segments = self.segments;
592 while self.first + 1 < segments.len() && segments[self.first].text_end() <= start {
593 self.first += 1;
594 }
595 let mut last = self.first;
596 while last + 1 < segments.len() && segments[last + 1].text_start() < eol_end {
597 last += 1;
598 }
599 let covering = if segments.is_empty() {
600 segments
601 } else {
602 &segments[self.first..=last]
603 };
604 let source_start = source_map::start_of(covering, start);
605 let source_end = if end == start {
606 source_start
607 } else {
608 source_map::end_of(covering, end)
609 };
610 let source_end_with_eol = if eol_end == end {
611 source_end
612 } else {
613 source_map::end_of(covering, eol_end)
614 };
615 Line {
616 text: &input[start..end],
617 eol: &input[end..eol_end],
618 start: source_start,
619 end: source_end,
620 end_with_eol: source_end_with_eol,
621 segments: covering,
622 text_offset: start,
623 column: 0,
624 }
625 }
626}
627
628pub(crate) fn frontmatter_fence_kind(line: &str) -> Option<FrontmatterKind> {
629 match line.trim_end_matches([' ', '\t']) {
630 "---" => Some(FrontmatterKind::Yaml),
631 "+++" => Some(FrontmatterKind::Toml),
632 _ => None,
633 }
634}
635
636fn directive_container_opener_prefix(input: &str) -> Option<(usize, &str)> {
637 let fence_len = input
638 .as_bytes()
639 .iter()
640 .take_while(|byte| **byte == b':')
641 .count();
642 if fence_len >= 3 {
643 Some((fence_len, &input[fence_len..]))
644 } else {
645 None
646 }
647}
648
649fn directive_container_closing_fence(input: &str, min_len: usize) -> Option<usize> {
650 let fence_len = input
651 .as_bytes()
652 .iter()
653 .take_while(|byte| **byte == b':')
654 .count();
655 if fence_len >= min_len && is_blank(&input[fence_len..]) {
656 Some(fence_len)
657 } else {
658 None
659 }
660}
661
662fn math_block_fence_length(input: &str) -> Option<usize> {
665 let length = input
666 .as_bytes()
667 .iter()
668 .take_while(|byte| **byte == b'$')
669 .count();
670 if length < 2 || input[length..].contains('$') {
671 return None;
672 }
673 Some(length)
674}
675
676fn math_block_fence_closes(input: &str, length: usize) -> bool {
679 let count = input
680 .as_bytes()
681 .iter()
682 .take_while(|byte| **byte == b'$')
683 .count();
684 count >= length && is_blank(&input[count..])
685}
686
687fn parse_alert_marker(line: &str) -> Option<(AlertKind, Option<String>)> {
688 let close = line.find(']')?;
689 let marker = line.get(0..close + 1)?;
690 if !marker.starts_with("[!") {
691 return None;
692 }
693 let kind = match &marker[2..close].to_ascii_lowercase()[..] {
694 "note" => AlertKind::Note,
695 "tip" => AlertKind::Tip,
696 "important" => AlertKind::Important,
697 "warning" => AlertKind::Warning,
698 "caution" => AlertKind::Caution,
699 _ => return None,
700 };
701 Some((kind, alert_title(&line[close + 1..])))
702}
703
704pub(crate) fn alert_title(text: &str) -> Option<String> {
707 let title = text.trim_matches([' ', '\t']);
708 (!title.is_empty()).then(|| title.into())
709}
710
711fn parse_thematic_break(line: Line<'_>) -> Option<Block> {
712 let text = trim_up_to_three_spaces(line.text)?;
713 let mut marker = None;
714 let mut count = 0;
715 for char in text.chars() {
716 if char == ' ' || char == '\t' {
717 continue;
718 }
719 let current = match char {
720 '-' => ThematicBreakMarker::Dash,
721 '*' => ThematicBreakMarker::Asterisk,
722 '_' => ThematicBreakMarker::Underscore,
723 _ => return None,
724 };
725 if marker.is_some_and(|marker| marker != current) {
726 return None;
727 }
728 marker = Some(current);
729 count += 1;
730 }
731 if count >= 3 {
732 Some(Block::ThematicBreak(ThematicBreak {
733 meta: NodeMeta::new(Some(Span::new(line.start, line.end))),
734 marker: marker?,
735 }))
736 } else {
737 None
738 }
739}
740
741fn definition_label(lines: &[Line<'_>], index: usize) -> Option<(String, usize, usize)> {
749 let text = trim_ascii_start(lines[index].text);
750 if !text.starts_with('[') {
751 return None;
752 }
753 let mut accumulated = String::from(text);
754 let mut label_end_line = index;
755 let mut cursor = 1;
756 loop {
757 let bytes = accumulated.as_bytes();
758 if cursor >= bytes.len() {
762 let next = label_end_line + 1;
763 if next >= lines.len() || accumulated.len() > 4 * REFERENCE_LABEL_MAX_CHARS + 2 {
764 return None;
765 }
766 accumulated.push('\n');
767 accumulated.push_str(lines[next].text);
768 label_end_line = next;
769 continue;
770 }
771 match reference_label_step(&accumulated, cursor) {
772 Step::Next(next) => cursor = next,
773 Step::Done(Some(close)) if reference_label_is_within_limit(&accumulated[1..close]) => {
774 if bytes.get(close + 1) != Some(&b':') {
776 return None;
777 }
778 return Some((accumulated, close, label_end_line));
779 }
780 Step::Done(_) => return None,
783 }
784 }
785}
786
787fn parse_definition(lines: &[Line<'_>], index: usize) -> Option<(Definition, usize)> {
792 let (accumulated, close, label_end_line) = definition_label(lines, index)?;
793 let label = String::from(&accumulated[1..close]);
794 if normalize_label(&label).is_empty() {
795 return None;
796 }
797 let mut source = String::from(&accumulated[close + 2..]);
798 let mut cursor = label_end_line;
799 let mut best_without_title = None;
800 let mut open_title: Option<char> = None;
804
805 loop {
806 let parses =
807 open_title.is_none_or(|closer| line_may_close_title(lines[cursor].text, closer));
808 if parses {
809 match parse_definition_destination_title(&source) {
810 Some(resource) => {
811 if resource.title.is_some() {
812 return Some((definition_node(&label, resource), cursor + 1));
813 }
814 best_without_title = Some((resource, cursor + 1));
815 let next = cursor + 1;
816 if next >= lines.len() || !line_can_start_definition_title(lines[next].text) {
817 break;
818 }
819 }
820 None if !is_blank(&source) => match open_definition_title(&source) {
823 Some(closer) if open_title.is_none() => open_title = Some(closer),
824 _ => break,
825 },
826 None => {}
827 }
828 }
829 let next = cursor + 1;
830 if next >= lines.len() {
831 break;
832 }
833 source.push('\n');
834 source.push_str(lines[next].text);
835 cursor = next;
836 }
837
838 let (resource, next) = best_without_title?;
839 Some((definition_node(&label, resource), next))
840}
841
842fn definition_node(label: &str, resource: ParsedLinkResource) -> Definition {
843 Definition {
844 meta: NodeMeta::default(),
845 label: label.into(),
846 identifier: normalize_label(label),
847 destination: resource.destination,
848 destination_kind: resource.destination_kind,
849 title: resource.title,
850 }
851}
852
853#[derive(Clone, Copy, Eq, PartialEq)]
854enum HtmlContainerTag {
855 Opening,
856 Closing,
857}
858
859#[derive(Clone, Copy)]
860struct HtmlContainerFence {
861 marker: FenceMarker,
862 length: usize,
863}
864
865fn html_container_close_step(lines: &[Line<'_>], cursor: usize, tag: &str) -> BracketStep {
869 let Some(line) = lines.get(cursor) else {
870 return BracketStep::End;
871 };
872 if let Some(fence) = html_container_fence_opens(line) {
873 let after_fence = (cursor + 1..lines.len())
874 .find(|close| html_container_fence_closes(&lines[*close], fence))
875 .map_or(lines.len(), |close| close + 1);
876 return BracketStep::Pass(after_fence);
877 }
878 if parse_html_container_tag_line(*line, tag, HtmlContainerTag::Closing).is_some() {
879 return BracketStep::Close(cursor + 1);
880 }
881 if parse_html_container_opening_line(*line, tag).is_some() {
882 return BracketStep::Open(cursor + 1);
883 }
884 BracketStep::Pass(cursor + 1)
885}
886
887fn parse_html_container_tag_line(
888 line: Line<'_>,
889 tag: &str,
890 kind: HtmlContainerTag,
891) -> Option<HtmlTag> {
892 let (trimmed, indent_bytes) = trim_html_container_line(&line)?;
893 let (end, name) = parse_html_tag(trimmed, 0)?;
894 if !name.eq_ignore_ascii_case(tag) || !is_blank(&trimmed[end..]) {
895 return None;
896 }
897
898 let closing = html_tag_is_closing(trimmed, 0);
899 if (kind == HtmlContainerTag::Opening && closing)
900 || (kind == HtmlContainerTag::Closing && !closing)
901 || html_tag_is_self_closing(&trimmed[..end])
902 {
903 return None;
904 }
905
906 Some(HtmlTag {
907 meta: NodeMeta::new(Some(Span::new(
908 line.source_start(indent_bytes),
909 line.source_end(indent_bytes + end),
910 ))),
911 name: tag.into(),
912 raw: trimmed[..end].into(),
913 })
914}
915
916fn parse_html_container_opening_line(line: Line<'_>, tag: &str) -> Option<HtmlTag> {
917 let (trimmed, indent_bytes) = trim_html_container_line(&line)?;
918 let (end, name) = parse_html_tag(trimmed, 0)?;
919 if !name.eq_ignore_ascii_case(tag)
920 || html_tag_is_closing(trimmed, 0)
921 || html_tag_is_self_closing(&trimmed[..end])
922 {
923 return None;
924 }
925
926 let rest_start = end + leading_ascii_whitespace_len(&trimmed[end..]);
927 if !trimmed[rest_start..].is_empty() && !html_container_line_has_summary(&trimmed[rest_start..])
928 {
929 return None;
930 }
931
932 Some(HtmlTag {
933 meta: NodeMeta::new(Some(Span::new(
934 line.source_start(indent_bytes),
935 line.source_end(indent_bytes + end),
936 ))),
937 name: tag.into(),
938 raw: trimmed[..end].into(),
939 })
940}
941
942fn html_container_line_has_summary(input: &str) -> bool {
943 let Some((open_end, name)) = parse_html_tag(input, 0) else {
944 return false;
945 };
946 if !name.eq_ignore_ascii_case("summary")
947 || html_tag_is_closing(input, 0)
948 || html_tag_is_self_closing(&input[..open_end])
949 {
950 return false;
951 }
952 let Some(close_start) = input[open_end..].find("</").map(|offset| open_end + offset) else {
953 return false;
954 };
955 let Some((close_end, close_name)) = parse_html_tag(input, close_start) else {
956 return false;
957 };
958 close_name.eq_ignore_ascii_case("summary")
959 && html_tag_is_closing(input, close_start)
960 && is_blank(&input[close_end..])
961}
962
963fn trim_html_container_line<'a>(line: &Line<'a>) -> Option<(&'a str, usize)> {
966 let (columns, bytes) = leading_indent_at(line.text, line.column);
967 (columns <= 3).then(|| (&line.text[bytes..], bytes))
968}
969
970fn leading_ascii_whitespace_len(input: &str) -> usize {
971 input
972 .as_bytes()
973 .iter()
974 .take_while(|byte| byte.is_ascii_whitespace())
975 .count()
976}
977
978fn html_tag_is_closing(input: &str, index: usize) -> bool {
979 input.as_bytes().get(index + 1) == Some(&b'/')
980}
981
982fn html_tag_is_self_closing(input: &str) -> bool {
983 input.trim_end_matches([' ', '\t']).ends_with("/>")
984}
985
986fn html_container_fence_opens(line: &Line<'_>) -> Option<HtmlContainerFence> {
987 let (trimmed, _) = trim_html_container_line(line)?;
988 let (marker, length) = fence_start(trimmed)?;
989 Some(HtmlContainerFence { marker, length })
990}
991
992fn html_container_fence_closes(line: &Line<'_>, fence: HtmlContainerFence) -> bool {
993 trim_html_container_line(line)
994 .is_some_and(|(trimmed, _)| fence_close(trimmed, fence.marker, fence.length))
995}
996
997fn html_block_start(input: &str) -> Option<HtmlBlockKind> {
998 let trimmed = input.trim_end_matches([' ', '\t']);
999 if !trimmed.starts_with('<') {
1000 return None;
1001 }
1002
1003 if raw_html_tag_start(trimmed) {
1004 return Some(HtmlBlockKind::RawTag);
1005 }
1006 if trimmed.starts_with("<!--") {
1007 return Some(HtmlBlockKind::Until("-->"));
1008 }
1009 if trimmed.starts_with("<?") {
1010 return Some(HtmlBlockKind::Until("?>"));
1011 }
1012 if is_declaration_start(trimmed) {
1013 return Some(HtmlBlockKind::Until(">"));
1014 }
1015 if trimmed.starts_with("<![CDATA[") {
1016 return Some(HtmlBlockKind::Until("]]>"));
1017 }
1018
1019 if html_block_tag_start(trimmed) {
1020 return Some(HtmlBlockKind::BlockTag);
1021 }
1022
1023 let Some((end, _tag_name)) = parse_html_tag(trimmed, 0) else {
1024 return None;
1025 };
1026 if is_blank(&trimmed[end..]) {
1027 Some(HtmlBlockKind::UntilBlank)
1028 } else {
1029 None
1030 }
1031}
1032
1033fn raw_html_tag_start(input: &str) -> bool {
1034 for tag in ["script", "pre", "style", "textarea"] {
1035 if html_raw_open_tag_prefix(input, tag) {
1036 return true;
1037 }
1038 }
1039 false
1040}
1041
1042fn html_raw_open_tag_prefix(input: &str, tag: &str) -> bool {
1043 let Some(rest) = input.strip_prefix('<') else {
1044 return false;
1045 };
1046 if rest.starts_with('/') || rest.len() < tag.len() {
1047 return false;
1048 }
1049 let rest_bytes = rest.as_bytes();
1050 let tag_bytes = tag.as_bytes();
1051 if !rest_bytes
1052 .get(..tag_bytes.len())
1053 .is_some_and(|name| name.eq_ignore_ascii_case(tag_bytes))
1054 {
1055 return false;
1056 }
1057 match rest_bytes.get(tag.len()) {
1058 None => true,
1059 Some(b' ' | b'\t' | b'\n' | b'\r' | b'>') => true,
1060 Some(b'/') => {
1061 rest_bytes.get(tag.len() + 1) == Some(&b'>') && rest_bytes.get(tag.len() + 2).is_none()
1062 }
1063 _ => false,
1064 }
1065}
1066
1067fn line_contains_raw_closing_tag(input: &str, tag: &str) -> bool {
1068 let bytes = input.as_bytes();
1069 let tag_bytes = tag.as_bytes();
1070 let mut cursor = 0;
1071
1072 while cursor + 2 + tag_bytes.len() <= bytes.len() {
1073 let tag_start = cursor + 2;
1074 let tag_end = tag_start + tag_bytes.len();
1075 if bytes.get(cursor) == Some(&b'<')
1076 && bytes.get(cursor + 1) == Some(&b'/')
1077 && bytes
1078 .get(tag_start..tag_end)
1079 .is_some_and(|name| name.eq_ignore_ascii_case(tag_bytes))
1080 {
1081 match bytes.get(tag_end) {
1082 Some(b'>') => return true,
1083 Some(byte) if byte.is_ascii_whitespace() => {
1084 let mut after_space = tag_end;
1085 while bytes
1086 .get(after_space)
1087 .is_some_and(|byte| byte.is_ascii_whitespace())
1088 {
1089 after_space += 1;
1090 }
1091 if bytes.get(after_space) == Some(&b'>') {
1092 return true;
1093 }
1094 }
1095 _ => {}
1096 }
1097 }
1098 cursor += 1;
1099 }
1100
1101 false
1102}
1103
1104fn html_block_tag_start(input: &str) -> bool {
1105 let bytes = input.as_bytes();
1106 if bytes.first() != Some(&b'<') {
1107 return false;
1108 }
1109
1110 let mut cursor = 1;
1111 if bytes.get(cursor) == Some(&b'/') {
1112 cursor += 1;
1113 }
1114
1115 let name_start = cursor;
1116 if !bytes
1117 .get(cursor)
1118 .is_some_and(|byte| byte.is_ascii_alphabetic())
1119 {
1120 return false;
1121 }
1122 cursor += 1;
1123 while bytes.get(cursor).is_some_and(|byte| html_name_byte(*byte)) {
1124 cursor += 1;
1125 }
1126
1127 let name = &input[name_start..cursor];
1128 if !html_block_tag(name) {
1129 return false;
1130 }
1131
1132 match bytes.get(cursor) {
1133 None | Some(b' ' | b'\t' | b'\n' | b'\r' | b'>') => true,
1134 Some(b'/') if bytes.get(cursor + 1) == Some(&b'>') => true,
1135 _ => false,
1136 }
1137}
1138
1139fn html_block_tag(tag: &str) -> bool {
1140 matches!(
1141 tag.to_ascii_lowercase().as_str(),
1142 "address"
1143 | "article"
1144 | "aside"
1145 | "base"
1146 | "basefont"
1147 | "blockquote"
1148 | "body"
1149 | "caption"
1150 | "center"
1151 | "col"
1152 | "colgroup"
1153 | "dd"
1154 | "details"
1155 | "dialog"
1156 | "dir"
1157 | "div"
1158 | "dl"
1159 | "dt"
1160 | "fieldset"
1161 | "figcaption"
1162 | "figure"
1163 | "footer"
1164 | "form"
1165 | "frame"
1166 | "frameset"
1167 | "h1"
1168 | "h2"
1169 | "h3"
1170 | "h4"
1171 | "h5"
1172 | "h6"
1173 | "head"
1174 | "header"
1175 | "hr"
1176 | "html"
1177 | "iframe"
1178 | "legend"
1179 | "li"
1180 | "link"
1181 | "main"
1182 | "menu"
1183 | "menuitem"
1184 | "nav"
1185 | "noframes"
1186 | "ol"
1187 | "optgroup"
1188 | "option"
1189 | "p"
1190 | "param"
1191 | "search"
1192 | "section"
1193 | "summary"
1194 | "table"
1195 | "tbody"
1196 | "td"
1197 | "tfoot"
1198 | "th"
1199 | "thead"
1200 | "title"
1201 | "tr"
1202 | "track"
1203 | "ul"
1204 )
1205}
1206
1207fn is_declaration_start(input: &str) -> bool {
1208 input
1209 .as_bytes()
1210 .get(2)
1211 .is_some_and(|byte| input.starts_with("<!") && byte.is_ascii_alphabetic())
1212}
1213
1214fn setext_underline_depth(input: &str) -> Option<u8> {
1215 let underline = trim_up_to_three_spaces(input)?.trim_matches([' ', '\t']);
1216 match underline {
1217 text if !text.is_empty() && text.chars().all(|char| char == '=') => Some(1),
1218 text if !text.is_empty() && text.chars().all(|char| char == '-') => Some(2),
1219 _ => None,
1220 }
1221}
1222
1223#[derive(Clone, Copy)]
1228struct DelimMarker {
1229 node_index: usize,
1233 marker: u8,
1234 length: usize,
1236 run_length: usize,
1240 can_open: bool,
1242 can_close: bool,
1243 recloses: usize,
1246 enclosed_from: usize,
1249 position: usize,
1251}
1252
1253const NIL: usize = usize::MAX;
1255
1256const MAX_EMPHASIS_NESTING: usize = 16;
1261
1262#[derive(Clone, Copy, Default)]
1270struct Nesting {
1271 emphasis: usize,
1272 total: usize,
1273}
1274
1275impl Nesting {
1276 fn max(self, other: Self) -> Self {
1277 Self {
1278 emphasis: self.emphasis.max(other.emphasis),
1279 total: self.total.max(other.total),
1280 }
1281 }
1282}
1283
1284struct DelimiterLinks {
1288 prev: Vec<usize>,
1289 next: Vec<usize>,
1290 live: Vec<bool>,
1291 nesting_after: Vec<Nesting>,
1292}
1293
1294impl DelimiterLinks {
1295 fn new(count: usize) -> Self {
1296 Self {
1297 prev: (0..count).map(|index| index.wrapping_sub(1)).collect(),
1298 next: (1..=count)
1299 .map(|index| if index < count { index } else { NIL })
1300 .collect(),
1301 live: alloc::vec![true; count],
1302 nesting_after: alloc::vec![Nesting::default(); count],
1303 }
1304 }
1305
1306 fn unlink(&mut self, index: usize) {
1308 let (before, after) = (self.prev[index], self.next[index]);
1309 self.live[index] = false;
1310 if before != NIL {
1311 self.next[before] = after;
1312 self.nesting_after[before] = self.nesting_after[before].max(self.nesting_after[index]);
1313 }
1314 if after != NIL {
1315 self.prev[after] = before;
1316 }
1317 }
1318
1319 fn close_span(&mut self, opener: usize, closer: usize) -> Nesting {
1322 let mut nesting = self.nesting_after[opener];
1323 let mut inner = self.next[opener];
1324 while inner != closer {
1325 nesting = nesting.max(self.nesting_after[inner]);
1326 self.live[inner] = false;
1327 inner = self.next[inner];
1328 }
1329 self.next[opener] = closer;
1330 self.prev[closer] = opener;
1331 nesting
1332 }
1333}
1334
1335struct InlineList {
1340 slots: Vec<Option<Inline>>,
1341 prev: Vec<usize>,
1342 next: Vec<usize>,
1343 head: usize,
1344}
1345
1346impl InlineList {
1347 fn new(nodes: Vec<Inline>) -> Self {
1348 let count = nodes.len();
1349 Self {
1350 slots: nodes.into_iter().map(Some).collect(),
1351 prev: (0..count).map(|slot| slot.wrapping_sub(1)).collect(),
1352 next: (1..=count)
1353 .map(|slot| if slot < count { slot } else { NIL })
1354 .collect(),
1355 head: if count == 0 { NIL } else { 0 },
1356 }
1357 }
1358
1359 fn node_mut(&mut self, slot: usize) -> &mut Inline {
1360 self.slots[slot]
1361 .as_mut()
1362 .expect("delimiter placeholder slot is live")
1363 }
1364
1365 fn wrap_between(
1368 &mut self,
1369 before: usize,
1370 after: usize,
1371 wrap: impl FnOnce(Vec<Inline>) -> Inline,
1372 ) {
1373 let mut children = Vec::new();
1374 let mut cursor = self.next[before];
1375 while cursor != after {
1376 children.push(self.slots[cursor].take().expect("linked slot is live"));
1377 cursor = self.next[cursor];
1378 }
1379 let slot = self.slots.len();
1380 self.slots.push(Some(wrap(children)));
1381 self.prev.push(before);
1382 self.next.push(after);
1383 self.next[before] = slot;
1384 self.prev[after] = slot;
1385 }
1386
1387 fn remove(&mut self, slot: usize) {
1388 let (prev, next) = (self.prev[slot], self.next[slot]);
1389 if prev == NIL {
1390 self.head = next;
1391 } else {
1392 self.next[prev] = next;
1393 }
1394 if next != NIL {
1395 self.prev[next] = prev;
1396 }
1397 self.slots[slot] = None;
1398 }
1399
1400 fn into_vec(mut self) -> Vec<Inline> {
1401 let mut nodes = Vec::new();
1402 let mut cursor = self.head;
1403 while cursor != NIL {
1404 nodes.push(self.slots[cursor].take().expect("linked slot is live"));
1405 cursor = self.next[cursor];
1406 }
1407 nodes
1408 }
1409}
1410
1411fn push_text(nodes: &mut Vec<Inline>, start: usize, value: &str) {
1414 nodes.push(Inline::Text(Text {
1415 meta: NodeMeta::new(Some(Span::new(start, start + value.len()))),
1416 value: value.into(),
1417 }));
1418}
1419
1420#[allow(clippy::too_many_arguments)]
1424fn close_bracket(
1425 input: &str,
1426 base_offset: usize,
1427 close: usize,
1428 definitions: Option<Labels<'_>>,
1429 nodes: &mut Vec<Inline>,
1430 delimiters: &mut Vec<DelimMarker>,
1431 brackets: &mut Brackets,
1432 pass: &mut InlinePass,
1433) -> Option<usize> {
1434 let mut opener = brackets.openers.pop()?;
1435 let depth = pass.state.depth + brackets.openers.len();
1436 let link_active = brackets.openers.len() >= brackets.link_floor;
1439 brackets.link_floor = brackets.link_floor.min(brackets.openers.len());
1440
1441 if opener.kind == BracketKind::InlineFootnote {
1442 if !input[opener.position + 2..close].trim().is_empty() {
1443 let (children, marks) = take_label(input, nodes, delimiters, brackets, &opener, depth);
1444 nodes.truncate(opener.node_index - 1);
1445 nodes.push(Inline::InlineFootnote(InlineFootnote {
1446 meta: NodeMeta::new(Some(Span::new(
1447 base_offset + opener.position,
1448 base_offset + close + 1,
1449 ))),
1450 children,
1451 }));
1452 brackets.formed.push((nodes.len() - 1, marks + 1));
1453 return Some(close + 1);
1454 }
1455 opener.kind = BracketKind::Link;
1457 opener.position += 1;
1458 }
1459
1460 if opener.kind == BracketKind::Image {
1461 let target = match_link_target(
1462 &mut pass.scan,
1463 input,
1464 opener.position + 2,
1465 close,
1466 definitions,
1467 );
1468 if let Some((end, target)) = target {
1469 let (children, marks) = take_label(input, nodes, delimiters, brackets, &opener, depth);
1470 nodes.truncate(opener.node_index - 1);
1471 let span = Span::new(base_offset + opener.position, base_offset + end);
1472 nodes.push(link_node(target, true, span, children));
1473 brackets.formed.push((nodes.len() - 1, marks + 1));
1474 return Some(end);
1475 }
1476 opener.kind = BracketKind::Link;
1478 opener.position += 1;
1479 }
1480
1481 if link_active {
1482 let target = match_link_target(
1483 &mut pass.scan,
1484 input,
1485 opener.position + 1,
1486 close,
1487 definitions,
1488 );
1489 if let Some((end, target)) = target {
1490 let (children, marks) = take_label(input, nodes, delimiters, brackets, &opener, depth);
1491 nodes.truncate(opener.node_index);
1492 let span = Span::new(base_offset + opener.position, base_offset + end);
1493 nodes.push(link_node(target, false, span, children));
1494 brackets.formed.push((nodes.len() - 1, marks + 1));
1495 brackets.close_links();
1497 return Some(end);
1498 }
1499 }
1500
1501 let holds_formed = brackets
1504 .formed
1505 .last()
1506 .is_some_and(|&(node, _)| node > opener.node_index);
1507 if !holds_formed && input[opener.position + 1..].starts_with('^') {
1508 let label = &input[opener.position + 2..close];
1509 if is_footnote_label(label) {
1510 drop_label(nodes, delimiters, brackets, &opener);
1511 nodes.push(Inline::FootnoteReference(FootnoteReference {
1512 meta: NodeMeta::new(Some(Span::new(
1513 base_offset + opener.position,
1514 base_offset + close + 1,
1515 ))),
1516 label: label.into(),
1517 identifier: normalize_label(label),
1518 }));
1519 return Some(close + 1);
1520 }
1521 }
1522 None
1523}
1524
1525fn drop_label(
1527 nodes: &mut Vec<Inline>,
1528 delimiters: &mut Vec<DelimMarker>,
1529 brackets: &mut Brackets,
1530 opener: &BracketOpener,
1531) {
1532 nodes.truncate(opener.node_index);
1533 delimiters.truncate(opener.delimiter_bottom);
1534 let kept = brackets
1535 .formed
1536 .partition_point(|&(node, _)| node < opener.node_index);
1537 brackets.formed.truncate(kept);
1538}
1539
1540fn push_delimiter(
1543 nodes: &mut Vec<Inline>,
1544 delimiters: &mut Vec<DelimMarker>,
1545 index: usize,
1546 base_offset: usize,
1547 marker: u8,
1548 length: usize,
1549) {
1550 let node_index = nodes.len();
1551 nodes.push(Inline::Text(Text {
1552 meta: NodeMeta::new(Some(Span::new(
1553 base_offset + index,
1554 base_offset + index + length,
1555 ))),
1556 value: String::from(marker as char).repeat(length),
1557 }));
1558 delimiters.push(DelimMarker {
1559 node_index,
1560 marker,
1561 length,
1562 run_length: length,
1563 can_open: false,
1564 can_close: false,
1565 recloses: 0,
1566 enclosed_from: NIL,
1567 position: index,
1568 });
1569}
1570
1571fn emphasis_roles(
1586 input: &str,
1587 index: usize,
1588 length: usize,
1589 marker: u8,
1590 bounded_before: bool,
1591 bounded_after: bool,
1592) -> (bool, bool) {
1593 let flanking = delimiter_flanking_within(input, index, length, bounded_before, bounded_after);
1594 let (mut can_open, mut can_close) = if marker == b'_' {
1595 (
1596 flanking.left && (!flanking.right || flanking.previous_punctuation),
1597 flanking.right && (!flanking.left || flanking.next_punctuation),
1598 )
1599 } else {
1600 (flanking.left, flanking.right)
1601 };
1602
1603 if marker == b'*' {
1607 if flanking.next == Some('~') {
1608 can_open = true;
1609 }
1610 if flanking.previous == Some('~') {
1611 can_close = true;
1612 }
1613 }
1614 (can_open, can_close)
1615}
1616
1617fn assign_emphasis_roles(input: &str, delimiters: &mut [DelimMarker]) {
1622 let mut opens_span = alloc::vec![false; delimiters.len()];
1623 let mut closes_span = alloc::vec![false; delimiters.len()];
1624 let mut open_marks: Vec<usize> = Vec::new();
1625 let mut span_close = alloc::vec![NIL; delimiters.len()];
1627 for (index, run) in delimiters.iter_mut().enumerate() {
1628 if run.marker != b'=' {
1629 continue;
1630 }
1631 assign_double_mark_roles(input, run);
1632 let mut closed = 0;
1633 if run.can_close {
1634 while closed <= run.recloses {
1635 let Some(opener) = open_marks.pop() else {
1636 break;
1637 };
1638 opens_span[opener] = true;
1639 closes_span[index] = true;
1640 span_close[opener] = index;
1641 closed += 1;
1642 }
1643 }
1644 if run.can_open && run.length >= 2 * closed + 2 {
1646 open_marks.push(index);
1647 }
1648 }
1649
1650 for index in 0..delimiters.len() {
1651 let run = delimiters[index];
1652 if run.marker == b'=' {
1653 continue;
1654 }
1655 let bounded_before = index > 0 && {
1656 let before = &delimiters[index - 1];
1657 before.position + before.length == run.position && opens_span[index - 1]
1658 };
1659 let bounded_after = index + 1 < delimiters.len() && {
1660 let after = &delimiters[index + 1];
1661 after.position == run.position + run.length && closes_span[index + 1]
1662 };
1663 let (can_open, can_close) = emphasis_roles(
1664 input,
1665 run.position,
1666 run.length,
1667 run.marker,
1668 bounded_before,
1669 bounded_after,
1670 );
1671 delimiters[index].can_open = can_open;
1672 delimiters[index].can_close = can_close;
1673 }
1674
1675 let mut open_spans: Vec<usize> = Vec::new();
1679 for (index, run) in delimiters.iter_mut().enumerate() {
1680 while open_spans
1681 .last()
1682 .is_some_and(|&opener| span_close[opener] <= index)
1683 {
1684 open_spans.pop();
1685 }
1686 if let Some(&opener) = open_spans.last() {
1687 run.enclosed_from = opener;
1688 }
1689 if span_close[index] != NIL {
1690 open_spans.push(index);
1691 }
1692 }
1693}
1694
1695fn assign_double_mark_roles(input: &str, run: &mut DelimMarker) {
1700 let (index, length) = (run.position, run.length);
1701 let after_escaped_mark =
1702 index > 0 && input.as_bytes()[index - 1] == run.marker && is_escaped_at(input, index - 1);
1703 run.can_close = !after_escaped_mark && can_close_delimited(input, index, 2);
1704 run.recloses = if run.can_close {
1705 (1..length / 2)
1706 .take_while(|pair| can_close_delimited(input, index + 2 * pair, 2))
1707 .count()
1708 } else {
1709 0
1710 };
1711 run.can_open = can_open_delimited(input, index + length - 2, 2);
1712}
1713
1714fn process_emphasis(
1726 nodes: Vec<Inline>,
1727 mut delimiters: Vec<DelimMarker>,
1728 depth: usize,
1729 formed: &[(usize, usize)],
1730) -> (Vec<Inline>, usize) {
1731 let mut deepest = formed.iter().map(|&(_, depth)| depth).max().unwrap_or(0);
1732 if delimiters.is_empty() {
1733 let mut nodes = nodes;
1734 merge_adjacent_text(&mut nodes);
1735 return (nodes, deepest);
1736 }
1737 let mut nodes = InlineList::new(nodes);
1738
1739 let mut links = DelimiterLinks::new(delimiters.len());
1743 for &(node, depth) in formed {
1744 let before = delimiters.partition_point(|run| run.node_index < node);
1745 if before > 0 {
1746 let gap = &mut links.nesting_after[before - 1].total;
1747 *gap = (*gap).max(depth);
1748 }
1749 }
1750
1751 let mut openers_bottom: [Option<usize>; NESTED_MARKERS * 6] = [None; NESTED_MARKERS * 6];
1756 let mut span_bottom: Vec<Vec<(usize, usize)>> = Vec::new();
1757 let mut closer_idx = 0;
1758
1759 while closer_idx < delimiters.len() {
1762 let closer = delimiters[closer_idx];
1763
1764 let opener = if closer.can_close {
1765 nested_opener(
1766 &delimiters,
1767 &links,
1768 &openers_bottom,
1769 &span_bottom,
1770 closer_idx,
1771 )
1772 } else {
1773 None
1774 };
1775
1776 let Some(opener_idx) = opener else {
1777 if closer.can_close {
1778 let key = openers_bottom_key(&closer);
1781 match span_floor(&closer) {
1782 Some(span) => {
1783 if span_bottom.len() <= span {
1784 span_bottom.resize_with(span + 1, Vec::new);
1785 }
1786 match span_bottom[span].iter_mut().find(|(k, _)| *k == key) {
1787 Some(bound) => bound.1 = closer_idx,
1788 None => span_bottom[span].push((key, closer_idx)),
1789 }
1790 }
1791 None => openers_bottom[key] = Some(closer_idx),
1792 }
1793 }
1794 if !closer.can_open {
1795 links.unlink(closer_idx);
1796 }
1797 closer_idx += 1;
1798 continue;
1799 };
1800
1801 let (used, wrap) = pair_shape(&delimiters[opener_idx], &delimiters[closer_idx]);
1802
1803 let inner = links.close_span(opener_idx, closer_idx);
1806 let fits = depth + inner.total < MAX_INLINE_NESTING
1807 && (wrap.is_mark() || inner.emphasis < MAX_EMPHASIS_NESTING);
1808 if fits {
1809 apply_emphasis(
1810 &mut nodes,
1811 &mut delimiters,
1812 opener_idx,
1813 closer_idx,
1814 used,
1815 wrap,
1816 );
1817 links.nesting_after[opener_idx] = Nesting {
1818 emphasis: if wrap.is_mark() {
1819 0
1820 } else {
1821 inner.emphasis + 1
1822 },
1823 total: inner.total + 1,
1824 };
1825 deepest = deepest.max(inner.total + 1);
1826 } else {
1827 delimiters[opener_idx].length = 0;
1831 delimiters[closer_idx].length = 0;
1832 links.nesting_after[opener_idx] = inner;
1833 }
1834 if closer.marker == b'=' {
1835 delimiters[opener_idx].can_open = false;
1838 let run = &mut delimiters[closer_idx];
1839 run.can_close = run.recloses > 0;
1840 run.recloses = run.recloses.saturating_sub(1);
1841 }
1842
1843 if delimiters[opener_idx].length == 0 {
1844 links.unlink(opener_idx);
1845 }
1846 if delimiters[closer_idx].length == 0 {
1847 links.unlink(closer_idx);
1848 closer_idx += 1;
1849 }
1850 }
1854
1855 let mut nodes = nodes.into_vec();
1859 merge_adjacent_text(&mut nodes);
1860 (nodes, deepest)
1861}
1862
1863const NESTED_MARKERS: usize = 4;
1865
1866fn nested_opener(
1873 delimiters: &[DelimMarker],
1874 links: &DelimiterLinks,
1875 openers_bottom: &[Option<usize>],
1876 span_bottom: &[Vec<(usize, usize)>],
1877 closer_idx: usize,
1878) -> Option<usize> {
1879 let closer = &delimiters[closer_idx];
1880 let key = openers_bottom_key(closer);
1881 let mut bottom = openers_bottom[key];
1882 if let Some(span) = span_floor(closer) {
1883 let span_floor = span_bottom
1884 .get(span)
1885 .and_then(|bounds| bounds.iter().find(|(k, _)| *k == key))
1886 .map_or(span + 1, |&(_, bound)| bound);
1887 bottom = bottom.max(Some(span_floor));
1888 }
1889 let mut search = links.prev[closer_idx];
1890 while search != NIL {
1891 if bottom.is_some_and(|bottom| search < bottom) {
1892 return None;
1893 }
1894 let candidate = &delimiters[search];
1895 if candidate.marker == closer.marker
1896 && candidate.can_open
1897 && emphasis_delimiters_match(candidate, closer)
1898 {
1899 return Some(search);
1900 }
1901 search = links.prev[search];
1902 }
1903 None
1904}
1905
1906fn span_floor(closer: &DelimMarker) -> Option<usize> {
1909 (closer.can_open && closer.enclosed_from != NIL).then_some(closer.enclosed_from)
1910}
1911
1912fn pair_shape(opener: &DelimMarker, closer: &DelimMarker) -> (usize, EmphasisWrap) {
1914 match closer.marker {
1915 b'~' => (2, EmphasisWrap::Delete),
1917 b'=' => (2, EmphasisWrap::Mark),
1918 marker => {
1919 let delimiter = if marker == b'_' {
1920 EmphasisDelimiter::Underscore
1921 } else {
1922 EmphasisDelimiter::Asterisk
1923 };
1924 if opener.length >= 2 && closer.length >= 2 {
1925 (2, EmphasisWrap::Strong(delimiter))
1926 } else {
1927 (1, EmphasisWrap::Emphasis(delimiter))
1928 }
1929 }
1930 }
1931}
1932
1933fn merge_adjacent_text(nodes: &mut Vec<Inline>) {
1937 let mut write = 0;
1938 for read in 0..nodes.len() {
1939 if read != write {
1940 nodes.swap(read, write);
1941 }
1942 if write > 0 {
1943 let (head, tail) = nodes.split_at_mut(write);
1944 if let (Inline::Text(previous), Inline::Text(current)) =
1945 (&mut head[write - 1], &tail[0])
1946 {
1947 previous.value.push_str(¤t.value);
1948 if let (Some(previous_span), Some(current_span)) =
1949 (previous.meta.span.as_mut(), current.meta.span)
1950 {
1951 previous_span.end = current_span.end;
1952 }
1953 continue;
1954 }
1955 }
1956 write += 1;
1957 }
1958 nodes.truncate(write);
1959
1960 for node in nodes.iter_mut() {
1961 match node {
1962 Inline::Emphasis(node) => merge_adjacent_text(&mut node.children),
1963 Inline::Strong(node) => merge_adjacent_text(&mut node.children),
1964 Inline::Delete(node) => merge_adjacent_text(&mut node.children),
1965 Inline::Mark(node) => merge_adjacent_text(&mut node.children),
1966 _ => {}
1967 }
1968 }
1969}
1970
1971fn openers_bottom_key(closer: &DelimMarker) -> usize {
1974 let marker = match closer.marker {
1975 b'_' => 1,
1976 b'~' => 2,
1977 b'=' => 3,
1978 _ => 0,
1979 };
1980 let both = usize::from(closer.can_open && closer.can_close);
1981 let length = match closer.marker {
1985 b'*' | b'_' => closer.run_length,
1986 _ => closer.length,
1987 };
1988 let modulo = length % 3;
1989 ((marker * 2) + both) * 3 + modulo
1990}
1991
1992fn emphasis_delimiters_match(opener: &DelimMarker, closer: &DelimMarker) -> bool {
1994 match opener.marker {
1995 b'~' => true,
1999 b'=' => opener.length >= 2 && closer.length >= 2,
2001 _ => {
2002 let opener_both = opener.can_open && opener.can_close;
2007 let closer_both = closer.can_open && closer.can_close;
2008 if opener_both || closer_both {
2009 let (opener_run, closer_run) = (opener.run_length, closer.run_length);
2010 let sum = opener_run + closer_run;
2011 if sum % 3 == 0 && !(opener_run % 3 == 0 && closer_run % 3 == 0) {
2012 return false;
2013 }
2014 }
2015 true
2016 }
2017 }
2018}
2019
2020#[derive(Clone, Copy)]
2022enum EmphasisWrap {
2023 Emphasis(EmphasisDelimiter),
2024 Strong(EmphasisDelimiter),
2025 Delete,
2026 Mark,
2027}
2028
2029impl EmphasisWrap {
2030 fn is_mark(self) -> bool {
2033 matches!(self, Self::Mark)
2034 }
2035}
2036
2037fn apply_emphasis(
2040 nodes: &mut InlineList,
2041 delimiters: &mut [DelimMarker],
2042 opener_idx: usize,
2043 closer_idx: usize,
2044 used: usize,
2045 wrap: EmphasisWrap,
2046) {
2047 let opener_node = delimiters[opener_idx].node_index;
2048 let closer_node = delimiters[closer_idx].node_index;
2049
2050 let span_start = nodes
2054 .node_mut(opener_node)
2055 .span()
2056 .map(|span| span.end - used);
2057 let span_end = nodes
2058 .node_mut(closer_node)
2059 .span()
2060 .map(|span| span.start + used);
2061 trim_delimiter_text_tail(nodes.node_mut(opener_node), used);
2064 trim_delimiter_text_head(nodes.node_mut(closer_node), used);
2065 delimiters[opener_idx].length -= used;
2066 let closer = &mut delimiters[closer_idx];
2067 closer.length -= used;
2068 closer.position += used;
2069
2070 let meta = NodeMeta::new(
2073 span_start
2074 .zip(span_end)
2075 .map(|(start, end)| Span::new(start, end)),
2076 );
2077 nodes.wrap_between(opener_node, closer_node, |children| match wrap {
2078 EmphasisWrap::Strong(delimiter) => Inline::Strong(Strong {
2079 meta,
2080 delimiter,
2081 children,
2082 }),
2083 EmphasisWrap::Emphasis(delimiter) => Inline::Emphasis(Emphasis {
2084 meta,
2085 delimiter,
2086 children,
2087 }),
2088 EmphasisWrap::Delete => Inline::Delete(Delete { meta, children }),
2089 EmphasisWrap::Mark => Inline::Mark(Mark { meta, children }),
2090 });
2091
2092 if delimiters[closer_idx].length == 0 {
2095 nodes.remove(closer_node);
2096 }
2097 if delimiters[opener_idx].length == 0 {
2098 nodes.remove(opener_node);
2099 }
2100}
2101
2102#[derive(Clone, Copy, PartialEq, Eq)]
2104enum BracketKind {
2105 Link,
2106 Image,
2107 InlineFootnote,
2108}
2109
2110struct BracketOpener {
2113 kind: BracketKind,
2114 node_index: usize,
2116 position: usize,
2118 delimiter_bottom: usize,
2121}
2122
2123enum LinkTarget {
2125 Resource(ParsedLinkResource),
2126 Reference {
2127 identifier: String,
2128 kind: ReferenceKind,
2129 },
2130}
2131
2132fn match_link_target(
2138 scan: &mut InlineScan,
2139 input: &str,
2140 label_start: usize,
2141 close: usize,
2142 definitions: Option<Labels<'_>>,
2143) -> Option<(usize, LinkTarget)> {
2144 let label = &input[label_start..close];
2145 let after = close + 1;
2146 if input.as_bytes().get(after) == Some(&b'(') {
2147 match parse_link_resource(&mut scan.lookups, input, after) {
2148 Some((end, resource, _)) => return Some((end, LinkTarget::Resource(resource))),
2149 None => {}
2154 }
2155 }
2156 let reference_close = if input.as_bytes().get(after) == Some(&b'[') {
2159 scan.reference_label_end(after)
2160 } else {
2161 None
2162 };
2163 if let Some(reference_close) = reference_close {
2164 let reference = &input[after + 1..reference_close];
2165 let identifier = if reference.is_empty() {
2166 label
2167 } else {
2168 reference
2169 };
2170 return definition_exists(definitions, identifier).then(|| {
2174 let kind = if reference.is_empty() {
2175 ReferenceKind::Collapsed
2176 } else {
2177 ReferenceKind::Full
2178 };
2179 (
2180 reference_close + 1,
2181 LinkTarget::Reference {
2182 identifier: identifier.into(),
2183 kind,
2184 },
2185 )
2186 });
2187 }
2188 definition_exists(definitions, label).then(|| {
2189 (
2190 after,
2191 LinkTarget::Reference {
2192 identifier: label.into(),
2193 kind: ReferenceKind::Shortcut,
2194 },
2195 )
2196 })
2197}
2198
2199fn link_node(target: LinkTarget, image: bool, span: Span, mut children: Vec<Inline>) -> Inline {
2201 let meta = NodeMeta::new(Some(span));
2202 if !image {
2203 demote_links(&mut children);
2204 }
2205 match (target, image) {
2206 (LinkTarget::Resource(resource), false) => Inline::Link(Link {
2207 meta,
2208 destination: resource.destination,
2209 destination_kind: resource.destination_kind,
2210 title: resource.title,
2211 children,
2212 }),
2213 (LinkTarget::Resource(resource), true) => Inline::Image(Image {
2214 meta,
2215 destination: resource.destination,
2216 destination_kind: resource.destination_kind,
2217 title: resource.title,
2218 alt: children,
2219 }),
2220 (LinkTarget::Reference { identifier, kind }, false) => {
2221 Inline::LinkReference(LinkReference {
2222 meta,
2223 identifier: normalize_label(&identifier),
2224 label: identifier,
2225 kind,
2226 children,
2227 })
2228 }
2229 (LinkTarget::Reference { identifier, kind }, true) => {
2230 Inline::ImageReference(ImageReference {
2231 meta,
2232 identifier: normalize_label(&identifier),
2233 label: identifier,
2234 kind,
2235 alt: children,
2236 })
2237 }
2238 }
2239}
2240
2241fn autolink_node(form: AutolinkForm, span: Span, text: &str) -> Inline {
2243 Inline::Autolink(Autolink {
2244 meta: NodeMeta::new(Some(span)),
2245 form,
2246 text: text.into(),
2247 })
2248}
2249
2250fn demote_links(nodes: &mut Vec<Inline>) {
2254 for node in nodes.iter_mut() {
2255 match node {
2256 Inline::Autolink(autolink) => {
2257 *node = Inline::Text(Text {
2258 meta: autolink.meta.clone(),
2259 value: core::mem::take(&mut autolink.text),
2260 });
2261 }
2262 Inline::Emphasis(node) => demote_links(&mut node.children),
2263 Inline::Strong(node) => demote_links(&mut node.children),
2264 Inline::Delete(node) => demote_links(&mut node.children),
2265 Inline::Mark(node) => demote_links(&mut node.children),
2266 Inline::InlineFootnote(node) => demote_links(&mut node.children),
2267 Inline::TextDirective(node) => demote_links(&mut node.label),
2268 Inline::Image(node) => demote_links(&mut node.alt),
2269 Inline::ImageReference(node) => demote_links(&mut node.alt),
2270 _ => {}
2271 }
2272 }
2273 merge_adjacent_text(nodes);
2274}
2275
2276struct Brackets {
2278 openers: Vec<BracketOpener>,
2279 link_floor: usize,
2282 overflow: usize,
2285 formed: Vec<(usize, usize)>,
2288}
2289
2290impl Brackets {
2291 fn new() -> Self {
2292 Self {
2293 openers: Vec::new(),
2294 link_floor: 0,
2295 overflow: 0,
2296 formed: Vec::new(),
2297 }
2298 }
2299
2300 fn open(&self) -> bool {
2305 self.overflow > 0
2306 || self
2307 .openers
2308 .iter()
2309 .any(|opener| !matches!(opener.kind, BracketKind::InlineFootnote))
2310 }
2311
2312 fn close_links(&mut self) {
2314 self.link_floor = self.openers.len();
2315 }
2316}
2317
2318fn take_label(
2322 input: &str,
2323 nodes: &mut Vec<Inline>,
2324 delimiters: &mut Vec<DelimMarker>,
2325 brackets: &mut Brackets,
2326 opener: &BracketOpener,
2327 depth: usize,
2328) -> (Vec<Inline>, usize) {
2329 let first = opener.node_index + 1;
2330 let children: Vec<Inline> = nodes.drain(first..).collect();
2331 let mut runs = delimiters.split_off(opener.delimiter_bottom);
2332 for run in &mut runs {
2333 run.node_index -= first;
2334 }
2335 let split = brackets.formed.partition_point(|&(node, _)| node < first);
2336 let formed: Vec<(usize, usize)> = brackets
2337 .formed
2338 .split_off(split)
2339 .into_iter()
2340 .map(|(node, marks)| (node - first, marks))
2341 .collect();
2342 assign_emphasis_roles(input, &mut runs);
2343 process_emphasis(children, runs, depth, &formed)
2344}
2345
2346fn trim_delimiter_text_tail(node: &mut Inline, count: usize) {
2348 if let Inline::Text(text) = node {
2349 let new_len = text.value.len().saturating_sub(count);
2350 text.value.truncate(new_len);
2351 if let Some(span) = text.meta.span.as_mut() {
2352 span.end = span.end.saturating_sub(count);
2353 }
2354 }
2355}
2356
2357fn trim_delimiter_text_head(node: &mut Inline, count: usize) {
2361 if let Inline::Text(text) = node {
2362 let count = count.min(text.value.len());
2363 text.value.truncate(text.value.len() - count);
2364 if let Some(span) = text.meta.span.as_mut() {
2365 span.start += count;
2366 }
2367 }
2368}
2369
2370trait Lookups {
2375 fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize>;
2377 fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize>;
2380 fn angle_destination_stop(&mut self, from: usize) -> Option<usize>;
2382 fn unescaped_dollar(&mut self, from: usize) -> Option<usize>;
2384}
2385
2386struct DirectLookups<'a> {
2387 input: &'a str,
2388}
2389
2390impl Lookups for DirectLookups<'_> {
2391 fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize> {
2392 self.input[from..].find(pattern).map(|offset| from + offset)
2393 }
2394
2395 fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize> {
2396 find_code_span_close(self.input, start, len)
2397 }
2398
2399 fn angle_destination_stop(&mut self, from: usize) -> Option<usize> {
2400 (from..self.input.len()).find(|index| is_angle_destination_stop(self.input, *index))
2401 }
2402
2403 fn unescaped_dollar(&mut self, from: usize) -> Option<usize> {
2404 (from..self.input.len()).find(|index| is_unescaped_dollar(self.input, *index))
2405 }
2406}
2407
2408const CACHED_PATTERNS: [&str; 8] = [">", "-->", "?>", "]]>", "\"", "'", "`$", "$$"];
2410
2411struct InlineLookups<'a> {
2414 input: &'a str,
2415 patterns: [Positions; CACHED_PATTERNS.len()],
2416 backtick_runs: Option<BTreeMap<usize, Vec<usize>>>,
2417 angle_destination_stops: Positions,
2418 unescaped_dollars: Positions,
2419}
2420
2421impl<'a> InlineLookups<'a> {
2422 fn new(input: &'a str) -> Self {
2423 Self {
2424 input,
2425 patterns: Default::default(),
2426 backtick_runs: None,
2427 angle_destination_stops: Positions::default(),
2428 unescaped_dollars: Positions::default(),
2429 }
2430 }
2431}
2432
2433impl Lookups for InlineLookups<'_> {
2434 fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize> {
2435 let input = self.input;
2436 let Some(slot) = CACHED_PATTERNS.iter().position(|cached| *cached == pattern) else {
2437 return DirectLookups { input }.find(pattern, from);
2438 };
2439 self.patterns[slot].first_at_or_after(from, || pattern_starts(input, pattern))
2440 }
2441
2442 fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize> {
2443 let input = self.input;
2444 let runs = self
2445 .backtick_runs
2446 .get_or_insert_with(|| backtick_runs_by_length(input));
2447 let starts = runs.get(&len)?;
2448 starts
2449 .get(starts.partition_point(|run| *run < start))
2450 .copied()
2451 }
2452
2453 fn angle_destination_stop(&mut self, from: usize) -> Option<usize> {
2454 let input = self.input;
2455 self.angle_destination_stops.first_at_or_after(from, || {
2456 (0..input.len())
2457 .filter(|index| is_angle_destination_stop(input, *index))
2458 .collect()
2459 })
2460 }
2461
2462 fn unescaped_dollar(&mut self, from: usize) -> Option<usize> {
2463 let input = self.input;
2464 self.unescaped_dollars.first_at_or_after(from, || {
2465 (0..input.len())
2466 .filter(|index| is_unescaped_dollar(input, *index))
2467 .collect()
2468 })
2469 }
2470}
2471
2472fn backtick_runs_by_length(input: &str) -> BTreeMap<usize, Vec<usize>> {
2474 let mut runs: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
2475 let mut index = 0;
2476 while index < input.len() {
2477 if input.as_bytes()[index] == b'`' {
2478 let len = backtick_run_len(input, index);
2479 runs.entry(len).or_default().push(index);
2480 index += len;
2481 } else {
2482 index += 1;
2483 }
2484 }
2485 runs
2486}
2487
2488fn backtick_run_len(input: &str, index: usize) -> usize {
2489 input.as_bytes()[index..]
2490 .iter()
2491 .take_while(|byte| **byte == b'`')
2492 .count()
2493}
2494
2495fn is_unescaped_dollar(input: &str, index: usize) -> bool {
2498 let bytes = input.as_bytes();
2499 bytes[index] == b'$' && (index == 0 || bytes[index - 1] != b'\\')
2500}
2501
2502fn is_angle_destination_stop(input: &str, index: usize) -> bool {
2503 match input.as_bytes()[index] {
2504 b'<' | b'>' => !is_escaped_at(input, index),
2505 b'\n' | b'\r' => true,
2506 _ => false,
2507 }
2508}
2509
2510struct InlineScan<'a> {
2513 input: &'a str,
2514 lookups: InlineLookups<'a>,
2515 label_ends: BracketMemo,
2516 reference_label_ends: PathMemo,
2517 wikilink_closes: PathMemo,
2518 directive_attribute_closes: PathMemo,
2519 literal_autolinks: LiteralAutolinkScan,
2520}
2521
2522impl<'a> InlineScan<'a> {
2523 fn new(input: &'a str) -> Self {
2524 Self {
2525 input,
2526 lookups: InlineLookups::new(input),
2527 label_ends: BracketMemo::default(),
2528 reference_label_ends: PathMemo::default(),
2529 wikilink_closes: PathMemo::default(),
2530 directive_attribute_closes: PathMemo::default(),
2531 literal_autolinks: LiteralAutolinkScan::default(),
2532 }
2533 }
2534
2535 fn link_label_end(&mut self, open: usize) -> Option<usize> {
2537 let input = self.input;
2538 if input.as_bytes().get(open) != Some(&b'[') {
2539 return None;
2540 }
2541 let lookups = &mut self.lookups;
2542 self.label_ends
2543 .resolve(input.len() + 1, open + 1, |cursor| {
2544 link_label_step(lookups, input, cursor)
2545 })
2546 }
2547
2548 fn reference_label_end(&mut self, open: usize) -> Option<usize> {
2550 let input = self.input;
2551 if input.as_bytes().get(open) != Some(&b'[') {
2552 return None;
2553 }
2554 let close = self
2555 .reference_label_ends
2556 .resolve(input.len() + 1, open + 1, |cursor| {
2557 reference_label_step(input, cursor)
2558 })?;
2559 reference_label_is_within_limit(&input[open + 1..close]).then_some(close)
2560 }
2561
2562 fn wikilink_close(&mut self, start: usize) -> Option<usize> {
2565 let input = self.input;
2566 self.wikilink_closes
2567 .resolve(input.len() + 1, start, |cursor| {
2568 wikilink_close_step(input, cursor)
2569 })
2570 }
2571
2572 fn directive_attributes_close(&mut self, open: usize) -> Option<usize> {
2574 let input = self.input;
2575 if input.as_bytes().get(open) != Some(&b'{') {
2576 return None;
2577 }
2578 self.directive_attribute_closes.resolve(
2579 (input.len() + 1) * DIRECTIVE_QUOTE_STATES,
2580 (open + 1) * DIRECTIVE_QUOTE_STATES,
2581 |node| directive_attributes_step(input, node),
2582 )
2583 }
2584}
2585
2586fn parse_inlines(
2590 input: &str,
2591 map: &SourceMap,
2592 definitions: Option<Labels<'_>>,
2593 diagnostics: &mut Vec<Diagnostic>,
2594 tokens: Option<&mut Vec<InlineToken>>,
2595) -> Vec<Inline> {
2596 if definitions.is_none() {
2598 return Vec::new();
2599 }
2600 let first_diagnostic = diagnostics.len();
2601 let mut nodes = parse_inlines_with_context(
2602 input,
2603 0,
2604 definitions,
2605 diagnostics,
2606 &mut InlineState::default(),
2607 );
2608 tags::split_tags(input, &mut nodes);
2609 if let Some(tokens) = tokens {
2610 *tokens = tokens::tokens(input, &nodes);
2611 source_map::translate_tokens(map, tokens);
2612 }
2613 source_map::translate_inlines(map, &mut nodes, &mut diagnostics[first_diagnostic..]);
2614 nodes
2615}
2616
2617const MAX_INLINE_NESTING: usize = 32;
2622
2623#[derive(Default)]
2627struct InlineState {
2628 depth: usize,
2630}
2631
2632struct InlinePass<'p, 'a> {
2635 state: &'p mut InlineState,
2636 scan: InlineScan<'a>,
2637}
2638
2639fn parse_inlines_with_context(
2640 input: &str,
2641 base_offset: usize,
2642 definitions: Option<Labels<'_>>,
2643 diagnostics: &mut Vec<Diagnostic>,
2644 state: &mut InlineState,
2645) -> Vec<Inline> {
2646 if state.depth >= MAX_INLINE_NESTING {
2647 if input.is_empty() {
2648 return Vec::new();
2649 }
2650 return vec![Inline::Text(Text {
2651 meta: NodeMeta::new(Some(Span::new(base_offset, base_offset + input.len()))),
2652 value: input.into(),
2653 })];
2654 }
2655 state.depth += 1;
2656 let nodes = parse_inline_content(input, base_offset, definitions, diagnostics, state);
2657 state.depth -= 1;
2658 nodes
2659}
2660
2661fn parse_inline_content(
2662 input: &str,
2663 base_offset: usize,
2664 definitions: Option<Labels<'_>>,
2665 diagnostics: &mut Vec<Diagnostic>,
2666 state: &mut InlineState,
2667) -> Vec<Inline> {
2668 let bytes = input.as_bytes();
2669 let mut nodes = Vec::new();
2670 let mut text_start = 0;
2671 let mut text = String::new();
2672 let mut index = 0;
2673 let mut delimiters: Vec<DelimMarker> = Vec::new();
2678 let mut brackets = Brackets::new();
2679 let mut pass = InlinePass {
2680 state,
2681 scan: InlineScan::new(input),
2682 };
2683 let literal_autolinks = bytes.contains(&b'@')
2686 || input.contains("://")
2687 || bytes
2688 .windows(4)
2689 .any(|window| window.eq_ignore_ascii_case(b"www."));
2690
2691 while index < bytes.len() {
2692 if bytes[index] == b'\\' {
2693 if let Some((next_index, char)) = next_char(input, index + 1) {
2694 if char.is_ascii_punctuation() {
2695 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2696 nodes.push(Inline::Escape(Escape {
2697 meta: NodeMeta::new(Some(Span::new(
2698 base_offset + index,
2699 base_offset + next_index,
2700 ))),
2701 value: char,
2702 }));
2703 index = next_index;
2704 text_start = index;
2705 continue;
2706 }
2707 }
2708 }
2709
2710 if bytes[index] == b'&' {
2711 if let Some((end, _)) = parse_character_reference(input, index) {
2712 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2713 nodes.push(Inline::CharacterReference(CharacterReference {
2714 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
2715 reference: input[index..end].into(),
2716 }));
2717 index = end;
2718 text_start = index;
2719 continue;
2720 }
2721 }
2722
2723 if bytes[index] == b'\n' {
2724 if index > 0
2727 && bytes[index - 1] == b'\\'
2728 && !is_escaped_at(input, index - 1)
2729 && text.ends_with('\\')
2730 {
2731 text.pop();
2732 flush_text(
2733 &mut nodes,
2734 &mut text,
2735 text_start,
2736 base_offset + index.saturating_sub(1),
2737 );
2738 nodes.push(Inline::LineBreak(LineBreak {
2739 meta: NodeMeta::new(Some(Span::new(
2740 base_offset + index.saturating_sub(1),
2741 base_offset + index + 1,
2742 ))),
2743 kind: LineBreakKind::Backslash,
2744 }));
2745 index += 1;
2746 text_start = index;
2747 continue;
2748 }
2749 let trailing_spaces = trailing_space_count(&text);
2751 if is_hard_break_suffix(&text, trailing_spaces) {
2752 text.truncate(text.len() - trailing_spaces);
2753 flush_text(
2754 &mut nodes,
2755 &mut text,
2756 text_start,
2757 base_offset + index.saturating_sub(trailing_spaces),
2758 );
2759 nodes.push(Inline::LineBreak(LineBreak {
2760 meta: NodeMeta::new(Some(Span::new(
2761 base_offset + index.saturating_sub(trailing_spaces),
2762 base_offset + index + 1,
2763 ))),
2764 kind: LineBreakKind::Spaces,
2765 }));
2766 index += 1;
2767 text_start = index;
2768 continue;
2769 }
2770 let stripped_alone = trailing_spaces > 0 && text.len() == trailing_spaces;
2773 if trailing_spaces > 0 {
2774 text.truncate(text.len() - trailing_spaces);
2775 }
2776 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2777 let start = if stripped_alone {
2778 index - trailing_spaces
2779 } else {
2780 index
2781 };
2782 nodes.push(Inline::SoftBreak(SoftBreak {
2783 meta: NodeMeta::new(Some(Span::new(
2784 base_offset + start,
2785 base_offset + index + 1,
2786 ))),
2787 }));
2788 index += 1;
2789 text_start = index;
2790 continue;
2791 }
2792
2793 if bytes[index] == b'`' {
2794 if let Some((end, value)) = parse_code_span_with(&mut pass.scan.lookups, input, index) {
2795 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2796 nodes.push(Inline::Code(CodeInline {
2797 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
2798 value,
2799 }));
2800 index = end;
2801 text_start = index;
2802 continue;
2803 } else {
2804 let run = bytes[index..]
2810 .iter()
2811 .take_while(|byte| **byte == b'`')
2812 .count();
2813 if text.is_empty() {
2814 text_start = base_offset + index;
2815 }
2816 for _ in 0..run {
2817 text.push('`');
2818 }
2819 index += run;
2820 continue;
2821 }
2822 }
2823
2824 if bytes[index] == b'*' && starts_delimiter_run(input, index, b'*') {
2825 let run_len = delimiter_byte_run_len(input, index, b'*');
2826 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2827 push_delimiter(
2828 &mut nodes,
2829 &mut delimiters,
2830 index,
2831 base_offset,
2832 b'*',
2833 run_len,
2834 );
2835 index += run_len;
2836 text_start = index;
2837 continue;
2838 }
2839
2840 if bytes[index] == b'_' && starts_delimiter_run(input, index, b'_') {
2843 if literal_autolinks {
2848 if let Some((end, _)) = parse_literal_autolink(
2849 input,
2850 index,
2851 &mut pass.scan.literal_autolinks,
2852 !brackets.open(),
2853 ) {
2854 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2855 nodes.push(autolink_node(
2856 AutolinkForm::Literal,
2857 Span::new(base_offset + index, base_offset + end),
2858 &input[index..end],
2859 ));
2860 index = end;
2861 text_start = index;
2862 continue;
2863 }
2864 }
2865 let run_len = delimiter_byte_run_len(input, index, b'_');
2866 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2867 push_delimiter(
2868 &mut nodes,
2869 &mut delimiters,
2870 index,
2871 base_offset,
2872 b'_',
2873 run_len,
2874 );
2875 index += run_len;
2876 text_start = index;
2877 continue;
2878 }
2879
2880 if bytes[index] == b'=' {
2881 let run_len = delimiter_byte_run_len(input, index, b'=');
2882 if run_len >= 2 {
2883 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2884 push_delimiter(
2885 &mut nodes,
2886 &mut delimiters,
2887 index,
2888 base_offset,
2889 b'=',
2890 run_len,
2891 );
2892 index += run_len;
2893 text_start = index;
2894 continue;
2895 }
2896 }
2897
2898 let bracket_room = pass.state.depth - 1 + brackets.openers.len() < MAX_INLINE_NESTING;
2899
2900 if bracket_room && bytes[index] == b'^' && bytes.get(index + 1) == Some(&b'[') {
2902 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2903 push_text(&mut nodes, base_offset + index, "^");
2904 push_text(&mut nodes, base_offset + index + 1, "[");
2905 brackets.openers.push(BracketOpener {
2906 kind: BracketKind::InlineFootnote,
2907 node_index: nodes.len() - 1,
2908 position: index,
2909 delimiter_bottom: delimiters.len(),
2910 });
2911 index += 2;
2912 text_start = index;
2913 continue;
2914 }
2915
2916 if bytes[index] == b'~' && starts_delimiter_run(input, index, b'~') {
2920 let run_len = delimiter_byte_run_len(input, index, b'~');
2921 if run_len == 2 {
2922 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2923 push_delimiter(
2924 &mut nodes,
2925 &mut delimiters,
2926 index,
2927 base_offset,
2928 b'~',
2929 run_len,
2930 );
2931 index += run_len;
2932 text_start = index;
2933 continue;
2934 }
2935 }
2936
2937 if bytes[index] == b'!' && bytes.get(index + 1) == Some(&b'[') {
2938 if let Some((end, wikilink)) =
2942 parse_wikilink(input, index + 1, base_offset, &mut pass.scan)
2943 {
2944 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2945 nodes.push(embed(wikilink, base_offset + index));
2946 brackets.close_links();
2948 index = end;
2949 text_start = index;
2950 continue;
2951 }
2952 }
2953
2954 if bytes[index] == b'!' && bytes.get(index + 1) == Some(&b'[') && bracket_room {
2955 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2956 push_text(&mut nodes, base_offset + index, "!");
2957 push_text(&mut nodes, base_offset + index + 1, "[");
2958 brackets.openers.push(BracketOpener {
2959 kind: BracketKind::Image,
2960 node_index: nodes.len() - 1,
2961 position: index,
2962 delimiter_bottom: delimiters.len(),
2963 });
2964 index += 2;
2965 text_start = index;
2966 continue;
2967 }
2968
2969 if bytes[index] == b'[' {
2970 if let Some((end, wikilink)) = parse_wikilink(input, index, base_offset, &mut pass.scan)
2971 {
2972 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2973 nodes.push(wikilink);
2974 brackets.close_links();
2975 index = end;
2976 text_start = index;
2977 continue;
2978 }
2979 if bracket_room {
2980 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2981 push_text(&mut nodes, base_offset + index, "[");
2982 brackets.openers.push(BracketOpener {
2983 kind: BracketKind::Link,
2984 node_index: nodes.len() - 1,
2985 position: index,
2986 delimiter_bottom: delimiters.len(),
2987 });
2988 index += 1;
2989 text_start = index;
2990 continue;
2991 }
2992 brackets.overflow += 1;
2993 }
2994
2995 if bytes[index] == b']' && brackets.overflow > 0 {
2996 brackets.overflow -= 1;
2998 } else if bytes[index] == b']' && !brackets.openers.is_empty() {
2999 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3000 if let Some(end) = close_bracket(
3001 input,
3002 base_offset,
3003 index,
3004 definitions,
3005 &mut nodes,
3006 &mut delimiters,
3007 &mut brackets,
3008 &mut pass,
3009 ) {
3010 index = end;
3011 text_start = index;
3012 continue;
3013 }
3014 }
3015
3016 if bytes[index] == b'$' {
3017 if let Some((end, value, kind)) =
3018 parse_math_inline(&mut pass.scan.lookups, input, index)
3019 {
3020 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3021 nodes.push(Inline::Math(MathInline {
3022 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
3023 value,
3024 kind,
3025 }));
3026 index = end;
3027 text_start = index;
3028 continue;
3029 }
3030 let run = bytes[index..]
3037 .iter()
3038 .take_while(|byte| **byte == b'$')
3039 .count();
3040 if run > 1 {
3041 if text.is_empty() {
3042 text_start = base_offset + index;
3043 }
3044 text.push_str(&input[index..index + run]);
3045 index += run;
3046 continue;
3047 }
3048 }
3049
3050 if literal_autolinks {
3053 if let Some((end, _)) = parse_literal_autolink(
3054 input,
3055 index,
3056 &mut pass.scan.literal_autolinks,
3057 !brackets.open(),
3058 ) {
3059 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3060 nodes.push(autolink_node(
3061 AutolinkForm::Literal,
3062 Span::new(base_offset + index, base_offset + end),
3063 &input[index..end],
3064 ));
3065 index = end;
3066 text_start = index;
3067 continue;
3068 }
3069 }
3070
3071 if bytes[index] == b'<' {
3072 if let Some(end) = pass.scan.lookups.find(">", index).map(|close| close + 1) {
3073 let uri = &input[index + 1..end - 1];
3074 if is_angle_autolink(uri) {
3075 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3076 nodes.push(autolink_node(
3077 AutolinkForm::Angle,
3078 Span::new(base_offset + index, base_offset + end),
3079 uri,
3080 ));
3081 index = end;
3082 text_start = index;
3083 continue;
3084 }
3085 }
3086 if let Some(end) = html_inline_end(&mut pass.scan.lookups, input, index) {
3087 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3088 nodes.push(Inline::Html(HtmlInline {
3089 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
3090 value: input[index..end].into(),
3091 }));
3092 index = end;
3093 text_start = index;
3094 continue;
3095 }
3096 }
3097
3098 if bytes[index] == b':' {
3099 if let Some((end, name)) = parse_shortcode(input, index) {
3100 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3101 nodes.push(Inline::Shortcode(Shortcode {
3102 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
3103 name,
3104 }));
3105 index = end;
3106 text_start = index;
3107 continue;
3108 }
3109 }
3110
3111 if bytes[index] == b':' && pass.state.depth + brackets.openers.len() <= MAX_INLINE_NESTING {
3114 pass.state.depth += brackets.openers.len();
3117 let directive = parse_text_directive(
3118 input,
3119 index,
3120 base_offset,
3121 definitions,
3122 diagnostics,
3123 &mut pass,
3124 );
3125 pass.state.depth -= brackets.openers.len();
3126 if let Some((end, directive)) = directive {
3127 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
3128 if let Inline::TextDirective(node) = &directive {
3129 if contains_link_inline(&node.label) {
3132 brackets.close_links();
3133 }
3134 let depth = inline_nesting(&node.label) + 1;
3135 brackets.formed.push((nodes.len(), depth));
3136 }
3137 nodes.push(directive);
3138 index = end;
3139 text_start = index;
3140 continue;
3141 }
3142 }
3143
3144 let (next_index, char) = next_char(input, index).expect("valid UTF-8 byte index");
3145 if text.is_empty() {
3146 text_start = base_offset + index;
3147 }
3148 text.push(char);
3149 index = next_index;
3150 }
3151
3152 flush_text(&mut nodes, &mut text, text_start, base_offset + input.len());
3153 assign_emphasis_roles(input, &mut delimiters);
3154 process_emphasis(nodes, delimiters, pass.state.depth - 1, &brackets.formed).0
3155}
3156
3157fn parse_shortcode(input: &str, index: usize) -> Option<(usize, String)> {
3161 if input[index..].starts_with("::") {
3162 return None;
3163 }
3164 if input[..index]
3165 .chars()
3166 .next_back()
3167 .is_some_and(char::is_alphanumeric)
3168 {
3169 return None;
3170 }
3171
3172 let mut cursor = index + 1;
3173 while let Some((next, char)) = next_char(input, cursor) {
3174 if char == ':' {
3175 let name = &input[index + 1..cursor];
3176 if crate::gemoji::glyph(name).is_none()
3177 || input[next..]
3178 .chars()
3179 .next()
3180 .is_some_and(char::is_alphanumeric)
3181 {
3182 return None;
3183 }
3184 return Some((next, name.into()));
3185 }
3186 if !(char.is_ascii_alphanumeric() || matches!(char, '_' | '-' | '+')) {
3187 return None;
3188 }
3189 cursor = next;
3190 }
3191 None
3192}
3193
3194fn parse_wikilink(
3195 input: &str,
3196 index: usize,
3197 base_offset: usize,
3198 scan: &mut InlineScan,
3199) -> Option<(usize, Inline)> {
3200 if input.as_bytes().get(index) != Some(&b'[') || input.as_bytes().get(index + 1) != Some(&b'[')
3201 {
3202 return None;
3203 }
3204
3205 let close = scan.wikilink_close(index + 2)?;
3206 let source = &input[index + 2..close];
3207 if source.is_empty() || source.len() > WIKILINK_MAX_BYTES {
3208 return None;
3209 }
3210
3211 let (target_source, label_source) = match find_wikilink_separator(source) {
3212 Some(separator) => (&source[..separator], &source[separator + 1..]),
3213 None => (source, source),
3214 };
3215
3216 if target_source.is_empty() {
3219 return None;
3220 }
3221 let target = String::from(target_source);
3222 let label = String::from(label_source);
3223 let end = close + 2;
3224 Some((
3225 end,
3226 Inline::WikiLink(WikiLink {
3227 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
3228 target,
3229 label,
3230 embed: false,
3231 }),
3232 ))
3233}
3234
3235fn embed(mut wikilink: Inline, bang: usize) -> Inline {
3237 if let Inline::WikiLink(node) = &mut wikilink {
3238 node.embed = true;
3239 node.meta.span = node.meta.span.map(|span| Span::new(bang, span.end));
3240 }
3241 wikilink
3242}
3243
3244fn wikilink_close_step(input: &str, cursor: usize) -> Step {
3247 let bytes = input.as_bytes();
3248 match bytes.get(cursor) {
3249 None | Some(b'\n' | b'\r' | b'[') => Step::Done(None),
3250 Some(b'\\') => {
3251 let escaped = cursor + 1;
3252 Step::Next(next_char(input, escaped).map_or(escaped, |(after_escape, _)| after_escape))
3253 }
3254 Some(b']') if bytes.get(cursor + 1) == Some(&b']') => Step::Done(Some(cursor)),
3255 Some(b']') => Step::Done(None),
3256 Some(_) => Step::Next(next_char(input, cursor).map_or(input.len(), |(next, _)| next)),
3257 }
3258}
3259
3260fn find_wikilink_separator(input: &str) -> Option<usize> {
3261 let bytes = input.as_bytes();
3262 let mut cursor = 0;
3263 while cursor < input.len() {
3264 match bytes[cursor] {
3265 b'\\' => {
3266 cursor += 1;
3267 if cursor < input.len() {
3268 cursor = next_char(input, cursor)?.0;
3269 }
3270 }
3271 b'|' => return Some(cursor),
3272 _ => cursor = next_char(input, cursor)?.0,
3273 }
3274 }
3275 None
3276}
3277
3278fn trailing_space_count(input: &str) -> usize {
3279 input
3280 .as_bytes()
3281 .iter()
3282 .rev()
3283 .take_while(|byte| matches!(**byte, b' ' | b'\t'))
3284 .count()
3285}
3286
3287fn is_hard_break_suffix(input: &str, trailing: usize) -> bool {
3288 let bytes = input.as_bytes();
3292 trailing >= 2
3293 && bytes[bytes.len() - trailing..]
3294 .iter()
3295 .all(|byte| *byte == b' ')
3296}
3297
3298#[cfg(test)]
3301fn find_reference_label_end(input: &str, open: usize) -> Option<usize> {
3302 if input.as_bytes().get(open) != Some(&b'[') {
3305 return None;
3306 }
3307 let close = path_walk(open + 1, |cursor| reference_label_step(input, cursor))?;
3308 reference_label_is_within_limit(&input[open + 1..close]).then_some(close)
3309}
3310
3311fn reference_label_step(input: &str, cursor: usize) -> Step {
3313 let Some((next, char)) = next_char(input, cursor) else {
3314 return Step::Done(None);
3315 };
3316 match char {
3317 '\\' => Step::Next(next_char(input, next).map_or(next, |(after_escape, _)| after_escape)),
3318 '[' => Step::Done(None),
3319 ']' => Step::Done(Some(cursor)),
3320 _ => Step::Next(next),
3321 }
3322}
3323
3324fn inline_nesting(inlines: &[Inline]) -> usize {
3326 let mut deepest = 0;
3327 let mut pending = vec![(inlines, 0)];
3328 while let Some((nodes, depth)) = pending.pop() {
3329 for node in nodes {
3330 if is_inline_container(node) {
3331 deepest = deepest.max(depth + 1);
3332 pending.push((node.children(), depth + 1));
3333 }
3334 }
3335 }
3336 deepest
3337}
3338
3339fn is_inline_container(inline: &Inline) -> bool {
3340 matches!(
3341 inline,
3342 Inline::Emphasis(_)
3343 | Inline::Strong(_)
3344 | Inline::Delete(_)
3345 | Inline::Mark(_)
3346 | Inline::Link(_)
3347 | Inline::Image(_)
3348 | Inline::LinkReference(_)
3349 | Inline::ImageReference(_)
3350 | Inline::InlineFootnote(_)
3351 | Inline::TextDirective(_)
3352 )
3353}
3354
3355fn contains_link_inline(inlines: &[Inline]) -> bool {
3358 inlines.iter().any(|inline| {
3359 matches!(
3360 inline,
3361 Inline::Link(_) | Inline::LinkReference(_) | Inline::WikiLink(_)
3362 ) || contains_link_inline(inline.children())
3363 })
3364}
3365
3366fn find_link_label_end(input: &str, open: usize) -> Option<usize> {
3367 if input.as_bytes().get(open) != Some(&b'[') {
3368 return None;
3369 }
3370 let mut lookups = DirectLookups { input };
3371 bracket_walk(open + 1, |cursor| {
3372 link_label_step(&mut lookups, input, cursor)
3373 })
3374}
3375
3376fn link_label_step(lookups: &mut impl Lookups, input: &str, cursor: usize) -> BracketStep {
3380 let Some((next, char)) = next_char(input, cursor) else {
3381 return BracketStep::End;
3382 };
3383 match char {
3384 '\\' => {
3385 BracketStep::Pass(next_char(input, next).map_or(next, |(after_escape, _)| after_escape))
3386 }
3387 '`' => BracketStep::Pass(code_span_end(lookups, input, cursor).unwrap_or(next)),
3388 '<' => BracketStep::Pass(
3389 autolink_end(lookups, input, cursor)
3390 .or_else(|| html_inline_end(lookups, input, cursor))
3391 .unwrap_or(next),
3392 ),
3393 '[' => BracketStep::Open(next),
3394 ']' => BracketStep::Close(next),
3395 _ => BracketStep::Pass(next),
3396 }
3397}
3398
3399fn parse_text_directive(
3400 input: &str,
3401 index: usize,
3402 base_offset: usize,
3403 definitions: Option<Labels<'_>>,
3404 diagnostics: &mut Vec<Diagnostic>,
3405 pass: &mut InlinePass,
3406) -> Option<(usize, Inline)> {
3407 if input[index..].starts_with("::") {
3408 return None;
3409 }
3410 if index > 0 {
3411 let previous = input[..index].chars().next_back()?;
3412 if !previous.is_whitespace() && !matches!(previous, '(' | '[' | '{') {
3413 return None;
3414 }
3415 }
3416 let opener_offset = index + 1;
3417 let opener = parse_directive_opener_with(
3418 &input[opener_offset..],
3419 |close, open| {
3420 let found = match close {
3421 DirectiveClose::Label => pass.scan.link_label_end(opener_offset + open),
3422 DirectiveClose::Attributes => {
3423 pass.scan.directive_attributes_close(opener_offset + open)
3424 }
3425 };
3426 found.map(|position| position - opener_offset)
3427 },
3428 |label, attributes, rest| text_directive_ends(label, attributes, rest.bytes().next()),
3429 );
3430 let opener = match opener {
3431 Ok(opener) => opener,
3432 Err(refused) => {
3433 if refused == Refused::Unclosed {
3436 diagnostics.push(Diagnostic::new(
3437 DiagnosticSeverity::Error,
3438 DiagnosticCode::InvalidDirectiveName,
3439 Span::new(base_offset + index, base_offset + input.len()),
3440 "text directive opener is malformed",
3441 ));
3442 }
3443 return None;
3444 }
3445 };
3446 let label = opener
3447 .label
3448 .map(|source| {
3449 parse_inlines_with_context(
3450 source,
3451 base_offset + index + 1 + opener.name.len() + 1,
3452 definitions,
3453 diagnostics,
3454 pass.state,
3455 )
3456 })
3457 .unwrap_or_default();
3458 let at = base_offset + opener_offset;
3459 opener.report_dropped(diagnostics, |start, end| Span::new(at + start, at + end));
3460 let DirectiveOpener {
3461 name,
3462 attributes,
3463 consumed,
3464 ..
3465 } = opener;
3466 Some((
3467 index + 1 + consumed,
3468 Inline::TextDirective(TextDirective {
3469 meta: NodeMeta::new(Some(Span::new(
3470 base_offset + index,
3471 base_offset + index + 1 + consumed,
3472 ))),
3473 name,
3474 label,
3475 attributes,
3476 }),
3477 ))
3478}
3479
3480fn text_directive_ends(label: Option<&str>, attributes: Option<&str>, next: Option<u8>) -> bool {
3487 match next {
3488 None | Some(b' ' | b'\t' | b'\n' | b'\r') => true,
3489 Some(byte) if byte.is_ascii_punctuation() => {
3490 label.is_some_and(|label| !label.is_empty())
3491 || attributes
3492 .is_some_and(|source| source.bytes().any(|byte| !byte.is_ascii_whitespace()))
3493 }
3494 Some(_) => false,
3495 }
3496}
3497
3498struct DirectiveOpener<'a> {
3501 name: String,
3502 label: Option<&'a str>,
3503 attributes: Vec<DirectiveAttribute>,
3504 dropped: Vec<(usize, usize)>,
3506 consumed: usize,
3508}
3509
3510impl DirectiveOpener<'_> {
3511 fn report_dropped(
3513 &self,
3514 diagnostics: &mut Vec<Diagnostic>,
3515 span: impl Fn(usize, usize) -> Span,
3516 ) {
3517 for &(start, end) in &self.dropped {
3518 diagnostics.push(Diagnostic::new(
3519 DiagnosticSeverity::Warning,
3520 DiagnosticCode::InvalidDirectiveAttribute,
3521 span(start, end),
3522 "directive attribute without a valid name is dropped",
3523 ));
3524 }
3525 }
3526}
3527
3528#[derive(Clone, Copy, PartialEq, Eq)]
3530enum Refused {
3531 Name,
3533 Unclosed,
3535 Follow,
3537}
3538
3539fn parse_directive_opener<'a>(
3542 input: &'a str,
3543 accept: impl FnOnce(Option<&str>, Option<&str>, &str) -> bool,
3544) -> Result<DirectiveOpener<'a>, Refused> {
3545 parse_directive_opener_with(
3546 input,
3547 |close, open| match close {
3548 DirectiveClose::Label => find_link_label_end(input, open),
3549 DirectiveClose::Attributes => find_directive_attributes_close(input, open),
3550 },
3551 accept,
3552 )
3553}
3554
3555#[derive(Clone, Copy)]
3557enum DirectiveClose {
3558 Label,
3560 Attributes,
3562}
3563
3564fn parse_directive_opener_with<'a>(
3577 input: &'a str,
3578 mut find_close: impl FnMut(DirectiveClose, usize) -> Option<usize>,
3579 accept: impl FnOnce(Option<&str>, Option<&str>, &str) -> bool,
3580) -> Result<DirectiveOpener<'a>, Refused> {
3581 let mut index = 0;
3582 while let Some((next, char)) = next_char(input, index) {
3583 if char.is_ascii_alphanumeric() || char == '_' || char == '-' {
3584 index = next;
3585 } else {
3586 break;
3587 }
3588 }
3589 let name = &input[..index];
3590 if !is_directive_name(name) {
3591 return Err(Refused::Name);
3592 }
3593
3594 let mut label = None;
3595 let mut attribute_source = None;
3596 let mut consumed = index;
3597 if input.as_bytes().get(consumed) == Some(&b'[') {
3598 let close = find_close(DirectiveClose::Label, consumed).ok_or(Refused::Unclosed)?;
3599 label = Some(&input[consumed + 1..close]);
3600 consumed = close + 1;
3601 }
3602 if input.as_bytes().get(consumed) == Some(&b'{') {
3603 let close = find_close(DirectiveClose::Attributes, consumed).ok_or(Refused::Unclosed)?;
3604 attribute_source = Some((consumed + 1, &input[consumed + 1..close]));
3605 consumed = close + 1;
3606 }
3607 if !accept(
3608 label,
3609 attribute_source.map(|(_, source)| source),
3610 &input[consumed..],
3611 ) {
3612 return Err(Refused::Follow);
3613 }
3614 let (attributes, dropped) = match attribute_source {
3615 Some((start, source)) => {
3616 let (attributes, mut dropped) = parse_attributes(source);
3617 for range in &mut dropped {
3618 *range = (range.0 + start, range.1 + start);
3619 }
3620 (attributes, dropped)
3621 }
3622 None => (Vec::new(), Vec::new()),
3623 };
3624
3625 Ok(DirectiveOpener {
3626 name: name.into(),
3627 label,
3628 attributes,
3629 dropped,
3630 consumed,
3631 })
3632}
3633
3634const DIRECTIVE_QUOTE_STATES: usize = 3;
3637
3638fn find_directive_attributes_close(input: &str, open: usize) -> Option<usize> {
3639 if input.as_bytes().get(open) != Some(&b'{') {
3640 return None;
3641 }
3642 path_walk((open + 1) * DIRECTIVE_QUOTE_STATES, |node| {
3643 directive_attributes_step(input, node)
3644 })
3645}
3646
3647fn directive_attributes_step(input: &str, node: usize) -> Step {
3651 let bytes = input.as_bytes();
3652 let (cursor, quote) = (node / DIRECTIVE_QUOTE_STATES, node % DIRECTIVE_QUOTE_STATES);
3653 let at = |position: usize, quote: usize| Step::Next(position * DIRECTIVE_QUOTE_STATES + quote);
3654 let Some(&byte) = bytes.get(cursor) else {
3655 return Step::Done(None);
3656 };
3657 if byte == b'\\' {
3658 return at((cursor + 2).min(bytes.len()), quote);
3659 }
3660 match (quote, byte) {
3661 (0, b'"') => at(cursor + 1, 1),
3662 (0, b'\'') => at(cursor + 1, 2),
3663 (0, b'}') => Step::Done(Some(cursor)),
3664 (1, b'"') | (2, b'\'') => at(cursor + 1, 0),
3665 _ => at(cursor + 1, quote),
3666 }
3667}
3668
3669fn parse_attributes(input: &str) -> (Vec<DirectiveAttribute>, Vec<(usize, usize)>) {
3672 let mut attributes = Vec::new();
3673 let mut dropped = Vec::new();
3674 let mut cursor = 0;
3675 while cursor < input.len() {
3676 cursor = skip_spaces(input, cursor);
3677 if cursor >= input.len() {
3678 break;
3679 }
3680
3681 if input.as_bytes().get(cursor) == Some(&b'#') {
3682 let (id, next) = parse_attribute_token(input, cursor + 1);
3683 if !id.is_empty() {
3684 attributes.push(DirectiveAttribute {
3685 name: "id".into(),
3686 value: Some(id.into()),
3687 });
3688 }
3689 cursor = next;
3690 continue;
3691 }
3692
3693 if input.as_bytes().get(cursor) == Some(&b'.') {
3694 let (class, next) = parse_attribute_token(input, cursor + 1);
3695 if !class.is_empty() {
3696 attributes.push(DirectiveAttribute {
3697 name: "class".into(),
3698 value: Some(class.into()),
3699 });
3700 }
3701 cursor = next;
3702 continue;
3703 }
3704
3705 let token_start = cursor;
3706 let (name, next) = parse_attribute_name(input, cursor);
3707 if name.is_empty() {
3708 dropped.push((token_start, input.trim_end().len().max(token_start)));
3709 break;
3710 }
3711 let mut token_end = next;
3712 cursor = skip_spaces(input, next);
3713 let value = if input.as_bytes().get(cursor) == Some(&b'=') {
3714 cursor = skip_spaces(input, cursor + 1);
3715 token_end = cursor;
3716 if let Some((value, next)) = parse_attribute_value(input, cursor) {
3717 cursor = next;
3718 token_end = next;
3719 Some(value)
3720 } else {
3721 Some(String::new())
3722 }
3723 } else {
3724 None
3725 };
3726 if crate::validate::is_attribute_name(name) {
3729 attributes.push(DirectiveAttribute {
3730 name: name.into(),
3731 value,
3732 });
3733 } else {
3734 dropped.push((token_start, token_end));
3735 }
3736 }
3737 (attributes, dropped)
3738}
3739
3740fn parse_attribute_token(input: &str, index: usize) -> (&str, usize) {
3741 let mut cursor = index;
3742 while let Some((next, char)) = next_char(input, cursor) {
3743 if char.is_whitespace() {
3744 break;
3745 }
3746 cursor = next;
3747 }
3748 (&input[index..cursor], cursor)
3749}
3750
3751fn parse_attribute_name(input: &str, index: usize) -> (&str, usize) {
3752 let mut cursor = index;
3753 while let Some((next, char)) = next_char(input, cursor) {
3754 if char.is_whitespace() || char == '=' {
3755 break;
3756 }
3757 cursor = next;
3758 }
3759 (&input[index..cursor], cursor)
3760}
3761
3762fn parse_attribute_value(input: &str, index: usize) -> Option<(String, usize)> {
3763 let quote = input.as_bytes().get(index).copied();
3764 if matches!(quote, Some(b'"' | b'\'')) {
3765 let quote = quote?;
3766 let mut cursor = index + 1;
3767 while cursor < input.len() {
3768 let (next, char) = next_char(input, cursor)?;
3769 if char as u8 == quote && !is_escaped_at(input, cursor) {
3770 return Some((
3771 decode_escapes_and_references(&input[index + 1..cursor]),
3772 next,
3773 ));
3774 }
3775 cursor = next;
3776 }
3777 return None;
3778 }
3779
3780 let (value, next) = parse_attribute_token(input, index);
3781 Some((
3782 decode_selected_escapes_and_references(value, |char| matches!(char, '\\' | '&')),
3783 next,
3784 ))
3785}
3786
3787fn parse_code_span_with(
3789 lookups: &mut impl Lookups,
3790 input: &str,
3791 index: usize,
3792) -> Option<(usize, String)> {
3793 let len = backtick_run_len(input, index);
3794 let search_start = index + len;
3795 let close = lookups.code_span_close(search_start, len)?;
3796 Some((
3797 close + len,
3798 normalize_code_span(&input[search_start..close]),
3799 ))
3800}
3801
3802fn code_span_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
3804 let len = backtick_run_len(input, index);
3805 Some(lookups.code_span_close(index + len, len)? + len)
3806}
3807
3808fn find_code_span_close(input: &str, start: usize, marker_len: usize) -> Option<usize> {
3809 let bytes = input.as_bytes();
3810 let mut cursor = start;
3811 while cursor < bytes.len() {
3812 if bytes[cursor] != b'`' {
3813 cursor = next_char(input, cursor)
3814 .map(|(next, _)| next)
3815 .unwrap_or(bytes.len());
3816 continue;
3817 }
3818 let run_len = bytes[cursor..]
3819 .iter()
3820 .take_while(|byte| **byte == b'`')
3821 .count();
3822 if run_len == marker_len {
3823 return Some(cursor);
3824 }
3825 cursor += run_len;
3826 }
3827 None
3828}
3829
3830fn normalize_code_span(input: &str) -> String {
3831 let (before, after) = code_padding(input);
3832 let mut normalized = String::new();
3833 let content = &input[before..input.len() - after];
3834 let mut cursor = 0;
3835 while cursor < content.len() {
3836 let (next, char) = next_char(content, cursor).expect("valid UTF-8 byte index");
3837 if char == '\r' {
3838 if content.as_bytes().get(next) == Some(&b'\n') {
3839 cursor = next + 1;
3840 } else {
3841 cursor = next;
3842 }
3843 normalized.push(' ');
3844 continue;
3845 }
3846 if char == '\n' {
3847 normalized.push(' ');
3848 cursor = next;
3849 continue;
3850 }
3851 normalized.push(char);
3852 cursor = next;
3853 }
3854 normalized
3855}
3856
3857pub(crate) fn code_padding(input: &str) -> (usize, usize) {
3861 let space = |text: &str| -> Option<usize> {
3862 match text.as_bytes().first()? {
3863 b' ' | b'\n' => Some(1),
3864 b'\r' if text.as_bytes().get(1) == Some(&b'\n') => Some(2),
3865 b'\r' => Some(1),
3866 _ => None,
3867 }
3868 };
3869 let end_space = |text: &str| -> Option<usize> {
3870 match text.as_bytes().last()? {
3871 b' ' | b'\r' => Some(1),
3872 b'\n' if text.len() >= 2 && text.as_bytes()[text.len() - 2] == b'\r' => Some(2),
3873 b'\n' => Some(1),
3874 _ => None,
3875 }
3876 };
3877 match (space(input), end_space(input)) {
3878 (Some(before), Some(after))
3879 if input
3880 .bytes()
3881 .any(|byte| !matches!(byte, b' ' | b'\n' | b'\r')) =>
3882 {
3883 (before, after)
3884 }
3885 _ => (0, 0),
3886 }
3887}
3888
3889fn can_open_delimited(input: &str, index: usize, marker_len: usize) -> bool {
3890 delimiter_flanking(input, index, marker_len).left
3891}
3892
3893fn can_close_delimited(input: &str, index: usize, marker_len: usize) -> bool {
3894 delimiter_flanking(input, index, marker_len).right
3895}
3896
3897fn starts_delimiter_run(input: &str, index: usize, marker: u8) -> bool {
3901 index == 0 || input.as_bytes()[index - 1] != marker || is_escaped_at(input, index - 1)
3902}
3903
3904fn delimiter_byte_run_len(input: &str, index: usize, marker: u8) -> usize {
3905 let bytes = input.as_bytes();
3906 let mut cursor = index;
3907 while bytes.get(cursor) == Some(&marker) {
3908 cursor += 1;
3909 }
3910 cursor - index
3911}
3912
3913#[derive(Clone, Copy)]
3914struct DelimiterFlanking {
3915 left: bool,
3916 right: bool,
3917 previous: Option<char>,
3918 next: Option<char>,
3919 previous_punctuation: bool,
3922 next_punctuation: bool,
3923}
3924
3925#[derive(Clone, Copy, Default)]
3927struct FlankingSide {
3928 whitespace: bool,
3929 punctuation: bool,
3930 cjk: bool,
3931 ideographic_variation_selector: bool,
3932}
3933
3934impl FlankingSide {
3935 fn of(value: Option<char>) -> Self {
3936 let Some(value) = value else {
3937 return Self {
3938 whitespace: true,
3939 ..Self::default()
3940 };
3941 };
3942 Self {
3943 whitespace: value.is_whitespace(),
3944 punctuation: is_flanking_punctuation(value),
3945 cjk: crate::unicode_cjk::is_cjk(value),
3946 ideographic_variation_selector: crate::unicode_cjk::is_ideographic_variation_selector(
3947 value,
3948 ),
3949 }
3950 }
3951
3952 fn before(previous: Option<char>, before_previous: impl FnOnce() -> Option<char>) -> Self {
3959 match previous {
3960 Some(selector) if crate::unicode_cjk::is_general_use_variation_selector(selector) => {
3961 match before_previous() {
3962 Some(base) if !base.is_whitespace() => {
3963 if selector == '\u{FE01}' && matches!(base, '‘' | '’' | '“' | '”') {
3964 return Self {
3965 punctuation: true,
3966 cjk: true,
3967 ..Self::default()
3968 };
3969 }
3970 Self {
3971 ideographic_variation_selector: false,
3972 ..Self::of(Some(base))
3973 }
3974 }
3975 _ => Self::default(),
3976 }
3977 }
3978 _ => Self::of(previous),
3979 }
3980 }
3981}
3982
3983fn delimiter_flanking(input: &str, index: usize, marker_len: usize) -> DelimiterFlanking {
3984 delimiter_flanking_within(input, index, marker_len, false, false)
3985}
3986
3987fn delimiter_flanking_within(
3999 input: &str,
4000 index: usize,
4001 marker_len: usize,
4002 bounded_before: bool,
4003 bounded_after: bool,
4004) -> DelimiterFlanking {
4005 let previous = if bounded_before {
4006 None
4007 } else {
4008 input[..index].chars().next_back().map(source_char)
4009 };
4010 let next = if bounded_after {
4011 None
4012 } else {
4013 input[index + marker_len..].chars().next().map(source_char)
4014 };
4015
4016 let before = FlankingSide::before(previous, || {
4017 let mut chars = input[..index].chars();
4018 chars.next_back();
4019 chars.next_back().map(source_char)
4020 });
4021 let after = FlankingSide::of(next);
4022
4023 let left = !after.whitespace
4024 && (!after.punctuation
4025 || before.whitespace
4026 || before.punctuation
4027 || after.cjk
4028 || before.cjk
4029 || before.ideographic_variation_selector);
4030 let right = !before.whitespace
4031 && (!before.punctuation
4032 || after.whitespace
4033 || after.punctuation
4034 || before.cjk
4035 || after.cjk);
4036
4037 DelimiterFlanking {
4038 left,
4039 right,
4040 previous,
4041 next,
4042 previous_punctuation: before.punctuation,
4043 next_punctuation: after.punctuation,
4044 }
4045}
4046
4047fn parse_math_inline(
4068 lookups: &mut impl Lookups,
4069 input: &str,
4070 index: usize,
4071) -> Option<(usize, String, MathInlineKind)> {
4072 if let Some((end, value)) = parse_math_code_inline(lookups, input, index) {
4073 return Some((end, value, MathInlineKind::Code));
4074 }
4075
4076 let bytes = input.as_bytes();
4077 let open_dollars = bytes[index..]
4078 .iter()
4079 .take_while(|byte| **byte == b'$')
4080 .count();
4081 if open_dollars == 0 || open_dollars > 2 {
4084 return None;
4085 }
4086
4087 let content_start = index + open_dollars;
4088 let close = scan_to_closing_dollar(lookups, input, content_start, open_dollars)?;
4089 let content_end = close - open_dollars;
4090 if content_end <= content_start {
4093 return None;
4094 }
4095
4096 let raw = &input[content_start..content_end];
4097 let dollars = u8::try_from(open_dollars).unwrap_or(u8::MAX);
4098 let kind = MathInlineKind::Dollar { dollars };
4099 let value = if math_is_padded(kind) {
4100 normalize_math_text(raw)
4101 } else {
4102 raw.into()
4103 };
4104 Some((close, value, kind))
4105}
4106
4107pub(crate) fn math_is_padded(kind: MathInlineKind) -> bool {
4111 matches!(kind, MathInlineKind::Dollar { dollars: 1 })
4112}
4113
4114pub(crate) fn reads_as_inline_math(written: &str, value: &str, kind: MathInlineKind) -> bool {
4117 parse_math_inline(&mut DirectLookups { input: written }, written, 0).is_some_and(
4118 |(end, read, read_kind)| end == written.len() && read == value && read_kind == kind,
4119 )
4120}
4121
4122fn scan_to_closing_dollar(
4126 lookups: &mut impl Lookups,
4127 input: &str,
4128 start: usize,
4129 open_dollars: usize,
4130) -> Option<usize> {
4131 let bytes = input.as_bytes();
4132 if open_dollars == 1 {
4133 if bytes.get(start).is_some_and(|byte| is_math_space(*byte)) {
4135 return None;
4136 }
4137 let close = lookups.unescaped_dollar(start)?;
4142 if is_math_space(bytes[close - 1]) || bytes.get(close + 1).is_some_and(u8::is_ascii_digit) {
4143 return None;
4144 }
4145 return Some(close + 1);
4146 }
4147 lookups.find("$$", start).map(|close| close + 2)
4150}
4151
4152fn is_math_space(byte: u8) -> bool {
4154 matches!(byte, b'\t' | b'\n' | b'\r' | b' ')
4155}
4156
4157fn normalize_math_text(input: &str) -> String {
4160 normalize_code_span(input)
4161}
4162
4163fn parse_math_code_inline(
4164 lookups: &mut impl Lookups,
4165 input: &str,
4166 index: usize,
4167) -> Option<(usize, String)> {
4168 if !input[index..].starts_with("$`") {
4169 return None;
4170 }
4171
4172 let search_start = index + 2;
4173 let close = lookups.find("`$", search_start)?;
4174 if close == search_start {
4175 return None;
4176 }
4177
4178 Some((close + 2, input[search_start..close].into()))
4179}
4180
4181fn parse_link_resource(
4182 lookups: &mut impl Lookups,
4183 input: &str,
4184 open: usize,
4185) -> Option<(usize, ParsedLinkResource, LinkTailRanges)> {
4186 let bytes = input.as_bytes();
4187 if bytes.get(open) != Some(&b'(') {
4188 return None;
4189 }
4190 let (mut cursor, initial_space) = skip_link_resource_space_with_info(input, open + 1)?;
4191 if bytes.get(cursor) == Some(&b')') {
4192 return Some((
4193 cursor + 1,
4194 ParsedLinkResource {
4195 destination: String::new(),
4196 destination_kind: LinkDestinationKind::Omitted,
4197 title: None,
4198 },
4199 LinkTailRanges {
4200 destination: (cursor, cursor),
4201 title: None,
4202 },
4203 ));
4204 }
4205 if initial_space && matches!(bytes.get(cursor), Some(b'"' | b'\'' | b'(')) {
4206 let omitted = cursor;
4207 let (title, next) = parse_link_title(input, cursor)?;
4208 let title_range = (cursor + 1, next - 1);
4209 cursor = skip_link_resource_space(input, next)?;
4210 if bytes.get(cursor) == Some(&b')') {
4211 return Some((
4212 cursor + 1,
4213 ParsedLinkResource {
4214 destination: String::new(),
4215 destination_kind: LinkDestinationKind::Omitted,
4216 title: Some(title),
4217 },
4218 LinkTailRanges {
4219 destination: (omitted, omitted),
4220 title: Some(title_range),
4221 },
4222 ));
4223 }
4224 return None;
4225 }
4226 let destination_start = cursor;
4227 let (destination, destination_kind, next) = parse_link_destination(lookups, input, cursor)?;
4228 let destination_range = match destination_kind {
4229 LinkDestinationKind::Angle => (destination_start + 1, next - 1),
4230 _ => (destination_start, next),
4231 };
4232 let (after_destination, had_space) = skip_link_resource_space_with_info(input, next)?;
4233 cursor = after_destination;
4234 if bytes.get(cursor) == Some(&b')') {
4235 return Some((
4236 cursor + 1,
4237 ParsedLinkResource {
4238 destination,
4239 destination_kind,
4240 title: None,
4241 },
4242 LinkTailRanges {
4243 destination: destination_range,
4244 title: None,
4245 },
4246 ));
4247 }
4248 if !had_space {
4249 return None;
4250 }
4251
4252 let (title, next) = parse_link_title(input, cursor)?;
4253 let title_range = (cursor + 1, next - 1);
4254 cursor = skip_link_resource_space(input, next)?;
4255 if bytes.get(cursor) == Some(&b')') {
4256 Some((
4257 cursor + 1,
4258 ParsedLinkResource {
4259 destination,
4260 destination_kind,
4261 title: Some(title),
4262 },
4263 LinkTailRanges {
4264 destination: destination_range,
4265 title: Some(title_range),
4266 },
4267 ))
4268 } else {
4269 None
4270 }
4271}
4272
4273fn parse_link_destination(
4274 lookups: &mut impl Lookups,
4275 input: &str,
4276 index: usize,
4277) -> Option<(String, LinkDestinationKind, usize)> {
4278 if input.as_bytes().get(index) == Some(&b'<') {
4279 let stop = lookups.angle_destination_stop(index + 1)?;
4280 if input.as_bytes()[stop] != b'>' {
4281 return None;
4282 }
4283 return Some((
4284 decode_escapes_and_references(&input[index + 1..stop]),
4285 LinkDestinationKind::Angle,
4286 stop + 1,
4287 ));
4288 }
4289
4290 let mut cursor = index;
4291 let mut depth = 0usize;
4292 while cursor < input.len() {
4293 let (next, char) = next_char(input, cursor)?;
4294 if char == ' ' || source_char(char).is_ascii_control() {
4299 break;
4300 }
4301 if char == '(' && !is_escaped_at(input, cursor) {
4302 depth += 1;
4303 if depth > 32 {
4305 return None;
4306 }
4307 } else if char == ')' && !is_escaped_at(input, cursor) {
4308 if depth == 0 {
4309 break;
4310 }
4311 depth -= 1;
4312 }
4313 cursor = next;
4314 }
4315
4316 if cursor == index || depth > 0 {
4317 None
4318 } else {
4319 Some((
4320 decode_escapes_and_references(&input[index..cursor]),
4321 LinkDestinationKind::Bare,
4322 cursor,
4323 ))
4324 }
4325}
4326
4327fn parse_link_title(input: &str, index: usize) -> Option<(Title, usize)> {
4328 let opener = input.as_bytes().get(index).copied()?;
4329 let (closer, title_kind) = match opener {
4330 b'"' => ('"', LinkTitleKind::DoubleQuote),
4331 b'\'' => ('\'', LinkTitleKind::SingleQuote),
4332 b'(' => (')', LinkTitleKind::Paren),
4333 _ => return None,
4334 };
4335 let mut cursor = index + 1;
4336 while cursor < input.len() {
4337 let (next, char) = next_char(input, cursor)?;
4338 if char == closer && !is_escaped_at(input, cursor) {
4339 if contains_blank_line(&input[index + 1..cursor]) {
4340 return None;
4341 }
4342 let value = decode_escapes_and_references(&input[index + 1..cursor]);
4343 return Some((Title::new(value, title_kind), next));
4344 }
4345 if opener == b'(' && char == '(' && !is_escaped_at(input, cursor) {
4346 return None;
4347 }
4348 cursor = next;
4349 }
4350 None
4351}
4352
4353fn contains_blank_line(input: &str) -> bool {
4354 if !input.bytes().any(|byte| matches!(byte, b'\n' | b'\r')) {
4355 return false;
4356 }
4357 let map = SourceMap::verbatim(input.len(), 0);
4362 let lines = collect_lines(input, &map);
4363 let interior = lines.len().saturating_sub(1);
4364 lines
4365 .iter()
4366 .take(interior)
4367 .skip(1)
4368 .any(|line| is_blank(line.text))
4369}
4370
4371fn skip_link_resource_space(input: &str, index: usize) -> Option<usize> {
4372 skip_link_resource_space_with_info(input, index).map(|(index, _)| index)
4373}
4374
4375fn skip_link_resource_space_with_info(input: &str, mut index: usize) -> Option<(usize, bool)> {
4376 let mut line_breaks = 0usize;
4377 let mut had_space = false;
4378 while input
4379 .as_bytes()
4380 .get(index)
4381 .is_some_and(|byte| matches!(*byte, b' ' | b'\t' | b'\n' | b'\r'))
4382 {
4383 had_space = true;
4384 match input.as_bytes()[index] {
4385 b'\n' => {
4386 line_breaks += 1;
4387 if line_breaks > 1 {
4388 return None;
4389 }
4390 index += 1;
4391 }
4392 b'\r' => {
4393 line_breaks += 1;
4394 if line_breaks > 1 {
4395 return None;
4396 }
4397 if input.as_bytes().get(index + 1) == Some(&b'\n') {
4398 index += 2;
4399 } else {
4400 index += 1;
4401 }
4402 }
4403 _ => index += 1,
4404 }
4405 }
4406 Some((index, had_space))
4407}
4408
4409pub(crate) fn is_escaped_at(input: &str, index: usize) -> bool {
4410 let bytes = input.as_bytes();
4411 let mut cursor = index;
4412 let mut count = 0;
4413 while cursor > 0 && bytes[cursor - 1] == b'\\' {
4414 count += 1;
4415 cursor -= 1;
4416 }
4417 count % 2 == 1
4418}
4419
4420fn parse_definition_destination_title(input: &str) -> Option<ParsedLinkResource> {
4421 let (mut cursor, _) = skip_link_resource_space_with_info(input, 0)?;
4422 let (destination, destination_kind, next) =
4423 parse_link_destination(&mut DirectLookups { input }, input, cursor)?;
4424 cursor = next;
4425
4426 let (next, had_space) = skip_link_resource_space_with_info(input, cursor)?;
4427 cursor = next;
4428 if cursor >= input.len() {
4429 return Some(ParsedLinkResource {
4430 destination,
4431 destination_kind,
4432 title: None,
4433 });
4434 }
4435 if !had_space {
4436 return None;
4437 }
4438
4439 let (title, next) = parse_link_title(input, cursor)?;
4440 let after_title = skip_link_resource_space(input, next)?;
4441 (after_title == input.len()).then_some(ParsedLinkResource {
4442 destination,
4443 destination_kind,
4444 title: Some(title),
4445 })
4446}
4447
4448fn open_definition_title(source: &str) -> Option<char> {
4451 let (cursor, _) = skip_link_resource_space_with_info(source, 0)?;
4452 let (_, _, next) =
4453 parse_link_destination(&mut DirectLookups { input: source }, source, cursor)?;
4454 let (cursor, had_space) = skip_link_resource_space_with_info(source, next)?;
4455 if !had_space {
4456 return None;
4457 }
4458 let closer = match source.as_bytes().get(cursor)? {
4459 b'"' => '"',
4460 b'\'' => '\'',
4461 b'(' => ')',
4462 _ => return None,
4463 };
4464 (!line_may_close_title(&source[cursor + 1..], closer)).then_some(closer)
4465}
4466
4467fn line_may_close_title(line: &str, closer: char) -> bool {
4471 line.char_indices().any(|(index, char)| {
4472 (char == closer || (closer == ')' && char == '(')) && !is_escaped_at(line, index)
4473 })
4474}
4475
4476fn line_can_start_definition_title(input: &str) -> bool {
4477 let trimmed = trim_ascii_start(input);
4478 matches!(trimmed.as_bytes().first(), Some(b'"' | b'\'' | b'('))
4479}
4480
4481fn push_line(output: &mut String, line: &str) {
4482 if !output.is_empty() {
4483 output.push('\n');
4484 }
4485 output.push_str(line);
4486}
4487
4488fn end_last_line(value: &mut String) {
4492 if value.is_empty() || ends_with_line_ending(value) {
4493 return;
4494 }
4495 value.push_str(value_line_ending(value));
4496}
4497
4498fn value_line_ending(value: &str) -> &'static str {
4501 match value.find(['\r', '\n']) {
4502 Some(index) if value[index..].starts_with("\r\n") => "\r\n",
4503 Some(index) if value[index..].starts_with('\r') => "\r",
4504 _ => "\n",
4505 }
4506}
4507
4508fn ends_with_line_ending(input: &str) -> bool {
4509 input.ends_with('\n') || input.ends_with('\r')
4510}
4511
4512fn flush_text(nodes: &mut Vec<Inline>, text: &mut String, text_start: usize, end: usize) {
4513 if !text.is_empty() {
4514 nodes.push(Inline::Text(Text {
4515 meta: NodeMeta::new(Some(Span::new(text_start, end))),
4516 value: core::mem::take(text),
4517 }));
4518 }
4519}
4520
4521fn next_char(input: &str, index: usize) -> Option<(usize, char)> {
4522 let char = input[index..].chars().next()?;
4523 Some((index + char.len_utf8(), char))
4524}
4525
4526fn is_flanking_punctuation(value: char) -> bool {
4531 value.is_ascii_punctuation() || crate::unicode_punctuation::is_unicode_punctuation(value)
4532}
4533
4534pub(crate) fn normalize_label(label: &str) -> String {
4544 label
4545 .replace('ẞ', "ss")
4551 .replace('\0', "\u{FFFD}")
4552 .split([' ', '\t', '\n', '\r'])
4553 .filter(|word| !word.is_empty())
4554 .collect::<Vec<_>>()
4555 .join(" ")
4556 .to_uppercase()
4557 .to_lowercase()
4558}
4559
4560#[derive(Clone, Copy)]
4563pub(crate) struct Labels<'a> {
4564 pub(crate) own: &'a [String],
4565}
4566
4567fn definition_exists(definitions: Option<Labels<'_>>, label: &str) -> bool {
4568 if label.is_empty() || !reference_label_is_within_limit(label) {
4569 return false;
4570 }
4571 let Some(definitions) = definitions else {
4572 return false;
4573 };
4574 definitions.own.binary_search(&identifier(label)).is_ok()
4575}
4576
4577fn identifier(label: &str) -> String {
4580 let identifier = normalize_label(label);
4581 if identifier.contains('\0') {
4582 identifier.replace('\0', "\u{FFFD}")
4583 } else {
4584 identifier
4585 }
4586}
4587
4588fn reference_label_is_within_limit(label: &str) -> bool {
4589 label.chars().take(REFERENCE_LABEL_MAX_CHARS + 1).count() <= REFERENCE_LABEL_MAX_CHARS
4590}
4591
4592pub(crate) fn is_reference_label(label: &str) -> bool {
4597 let written = alloc::format!("[{label}]");
4598 path_walk(1, |cursor| reference_label_step(&written, cursor)) == Some(written.len() - 1)
4599 && reference_label_is_within_limit(label)
4600 && !normalize_label(label).is_empty()
4601 && !holds_blank_line(label)
4602}
4603
4604pub(crate) fn is_written_footnote_label(label: &str) -> bool {
4607 is_footnote_label(label) && !is_escaped_at(&alloc::format!("{label}]"), label.len())
4608}
4609
4610fn holds_blank_line(text: &str) -> bool {
4613 let text = text.replace("\r\n", "\n");
4614 let lines: Vec<&str> = text.split(['\n', '\r']).collect();
4615 lines.len() > 2 && lines[1..lines.len() - 1].iter().any(|line| is_blank(line))
4616}
4617
4618fn trim_up_to_three_spaces(input: &str) -> Option<&str> {
4619 let (columns, bytes) = leading_indent(input);
4620 if columns <= 3 {
4621 Some(&input[bytes..])
4622 } else {
4623 None
4624 }
4625}
4626
4627fn fence_start(input: &str) -> Option<(FenceMarker, usize)> {
4630 let marker = match input.as_bytes().first()? {
4631 b'`' => FenceMarker::Backtick,
4632 b'~' => FenceMarker::Tilde,
4633 _ => return None,
4634 };
4635 let byte = match marker {
4636 FenceMarker::Backtick => b'`',
4637 FenceMarker::Tilde => b'~',
4638 };
4639 let length = input
4640 .as_bytes()
4641 .iter()
4642 .take_while(|item| **item == byte)
4643 .count();
4644 let opens = length >= 3 && (byte == b'~' || !input[length..].contains('`'));
4645 opens.then_some((marker, length))
4646}
4647
4648fn fence_close(input: &str, marker: FenceMarker, length: usize) -> bool {
4649 let byte = match marker {
4650 FenceMarker::Backtick => b'`',
4651 FenceMarker::Tilde => b'~',
4652 };
4653 let count = input
4654 .as_bytes()
4655 .iter()
4656 .take_while(|item| **item == byte)
4657 .count();
4658 count >= length && is_blank(&input[count..])
4659}
4660
4661fn trim_closing_hashes(input: &str) -> &str {
4662 let input = input.trim_end_matches([' ', '\t']);
4663 let hash_start = input.trim_end_matches('#').len();
4664 if hash_start == input.len() {
4665 return input;
4666 }
4667 if hash_start == 0 {
4668 return "";
4669 }
4670
4671 let before = &input[..hash_start];
4672 if before.ends_with(' ') || before.ends_with('\t') {
4673 before.trim_end_matches([' ', '\t'])
4674 } else {
4675 input
4676 }
4677}
4678
4679fn opens_list_item(input: &str) -> bool {
4681 trim_up_to_three_spaces(input).is_some_and(|rest| blocks::list_marker_head(rest).is_some())
4682}
4683
4684fn slice_offset_in(text: &str, slice: &str) -> Option<usize> {
4686 let offset = (slice.as_ptr() as usize).checked_sub(text.as_ptr() as usize)?;
4687 (offset + slice.len() <= text.len()).then_some(offset)
4688}
4689
4690fn leading_indent(input: &str) -> (usize, usize) {
4691 leading_indent_at(input, 0)
4692}
4693
4694fn leading_indent_at(input: &str, start: usize) -> (usize, usize) {
4697 let mut column = start;
4698 let mut bytes = 0usize;
4699 for byte in input.as_bytes() {
4700 match *byte {
4701 b' ' => column += 1,
4702 b'\t' => column += 4 - (column % 4),
4703 _ => break,
4704 }
4705 bytes += 1;
4706 }
4707 (column - start, bytes)
4708}
4709
4710fn task_marker_checked(input: &str) -> Option<bool> {
4711 if input.starts_with("[ ]") {
4712 Some(false)
4713 } else if input.starts_with("[x]") || input.starts_with("[X]") {
4714 Some(true)
4715 } else {
4716 None
4717 }
4718}
4719
4720pub(crate) fn is_blank(text: &str) -> bool {
4722 text.bytes().all(|byte| matches!(byte, b' ' | b'\t'))
4723}
4724
4725fn trim_ascii_start(input: &str) -> &str {
4726 input.trim_start_matches(|char| matches!(char, ' ' | '\t'))
4727}
4728
4729fn leading_trim_bytes(input: &str) -> usize {
4730 input.len() - trim_ascii_start(input).len()
4731}
4732
4733fn parse_table_delimiter(input: &str) -> Option<Vec<TableAlignment>> {
4734 if !input.contains('-')
4737 || !input
4738 .chars()
4739 .all(|char| matches!(char, '|' | '-' | ':' | ' ' | '\t'))
4740 {
4741 return None;
4742 }
4743 let cells = table_row_cell_ranges(input);
4744 if cells.is_empty() {
4745 return None;
4746 }
4747 let mut alignments = Vec::new();
4748 for (start, end) in cells {
4749 alignments.push(table_delimiter_alignment(
4750 input[start..end].trim_matches([' ', '\t']),
4751 )?);
4752 }
4753 Some(alignments)
4754}
4755
4756fn table_delimiter_alignment(cell: &str) -> Option<TableAlignment> {
4759 let bytes = cell.as_bytes();
4760 let mut cursor = 0;
4761 let left = bytes.first() == Some(&b':');
4762 if left {
4763 cursor += 1;
4764 }
4765 let dash_start = cursor;
4766 while bytes.get(cursor) == Some(&b'-') {
4767 cursor += 1;
4768 }
4769 if cursor == dash_start {
4770 return None;
4771 }
4772 let right = bytes.get(cursor) == Some(&b':');
4773 if right {
4774 cursor += 1;
4775 }
4776 if cursor != bytes.len() {
4777 return None;
4778 }
4779 Some(match (left, right) {
4780 (true, true) => TableAlignment::Center,
4781 (true, false) => TableAlignment::Left,
4782 (false, true) => TableAlignment::Right,
4783 (false, false) => TableAlignment::None,
4784 })
4785}
4786
4787fn table_row_delimiters(row: &str) -> Vec<usize> {
4791 let bytes = row.as_bytes();
4792 let mut delimiters = Vec::new();
4793 let mut cursor = 0;
4794 while cursor < bytes.len() {
4795 match bytes[cursor] {
4796 b'\\' => {
4797 let backslashes = delimiter_byte_run_len(row, cursor, b'\\');
4798 let pipe = cursor + backslashes;
4799 cursor = if bytes.get(pipe) == Some(&b'|') && backslashes % 2 == 1 {
4800 pipe + 1
4801 } else {
4802 pipe
4803 };
4804 }
4805 b'|' => {
4806 delimiters.push(cursor);
4807 cursor += 1;
4808 }
4809 _ => cursor += 1,
4810 }
4811 }
4812 delimiters
4813}
4814
4815fn table_row_cell_ranges(input: &str) -> Vec<(usize, usize)> {
4817 let trimmed = input.trim_matches([' ', '\t']);
4818 let offset = leading_trim_bytes(input);
4819 let delimiters = table_row_delimiters(trimmed);
4820 let mut cells = Vec::new();
4821 let mut start = 0;
4822 for &pipe in &delimiters {
4823 cells.push((offset + start, offset + pipe));
4824 start = pipe + 1;
4825 }
4826 cells.push((offset + start, offset + trimmed.len()));
4827
4828 if delimiters.first() == Some(&0) {
4831 cells.remove(0);
4832 }
4833 if delimiters
4834 .last()
4835 .is_some_and(|&pipe| is_blank(&trimmed[pipe + 1..]))
4836 {
4837 cells.pop();
4838 }
4839 cells
4840}
4841
4842struct TableCellSource {
4845 text: DerivedText,
4846 span: Span,
4847}
4848
4849fn table_row_cells(line: &Line<'_>, row: &str) -> Vec<TableCellSource> {
4852 let row_offset = slice_offset_in(line.text, row).unwrap_or(0);
4853 table_row_cell_ranges(row)
4854 .into_iter()
4855 .map(|(start, end)| {
4856 let raw = &row[start..end];
4857 let leading = leading_trim_bytes(raw);
4858 let content = raw.trim_matches([' ', '\t']);
4859 let offset = row_offset + start + leading;
4860 let span = Span::new(
4861 line.source_start(offset),
4862 line.source_end(offset + content.len()),
4863 );
4864 let mut text = DerivedText::default();
4865 let bytes = content.as_bytes();
4870 let mut copied = 0;
4871 let mut cursor = 0;
4872 while cursor < bytes.len() {
4873 if bytes[cursor] == b'\\' {
4874 let pipe = cursor + delimiter_byte_run_len(content, cursor, b'\\');
4875 if bytes.get(pipe) == Some(&b'|') && (pipe - cursor) % 2 == 1 {
4876 text.append(line, &content[copied..pipe - 1]);
4877 text.append_replacing(
4878 "|",
4879 line.source_start(offset + pipe - 1),
4880 line.source_end(offset + pipe + 1),
4881 );
4882 copied = pipe + 1;
4883 }
4884 cursor = pipe;
4885 } else {
4886 cursor += 1;
4887 }
4888 }
4889 text.append(line, &content[copied..]);
4890 TableCellSource { text, span }
4891 })
4892 .collect()
4893}
4894
4895fn table_has_separator(header: &str, delimiter: &str) -> bool {
4896 let Some(alignments) = parse_table_delimiter(delimiter) else {
4904 return true;
4905 };
4906 if alignments.len() == 1 {
4907 return contains_unescaped_pipe(header)
4908 || contains_unescaped_pipe(delimiter)
4909 || delimiter.contains(':');
4910 }
4911 true
4912}
4913
4914fn contains_unescaped_pipe(input: &str) -> bool {
4917 !table_row_delimiters(input).is_empty()
4918}
4919
4920fn autolink_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
4924 let end = lookups.find(">", index)? + 1;
4925 is_angle_autolink(&input[index + 1..end - 1]).then_some(end)
4926}
4927
4928fn html_inline_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
4931 let rest = &input[index..];
4932 if rest.starts_with("<!--") {
4933 return Some(lookups.find("-->", index)? + 3);
4934 }
4935 if rest.starts_with("<?") {
4937 return Some(lookups.find("?>", index + 2)? + 2);
4938 }
4939 if rest.starts_with("<![CDATA[") {
4940 return Some(lookups.find("]]>", index)? + 3);
4941 }
4942 if is_declaration_start(rest) {
4943 return Some(lookups.find(">", index)? + 1);
4944 }
4945 parse_html_tag_with(lookups, input, index).map(|(end, _)| end)
4946}
4947
4948fn parse_html_tag(input: &str, index: usize) -> Option<(usize, &str)> {
4949 parse_html_tag_with(&mut DirectLookups { input }, input, index)
4950}
4951
4952fn parse_html_tag_with<'a>(
4953 lookups: &mut impl Lookups,
4954 input: &'a str,
4955 index: usize,
4956) -> Option<(usize, &'a str)> {
4957 let bytes = input.as_bytes();
4958 if bytes.get(index) != Some(&b'<') {
4959 return None;
4960 }
4961
4962 let closing = bytes.get(index + 1) == Some(&b'/');
4963 let name_start = index + if closing { 2 } else { 1 };
4964 let first = *bytes.get(name_start)?;
4965 if !first.is_ascii_alphabetic() {
4966 return None;
4967 }
4968
4969 let mut cursor = name_start + 1;
4970 while bytes.get(cursor).is_some_and(|byte| html_name_byte(*byte)) {
4971 cursor += 1;
4972 }
4973 let name = &input[name_start..cursor];
4974
4975 if closing {
4976 cursor = skip_spaces(input, cursor);
4977 if bytes.get(cursor) == Some(&b'>') {
4978 return Some((cursor + 1, name));
4979 }
4980 return None;
4981 }
4982
4983 let mut needs_space = false;
4984 loop {
4985 let before_spaces = cursor;
4986 cursor = skip_spaces(input, cursor);
4987 let had_space = cursor > before_spaces;
4988 match bytes.get(cursor) {
4989 Some(b'>') => return Some((cursor + 1, name)),
4990 Some(b'/') if bytes.get(cursor + 1) == Some(&b'>') => return Some((cursor + 2, name)),
4991 Some(byte) if had_space && html_attribute_name_start(*byte) => {
4992 cursor += 1;
4993 while bytes
4994 .get(cursor)
4995 .is_some_and(|byte| html_attribute_name_byte(*byte))
4996 {
4997 cursor += 1;
4998 }
4999 let after_name = cursor;
5000 let after_spaces = skip_spaces(input, cursor);
5001 if bytes.get(after_spaces) == Some(&b'=') {
5002 cursor = skip_spaces(input, after_spaces + 1);
5003 cursor = parse_html_attribute_value(lookups, input, cursor)?;
5004 } else {
5005 cursor = after_name;
5006 }
5007 needs_space = true;
5008 }
5009 Some(_) if needs_space => return None,
5010 _ => return None,
5011 }
5012 }
5013}
5014
5015fn parse_html_attribute_value(
5016 lookups: &mut impl Lookups,
5017 input: &str,
5018 index: usize,
5019) -> Option<usize> {
5020 let bytes = input.as_bytes();
5021 match bytes.get(index)? {
5022 b'"' => Some(lookups.find("\"", index + 1)? + 1),
5023 b'\'' => Some(lookups.find("'", index + 1)? + 1),
5024 b'=' | b'<' | b'>' | b'`' => None,
5025 _ => {
5026 let mut cursor = index;
5027 while bytes.get(cursor).is_some_and(|byte| {
5028 !byte.is_ascii_whitespace()
5029 && !matches!(*byte, b'"' | b'\'' | b'=' | b'<' | b'>' | b'`')
5030 }) {
5031 cursor += 1;
5032 }
5033 if cursor == index {
5034 None
5035 } else {
5036 Some(cursor)
5037 }
5038 }
5039 }
5040}
5041
5042fn html_name_byte(byte: u8) -> bool {
5043 byte.is_ascii_alphanumeric() || byte == b'-'
5044}
5045
5046fn html_attribute_name_start(byte: u8) -> bool {
5047 byte.is_ascii_alphabetic() || byte == b'_' || byte == b':'
5048}
5049
5050fn html_attribute_name_byte(byte: u8) -> bool {
5051 byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b':' | b'.' | b'-')
5052}
5053
5054fn skip_spaces(input: &str, mut index: usize) -> usize {
5055 while input
5056 .as_bytes()
5057 .get(index)
5058 .is_some_and(|byte| matches!(*byte, b' ' | b'\t' | b'\n' | b'\r'))
5059 {
5060 index += 1;
5061 }
5062 index
5063}
5064
5065pub(crate) fn angle_autolink_destination(uri: &str) -> Option<String> {
5068 if is_uri_autolink(uri) {
5069 Some(String::from(uri))
5070 } else if is_email_autolink(uri) {
5071 Some(alloc::format!("mailto:{uri}"))
5072 } else {
5073 None
5074 }
5075}
5076
5077fn is_angle_autolink(uri: &str) -> bool {
5079 is_uri_autolink(uri) || is_email_autolink(uri)
5080}
5081
5082fn is_uri_autolink(input: &str) -> bool {
5083 let Some(colon) = input
5085 .as_bytes()
5086 .iter()
5087 .take(33)
5088 .position(|byte| *byte == b':')
5089 else {
5090 return false;
5091 };
5092 let scheme = &input[..colon];
5093 if scheme.len() < 2 || scheme.len() > 32 {
5094 return false;
5095 }
5096 let mut bytes = scheme.bytes();
5097 if !bytes.next().is_some_and(|byte| byte.is_ascii_alphabetic()) {
5098 return false;
5099 }
5100 if !bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'.' | b'-')) {
5101 return false;
5102 }
5103 input[colon + 1..]
5104 .chars()
5105 .map(source_char)
5106 .all(|char| !matches!(char, '<' | '>' | ' ') && !char.is_ascii_control())
5107}
5108
5109fn is_email_autolink(input: &str) -> bool {
5110 if input
5113 .chars()
5114 .any(|char| char.is_whitespace() || char == '<')
5115 {
5116 return false;
5117 }
5118 let Some(at) = input.find('@') else {
5119 return false;
5120 };
5121 if at == 0 || at + 1 >= input.len() {
5122 return false;
5123 }
5124 is_email_local_part(&input[..at]) && is_email_domain(&input[at + 1..], 1)
5127}
5128
5129pub(crate) fn literal_autolink_destination(text: &str) -> Option<String> {
5140 let mut input = String::from(text);
5141 input.push_str(".. ");
5142 let (end, prefix) =
5143 parse_literal_autolink(&input, 0, &mut LiteralAutolinkScan::default(), true)?;
5144 (end == text.len()).then(|| {
5145 let mut destination = String::from(prefix);
5146 destination.push_str(text);
5147 destination
5148 })
5149}
5150
5151fn parse_literal_autolink(
5164 input: &str,
5165 index: usize,
5166 scan: &mut LiteralAutolinkScan,
5167 urls: bool,
5168) -> Option<(usize, &'static str)> {
5169 let rest = &input[index..];
5170 let www = rest
5171 .as_bytes()
5172 .get(..4)
5173 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"www."));
5174 if !urls {
5175 if www {
5178 return None;
5179 }
5180 return parse_literal_email(input, index, scan);
5181 }
5182
5183 if let Some(scheme_len) = rest
5186 .starts_with("http://")
5187 .then_some(7)
5188 .or_else(|| rest.starts_with("https://").then_some(8))
5189 {
5190 if !literal_scheme_prefix_ok(input, index) {
5191 return None;
5192 }
5193 if !http_literal_host_ok(&input[index + scheme_len..]) {
5196 return None;
5197 }
5198 let end = autolink_url_end(input, index + scheme_len);
5201 if end <= index + scheme_len {
5202 return None;
5203 }
5204 return Some((end, ""));
5205 }
5206
5207 if www {
5210 if !literal_www_prefix_ok(input, index) {
5211 return None;
5212 }
5213 if !check_domain(rest, false) {
5214 return None;
5215 }
5216 let end = autolink_url_end(input, index);
5217 if end <= index || (end <= index + 3 && !literal_starts_line(input, index)) {
5218 return None;
5219 }
5220 return Some((end, "http://"));
5221 }
5222
5223 parse_literal_email(input, index, scan)
5224}
5225
5226fn literal_scheme_prefix_ok(input: &str, index: usize) -> bool {
5230 if index == 0 {
5231 return true;
5232 }
5233 let Some(previous) = input[..index].chars().next_back() else {
5234 return true;
5235 };
5236 !previous.is_ascii_alphabetic()
5237}
5238
5239fn literal_www_prefix_ok(input: &str, index: usize) -> bool {
5243 if index == 0 {
5244 return true;
5245 }
5246 let Some(previous) = input[..index].chars().next_back() else {
5247 return true;
5248 };
5249 if matches!(previous, '*' | '_' | '~' | '(' | '[' | ']') {
5250 return true;
5251 }
5252 matches!(previous, ' ' | '\t' | '\n' | '\r')
5253}
5254
5255fn literal_starts_line(input: &str, index: usize) -> bool {
5256 index == 0
5257 || input
5258 .as_bytes()
5259 .get(index - 1)
5260 .is_some_and(|byte| matches!(byte, b'\n' | b'\r'))
5261}
5262
5263fn http_literal_host_ok(host: &str) -> bool {
5264 match host.chars().next() {
5265 Some(char) if char.is_ascii() && char.is_ascii_alphanumeric() => check_domain(host, true),
5266 Some(char) if !char.is_ascii() && is_valid_hostchar(source_char(char)) => {
5267 check_domain(host, true)
5268 }
5269 _ => false,
5270 }
5271}
5272
5273fn is_valid_hostchar(char: char) -> bool {
5276 !char.is_whitespace() && !crate::unicode_punctuation::is_unicode_punctuation(char)
5277}
5278
5279fn check_domain(data: &str, allow_short: bool) -> bool {
5292 let mut np = 0usize;
5293 let mut uscore1 = 0usize;
5294 let mut uscore2 = 0usize;
5295
5296 let mut chars = data.char_indices().peekable();
5297 while let Some((offset, char)) = chars.next() {
5298 let account = offset != 0 && chars.peek().is_some();
5303 match char {
5304 '\\' => {
5305 chars.next();
5307 }
5308 '_' if account => uscore2 += 1,
5309 '.' if account => {
5310 uscore1 = uscore2;
5311 uscore2 = 0;
5312 np += 1;
5313 if np > 10 {
5314 return true;
5315 }
5316 }
5317 '_' | '.' | '-' => {}
5318 _ => {
5319 if !is_valid_hostchar(source_char(char)) {
5320 break;
5321 }
5322 }
5323 }
5324 }
5325
5326 if (uscore1 > 0 || uscore2 > 0) && np <= 10 {
5327 return false;
5328 }
5329
5330 allow_short || np > 0
5331}
5332
5333fn autolink_url_end(input: &str, start: usize) -> usize {
5340 let bytes = input.as_bytes();
5341 let mut end = start;
5342 let mut strict_has_open_bracket = false;
5345 let mut strict_inside_backticks = false;
5346 for (offset, char) in input[start..].char_indices() {
5347 if char.is_whitespace()
5348 || char == '<'
5349 || is_autolink_terminating_control(char)
5350 || (!char.is_ascii()
5351 && char != '\u{FFFD}'
5352 && crate::unicode_punctuation::is_unicode_punctuation(char))
5353 || input[start + offset..].starts_with("[[")
5354 {
5355 break;
5356 }
5357 match char {
5358 '[' => strict_has_open_bracket = true,
5359 '`' => strict_inside_backticks = !strict_inside_backticks,
5360 ']' if !strict_has_open_bracket && !strict_inside_backticks => break,
5361 _ => {}
5362 }
5363 if char == '\\' {
5370 if let Some(&next) = bytes.get(start + offset + 1) {
5371 if next.is_ascii_punctuation() && next != b'.' {
5372 break;
5373 }
5374 }
5375 }
5376 end = start + offset + char.len_utf8();
5377 }
5378 autolink_delim(input, start, end)
5379}
5380
5381fn is_autolink_terminating_control(char: char) -> bool {
5382 matches!(char, '\u{2066}'..='\u{2069}')
5383}
5384
5385fn autolink_delim(input: &str, start: usize, mut end: usize) -> usize {
5390 let bytes = input.as_bytes();
5391 let mut opening = 0usize;
5392 let mut closing = 0usize;
5393 for &byte in &bytes[start..end] {
5394 match byte {
5395 b'(' => opening += 1,
5396 b')' => closing += 1,
5397 _ => {}
5398 }
5399 }
5400
5401 while end > start {
5402 match bytes[end - 1] {
5403 b')' => {
5404 if closing <= opening {
5405 break;
5406 }
5407 closing -= 1;
5408 end -= 1;
5409 }
5410 b'?' | b'!' | b'.' | b',' | b':' | b'*' | b'_' | b'~' | b'\'' | b'"' => {
5411 end -= 1;
5412 }
5413 b';' => {
5414 let mut new_end = end - 1;
5417 while new_end > start && bytes[new_end - 1].is_ascii_alphanumeric() {
5418 new_end -= 1;
5419 }
5420 if new_end > start && new_end < end - 1 && bytes[new_end - 1] == b'&' {
5421 end = new_end - 1;
5422 } else {
5423 end -= 1;
5424 }
5425 }
5426 _ => break,
5427 }
5428 }
5429 end
5430}
5431
5432#[derive(Default)]
5436struct LiteralAutolinkScan {
5437 email_local: ByteRun,
5439 email_at: EmailAt,
5441 email_prefix: EmailPrefix,
5443}
5444
5445#[derive(Default)]
5449struct EmailAt {
5450 at: Option<usize>,
5451 from: usize,
5452 last_colon: Option<usize>,
5453 domain_end: [Option<Option<usize>>; 2],
5454}
5455
5456impl EmailAt {
5457 fn update(&mut self, input: &str, index: usize, at: usize) {
5458 if self.at != Some(at) {
5459 *self = Self {
5460 at: Some(at),
5461 from: usize::MAX,
5462 ..Self::default()
5463 };
5464 }
5465 if index < self.from {
5466 self.last_colon = input[index..at].rfind(':').map(|offset| index + offset);
5467 self.from = index;
5468 }
5469 }
5470
5471 fn domain_end(&mut self, input: &str, is_xmpp: bool) -> Option<usize> {
5473 let at = self.at.expect("updated before use");
5474 *self.domain_end[usize::from(is_xmpp)].get_or_insert_with(|| {
5475 let start = at + 1;
5476 let end = literal_email_domain_end(input, start, is_xmpp)?;
5477 let trimmed = autolink_delim(input, start, end);
5478 (trimmed > start && is_gfm_email_domain(&input[start..trimmed], is_xmpp))
5479 .then_some(trimmed)
5480 })
5481 }
5482}
5483
5484#[derive(Default)]
5490struct EmailPrefix {
5491 cursor: usize,
5492 segment: usize,
5493 non_local: Option<usize>,
5494 at: Option<usize>,
5495 domain_bad: Option<usize>,
5496 domain_dot: Option<usize>,
5497 domain_empty_label: Option<usize>,
5498}
5499
5500impl EmailPrefix {
5501 fn advance(&mut self, input: &str, end: usize) {
5502 if end < self.cursor {
5503 *self = Self::default();
5504 }
5505 let bytes = input.as_bytes();
5506 let from = self.cursor;
5507 for (offset, char) in input[from..end].char_indices() {
5508 let position = from + offset;
5509 if char.is_whitespace() {
5510 *self = Self {
5511 segment: position + char.len_utf8(),
5512 ..Self::default()
5513 };
5514 continue;
5515 }
5516 let byte = char.is_ascii().then_some(char as u8);
5517 if self.non_local.is_none() && !byte.is_some_and(is_gfm_email_local_byte) {
5518 self.non_local = Some(position);
5519 }
5520 if char == '@' {
5521 self.at = Some(position);
5522 self.domain_bad = None;
5523 self.domain_dot = None;
5524 self.domain_empty_label = None;
5525 continue;
5526 }
5527 let Some(at) = self.at else {
5528 continue;
5529 };
5530 match byte {
5531 Some(b'.') => {
5532 self.domain_dot.get_or_insert(position);
5533 if position == at + 1 || bytes[position - 1] == b'.' {
5534 self.domain_empty_label.get_or_insert(position);
5535 }
5536 }
5537 Some(byte) if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_') => {}
5538 _ => {
5539 self.domain_bad.get_or_insert(position);
5540 }
5541 }
5542 }
5543 self.cursor = end;
5544 }
5545
5546 fn ends_with_email(&mut self, input: &str, end: usize) -> bool {
5549 self.advance(input, end);
5550 let Some(at) = self.at else {
5551 return false;
5552 };
5553 let before = |position: Option<usize>| position.is_some_and(|position| position < end);
5554 at > self.segment
5555 && !self.non_local.is_some_and(|position| position < at)
5556 && end > at + 1
5557 && !before(self.domain_bad)
5558 && before(self.domain_dot)
5559 && !before(self.domain_empty_label)
5560 && !matches!(input.as_bytes()[end - 1], b'.' | b'-' | b'_')
5561 }
5562}
5563
5564#[derive(Default)]
5568struct ByteRun {
5569 start: usize,
5570 end: Option<usize>,
5571}
5572
5573impl ByteRun {
5574 fn end(&mut self, input: &str, index: usize, in_run: fn(u8) -> bool) -> usize {
5577 if let Some(end) = self.end {
5578 if self.start <= index && index <= end {
5579 return end;
5580 }
5581 }
5582 let end = input.as_bytes()[index..]
5583 .iter()
5584 .position(|byte| !in_run(*byte))
5585 .map_or(input.len(), |offset| index + offset);
5586 self.start = index;
5587 self.end = Some(end);
5588 end
5589 }
5590}
5591
5592fn is_email_local_or_scheme_byte(byte: u8) -> bool {
5593 is_gfm_email_local_byte(byte) || byte == b':'
5594}
5595
5596fn parse_literal_email(
5602 input: &str,
5603 index: usize,
5604 scan: &mut LiteralAutolinkScan,
5605) -> Option<(usize, &'static str)> {
5606 let at_index = scan
5609 .email_local
5610 .end(input, index, is_email_local_or_scheme_byte);
5611 if input.as_bytes().get(at_index) != Some(&b'@') || at_index == index {
5612 return None;
5613 }
5614
5615 let (auto_mailto, is_xmpp) = classify_email_local(&input[index..at_index]);
5619
5620 if !email_left_boundary_ok(input, index, auto_mailto, |end| {
5625 scan.email_prefix.ends_with_email(input, end)
5626 }) {
5627 return None;
5628 }
5629
5630 let body_start = if auto_mailto {
5635 index
5636 } else if is_xmpp {
5637 index + "xmpp:".len()
5638 } else {
5639 index + "mailto:".len()
5640 };
5641 scan.email_at.update(input, index, at_index);
5642 if body_start >= at_index
5643 || scan
5644 .email_at
5645 .last_colon
5646 .is_some_and(|colon| colon >= body_start)
5647 {
5648 return None;
5649 }
5650 let end = scan.email_at.domain_end(input, is_xmpp)?;
5651 Some((end, if auto_mailto { "mailto:" } else { "" }))
5652}
5653
5654fn classify_email_local(local: &str) -> (bool, bool) {
5659 if let Some(rest) = strip_ci_prefix(local, "mailto:") {
5660 if !rest.is_empty() {
5661 return (false, false);
5662 }
5663 }
5664 if let Some(rest) = strip_ci_prefix(local, "xmpp:") {
5665 if !rest.is_empty() {
5666 return (false, true);
5667 }
5668 }
5669 (true, false)
5670}
5671
5672fn strip_ci_prefix<'a>(input: &'a str, prefix: &str) -> Option<&'a str> {
5673 let bytes = input.as_bytes();
5674 let plen = prefix.len();
5675 if bytes.len() >= plen && bytes[..plen].eq_ignore_ascii_case(prefix.as_bytes()) {
5676 Some(&input[plen..])
5677 } else {
5678 None
5679 }
5680}
5681
5682fn email_left_boundary_ok(
5689 input: &str,
5690 index: usize,
5691 auto_mailto: bool,
5692 prefix_ends_with_email: impl FnOnce(usize) -> bool,
5693) -> bool {
5694 if index == 0 {
5695 return true;
5696 }
5697 let Some(previous) = input[..index].chars().next_back() else {
5698 return true;
5699 };
5700 if previous.is_ascii_alphanumeric() {
5701 if auto_mailto && input[index..].starts_with('+') && prefix_ends_with_email(index) {
5702 return true;
5703 }
5704 return false;
5705 }
5706 if auto_mailto && previous == '/' {
5707 return false;
5708 }
5709 true
5710}
5711
5712#[cfg(test)]
5713fn prefix_ends_with_gfm_email(input: &str, end: usize) -> bool {
5714 let start = input[..end]
5715 .char_indices()
5716 .rev()
5717 .find(|(_, char)| char.is_whitespace())
5718 .map_or(0, |(offset, char)| offset + char.len_utf8());
5719 let candidate = &input[start..end];
5720 let Some(at) = candidate.rfind('@') else {
5721 return false;
5722 };
5723 email_local_is_valid(&candidate[..at], true) && is_gfm_email_domain(&candidate[at + 1..], false)
5724}
5725
5726#[cfg(test)]
5730fn email_local_is_valid(local: &str, auto_mailto: bool) -> bool {
5731 let body = if auto_mailto {
5732 local
5733 } else if let Some(rest) = strip_ci_prefix(local, "mailto:") {
5734 rest
5735 } else if let Some(rest) = strip_ci_prefix(local, "xmpp:") {
5736 rest
5737 } else {
5738 local
5739 };
5740 !body.is_empty() && body.bytes().all(is_gfm_email_local_byte)
5741}
5742
5743fn is_gfm_email_local_byte(byte: u8) -> bool {
5746 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'+' | b'_' | b'-')
5747}
5748
5749fn is_email_local_part(input: &str) -> bool {
5750 !input.is_empty()
5751 && input
5752 .split('.')
5753 .all(|segment| !segment.is_empty() && segment.bytes().all(is_email_atext))
5754}
5755
5756fn is_email_atext(byte: u8) -> bool {
5757 byte.is_ascii_alphanumeric()
5758 || matches!(
5759 byte,
5760 b'!' | b'#'
5761 | b'$'
5762 | b'%'
5763 | b'&'
5764 | b'\''
5765 | b'*'
5766 | b'+'
5767 | b'/'
5768 | b'='
5769 | b'?'
5770 | b'^'
5771 | b'_'
5772 | b'`'
5773 | b'{'
5774 | b'|'
5775 | b'}'
5776 | b'~'
5777 | b'-'
5778 )
5779}
5780
5781fn literal_email_domain_end(input: &str, index: usize, is_xmpp: bool) -> Option<usize> {
5789 let bytes = input.as_bytes();
5790 let mut end = index;
5791 let mut np = 0usize;
5792 while end < bytes.len() {
5793 let byte = bytes[end];
5794 if byte.is_ascii_alphanumeric() {
5795 end += 1;
5796 } else if byte == b'.' && end + 1 < bytes.len() && bytes[end + 1].is_ascii_alphanumeric() {
5797 np += 1;
5798 end += 1;
5799 } else if byte == b'-' || byte == b'_' || (byte == b'/' && is_xmpp) {
5800 end += 1;
5803 } else {
5804 break;
5805 }
5806 }
5807 if end <= index {
5808 return None;
5809 }
5810 let len = end - index;
5811 let last = bytes[end - 1];
5812 if len < 1 || np == 0 || !(last.is_ascii_alphabetic() || last == b'.') {
5813 return None;
5814 }
5815 Some(end)
5816}
5817
5818fn is_gfm_email_domain(input: &str, is_xmpp: bool) -> bool {
5823 if input.is_empty() {
5824 return false;
5825 }
5826 let host = if is_xmpp {
5829 input.split('/').next().unwrap_or(input)
5830 } else {
5831 input
5832 };
5833 if !host.contains('.') {
5834 return false;
5835 }
5836 let last = host.as_bytes()[host.len() - 1];
5837 if matches!(last, b'-' | b'_') {
5840 return false;
5841 }
5842 host.split('.').all(|label| {
5843 !label.is_empty()
5844 && label
5845 .bytes()
5846 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
5847 })
5848}
5849
5850fn is_email_domain(input: &str, min_labels: usize) -> bool {
5851 let mut label_count = 0usize;
5852 for label in input.split('.') {
5853 label_count += 1;
5854 let bytes = label.as_bytes();
5855 if bytes.is_empty()
5856 || bytes.len() > 63
5857 || !bytes
5858 .first()
5859 .is_some_and(|byte| byte.is_ascii_alphanumeric())
5860 || !bytes
5861 .last()
5862 .is_some_and(|byte| byte.is_ascii_alphanumeric())
5863 || !bytes
5864 .iter()
5865 .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-')
5866 {
5867 return false;
5868 }
5869 }
5870 label_count >= min_labels
5871}
5872
5873pub(crate) fn is_footnote_label(label: &str) -> bool {
5876 !label.is_empty()
5877 && reference_label_is_within_limit(label)
5878 && !label.contains([' ', '\t', '\n', '\r'])
5879 && !label
5880 .match_indices(['[', ']'])
5881 .any(|(index, _)| !is_escaped_at(label, index))
5882}
5883
5884fn find_footnote_definition_label_end(input: &str) -> Option<usize> {
5885 let close = find_footnote_reference_label_end(input, 2)?;
5886 if input.as_bytes().get(close + 1) == Some(&b':') {
5887 Some(close)
5888 } else {
5889 None
5890 }
5891}
5892
5893fn find_footnote_reference_label_end(input: &str, mut cursor: usize) -> Option<usize> {
5894 while cursor < input.len() {
5895 let (next, char) = next_char(input, cursor)?;
5896 if char == ']' && !is_escaped_at(input, cursor) {
5897 return Some(cursor);
5898 }
5899 cursor = next;
5900 }
5901 None
5902}