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