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 blocks;
23mod nul_replacement;
24#[cfg(test)]
25mod scan_tests;
26mod source_map;
27
28pub(crate) use blocks::interrupts_paragraph;
29use source_map::{DerivedText, Segment, SourceMap};
30
31pub(crate) const MAX_ORDERED_NUMBER: u64 = 999_999_999;
33
34#[derive(Clone, Debug, Eq, PartialEq)]
37pub struct ParseOutput {
38 pub document: Document,
40 pub diagnostics: Vec<Diagnostic>,
42}
43
44#[derive(Clone, Debug, Eq, PartialEq)]
45struct ParsedLinkResource {
46 destination: String,
47 destination_kind: LinkDestinationKind,
48 title: Option<Title>,
49}
50
51const REFERENCE_LABEL_MAX_CHARS: usize = 999;
52const WIKILINK_MAX_BYTES: usize = 999;
53
54#[derive(Clone, Copy, Debug)]
59struct Line<'a> {
60 text: &'a str,
61 eol: &'a str,
62 start: usize,
63 end: usize,
64 end_with_eol: usize,
65 segments: &'a [Segment],
68 text_offset: usize,
69 column: usize,
72}
73
74impl<'a> Line<'a> {
75 fn detached(text: &'a str) -> Self {
77 Line {
78 text,
79 eol: "",
80 start: 0,
81 end: text.len(),
82 end_with_eol: text.len(),
83 segments: &[],
84 text_offset: 0,
85 column: 0,
86 }
87 }
88
89 fn column_at(&self, offset: usize) -> usize {
91 advance_columns(self.column, &self.text[..offset])
92 }
93
94 fn source_start(&self, offset: usize) -> usize {
97 source_map::start_of(self.segments, self.text_offset + offset)
98 }
99
100 fn source_end(&self, offset: usize) -> usize {
102 source_map::end_of(self.segments, self.text_offset + offset)
103 }
104
105 fn eol_source(&self) -> (usize, usize) {
107 (self.end, self.end_with_eol)
108 }
109
110 fn copy_into(&self, map: &mut SourceMap, at: usize, from: usize, to: usize) {
113 map.copy(
114 at,
115 self.segments,
116 self.text_offset + from,
117 self.text_offset + to,
118 );
119 }
120}
121
122#[derive(Clone, Copy, Debug, Eq, PartialEq)]
123enum HtmlBlockKind {
124 RawTag,
125 BlockTag,
126 Until(&'static str),
127 UntilBlank,
128}
129
130pub fn parse(input: &str) -> ParseOutput {
133 let mut diagnostics = Vec::new();
134 let start = if input.starts_with('\u{feff}') {
137 '\u{feff}'.len_utf8()
138 } else {
139 0
140 };
141 let source = &input[start..];
142 let map = SourceMap::verbatim(source.len(), start);
143 let lines = collect_lines(source, &map);
144 let children = blocks::parse_document(&lines, &mut diagnostics);
145 let mut document = Document {
146 meta: NodeMeta::new(Some(Span::new(0, input.len()))),
147 children,
148 };
149 if source.contains('\0') {
150 nul_replacement::replace_in_document(&mut document);
151 }
152
153 ParseOutput {
154 document,
155 diagnostics,
156 }
157}
158
159fn source_char(char: char) -> char {
163 if char == '\0' {
164 '\u{FFFD}'
165 } else {
166 char
167 }
168}
169
170pub(crate) const MAX_BLOCK_NESTING: usize = 32;
174
175fn advance_columns(column: usize, text: &str) -> usize {
178 text.chars().fold(column, |column, char| {
179 if char == '\t' {
180 column + 4 - column % 4
181 } else {
182 column + 1
183 }
184 })
185}
186
187fn collect_lines<'a>(input: &'a str, map: &'a SourceMap) -> Vec<Line<'a>> {
190 let bytes = input.as_bytes();
191 let mut lines = Vec::new();
192 let mut segments = LineSegments {
193 segments: map.segments(),
194 first: 0,
195 };
196 let mut start = 0;
197 let mut index = 0;
198
199 while index < bytes.len() {
200 let eol_end = match bytes[index] {
201 b'\n' => index + 1,
202 b'\r' if bytes.get(index + 1) == Some(&b'\n') => index + 2,
203 b'\r' => index + 1,
204 _ => {
205 index += 1;
206 continue;
207 }
208 };
209 lines.push(segments.line(input, start, index, eol_end));
210 index = eol_end;
211 start = index;
212 }
213
214 if start < bytes.len() || input.is_empty() {
215 lines.push(segments.line(input, start, bytes.len(), bytes.len()));
216 }
217
218 lines
219}
220
221struct LineSegments<'a> {
224 segments: &'a [Segment],
225 first: usize,
226}
227
228impl<'a> LineSegments<'a> {
229 fn line(&mut self, input: &'a str, start: usize, end: usize, eol_end: usize) -> Line<'a> {
230 let segments = self.segments;
231 while self.first + 1 < segments.len() && segments[self.first].text_end() <= start {
232 self.first += 1;
233 }
234 let mut last = self.first;
235 while last + 1 < segments.len() && segments[last + 1].text_start() < eol_end {
236 last += 1;
237 }
238 let covering = if segments.is_empty() {
239 segments
240 } else {
241 &segments[self.first..=last]
242 };
243 let source_start = source_map::start_of(covering, start);
244 let source_end = if end == start {
245 source_start
246 } else {
247 source_map::end_of(covering, end)
248 };
249 let source_end_with_eol = if eol_end == end {
250 source_end
251 } else {
252 source_map::end_of(covering, eol_end)
253 };
254 Line {
255 text: &input[start..end],
256 eol: &input[end..eol_end],
257 start: source_start,
258 end: source_end,
259 end_with_eol: source_end_with_eol,
260 segments: covering,
261 text_offset: start,
262 column: 0,
263 }
264 }
265}
266
267pub(crate) fn frontmatter_fence_kind(line: &str) -> Option<FrontmatterKind> {
268 match line.trim_end_matches([' ', '\t']) {
269 "---" => Some(FrontmatterKind::Yaml),
270 "+++" => Some(FrontmatterKind::Toml),
271 _ => None,
272 }
273}
274
275fn directive_container_opener_prefix(input: &str) -> Option<(usize, &str)> {
276 let fence_len = input
277 .as_bytes()
278 .iter()
279 .take_while(|byte| **byte == b':')
280 .count();
281 if fence_len >= 3 {
282 Some((fence_len, &input[fence_len..]))
283 } else {
284 None
285 }
286}
287
288fn directive_container_closing_fence(input: &str, min_len: usize) -> Option<usize> {
289 let fence_len = input
290 .as_bytes()
291 .iter()
292 .take_while(|byte| **byte == b':')
293 .count();
294 if fence_len >= min_len && is_blank(&input[fence_len..]) {
295 Some(fence_len)
296 } else {
297 None
298 }
299}
300
301fn math_block_fence_length(input: &str) -> Option<usize> {
304 let length = input
305 .as_bytes()
306 .iter()
307 .take_while(|byte| **byte == b'$')
308 .count();
309 if length < 2 || input[length..].contains('$') {
310 return None;
311 }
312 Some(length)
313}
314
315fn math_block_fence_closes(input: &str, length: usize) -> bool {
318 let count = input
319 .as_bytes()
320 .iter()
321 .take_while(|byte| **byte == b'$')
322 .count();
323 count >= length && is_blank(&input[count..])
324}
325
326fn parse_alert_marker(line: &str) -> Option<(AlertKind, Option<String>)> {
327 let close = line.find(']')?;
328 let marker = line.get(0..close + 1)?;
329 if !marker.starts_with("[!") {
330 return None;
331 }
332 let kind = match &marker[2..close].to_ascii_lowercase()[..] {
333 "note" => AlertKind::Note,
334 "tip" => AlertKind::Tip,
335 "important" => AlertKind::Important,
336 "warning" => AlertKind::Warning,
337 "caution" => AlertKind::Caution,
338 _ => return None,
339 };
340 Some((kind, alert_title(&line[close + 1..])))
341}
342
343pub(crate) fn alert_title(text: &str) -> Option<String> {
346 let title = text.trim_matches([' ', '\t']);
347 (!title.is_empty()).then(|| title.into())
348}
349
350fn parse_thematic_break(line: Line<'_>) -> Option<Block> {
351 let text = trim_up_to_three_spaces(line.text)?;
352 let mut marker = None;
353 let mut count = 0;
354 for char in text.chars() {
355 if char == ' ' || char == '\t' {
356 continue;
357 }
358 let current = match char {
359 '-' => ThematicBreakMarker::Dash,
360 '*' => ThematicBreakMarker::Asterisk,
361 '_' => ThematicBreakMarker::Underscore,
362 _ => return None,
363 };
364 if marker.is_some_and(|marker| marker != current) {
365 return None;
366 }
367 marker = Some(current);
368 count += 1;
369 }
370 if count >= 3 {
371 Some(Block::ThematicBreak(ThematicBreak {
372 meta: NodeMeta::new(Some(Span::new(line.start, line.end))),
373 marker: marker?,
374 }))
375 } else {
376 None
377 }
378}
379
380fn definition_label(lines: &[Line<'_>], index: usize) -> Option<(String, usize, usize)> {
388 let text = trim_ascii_start(lines[index].text);
389 if !text.starts_with('[') {
390 return None;
391 }
392 let mut accumulated = String::from(text);
393 let mut label_end_line = index;
394 let mut cursor = 1;
395 loop {
396 let bytes = accumulated.as_bytes();
397 if cursor >= bytes.len() {
401 let next = label_end_line + 1;
402 if next >= lines.len() || accumulated.len() > 4 * REFERENCE_LABEL_MAX_CHARS + 2 {
403 return None;
404 }
405 accumulated.push('\n');
406 accumulated.push_str(lines[next].text);
407 label_end_line = next;
408 continue;
409 }
410 match reference_label_step(&accumulated, cursor) {
411 Step::Next(next) => cursor = next,
412 Step::Done(Some(close)) if reference_label_is_within_limit(&accumulated[1..close]) => {
413 if bytes.get(close + 1) != Some(&b':') {
415 return None;
416 }
417 return Some((accumulated, close, label_end_line));
418 }
419 Step::Done(_) => return None,
422 }
423 }
424}
425
426fn parse_definition(lines: &[Line<'_>], index: usize) -> Option<(Definition, usize)> {
431 let (accumulated, close, label_end_line) = definition_label(lines, index)?;
432 let label = String::from(&accumulated[1..close]);
433 if normalize_label(&label).is_empty() {
434 return None;
435 }
436 let mut source = String::from(&accumulated[close + 2..]);
437 let mut cursor = label_end_line;
438 let mut best_without_title = None;
439 let mut open_title: Option<char> = None;
443
444 loop {
445 let parses =
446 open_title.is_none_or(|closer| line_may_close_title(lines[cursor].text, closer));
447 if parses {
448 match parse_definition_destination_title(&source) {
449 Some(resource) => {
450 if resource.title.is_some() {
451 return Some((definition_node(&label, resource), cursor + 1));
452 }
453 best_without_title = Some((resource, cursor + 1));
454 let next = cursor + 1;
455 if next >= lines.len() || !line_can_start_definition_title(lines[next].text) {
456 break;
457 }
458 }
459 None if !is_blank(&source) => match open_definition_title(&source) {
462 Some(closer) if open_title.is_none() => open_title = Some(closer),
463 _ => break,
464 },
465 None => {}
466 }
467 }
468 let next = cursor + 1;
469 if next >= lines.len() {
470 break;
471 }
472 source.push('\n');
473 source.push_str(lines[next].text);
474 cursor = next;
475 }
476
477 let (resource, next) = best_without_title?;
478 Some((definition_node(&label, resource), next))
479}
480
481fn definition_node(label: &str, resource: ParsedLinkResource) -> Definition {
482 Definition {
483 meta: NodeMeta::default(),
484 label: label.into(),
485 identifier: normalize_label(label),
486 destination: resource.destination,
487 destination_kind: resource.destination_kind,
488 title: resource.title,
489 }
490}
491
492#[derive(Clone, Copy, Eq, PartialEq)]
493enum HtmlContainerTag {
494 Opening,
495 Closing,
496}
497
498#[derive(Clone, Copy)]
499struct HtmlContainerFence {
500 marker: FenceMarker,
501 length: usize,
502}
503
504fn html_container_close_step(lines: &[Line<'_>], cursor: usize, tag: &str) -> BracketStep {
508 let Some(line) = lines.get(cursor) else {
509 return BracketStep::End;
510 };
511 if let Some(fence) = html_container_fence_opens(line) {
512 let after_fence = (cursor + 1..lines.len())
513 .find(|close| html_container_fence_closes(&lines[*close], fence))
514 .map_or(lines.len(), |close| close + 1);
515 return BracketStep::Pass(after_fence);
516 }
517 if parse_html_container_tag_line(*line, tag, HtmlContainerTag::Closing).is_some() {
518 return BracketStep::Close(cursor + 1);
519 }
520 if parse_html_container_opening_line(*line, tag).is_some() {
521 return BracketStep::Open(cursor + 1);
522 }
523 BracketStep::Pass(cursor + 1)
524}
525
526fn parse_html_container_tag_line(
527 line: Line<'_>,
528 tag: &str,
529 kind: HtmlContainerTag,
530) -> Option<HtmlTag> {
531 let (trimmed, indent_bytes) = trim_html_container_line(&line)?;
532 let (end, name) = parse_html_tag(trimmed, 0)?;
533 if !name.eq_ignore_ascii_case(tag) || !is_blank(&trimmed[end..]) {
534 return None;
535 }
536
537 let closing = html_tag_is_closing(trimmed, 0);
538 if (kind == HtmlContainerTag::Opening && closing)
539 || (kind == HtmlContainerTag::Closing && !closing)
540 || html_tag_is_self_closing(&trimmed[..end])
541 {
542 return None;
543 }
544
545 Some(HtmlTag {
546 meta: NodeMeta::new(Some(Span::new(
547 line.source_start(indent_bytes),
548 line.source_end(indent_bytes + end),
549 ))),
550 name: tag.into(),
551 raw: trimmed[..end].into(),
552 })
553}
554
555fn parse_html_container_opening_line(line: Line<'_>, tag: &str) -> Option<HtmlTag> {
556 let (trimmed, indent_bytes) = trim_html_container_line(&line)?;
557 let (end, name) = parse_html_tag(trimmed, 0)?;
558 if !name.eq_ignore_ascii_case(tag)
559 || html_tag_is_closing(trimmed, 0)
560 || html_tag_is_self_closing(&trimmed[..end])
561 {
562 return None;
563 }
564
565 let rest_start = end + leading_ascii_whitespace_len(&trimmed[end..]);
566 if !trimmed[rest_start..].is_empty() && !html_container_line_has_summary(&trimmed[rest_start..])
567 {
568 return None;
569 }
570
571 Some(HtmlTag {
572 meta: NodeMeta::new(Some(Span::new(
573 line.source_start(indent_bytes),
574 line.source_end(indent_bytes + end),
575 ))),
576 name: tag.into(),
577 raw: trimmed[..end].into(),
578 })
579}
580
581fn html_container_line_has_summary(input: &str) -> bool {
582 let Some((open_end, name)) = parse_html_tag(input, 0) else {
583 return false;
584 };
585 if !name.eq_ignore_ascii_case("summary")
586 || html_tag_is_closing(input, 0)
587 || html_tag_is_self_closing(&input[..open_end])
588 {
589 return false;
590 }
591 let Some(close_start) = input[open_end..].find("</").map(|offset| open_end + offset) else {
592 return false;
593 };
594 let Some((close_end, close_name)) = parse_html_tag(input, close_start) else {
595 return false;
596 };
597 close_name.eq_ignore_ascii_case("summary")
598 && html_tag_is_closing(input, close_start)
599 && is_blank(&input[close_end..])
600}
601
602fn trim_html_container_line<'a>(line: &Line<'a>) -> Option<(&'a str, usize)> {
605 let (columns, bytes) = leading_indent_at(line.text, line.column);
606 (columns <= 3).then(|| (&line.text[bytes..], bytes))
607}
608
609fn leading_ascii_whitespace_len(input: &str) -> usize {
610 input
611 .as_bytes()
612 .iter()
613 .take_while(|byte| byte.is_ascii_whitespace())
614 .count()
615}
616
617fn html_tag_is_closing(input: &str, index: usize) -> bool {
618 input.as_bytes().get(index + 1) == Some(&b'/')
619}
620
621fn html_tag_is_self_closing(input: &str) -> bool {
622 input.trim_end_matches([' ', '\t']).ends_with("/>")
623}
624
625fn html_container_fence_opens(line: &Line<'_>) -> Option<HtmlContainerFence> {
626 let (trimmed, _) = trim_html_container_line(line)?;
627 let (marker, length) = fence_start(trimmed)?;
628 Some(HtmlContainerFence { marker, length })
629}
630
631fn html_container_fence_closes(line: &Line<'_>, fence: HtmlContainerFence) -> bool {
632 trim_html_container_line(line)
633 .is_some_and(|(trimmed, _)| fence_close(trimmed, fence.marker, fence.length))
634}
635
636fn html_block_start(input: &str) -> Option<HtmlBlockKind> {
637 let trimmed = input.trim_end_matches([' ', '\t']);
638 if !trimmed.starts_with('<') {
639 return None;
640 }
641
642 if raw_html_tag_start(trimmed) {
643 return Some(HtmlBlockKind::RawTag);
644 }
645 if trimmed.starts_with("<!--") {
646 return Some(HtmlBlockKind::Until("-->"));
647 }
648 if trimmed.starts_with("<?") {
649 return Some(HtmlBlockKind::Until("?>"));
650 }
651 if is_declaration_start(trimmed) {
652 return Some(HtmlBlockKind::Until(">"));
653 }
654 if trimmed.starts_with("<![CDATA[") {
655 return Some(HtmlBlockKind::Until("]]>"));
656 }
657
658 if html_block_tag_start(trimmed) {
659 return Some(HtmlBlockKind::BlockTag);
660 }
661
662 let Some((end, _tag_name)) = parse_html_tag(trimmed, 0) else {
663 return None;
664 };
665 if is_blank(&trimmed[end..]) {
666 Some(HtmlBlockKind::UntilBlank)
667 } else {
668 None
669 }
670}
671
672fn raw_html_tag_start(input: &str) -> bool {
673 for tag in ["script", "pre", "style", "textarea"] {
674 if html_raw_open_tag_prefix(input, tag) {
675 return true;
676 }
677 }
678 false
679}
680
681fn html_raw_open_tag_prefix(input: &str, tag: &str) -> bool {
682 let Some(rest) = input.strip_prefix('<') else {
683 return false;
684 };
685 if rest.starts_with('/') || rest.len() < tag.len() {
686 return false;
687 }
688 let rest_bytes = rest.as_bytes();
689 let tag_bytes = tag.as_bytes();
690 if !rest_bytes
691 .get(..tag_bytes.len())
692 .is_some_and(|name| name.eq_ignore_ascii_case(tag_bytes))
693 {
694 return false;
695 }
696 match rest_bytes.get(tag.len()) {
697 None => true,
698 Some(b' ' | b'\t' | b'\n' | b'\r' | b'>') => true,
699 Some(b'/') => {
700 rest_bytes.get(tag.len() + 1) == Some(&b'>') && rest_bytes.get(tag.len() + 2).is_none()
701 }
702 _ => false,
703 }
704}
705
706fn line_contains_raw_closing_tag(input: &str, tag: &str) -> bool {
707 let bytes = input.as_bytes();
708 let tag_bytes = tag.as_bytes();
709 let mut cursor = 0;
710
711 while cursor + 2 + tag_bytes.len() <= bytes.len() {
712 let tag_start = cursor + 2;
713 let tag_end = tag_start + tag_bytes.len();
714 if bytes.get(cursor) == Some(&b'<')
715 && bytes.get(cursor + 1) == Some(&b'/')
716 && bytes
717 .get(tag_start..tag_end)
718 .is_some_and(|name| name.eq_ignore_ascii_case(tag_bytes))
719 {
720 match bytes.get(tag_end) {
721 Some(b'>') => return true,
722 Some(byte) if byte.is_ascii_whitespace() => {
723 let mut after_space = tag_end;
724 while bytes
725 .get(after_space)
726 .is_some_and(|byte| byte.is_ascii_whitespace())
727 {
728 after_space += 1;
729 }
730 if bytes.get(after_space) == Some(&b'>') {
731 return true;
732 }
733 }
734 _ => {}
735 }
736 }
737 cursor += 1;
738 }
739
740 false
741}
742
743fn html_block_tag_start(input: &str) -> bool {
744 let bytes = input.as_bytes();
745 if bytes.first() != Some(&b'<') {
746 return false;
747 }
748
749 let mut cursor = 1;
750 if bytes.get(cursor) == Some(&b'/') {
751 cursor += 1;
752 }
753
754 let name_start = cursor;
755 if !bytes
756 .get(cursor)
757 .is_some_and(|byte| byte.is_ascii_alphabetic())
758 {
759 return false;
760 }
761 cursor += 1;
762 while bytes.get(cursor).is_some_and(|byte| html_name_byte(*byte)) {
763 cursor += 1;
764 }
765
766 let name = &input[name_start..cursor];
767 if !html_block_tag(name) {
768 return false;
769 }
770
771 match bytes.get(cursor) {
772 None | Some(b' ' | b'\t' | b'\n' | b'\r' | b'>') => true,
773 Some(b'/') if bytes.get(cursor + 1) == Some(&b'>') => true,
774 _ => false,
775 }
776}
777
778fn html_block_tag(tag: &str) -> bool {
779 matches!(
780 tag.to_ascii_lowercase().as_str(),
781 "address"
782 | "article"
783 | "aside"
784 | "base"
785 | "basefont"
786 | "blockquote"
787 | "body"
788 | "caption"
789 | "center"
790 | "col"
791 | "colgroup"
792 | "dd"
793 | "details"
794 | "dialog"
795 | "dir"
796 | "div"
797 | "dl"
798 | "dt"
799 | "fieldset"
800 | "figcaption"
801 | "figure"
802 | "footer"
803 | "form"
804 | "frame"
805 | "frameset"
806 | "h1"
807 | "h2"
808 | "h3"
809 | "h4"
810 | "h5"
811 | "h6"
812 | "head"
813 | "header"
814 | "hr"
815 | "html"
816 | "iframe"
817 | "legend"
818 | "li"
819 | "link"
820 | "main"
821 | "menu"
822 | "menuitem"
823 | "nav"
824 | "noframes"
825 | "ol"
826 | "optgroup"
827 | "option"
828 | "p"
829 | "param"
830 | "search"
831 | "section"
832 | "summary"
833 | "table"
834 | "tbody"
835 | "td"
836 | "tfoot"
837 | "th"
838 | "thead"
839 | "title"
840 | "tr"
841 | "track"
842 | "ul"
843 )
844}
845
846fn is_declaration_start(input: &str) -> bool {
847 input
848 .as_bytes()
849 .get(2)
850 .is_some_and(|byte| input.starts_with("<!") && byte.is_ascii_alphabetic())
851}
852
853fn setext_underline_depth(input: &str) -> Option<u8> {
854 let underline = trim_up_to_three_spaces(input)?.trim_matches([' ', '\t']);
855 match underline {
856 text if !text.is_empty() && text.chars().all(|char| char == '=') => Some(1),
857 text if !text.is_empty() && text.chars().all(|char| char == '-') => Some(2),
858 _ => None,
859 }
860}
861
862#[derive(Clone, Copy)]
867struct DelimMarker {
868 node_index: usize,
872 marker: u8,
873 length: usize,
875 run_length: usize,
879 can_open: bool,
881 can_close: bool,
882 recloses: usize,
885 enclosed_from: usize,
888 position: usize,
890}
891
892const NIL: usize = usize::MAX;
894
895const MAX_EMPHASIS_NESTING: usize = 16;
900
901#[derive(Clone, Copy, Default)]
909struct Nesting {
910 emphasis: usize,
911 total: usize,
912}
913
914impl Nesting {
915 fn max(self, other: Self) -> Self {
916 Self {
917 emphasis: self.emphasis.max(other.emphasis),
918 total: self.total.max(other.total),
919 }
920 }
921}
922
923struct DelimiterLinks {
927 prev: Vec<usize>,
928 next: Vec<usize>,
929 live: Vec<bool>,
930 nesting_after: Vec<Nesting>,
931}
932
933impl DelimiterLinks {
934 fn new(count: usize) -> Self {
935 Self {
936 prev: (0..count).map(|index| index.wrapping_sub(1)).collect(),
937 next: (1..=count)
938 .map(|index| if index < count { index } else { NIL })
939 .collect(),
940 live: alloc::vec![true; count],
941 nesting_after: alloc::vec![Nesting::default(); count],
942 }
943 }
944
945 fn unlink(&mut self, index: usize) {
947 let (before, after) = (self.prev[index], self.next[index]);
948 self.live[index] = false;
949 if before != NIL {
950 self.next[before] = after;
951 self.nesting_after[before] = self.nesting_after[before].max(self.nesting_after[index]);
952 }
953 if after != NIL {
954 self.prev[after] = before;
955 }
956 }
957
958 fn close_span(&mut self, opener: usize, closer: usize) -> Nesting {
961 let mut nesting = self.nesting_after[opener];
962 let mut inner = self.next[opener];
963 while inner != closer {
964 nesting = nesting.max(self.nesting_after[inner]);
965 self.live[inner] = false;
966 inner = self.next[inner];
967 }
968 self.next[opener] = closer;
969 self.prev[closer] = opener;
970 nesting
971 }
972}
973
974struct InlineList {
979 slots: Vec<Option<Inline>>,
980 prev: Vec<usize>,
981 next: Vec<usize>,
982 head: usize,
983}
984
985impl InlineList {
986 fn new(nodes: Vec<Inline>) -> Self {
987 let count = nodes.len();
988 Self {
989 slots: nodes.into_iter().map(Some).collect(),
990 prev: (0..count).map(|slot| slot.wrapping_sub(1)).collect(),
991 next: (1..=count)
992 .map(|slot| if slot < count { slot } else { NIL })
993 .collect(),
994 head: if count == 0 { NIL } else { 0 },
995 }
996 }
997
998 fn node_mut(&mut self, slot: usize) -> &mut Inline {
999 self.slots[slot]
1000 .as_mut()
1001 .expect("delimiter placeholder slot is live")
1002 }
1003
1004 fn wrap_between(
1007 &mut self,
1008 before: usize,
1009 after: usize,
1010 wrap: impl FnOnce(Vec<Inline>) -> Inline,
1011 ) {
1012 let mut children = Vec::new();
1013 let mut cursor = self.next[before];
1014 while cursor != after {
1015 children.push(self.slots[cursor].take().expect("linked slot is live"));
1016 cursor = self.next[cursor];
1017 }
1018 let slot = self.slots.len();
1019 self.slots.push(Some(wrap(children)));
1020 self.prev.push(before);
1021 self.next.push(after);
1022 self.next[before] = slot;
1023 self.prev[after] = slot;
1024 }
1025
1026 fn remove(&mut self, slot: usize) {
1027 let (prev, next) = (self.prev[slot], self.next[slot]);
1028 if prev == NIL {
1029 self.head = next;
1030 } else {
1031 self.next[prev] = next;
1032 }
1033 if next != NIL {
1034 self.prev[next] = prev;
1035 }
1036 self.slots[slot] = None;
1037 }
1038
1039 fn into_vec(mut self) -> Vec<Inline> {
1040 let mut nodes = Vec::new();
1041 let mut cursor = self.head;
1042 while cursor != NIL {
1043 nodes.push(self.slots[cursor].take().expect("linked slot is live"));
1044 cursor = self.next[cursor];
1045 }
1046 nodes
1047 }
1048}
1049
1050fn push_text(nodes: &mut Vec<Inline>, start: usize, value: &str) {
1053 nodes.push(Inline::Text(Text {
1054 meta: NodeMeta::new(Some(Span::new(start, start + value.len()))),
1055 value: value.into(),
1056 }));
1057}
1058
1059#[allow(clippy::too_many_arguments)]
1063fn close_bracket(
1064 input: &str,
1065 base_offset: usize,
1066 close: usize,
1067 definitions: Option<Definitions<'_>>,
1068 nodes: &mut Vec<Inline>,
1069 delimiters: &mut Vec<DelimMarker>,
1070 brackets: &mut Brackets,
1071 pass: &mut InlinePass,
1072) -> Option<usize> {
1073 let mut opener = brackets.openers.pop()?;
1074 let depth = pass.state.depth + brackets.openers.len();
1075 let link_active = brackets.openers.len() >= brackets.link_floor;
1078 brackets.link_floor = brackets.link_floor.min(brackets.openers.len());
1079
1080 if opener.kind == BracketKind::InlineFootnote {
1081 if !input[opener.position + 2..close].trim().is_empty() {
1082 let (children, marks) = take_label(input, nodes, delimiters, brackets, &opener, depth);
1083 nodes.truncate(opener.node_index - 1);
1084 nodes.push(Inline::InlineFootnote(InlineFootnote {
1085 meta: NodeMeta::new(Some(Span::new(
1086 base_offset + opener.position,
1087 base_offset + close + 1,
1088 ))),
1089 children,
1090 }));
1091 brackets.formed.push((nodes.len() - 1, marks + 1));
1092 return Some(close + 1);
1093 }
1094 opener.kind = BracketKind::Link;
1096 opener.position += 1;
1097 }
1098
1099 if opener.kind == BracketKind::Image {
1100 let target = match_link_target(
1101 &mut pass.scan,
1102 input,
1103 opener.position + 2,
1104 close,
1105 definitions,
1106 );
1107 if let Some((end, target)) = target {
1108 let (children, marks) = take_label(input, nodes, delimiters, brackets, &opener, depth);
1109 nodes.truncate(opener.node_index - 1);
1110 let span = Span::new(base_offset + opener.position, base_offset + end);
1111 nodes.push(link_node(target, true, span, children));
1112 brackets.formed.push((nodes.len() - 1, marks + 1));
1113 return Some(end);
1114 }
1115 opener.kind = BracketKind::Link;
1117 opener.position += 1;
1118 }
1119
1120 if link_active {
1121 let target = match_link_target(
1122 &mut pass.scan,
1123 input,
1124 opener.position + 1,
1125 close,
1126 definitions,
1127 );
1128 if let Some((end, target)) = target {
1129 let (children, marks) = take_label(input, nodes, delimiters, brackets, &opener, depth);
1130 nodes.truncate(opener.node_index);
1131 let span = Span::new(base_offset + opener.position, base_offset + end);
1132 nodes.push(link_node(target, false, span, children));
1133 brackets.formed.push((nodes.len() - 1, marks + 1));
1134 brackets.close_links();
1136 return Some(end);
1137 }
1138 }
1139
1140 let holds_formed = brackets
1143 .formed
1144 .last()
1145 .is_some_and(|&(node, _)| node > opener.node_index);
1146 if !holds_formed && input[opener.position + 1..].starts_with('^') {
1147 let label = &input[opener.position + 2..close];
1148 if is_footnote_label(label) {
1149 drop_label(nodes, delimiters, brackets, &opener);
1150 nodes.push(Inline::FootnoteReference(FootnoteReference {
1151 meta: NodeMeta::new(Some(Span::new(
1152 base_offset + opener.position,
1153 base_offset + close + 1,
1154 ))),
1155 label: label.into(),
1156 identifier: normalize_label(label),
1157 }));
1158 return Some(close + 1);
1159 }
1160 }
1161 None
1162}
1163
1164fn drop_label(
1166 nodes: &mut Vec<Inline>,
1167 delimiters: &mut Vec<DelimMarker>,
1168 brackets: &mut Brackets,
1169 opener: &BracketOpener,
1170) {
1171 nodes.truncate(opener.node_index);
1172 delimiters.truncate(opener.delimiter_bottom);
1173 let kept = brackets
1174 .formed
1175 .partition_point(|&(node, _)| node < opener.node_index);
1176 brackets.formed.truncate(kept);
1177}
1178
1179fn push_delimiter(
1182 nodes: &mut Vec<Inline>,
1183 delimiters: &mut Vec<DelimMarker>,
1184 index: usize,
1185 base_offset: usize,
1186 marker: u8,
1187 length: usize,
1188) {
1189 let node_index = nodes.len();
1190 nodes.push(Inline::Text(Text {
1191 meta: NodeMeta::new(Some(Span::new(
1192 base_offset + index,
1193 base_offset + index + length,
1194 ))),
1195 value: String::from(marker as char).repeat(length),
1196 }));
1197 delimiters.push(DelimMarker {
1198 node_index,
1199 marker,
1200 length,
1201 run_length: length,
1202 can_open: false,
1203 can_close: false,
1204 recloses: 0,
1205 enclosed_from: NIL,
1206 position: index,
1207 });
1208}
1209
1210fn emphasis_roles(
1224 input: &str,
1225 index: usize,
1226 length: usize,
1227 marker: u8,
1228 bounded_before: bool,
1229 bounded_after: bool,
1230) -> (bool, bool) {
1231 let flanking = delimiter_flanking_within(input, index, length, bounded_before, bounded_after);
1232 let (mut can_open, mut can_close) = if marker == b'_' {
1233 (
1234 flanking.left
1235 && (!flanking.right || flanking.previous.is_some_and(is_flanking_punctuation)),
1236 flanking.right
1237 && (!flanking.left || flanking.next.is_some_and(is_flanking_punctuation)),
1238 )
1239 } else {
1240 (flanking.left, flanking.right)
1241 };
1242
1243 if marker == b'*' {
1247 if flanking.next == Some('~') {
1248 can_open = true;
1249 }
1250 if flanking.previous == Some('~') {
1251 can_close = true;
1252 }
1253 }
1254 (can_open, can_close)
1255}
1256
1257fn assign_emphasis_roles(input: &str, delimiters: &mut [DelimMarker]) {
1262 let mut opens_span = alloc::vec![false; delimiters.len()];
1263 let mut closes_span = alloc::vec![false; delimiters.len()];
1264 let mut open_marks: Vec<usize> = Vec::new();
1265 let mut span_close = alloc::vec![NIL; delimiters.len()];
1267 for (index, run) in delimiters.iter_mut().enumerate() {
1268 if run.marker != b'=' {
1269 continue;
1270 }
1271 assign_double_mark_roles(input, run);
1272 let mut closed = 0;
1273 if run.can_close {
1274 while closed <= run.recloses {
1275 let Some(opener) = open_marks.pop() else {
1276 break;
1277 };
1278 opens_span[opener] = true;
1279 closes_span[index] = true;
1280 span_close[opener] = index;
1281 closed += 1;
1282 }
1283 }
1284 if run.can_open && run.length >= 2 * closed + 2 {
1286 open_marks.push(index);
1287 }
1288 }
1289
1290 for index in 0..delimiters.len() {
1291 let run = delimiters[index];
1292 if run.marker == b'=' {
1293 continue;
1294 }
1295 let bounded_before = index > 0 && {
1296 let before = &delimiters[index - 1];
1297 before.position + before.length == run.position && opens_span[index - 1]
1298 };
1299 let bounded_after = index + 1 < delimiters.len() && {
1300 let after = &delimiters[index + 1];
1301 after.position == run.position + run.length && closes_span[index + 1]
1302 };
1303 let (can_open, can_close) = emphasis_roles(
1304 input,
1305 run.position,
1306 run.length,
1307 run.marker,
1308 bounded_before,
1309 bounded_after,
1310 );
1311 delimiters[index].can_open = can_open;
1312 delimiters[index].can_close = can_close;
1313 }
1314
1315 let mut open_spans: Vec<usize> = Vec::new();
1319 for (index, run) in delimiters.iter_mut().enumerate() {
1320 while open_spans
1321 .last()
1322 .is_some_and(|&opener| span_close[opener] <= index)
1323 {
1324 open_spans.pop();
1325 }
1326 if let Some(&opener) = open_spans.last() {
1327 run.enclosed_from = opener;
1328 }
1329 if span_close[index] != NIL {
1330 open_spans.push(index);
1331 }
1332 }
1333}
1334
1335fn assign_double_mark_roles(input: &str, run: &mut DelimMarker) {
1340 let (index, length) = (run.position, run.length);
1341 let after_escaped_mark =
1342 index > 0 && input.as_bytes()[index - 1] == run.marker && is_escaped_at(input, index - 1);
1343 run.can_close = !after_escaped_mark && can_close_delimited(input, index, 2);
1344 run.recloses = if run.can_close {
1345 (1..length / 2)
1346 .take_while(|pair| can_close_delimited(input, index + 2 * pair, 2))
1347 .count()
1348 } else {
1349 0
1350 };
1351 run.can_open = can_open_delimited(input, index + length - 2, 2);
1352}
1353
1354fn process_emphasis(
1366 nodes: Vec<Inline>,
1367 mut delimiters: Vec<DelimMarker>,
1368 depth: usize,
1369 formed: &[(usize, usize)],
1370) -> (Vec<Inline>, usize) {
1371 let mut deepest = formed.iter().map(|&(_, depth)| depth).max().unwrap_or(0);
1372 if delimiters.is_empty() {
1373 let mut nodes = nodes;
1374 merge_adjacent_text(&mut nodes);
1375 return (nodes, deepest);
1376 }
1377 let mut nodes = InlineList::new(nodes);
1378
1379 let mut links = DelimiterLinks::new(delimiters.len());
1383 for &(node, depth) in formed {
1384 let before = delimiters.partition_point(|run| run.node_index < node);
1385 if before > 0 {
1386 let gap = &mut links.nesting_after[before - 1].total;
1387 *gap = (*gap).max(depth);
1388 }
1389 }
1390
1391 let mut openers_bottom: [Option<usize>; NESTED_MARKERS * 6] = [None; NESTED_MARKERS * 6];
1396 let mut span_bottom: Vec<Vec<(usize, usize)>> = Vec::new();
1397 let mut closer_idx = 0;
1398
1399 while closer_idx < delimiters.len() {
1402 let closer = delimiters[closer_idx];
1403
1404 let opener = if closer.can_close {
1405 nested_opener(
1406 &delimiters,
1407 &links,
1408 &openers_bottom,
1409 &span_bottom,
1410 closer_idx,
1411 )
1412 } else {
1413 None
1414 };
1415
1416 let Some(opener_idx) = opener else {
1417 if closer.can_close {
1418 let key = openers_bottom_key(&closer);
1421 match span_floor(&closer) {
1422 Some(span) => {
1423 if span_bottom.len() <= span {
1424 span_bottom.resize_with(span + 1, Vec::new);
1425 }
1426 match span_bottom[span].iter_mut().find(|(k, _)| *k == key) {
1427 Some(bound) => bound.1 = closer_idx,
1428 None => span_bottom[span].push((key, closer_idx)),
1429 }
1430 }
1431 None => openers_bottom[key] = Some(closer_idx),
1432 }
1433 }
1434 if !closer.can_open {
1435 links.unlink(closer_idx);
1436 }
1437 closer_idx += 1;
1438 continue;
1439 };
1440
1441 let (used, wrap) = pair_shape(&delimiters[opener_idx], &delimiters[closer_idx]);
1442
1443 let inner = links.close_span(opener_idx, closer_idx);
1446 let fits = depth + inner.total < MAX_INLINE_NESTING
1447 && (wrap.is_mark() || inner.emphasis < MAX_EMPHASIS_NESTING);
1448 if fits {
1449 apply_emphasis(
1450 &mut nodes,
1451 &mut delimiters,
1452 opener_idx,
1453 closer_idx,
1454 used,
1455 wrap,
1456 );
1457 links.nesting_after[opener_idx] = Nesting {
1458 emphasis: if wrap.is_mark() {
1459 0
1460 } else {
1461 inner.emphasis + 1
1462 },
1463 total: inner.total + 1,
1464 };
1465 deepest = deepest.max(inner.total + 1);
1466 } else {
1467 delimiters[opener_idx].length = 0;
1471 delimiters[closer_idx].length = 0;
1472 links.nesting_after[opener_idx] = inner;
1473 }
1474 if closer.marker == b'=' {
1475 delimiters[opener_idx].can_open = false;
1478 let run = &mut delimiters[closer_idx];
1479 run.can_close = run.recloses > 0;
1480 run.recloses = run.recloses.saturating_sub(1);
1481 }
1482
1483 if delimiters[opener_idx].length == 0 {
1484 links.unlink(opener_idx);
1485 }
1486 if delimiters[closer_idx].length == 0 {
1487 links.unlink(closer_idx);
1488 closer_idx += 1;
1489 }
1490 }
1494
1495 let mut nodes = nodes.into_vec();
1499 merge_adjacent_text(&mut nodes);
1500 (nodes, deepest)
1501}
1502
1503const NESTED_MARKERS: usize = 4;
1505
1506fn nested_opener(
1513 delimiters: &[DelimMarker],
1514 links: &DelimiterLinks,
1515 openers_bottom: &[Option<usize>],
1516 span_bottom: &[Vec<(usize, usize)>],
1517 closer_idx: usize,
1518) -> Option<usize> {
1519 let closer = &delimiters[closer_idx];
1520 let key = openers_bottom_key(closer);
1521 let mut bottom = openers_bottom[key];
1522 if let Some(span) = span_floor(closer) {
1523 let span_floor = span_bottom
1524 .get(span)
1525 .and_then(|bounds| bounds.iter().find(|(k, _)| *k == key))
1526 .map_or(span + 1, |&(_, bound)| bound);
1527 bottom = bottom.max(Some(span_floor));
1528 }
1529 let mut search = links.prev[closer_idx];
1530 while search != NIL {
1531 if bottom.is_some_and(|bottom| search < bottom) {
1532 return None;
1533 }
1534 let candidate = &delimiters[search];
1535 if candidate.marker == closer.marker
1536 && candidate.can_open
1537 && emphasis_delimiters_match(candidate, closer)
1538 {
1539 return Some(search);
1540 }
1541 search = links.prev[search];
1542 }
1543 None
1544}
1545
1546fn span_floor(closer: &DelimMarker) -> Option<usize> {
1549 (closer.can_open && closer.enclosed_from != NIL).then_some(closer.enclosed_from)
1550}
1551
1552fn pair_shape(opener: &DelimMarker, closer: &DelimMarker) -> (usize, EmphasisWrap) {
1554 match closer.marker {
1555 b'~' => (2, EmphasisWrap::Delete),
1557 b'=' => (2, EmphasisWrap::Mark),
1558 marker => {
1559 let delimiter = if marker == b'_' {
1560 EmphasisDelimiter::Underscore
1561 } else {
1562 EmphasisDelimiter::Asterisk
1563 };
1564 if opener.length >= 2 && closer.length >= 2 {
1565 (2, EmphasisWrap::Strong(delimiter))
1566 } else {
1567 (1, EmphasisWrap::Emphasis(delimiter))
1568 }
1569 }
1570 }
1571}
1572
1573fn merge_adjacent_text(nodes: &mut Vec<Inline>) {
1577 let mut write = 0;
1578 for read in 0..nodes.len() {
1579 if read != write {
1580 nodes.swap(read, write);
1581 }
1582 if write > 0 {
1583 let (head, tail) = nodes.split_at_mut(write);
1584 if let (Inline::Text(previous), Inline::Text(current)) =
1585 (&mut head[write - 1], &tail[0])
1586 {
1587 previous.value.push_str(¤t.value);
1588 if let (Some(previous_span), Some(current_span)) =
1589 (previous.meta.span.as_mut(), current.meta.span)
1590 {
1591 previous_span.end = current_span.end;
1592 }
1593 continue;
1594 }
1595 }
1596 write += 1;
1597 }
1598 nodes.truncate(write);
1599
1600 for node in nodes.iter_mut() {
1601 match node {
1602 Inline::Emphasis(node) => merge_adjacent_text(&mut node.children),
1603 Inline::Strong(node) => merge_adjacent_text(&mut node.children),
1604 Inline::Delete(node) => merge_adjacent_text(&mut node.children),
1605 Inline::Mark(node) => merge_adjacent_text(&mut node.children),
1606 _ => {}
1607 }
1608 }
1609}
1610
1611fn openers_bottom_key(closer: &DelimMarker) -> usize {
1614 let marker = match closer.marker {
1615 b'_' => 1,
1616 b'~' => 2,
1617 b'=' => 3,
1618 _ => 0,
1619 };
1620 let both = usize::from(closer.can_open && closer.can_close);
1621 let length = match closer.marker {
1625 b'*' | b'_' => closer.run_length,
1626 _ => closer.length,
1627 };
1628 let modulo = length % 3;
1629 ((marker * 2) + both) * 3 + modulo
1630}
1631
1632fn emphasis_delimiters_match(opener: &DelimMarker, closer: &DelimMarker) -> bool {
1634 match opener.marker {
1635 b'~' => true,
1639 b'=' => opener.length >= 2 && closer.length >= 2,
1641 _ => {
1642 let opener_both = opener.can_open && opener.can_close;
1647 let closer_both = closer.can_open && closer.can_close;
1648 if opener_both || closer_both {
1649 let (opener_run, closer_run) = (opener.run_length, closer.run_length);
1650 let sum = opener_run + closer_run;
1651 if sum % 3 == 0 && !(opener_run % 3 == 0 && closer_run % 3 == 0) {
1652 return false;
1653 }
1654 }
1655 true
1656 }
1657 }
1658}
1659
1660#[derive(Clone, Copy)]
1662enum EmphasisWrap {
1663 Emphasis(EmphasisDelimiter),
1664 Strong(EmphasisDelimiter),
1665 Delete,
1666 Mark,
1667}
1668
1669impl EmphasisWrap {
1670 fn is_mark(self) -> bool {
1673 matches!(self, Self::Mark)
1674 }
1675}
1676
1677fn apply_emphasis(
1680 nodes: &mut InlineList,
1681 delimiters: &mut [DelimMarker],
1682 opener_idx: usize,
1683 closer_idx: usize,
1684 used: usize,
1685 wrap: EmphasisWrap,
1686) {
1687 let opener_node = delimiters[opener_idx].node_index;
1688 let closer_node = delimiters[closer_idx].node_index;
1689
1690 let span_start = nodes
1694 .node_mut(opener_node)
1695 .span()
1696 .map(|span| span.end - used);
1697 let span_end = nodes
1698 .node_mut(closer_node)
1699 .span()
1700 .map(|span| span.start + used);
1701 trim_delimiter_text_tail(nodes.node_mut(opener_node), used);
1704 trim_delimiter_text_head(nodes.node_mut(closer_node), used);
1705 delimiters[opener_idx].length -= used;
1706 let closer = &mut delimiters[closer_idx];
1707 closer.length -= used;
1708 closer.position += used;
1709
1710 let meta = NodeMeta::new(
1713 span_start
1714 .zip(span_end)
1715 .map(|(start, end)| Span::new(start, end)),
1716 );
1717 nodes.wrap_between(opener_node, closer_node, |children| match wrap {
1718 EmphasisWrap::Strong(delimiter) => Inline::Strong(Strong {
1719 meta,
1720 delimiter,
1721 children,
1722 }),
1723 EmphasisWrap::Emphasis(delimiter) => Inline::Emphasis(Emphasis {
1724 meta,
1725 delimiter,
1726 children,
1727 }),
1728 EmphasisWrap::Delete => Inline::Delete(Delete { meta, children }),
1729 EmphasisWrap::Mark => Inline::Mark(Mark { meta, children }),
1730 });
1731
1732 if delimiters[closer_idx].length == 0 {
1735 nodes.remove(closer_node);
1736 }
1737 if delimiters[opener_idx].length == 0 {
1738 nodes.remove(opener_node);
1739 }
1740}
1741
1742#[derive(Clone, Copy, PartialEq, Eq)]
1744enum BracketKind {
1745 Link,
1746 Image,
1747 InlineFootnote,
1748}
1749
1750struct BracketOpener {
1753 kind: BracketKind,
1754 node_index: usize,
1756 position: usize,
1758 delimiter_bottom: usize,
1761}
1762
1763enum LinkTarget {
1765 Resource(ParsedLinkResource),
1766 Reference {
1767 identifier: String,
1768 kind: ReferenceKind,
1769 },
1770}
1771
1772fn match_link_target(
1778 scan: &mut InlineScan,
1779 input: &str,
1780 label_start: usize,
1781 close: usize,
1782 definitions: Option<Definitions<'_>>,
1783) -> Option<(usize, LinkTarget)> {
1784 let label = &input[label_start..close];
1785 let after = close + 1;
1786 if input.as_bytes().get(after) == Some(&b'(') {
1787 match parse_link_resource(&mut scan.lookups, input, after) {
1788 Some((end, resource)) => return Some((end, LinkTarget::Resource(resource))),
1789 None => {}
1794 }
1795 }
1796 let reference_close = if input.as_bytes().get(after) == Some(&b'[') {
1799 scan.reference_label_end(after)
1800 } else {
1801 None
1802 };
1803 if let Some(reference_close) = reference_close {
1804 let reference = &input[after + 1..reference_close];
1805 let identifier = if reference.is_empty() {
1806 label
1807 } else {
1808 reference
1809 };
1810 return definition_exists(definitions, identifier).then(|| {
1814 let kind = if reference.is_empty() {
1815 ReferenceKind::Collapsed
1816 } else {
1817 ReferenceKind::Full
1818 };
1819 (
1820 reference_close + 1,
1821 LinkTarget::Reference {
1822 identifier: identifier.into(),
1823 kind,
1824 },
1825 )
1826 });
1827 }
1828 definition_exists(definitions, label).then(|| {
1829 (
1830 after,
1831 LinkTarget::Reference {
1832 identifier: label.into(),
1833 kind: ReferenceKind::Shortcut,
1834 },
1835 )
1836 })
1837}
1838
1839fn link_node(target: LinkTarget, image: bool, span: Span, mut children: Vec<Inline>) -> Inline {
1841 let meta = NodeMeta::new(Some(span));
1842 if !image {
1843 demote_links(&mut children);
1844 }
1845 match (target, image) {
1846 (LinkTarget::Resource(resource), false) => Inline::Link(Link {
1847 meta,
1848 destination: resource.destination,
1849 destination_kind: resource.destination_kind,
1850 title: resource.title,
1851 children,
1852 }),
1853 (LinkTarget::Resource(resource), true) => Inline::Image(Image {
1854 meta,
1855 destination: resource.destination,
1856 destination_kind: resource.destination_kind,
1857 title: resource.title,
1858 alt: children,
1859 }),
1860 (LinkTarget::Reference { identifier, kind }, false) => {
1861 Inline::LinkReference(LinkReference {
1862 meta,
1863 identifier: normalize_label(&identifier),
1864 label: identifier,
1865 kind,
1866 children,
1867 })
1868 }
1869 (LinkTarget::Reference { identifier, kind }, true) => {
1870 Inline::ImageReference(ImageReference {
1871 meta,
1872 identifier: normalize_label(&identifier),
1873 label: identifier,
1874 kind,
1875 alt: children,
1876 })
1877 }
1878 }
1879}
1880
1881fn autolink_node(form: AutolinkForm, span: Span, text: &str) -> Inline {
1883 Inline::Autolink(Autolink {
1884 meta: NodeMeta::new(Some(span)),
1885 form,
1886 text: text.into(),
1887 })
1888}
1889
1890fn demote_links(nodes: &mut Vec<Inline>) {
1894 for node in nodes.iter_mut() {
1895 match node {
1896 Inline::Autolink(autolink) => {
1897 *node = Inline::Text(Text {
1898 meta: autolink.meta.clone(),
1899 value: core::mem::take(&mut autolink.text),
1900 });
1901 }
1902 Inline::Emphasis(node) => demote_links(&mut node.children),
1903 Inline::Strong(node) => demote_links(&mut node.children),
1904 Inline::Delete(node) => demote_links(&mut node.children),
1905 Inline::Mark(node) => demote_links(&mut node.children),
1906 Inline::InlineFootnote(node) => demote_links(&mut node.children),
1907 Inline::TextDirective(node) => demote_links(&mut node.label),
1908 Inline::Image(node) => demote_links(&mut node.alt),
1909 Inline::ImageReference(node) => demote_links(&mut node.alt),
1910 _ => {}
1911 }
1912 }
1913 merge_adjacent_text(nodes);
1914}
1915
1916struct Brackets {
1918 openers: Vec<BracketOpener>,
1919 link_floor: usize,
1922 overflow: usize,
1925 formed: Vec<(usize, usize)>,
1928}
1929
1930impl Brackets {
1931 fn new() -> Self {
1932 Self {
1933 openers: Vec::new(),
1934 link_floor: 0,
1935 overflow: 0,
1936 formed: Vec::new(),
1937 }
1938 }
1939
1940 fn open(&self) -> bool {
1945 self.overflow > 0
1946 || self
1947 .openers
1948 .iter()
1949 .any(|opener| !matches!(opener.kind, BracketKind::InlineFootnote))
1950 }
1951
1952 fn close_links(&mut self) {
1954 self.link_floor = self.openers.len();
1955 }
1956}
1957
1958fn take_label(
1962 input: &str,
1963 nodes: &mut Vec<Inline>,
1964 delimiters: &mut Vec<DelimMarker>,
1965 brackets: &mut Brackets,
1966 opener: &BracketOpener,
1967 depth: usize,
1968) -> (Vec<Inline>, usize) {
1969 let first = opener.node_index + 1;
1970 let children: Vec<Inline> = nodes.drain(first..).collect();
1971 let mut runs = delimiters.split_off(opener.delimiter_bottom);
1972 for run in &mut runs {
1973 run.node_index -= first;
1974 }
1975 let split = brackets.formed.partition_point(|&(node, _)| node < first);
1976 let formed: Vec<(usize, usize)> = brackets
1977 .formed
1978 .split_off(split)
1979 .into_iter()
1980 .map(|(node, marks)| (node - first, marks))
1981 .collect();
1982 assign_emphasis_roles(input, &mut runs);
1983 process_emphasis(children, runs, depth, &formed)
1984}
1985
1986fn trim_delimiter_text_tail(node: &mut Inline, count: usize) {
1988 if let Inline::Text(text) = node {
1989 let new_len = text.value.len().saturating_sub(count);
1990 text.value.truncate(new_len);
1991 if let Some(span) = text.meta.span.as_mut() {
1992 span.end = span.end.saturating_sub(count);
1993 }
1994 }
1995}
1996
1997fn trim_delimiter_text_head(node: &mut Inline, count: usize) {
2001 if let Inline::Text(text) = node {
2002 let count = count.min(text.value.len());
2003 text.value.truncate(text.value.len() - count);
2004 if let Some(span) = text.meta.span.as_mut() {
2005 span.start += count;
2006 }
2007 }
2008}
2009
2010trait Lookups {
2015 fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize>;
2017 fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize>;
2020 fn angle_destination_stop(&mut self, from: usize) -> Option<usize>;
2022 fn unescaped_dollar(&mut self, from: usize) -> Option<usize>;
2024}
2025
2026struct DirectLookups<'a> {
2027 input: &'a str,
2028}
2029
2030impl Lookups for DirectLookups<'_> {
2031 fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize> {
2032 self.input[from..].find(pattern).map(|offset| from + offset)
2033 }
2034
2035 fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize> {
2036 find_code_span_close(self.input, start, len)
2037 }
2038
2039 fn angle_destination_stop(&mut self, from: usize) -> Option<usize> {
2040 (from..self.input.len()).find(|index| is_angle_destination_stop(self.input, *index))
2041 }
2042
2043 fn unescaped_dollar(&mut self, from: usize) -> Option<usize> {
2044 (from..self.input.len()).find(|index| is_unescaped_dollar(self.input, *index))
2045 }
2046}
2047
2048const CACHED_PATTERNS: [&str; 8] = [">", "-->", "?>", "]]>", "\"", "'", "`$", "$$"];
2050
2051struct InlineLookups<'a> {
2054 input: &'a str,
2055 patterns: [Positions; CACHED_PATTERNS.len()],
2056 backtick_runs: Option<BTreeMap<usize, Vec<usize>>>,
2057 angle_destination_stops: Positions,
2058 unescaped_dollars: Positions,
2059}
2060
2061impl<'a> InlineLookups<'a> {
2062 fn new(input: &'a str) -> Self {
2063 Self {
2064 input,
2065 patterns: Default::default(),
2066 backtick_runs: None,
2067 angle_destination_stops: Positions::default(),
2068 unescaped_dollars: Positions::default(),
2069 }
2070 }
2071}
2072
2073impl Lookups for InlineLookups<'_> {
2074 fn find(&mut self, pattern: &'static str, from: usize) -> Option<usize> {
2075 let input = self.input;
2076 let Some(slot) = CACHED_PATTERNS.iter().position(|cached| *cached == pattern) else {
2077 return DirectLookups { input }.find(pattern, from);
2078 };
2079 self.patterns[slot].first_at_or_after(from, || pattern_starts(input, pattern))
2080 }
2081
2082 fn code_span_close(&mut self, start: usize, len: usize) -> Option<usize> {
2083 let input = self.input;
2084 let runs = self
2085 .backtick_runs
2086 .get_or_insert_with(|| backtick_runs_by_length(input));
2087 let starts = runs.get(&len)?;
2088 starts
2089 .get(starts.partition_point(|run| *run < start))
2090 .copied()
2091 }
2092
2093 fn angle_destination_stop(&mut self, from: usize) -> Option<usize> {
2094 let input = self.input;
2095 self.angle_destination_stops.first_at_or_after(from, || {
2096 (0..input.len())
2097 .filter(|index| is_angle_destination_stop(input, *index))
2098 .collect()
2099 })
2100 }
2101
2102 fn unescaped_dollar(&mut self, from: usize) -> Option<usize> {
2103 let input = self.input;
2104 self.unescaped_dollars.first_at_or_after(from, || {
2105 (0..input.len())
2106 .filter(|index| is_unescaped_dollar(input, *index))
2107 .collect()
2108 })
2109 }
2110}
2111
2112fn backtick_runs_by_length(input: &str) -> BTreeMap<usize, Vec<usize>> {
2114 let mut runs: BTreeMap<usize, Vec<usize>> = BTreeMap::new();
2115 let mut index = 0;
2116 while index < input.len() {
2117 if input.as_bytes()[index] == b'`' {
2118 let len = backtick_run_len(input, index);
2119 runs.entry(len).or_default().push(index);
2120 index += len;
2121 } else {
2122 index += 1;
2123 }
2124 }
2125 runs
2126}
2127
2128fn backtick_run_len(input: &str, index: usize) -> usize {
2129 input.as_bytes()[index..]
2130 .iter()
2131 .take_while(|byte| **byte == b'`')
2132 .count()
2133}
2134
2135fn is_unescaped_dollar(input: &str, index: usize) -> bool {
2138 let bytes = input.as_bytes();
2139 bytes[index] == b'$' && (index == 0 || bytes[index - 1] != b'\\')
2140}
2141
2142fn is_angle_destination_stop(input: &str, index: usize) -> bool {
2143 match input.as_bytes()[index] {
2144 b'<' | b'>' => !is_escaped_at(input, index),
2145 b'\n' | b'\r' => true,
2146 _ => false,
2147 }
2148}
2149
2150struct InlineScan<'a> {
2153 input: &'a str,
2154 lookups: InlineLookups<'a>,
2155 label_ends: BracketMemo,
2156 reference_label_ends: PathMemo,
2157 wikilink_closes: PathMemo,
2158 directive_attribute_closes: PathMemo,
2159 literal_autolinks: LiteralAutolinkScan,
2160}
2161
2162impl<'a> InlineScan<'a> {
2163 fn new(input: &'a str) -> Self {
2164 Self {
2165 input,
2166 lookups: InlineLookups::new(input),
2167 label_ends: BracketMemo::default(),
2168 reference_label_ends: PathMemo::default(),
2169 wikilink_closes: PathMemo::default(),
2170 directive_attribute_closes: PathMemo::default(),
2171 literal_autolinks: LiteralAutolinkScan::default(),
2172 }
2173 }
2174
2175 fn link_label_end(&mut self, open: usize) -> Option<usize> {
2177 let input = self.input;
2178 if input.as_bytes().get(open) != Some(&b'[') {
2179 return None;
2180 }
2181 let lookups = &mut self.lookups;
2182 self.label_ends
2183 .resolve(input.len() + 1, open + 1, |cursor| {
2184 link_label_step(lookups, input, cursor)
2185 })
2186 }
2187
2188 fn reference_label_end(&mut self, open: usize) -> Option<usize> {
2190 let input = self.input;
2191 if input.as_bytes().get(open) != Some(&b'[') {
2192 return None;
2193 }
2194 let close = self
2195 .reference_label_ends
2196 .resolve(input.len() + 1, open + 1, |cursor| {
2197 reference_label_step(input, cursor)
2198 })?;
2199 reference_label_is_within_limit(&input[open + 1..close]).then_some(close)
2200 }
2201
2202 fn wikilink_close(&mut self, start: usize) -> Option<usize> {
2205 let input = self.input;
2206 self.wikilink_closes
2207 .resolve(input.len() + 1, start, |cursor| {
2208 wikilink_close_step(input, cursor)
2209 })
2210 }
2211
2212 fn directive_attributes_close(&mut self, open: usize) -> Option<usize> {
2214 let input = self.input;
2215 if input.as_bytes().get(open) != Some(&b'{') {
2216 return None;
2217 }
2218 self.directive_attribute_closes.resolve(
2219 (input.len() + 1) * DIRECTIVE_QUOTE_STATES,
2220 (open + 1) * DIRECTIVE_QUOTE_STATES,
2221 |node| directive_attributes_step(input, node),
2222 )
2223 }
2224}
2225
2226fn parse_inlines(
2230 input: &str,
2231 map: &SourceMap,
2232 definitions: Option<Definitions<'_>>,
2233 diagnostics: &mut Vec<Diagnostic>,
2234) -> Vec<Inline> {
2235 parse_inlines_in(input, map, InlineState::default(), definitions, diagnostics)
2236}
2237
2238fn parse_cell_inlines(
2242 input: &str,
2243 map: &SourceMap,
2244 escaped_pipes: Vec<usize>,
2245 definitions: Option<Definitions<'_>>,
2246 diagnostics: &mut Vec<Diagnostic>,
2247) -> Vec<Inline> {
2248 let state = InlineState {
2249 escaped_pipes,
2250 ..InlineState::default()
2251 };
2252 parse_inlines_in(input, map, state, definitions, diagnostics)
2253}
2254
2255fn parse_inlines_in(
2256 input: &str,
2257 map: &SourceMap,
2258 mut state: InlineState,
2259 definitions: Option<Definitions<'_>>,
2260 diagnostics: &mut Vec<Diagnostic>,
2261) -> Vec<Inline> {
2262 if definitions.is_none() {
2264 return Vec::new();
2265 }
2266 let first_diagnostic = diagnostics.len();
2267 let mut nodes = parse_inlines_with_context(input, 0, definitions, diagnostics, &mut state);
2268 source_map::translate_inlines(map, &mut nodes, &mut diagnostics[first_diagnostic..]);
2269 nodes
2270}
2271
2272const MAX_INLINE_NESTING: usize = 32;
2277
2278#[derive(Default)]
2282struct InlineState {
2283 depth: usize,
2285 escaped_pipes: Vec<usize>,
2288}
2289
2290impl InlineState {
2291 fn is_escaped_pipe(&self, offset: usize) -> bool {
2292 !self.escaped_pipes.is_empty() && self.escaped_pipes.binary_search(&offset).is_ok()
2293 }
2294}
2295
2296struct InlinePass<'p, 'a> {
2299 state: &'p mut InlineState,
2300 scan: InlineScan<'a>,
2301}
2302
2303fn parse_inlines_with_context(
2304 input: &str,
2305 base_offset: usize,
2306 definitions: Option<Definitions<'_>>,
2307 diagnostics: &mut Vec<Diagnostic>,
2308 state: &mut InlineState,
2309) -> Vec<Inline> {
2310 if state.depth >= MAX_INLINE_NESTING {
2311 if input.is_empty() {
2312 return Vec::new();
2313 }
2314 return vec![Inline::Text(Text {
2315 meta: NodeMeta::new(Some(Span::new(base_offset, base_offset + input.len()))),
2316 value: input.into(),
2317 })];
2318 }
2319 state.depth += 1;
2320 let nodes = parse_inline_content(input, base_offset, definitions, diagnostics, state);
2321 state.depth -= 1;
2322 nodes
2323}
2324
2325fn parse_inline_content(
2326 input: &str,
2327 base_offset: usize,
2328 definitions: Option<Definitions<'_>>,
2329 diagnostics: &mut Vec<Diagnostic>,
2330 state: &mut InlineState,
2331) -> Vec<Inline> {
2332 let bytes = input.as_bytes();
2333 let mut nodes = Vec::new();
2334 let mut text_start = 0;
2335 let mut text = String::new();
2336 let mut index = 0;
2337 let mut delimiters: Vec<DelimMarker> = Vec::new();
2342 let mut brackets = Brackets::new();
2343 let mut pass = InlinePass {
2344 state,
2345 scan: InlineScan::new(input),
2346 };
2347 let literal_autolinks = bytes.contains(&b'@')
2350 || input.contains("://")
2351 || bytes
2352 .windows(4)
2353 .any(|window| window.eq_ignore_ascii_case(b"www."));
2354
2355 while index < bytes.len() {
2356 if bytes[index] == b'\\' {
2357 if let Some((next_index, char)) = next_char(input, index + 1) {
2358 if char.is_ascii_punctuation() {
2359 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2360 nodes.push(Inline::Escape(Escape {
2361 meta: NodeMeta::new(Some(Span::new(
2362 base_offset + index,
2363 base_offset + next_index,
2364 ))),
2365 value: char,
2366 }));
2367 index = next_index;
2368 text_start = index;
2369 continue;
2370 }
2371 }
2372 }
2373
2374 if bytes[index] == b'&' {
2375 if let Some((end, _)) = parse_character_reference(input, index) {
2376 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2377 nodes.push(Inline::CharacterReference(CharacterReference {
2378 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
2379 reference: input[index..end].into(),
2380 }));
2381 index = end;
2382 text_start = index;
2383 continue;
2384 }
2385 }
2386
2387 if bytes[index] == b'|' && pass.state.is_escaped_pipe(base_offset + index) {
2389 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2390 nodes.push(Inline::Escape(Escape {
2391 meta: NodeMeta::new(Some(Span::new(
2392 base_offset + index,
2393 base_offset + index + 1,
2394 ))),
2395 value: '|',
2396 }));
2397 index += 1;
2398 text_start = index;
2399 continue;
2400 }
2401
2402 if bytes[index] == b'\n' {
2403 if index > 0
2406 && bytes[index - 1] == b'\\'
2407 && !is_escaped_at(input, index - 1)
2408 && text.ends_with('\\')
2409 {
2410 text.pop();
2411 flush_text(
2412 &mut nodes,
2413 &mut text,
2414 text_start,
2415 base_offset + index.saturating_sub(1),
2416 );
2417 nodes.push(Inline::LineBreak(LineBreak {
2418 meta: NodeMeta::new(Some(Span::new(
2419 base_offset + index.saturating_sub(1),
2420 base_offset + index + 1,
2421 ))),
2422 kind: LineBreakKind::Backslash,
2423 }));
2424 index += 1;
2425 text_start = index;
2426 continue;
2427 }
2428 let trailing_spaces = trailing_space_count(&text);
2430 if is_hard_break_suffix(&text, trailing_spaces) {
2431 text.truncate(text.len() - trailing_spaces);
2432 flush_text(
2433 &mut nodes,
2434 &mut text,
2435 text_start,
2436 base_offset + index.saturating_sub(trailing_spaces),
2437 );
2438 nodes.push(Inline::LineBreak(LineBreak {
2439 meta: NodeMeta::new(Some(Span::new(
2440 base_offset + index.saturating_sub(trailing_spaces),
2441 base_offset + index + 1,
2442 ))),
2443 kind: LineBreakKind::Spaces,
2444 }));
2445 index += 1;
2446 text_start = index;
2447 continue;
2448 }
2449 if trailing_spaces > 0 {
2450 text.truncate(text.len() - trailing_spaces);
2451 }
2452 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2453 nodes.push(Inline::SoftBreak(SoftBreak {
2454 meta: NodeMeta::new(Some(Span::new(
2455 base_offset + index,
2456 base_offset + index + 1,
2457 ))),
2458 }));
2459 index += 1;
2460 text_start = index;
2461 continue;
2462 }
2463
2464 if bytes[index] == b'`' {
2465 if let Some((end, value)) = parse_code_span_with(&mut pass.scan.lookups, input, index) {
2466 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2467 nodes.push(Inline::Code(CodeInline {
2468 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
2469 value,
2470 }));
2471 index = end;
2472 text_start = index;
2473 continue;
2474 } else {
2475 let run = bytes[index..]
2481 .iter()
2482 .take_while(|byte| **byte == b'`')
2483 .count();
2484 if text.is_empty() {
2485 text_start = base_offset + index;
2486 }
2487 for _ in 0..run {
2488 text.push('`');
2489 }
2490 index += run;
2491 continue;
2492 }
2493 }
2494
2495 if bytes[index] == b'*' && starts_delimiter_run(input, index, b'*') {
2496 let run_len = delimiter_byte_run_len(input, index, b'*');
2497 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2498 push_delimiter(
2499 &mut nodes,
2500 &mut delimiters,
2501 index,
2502 base_offset,
2503 b'*',
2504 run_len,
2505 );
2506 index += run_len;
2507 text_start = index;
2508 continue;
2509 }
2510
2511 if bytes[index] == b'_' && starts_delimiter_run(input, index, b'_') {
2514 if literal_autolinks {
2519 if let Some((end, _)) = parse_literal_autolink(
2520 input,
2521 index,
2522 &mut pass.scan.literal_autolinks,
2523 !brackets.open(),
2524 ) {
2525 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2526 nodes.push(autolink_node(
2527 AutolinkForm::Literal,
2528 Span::new(base_offset + index, base_offset + end),
2529 &input[index..end],
2530 ));
2531 index = end;
2532 text_start = index;
2533 continue;
2534 }
2535 }
2536 let run_len = delimiter_byte_run_len(input, index, b'_');
2537 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2538 push_delimiter(
2539 &mut nodes,
2540 &mut delimiters,
2541 index,
2542 base_offset,
2543 b'_',
2544 run_len,
2545 );
2546 index += run_len;
2547 text_start = index;
2548 continue;
2549 }
2550
2551 if bytes[index] == b'=' {
2552 let run_len = delimiter_byte_run_len(input, index, b'=');
2553 if run_len >= 2 {
2554 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2555 push_delimiter(
2556 &mut nodes,
2557 &mut delimiters,
2558 index,
2559 base_offset,
2560 b'=',
2561 run_len,
2562 );
2563 index += run_len;
2564 text_start = index;
2565 continue;
2566 }
2567 }
2568
2569 let bracket_room = pass.state.depth - 1 + brackets.openers.len() < MAX_INLINE_NESTING;
2570
2571 if bracket_room && bytes[index] == b'^' && bytes.get(index + 1) == Some(&b'[') {
2573 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2574 push_text(&mut nodes, base_offset + index, "^");
2575 push_text(&mut nodes, base_offset + index + 1, "[");
2576 brackets.openers.push(BracketOpener {
2577 kind: BracketKind::InlineFootnote,
2578 node_index: nodes.len() - 1,
2579 position: index,
2580 delimiter_bottom: delimiters.len(),
2581 });
2582 index += 2;
2583 text_start = index;
2584 continue;
2585 }
2586
2587 if bytes[index] == b'~' && starts_delimiter_run(input, index, b'~') {
2591 let run_len = delimiter_byte_run_len(input, index, b'~');
2592 if run_len == 2 {
2593 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2594 push_delimiter(
2595 &mut nodes,
2596 &mut delimiters,
2597 index,
2598 base_offset,
2599 b'~',
2600 run_len,
2601 );
2602 index += run_len;
2603 text_start = index;
2604 continue;
2605 }
2606 }
2607
2608 if bytes[index] == b'!' && bytes.get(index + 1) == Some(&b'[') {
2609 if let Some((end, wikilink)) =
2613 parse_wikilink(input, index + 1, base_offset, &mut pass.scan)
2614 {
2615 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2616 nodes.push(embed(wikilink, base_offset + index));
2617 brackets.close_links();
2619 index = end;
2620 text_start = index;
2621 continue;
2622 }
2623 }
2624
2625 if bytes[index] == b'!' && bytes.get(index + 1) == Some(&b'[') && bracket_room {
2626 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2627 push_text(&mut nodes, base_offset + index, "!");
2628 push_text(&mut nodes, base_offset + index + 1, "[");
2629 brackets.openers.push(BracketOpener {
2630 kind: BracketKind::Image,
2631 node_index: nodes.len() - 1,
2632 position: index,
2633 delimiter_bottom: delimiters.len(),
2634 });
2635 index += 2;
2636 text_start = index;
2637 continue;
2638 }
2639
2640 if bytes[index] == b'[' {
2641 if let Some((end, wikilink)) = parse_wikilink(input, index, base_offset, &mut pass.scan)
2642 {
2643 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2644 nodes.push(wikilink);
2645 brackets.close_links();
2646 index = end;
2647 text_start = index;
2648 continue;
2649 }
2650 if bracket_room {
2651 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2652 push_text(&mut nodes, base_offset + index, "[");
2653 brackets.openers.push(BracketOpener {
2654 kind: BracketKind::Link,
2655 node_index: nodes.len() - 1,
2656 position: index,
2657 delimiter_bottom: delimiters.len(),
2658 });
2659 index += 1;
2660 text_start = index;
2661 continue;
2662 }
2663 brackets.overflow += 1;
2664 }
2665
2666 if bytes[index] == b']' && brackets.overflow > 0 {
2667 brackets.overflow -= 1;
2669 } else if bytes[index] == b']' && !brackets.openers.is_empty() {
2670 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2671 if let Some(end) = close_bracket(
2672 input,
2673 base_offset,
2674 index,
2675 definitions,
2676 &mut nodes,
2677 &mut delimiters,
2678 &mut brackets,
2679 &mut pass,
2680 ) {
2681 index = end;
2682 text_start = index;
2683 continue;
2684 }
2685 }
2686
2687 if bytes[index] == b'$' {
2688 if let Some((end, value, kind)) =
2689 parse_math_inline(&mut pass.scan.lookups, input, index)
2690 {
2691 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2692 nodes.push(Inline::Math(MathInline {
2693 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
2694 value,
2695 kind,
2696 }));
2697 index = end;
2698 text_start = index;
2699 continue;
2700 }
2701 let run = bytes[index..]
2708 .iter()
2709 .take_while(|byte| **byte == b'$')
2710 .count();
2711 if run > 1 {
2712 if text.is_empty() {
2713 text_start = base_offset + index;
2714 }
2715 text.push_str(&input[index..index + run]);
2716 index += run;
2717 continue;
2718 }
2719 }
2720
2721 if literal_autolinks {
2724 if let Some((end, _)) = parse_literal_autolink(
2725 input,
2726 index,
2727 &mut pass.scan.literal_autolinks,
2728 !brackets.open(),
2729 ) {
2730 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2731 nodes.push(autolink_node(
2732 AutolinkForm::Literal,
2733 Span::new(base_offset + index, base_offset + end),
2734 &input[index..end],
2735 ));
2736 index = end;
2737 text_start = index;
2738 continue;
2739 }
2740 }
2741
2742 if bytes[index] == b'<' {
2743 if let Some(end) = pass.scan.lookups.find(">", index).map(|close| close + 1) {
2744 let uri = &input[index + 1..end - 1];
2745 if is_angle_autolink(uri) {
2746 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2747 nodes.push(autolink_node(
2748 AutolinkForm::Angle,
2749 Span::new(base_offset + index, base_offset + end),
2750 uri,
2751 ));
2752 index = end;
2753 text_start = index;
2754 continue;
2755 }
2756 }
2757 if let Some(end) = html_inline_end(&mut pass.scan.lookups, input, index) {
2758 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2759 nodes.push(Inline::Html(HtmlInline {
2760 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
2761 value: input[index..end].into(),
2762 }));
2763 index = end;
2764 text_start = index;
2765 continue;
2766 }
2767 }
2768
2769 if bytes[index] == b':' {
2770 if let Some((end, name)) = parse_shortcode(input, index) {
2771 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2772 nodes.push(Inline::Shortcode(Shortcode {
2773 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
2774 name,
2775 }));
2776 index = end;
2777 text_start = index;
2778 continue;
2779 }
2780 }
2781
2782 if bytes[index] == b':' && pass.state.depth + brackets.openers.len() <= MAX_INLINE_NESTING {
2785 pass.state.depth += brackets.openers.len();
2788 let directive = parse_text_directive(
2789 input,
2790 index,
2791 base_offset,
2792 definitions,
2793 diagnostics,
2794 &mut pass,
2795 );
2796 pass.state.depth -= brackets.openers.len();
2797 if let Some((end, directive)) = directive {
2798 flush_text(&mut nodes, &mut text, text_start, base_offset + index);
2799 if let Inline::TextDirective(node) = &directive {
2800 if contains_link_inline(&node.label) {
2803 brackets.close_links();
2804 }
2805 let depth = inline_nesting(&node.label) + 1;
2806 brackets.formed.push((nodes.len(), depth));
2807 }
2808 nodes.push(directive);
2809 index = end;
2810 text_start = index;
2811 continue;
2812 }
2813 }
2814
2815 let (next_index, char) = next_char(input, index).expect("valid UTF-8 byte index");
2816 if text.is_empty() {
2817 text_start = base_offset + index;
2818 }
2819 text.push(char);
2820 index = next_index;
2821 }
2822
2823 flush_text(&mut nodes, &mut text, text_start, base_offset + input.len());
2824 assign_emphasis_roles(input, &mut delimiters);
2825 process_emphasis(nodes, delimiters, pass.state.depth - 1, &brackets.formed).0
2826}
2827
2828fn parse_shortcode(input: &str, index: usize) -> Option<(usize, String)> {
2832 if input[index..].starts_with("::") {
2833 return None;
2834 }
2835 if input[..index]
2836 .chars()
2837 .next_back()
2838 .is_some_and(char::is_alphanumeric)
2839 {
2840 return None;
2841 }
2842
2843 let mut cursor = index + 1;
2844 while let Some((next, char)) = next_char(input, cursor) {
2845 if char == ':' {
2846 let name = &input[index + 1..cursor];
2847 if crate::gemoji::glyph(name).is_none()
2848 || input[next..]
2849 .chars()
2850 .next()
2851 .is_some_and(char::is_alphanumeric)
2852 {
2853 return None;
2854 }
2855 return Some((next, name.into()));
2856 }
2857 if !(char.is_ascii_alphanumeric() || matches!(char, '_' | '-' | '+')) {
2858 return None;
2859 }
2860 cursor = next;
2861 }
2862 None
2863}
2864
2865fn parse_wikilink(
2866 input: &str,
2867 index: usize,
2868 base_offset: usize,
2869 scan: &mut InlineScan,
2870) -> Option<(usize, Inline)> {
2871 if input.as_bytes().get(index) != Some(&b'[') || input.as_bytes().get(index + 1) != Some(&b'[')
2872 {
2873 return None;
2874 }
2875
2876 let close = scan.wikilink_close(index + 2)?;
2877 let source = &input[index + 2..close];
2878 if source.is_empty() || source.len() > WIKILINK_MAX_BYTES {
2879 return None;
2880 }
2881
2882 let (target_source, label_source) = match find_wikilink_separator(source) {
2883 Some(separator) => (&source[..separator], &source[separator + 1..]),
2884 None => (source, source),
2885 };
2886
2887 if target_source.is_empty() {
2890 return None;
2891 }
2892 let target = String::from(target_source);
2893 let label = String::from(label_source);
2894 let end = close + 2;
2895 Some((
2896 end,
2897 Inline::WikiLink(WikiLink {
2898 meta: NodeMeta::new(Some(Span::new(base_offset + index, base_offset + end))),
2899 target,
2900 label,
2901 embed: false,
2902 }),
2903 ))
2904}
2905
2906fn embed(mut wikilink: Inline, bang: usize) -> Inline {
2908 if let Inline::WikiLink(node) = &mut wikilink {
2909 node.embed = true;
2910 node.meta.span = node.meta.span.map(|span| Span::new(bang, span.end));
2911 }
2912 wikilink
2913}
2914
2915fn wikilink_close_step(input: &str, cursor: usize) -> Step {
2918 let bytes = input.as_bytes();
2919 match bytes.get(cursor) {
2920 None | Some(b'\n' | b'\r' | b'[') => Step::Done(None),
2921 Some(b'\\') => {
2922 let escaped = cursor + 1;
2923 Step::Next(next_char(input, escaped).map_or(escaped, |(after_escape, _)| after_escape))
2924 }
2925 Some(b']') if bytes.get(cursor + 1) == Some(&b']') => Step::Done(Some(cursor)),
2926 Some(b']') => Step::Done(None),
2927 Some(_) => Step::Next(next_char(input, cursor).map_or(input.len(), |(next, _)| next)),
2928 }
2929}
2930
2931fn find_wikilink_separator(input: &str) -> Option<usize> {
2932 let bytes = input.as_bytes();
2933 let mut cursor = 0;
2934 while cursor < input.len() {
2935 match bytes[cursor] {
2936 b'\\' => {
2937 cursor += 1;
2938 if cursor < input.len() {
2939 cursor = next_char(input, cursor)?.0;
2940 }
2941 }
2942 b'|' => return Some(cursor),
2943 _ => cursor = next_char(input, cursor)?.0,
2944 }
2945 }
2946 None
2947}
2948
2949fn trailing_space_count(input: &str) -> usize {
2950 input
2951 .as_bytes()
2952 .iter()
2953 .rev()
2954 .take_while(|byte| matches!(**byte, b' ' | b'\t'))
2955 .count()
2956}
2957
2958fn is_hard_break_suffix(input: &str, trailing: usize) -> bool {
2959 let bytes = input.as_bytes();
2963 trailing >= 2
2964 && bytes[bytes.len() - trailing..]
2965 .iter()
2966 .all(|byte| *byte == b' ')
2967}
2968
2969#[cfg(test)]
2972fn find_reference_label_end(input: &str, open: usize) -> Option<usize> {
2973 if input.as_bytes().get(open) != Some(&b'[') {
2976 return None;
2977 }
2978 let close = path_walk(open + 1, |cursor| reference_label_step(input, cursor))?;
2979 reference_label_is_within_limit(&input[open + 1..close]).then_some(close)
2980}
2981
2982fn reference_label_step(input: &str, cursor: usize) -> Step {
2984 let Some((next, char)) = next_char(input, cursor) else {
2985 return Step::Done(None);
2986 };
2987 match char {
2988 '\\' => Step::Next(next_char(input, next).map_or(next, |(after_escape, _)| after_escape)),
2989 '[' => Step::Done(None),
2990 ']' => Step::Done(Some(cursor)),
2991 _ => Step::Next(next),
2992 }
2993}
2994
2995fn inline_nesting(inlines: &[Inline]) -> usize {
2997 let mut deepest = 0;
2998 let mut pending = vec![(inlines, 0)];
2999 while let Some((nodes, depth)) = pending.pop() {
3000 for node in nodes {
3001 if is_inline_container(node) {
3002 deepest = deepest.max(depth + 1);
3003 pending.push((node.children(), depth + 1));
3004 }
3005 }
3006 }
3007 deepest
3008}
3009
3010fn is_inline_container(inline: &Inline) -> bool {
3011 matches!(
3012 inline,
3013 Inline::Emphasis(_)
3014 | Inline::Strong(_)
3015 | Inline::Delete(_)
3016 | Inline::Mark(_)
3017 | Inline::Link(_)
3018 | Inline::Image(_)
3019 | Inline::LinkReference(_)
3020 | Inline::ImageReference(_)
3021 | Inline::InlineFootnote(_)
3022 | Inline::TextDirective(_)
3023 )
3024}
3025
3026fn contains_link_inline(inlines: &[Inline]) -> bool {
3029 inlines.iter().any(|inline| {
3030 matches!(
3031 inline,
3032 Inline::Link(_) | Inline::LinkReference(_) | Inline::WikiLink(_)
3033 ) || contains_link_inline(inline.children())
3034 })
3035}
3036
3037fn find_link_label_end(input: &str, open: usize) -> Option<usize> {
3038 if input.as_bytes().get(open) != Some(&b'[') {
3039 return None;
3040 }
3041 let mut lookups = DirectLookups { input };
3042 bracket_walk(open + 1, |cursor| {
3043 link_label_step(&mut lookups, input, cursor)
3044 })
3045}
3046
3047fn link_label_step(lookups: &mut impl Lookups, input: &str, cursor: usize) -> BracketStep {
3051 let Some((next, char)) = next_char(input, cursor) else {
3052 return BracketStep::End;
3053 };
3054 match char {
3055 '\\' => {
3056 BracketStep::Pass(next_char(input, next).map_or(next, |(after_escape, _)| after_escape))
3057 }
3058 '`' => BracketStep::Pass(code_span_end(lookups, input, cursor).unwrap_or(next)),
3059 '<' => BracketStep::Pass(
3060 autolink_end(lookups, input, cursor)
3061 .or_else(|| html_inline_end(lookups, input, cursor))
3062 .unwrap_or(next),
3063 ),
3064 '[' => BracketStep::Open(next),
3065 ']' => BracketStep::Close(next),
3066 _ => BracketStep::Pass(next),
3067 }
3068}
3069
3070fn parse_text_directive(
3071 input: &str,
3072 index: usize,
3073 base_offset: usize,
3074 definitions: Option<Definitions<'_>>,
3075 diagnostics: &mut Vec<Diagnostic>,
3076 pass: &mut InlinePass,
3077) -> Option<(usize, Inline)> {
3078 if input[index..].starts_with("::") {
3079 return None;
3080 }
3081 if index > 0 {
3082 let previous = input[..index].chars().next_back()?;
3083 if !previous.is_whitespace() && !matches!(previous, '(' | '[' | '{') {
3084 return None;
3085 }
3086 }
3087 let opener_offset = index + 1;
3088 let opener = parse_directive_opener_with(
3089 &input[opener_offset..],
3090 |close, open| {
3091 let found = match close {
3092 DirectiveClose::Label => pass.scan.link_label_end(opener_offset + open),
3093 DirectiveClose::Attributes => {
3094 pass.scan.directive_attributes_close(opener_offset + open)
3095 }
3096 };
3097 found.map(|position| position - opener_offset)
3098 },
3099 |label, attributes, rest| text_directive_ends(label, attributes, rest.bytes().next()),
3100 );
3101 let opener = match opener {
3102 Ok(opener) => opener,
3103 Err(refused) => {
3104 if refused == Refused::Unclosed {
3107 diagnostics.push(Diagnostic::new(
3108 DiagnosticSeverity::Error,
3109 DiagnosticCode::InvalidDirectiveName,
3110 Span::new(base_offset + index, base_offset + input.len()),
3111 "text directive opener is malformed",
3112 ));
3113 }
3114 return None;
3115 }
3116 };
3117 let label = opener
3118 .label
3119 .map(|source| {
3120 parse_inlines_with_context(
3121 source,
3122 base_offset + index + 1 + opener.name.len() + 1,
3123 definitions,
3124 diagnostics,
3125 pass.state,
3126 )
3127 })
3128 .unwrap_or_default();
3129 let at = base_offset + opener_offset;
3130 opener.report_dropped(diagnostics, |start, end| Span::new(at + start, at + end));
3131 let DirectiveOpener {
3132 name,
3133 attributes,
3134 consumed,
3135 ..
3136 } = opener;
3137 Some((
3138 index + 1 + consumed,
3139 Inline::TextDirective(TextDirective {
3140 meta: NodeMeta::new(Some(Span::new(
3141 base_offset + index,
3142 base_offset + index + 1 + consumed,
3143 ))),
3144 name,
3145 label,
3146 attributes,
3147 }),
3148 ))
3149}
3150
3151fn text_directive_ends(label: Option<&str>, attributes: Option<&str>, next: Option<u8>) -> bool {
3158 match next {
3159 None | Some(b' ' | b'\t' | b'\n' | b'\r') => true,
3160 Some(byte) if byte.is_ascii_punctuation() => {
3161 label.is_some_and(|label| !label.is_empty())
3162 || attributes
3163 .is_some_and(|source| source.bytes().any(|byte| !byte.is_ascii_whitespace()))
3164 }
3165 Some(_) => false,
3166 }
3167}
3168
3169struct DirectiveOpener<'a> {
3172 name: String,
3173 label: Option<&'a str>,
3174 attributes: Vec<DirectiveAttribute>,
3175 dropped: Vec<(usize, usize)>,
3177 consumed: usize,
3179}
3180
3181impl DirectiveOpener<'_> {
3182 fn report_dropped(
3184 &self,
3185 diagnostics: &mut Vec<Diagnostic>,
3186 span: impl Fn(usize, usize) -> Span,
3187 ) {
3188 for &(start, end) in &self.dropped {
3189 diagnostics.push(Diagnostic::new(
3190 DiagnosticSeverity::Warning,
3191 DiagnosticCode::InvalidDirectiveAttribute,
3192 span(start, end),
3193 "directive attribute without a valid name is dropped",
3194 ));
3195 }
3196 }
3197}
3198
3199#[derive(Clone, Copy, PartialEq, Eq)]
3201enum Refused {
3202 Name,
3204 Unclosed,
3206 Follow,
3208}
3209
3210fn parse_directive_opener<'a>(
3213 input: &'a str,
3214 accept: impl FnOnce(Option<&str>, Option<&str>, &str) -> bool,
3215) -> Result<DirectiveOpener<'a>, Refused> {
3216 parse_directive_opener_with(
3217 input,
3218 |close, open| match close {
3219 DirectiveClose::Label => find_link_label_end(input, open),
3220 DirectiveClose::Attributes => find_directive_attributes_close(input, open),
3221 },
3222 accept,
3223 )
3224}
3225
3226#[derive(Clone, Copy)]
3228enum DirectiveClose {
3229 Label,
3231 Attributes,
3233}
3234
3235fn parse_directive_opener_with<'a>(
3248 input: &'a str,
3249 mut find_close: impl FnMut(DirectiveClose, usize) -> Option<usize>,
3250 accept: impl FnOnce(Option<&str>, Option<&str>, &str) -> bool,
3251) -> Result<DirectiveOpener<'a>, Refused> {
3252 let mut index = 0;
3253 while let Some((next, char)) = next_char(input, index) {
3254 if char.is_ascii_alphanumeric() || char == '_' || char == '-' {
3255 index = next;
3256 } else {
3257 break;
3258 }
3259 }
3260 let name = &input[..index];
3261 if !is_directive_name(name) {
3262 return Err(Refused::Name);
3263 }
3264
3265 let mut label = None;
3266 let mut attribute_source = None;
3267 let mut consumed = index;
3268 if input.as_bytes().get(consumed) == Some(&b'[') {
3269 let close = find_close(DirectiveClose::Label, consumed).ok_or(Refused::Unclosed)?;
3270 label = Some(&input[consumed + 1..close]);
3271 consumed = close + 1;
3272 }
3273 if input.as_bytes().get(consumed) == Some(&b'{') {
3274 let close = find_close(DirectiveClose::Attributes, consumed).ok_or(Refused::Unclosed)?;
3275 attribute_source = Some((consumed + 1, &input[consumed + 1..close]));
3276 consumed = close + 1;
3277 }
3278 if !accept(
3279 label,
3280 attribute_source.map(|(_, source)| source),
3281 &input[consumed..],
3282 ) {
3283 return Err(Refused::Follow);
3284 }
3285 let (attributes, dropped) = match attribute_source {
3286 Some((start, source)) => {
3287 let (attributes, mut dropped) = parse_attributes(source);
3288 for range in &mut dropped {
3289 *range = (range.0 + start, range.1 + start);
3290 }
3291 (attributes, dropped)
3292 }
3293 None => (Vec::new(), Vec::new()),
3294 };
3295
3296 Ok(DirectiveOpener {
3297 name: name.into(),
3298 label,
3299 attributes,
3300 dropped,
3301 consumed,
3302 })
3303}
3304
3305const DIRECTIVE_QUOTE_STATES: usize = 3;
3308
3309fn find_directive_attributes_close(input: &str, open: usize) -> Option<usize> {
3310 if input.as_bytes().get(open) != Some(&b'{') {
3311 return None;
3312 }
3313 path_walk((open + 1) * DIRECTIVE_QUOTE_STATES, |node| {
3314 directive_attributes_step(input, node)
3315 })
3316}
3317
3318fn directive_attributes_step(input: &str, node: usize) -> Step {
3322 let bytes = input.as_bytes();
3323 let (cursor, quote) = (node / DIRECTIVE_QUOTE_STATES, node % DIRECTIVE_QUOTE_STATES);
3324 let at = |position: usize, quote: usize| Step::Next(position * DIRECTIVE_QUOTE_STATES + quote);
3325 let Some(&byte) = bytes.get(cursor) else {
3326 return Step::Done(None);
3327 };
3328 if byte == b'\\' {
3329 return at((cursor + 2).min(bytes.len()), quote);
3330 }
3331 match (quote, byte) {
3332 (0, b'"') => at(cursor + 1, 1),
3333 (0, b'\'') => at(cursor + 1, 2),
3334 (0, b'}') => Step::Done(Some(cursor)),
3335 (1, b'"') | (2, b'\'') => at(cursor + 1, 0),
3336 _ => at(cursor + 1, quote),
3337 }
3338}
3339
3340fn parse_attributes(input: &str) -> (Vec<DirectiveAttribute>, Vec<(usize, usize)>) {
3343 let mut attributes = Vec::new();
3344 let mut dropped = Vec::new();
3345 let mut cursor = 0;
3346 while cursor < input.len() {
3347 cursor = skip_spaces(input, cursor);
3348 if cursor >= input.len() {
3349 break;
3350 }
3351
3352 if input.as_bytes().get(cursor) == Some(&b'#') {
3353 let (id, next) = parse_attribute_token(input, cursor + 1);
3354 if !id.is_empty() {
3355 attributes.push(DirectiveAttribute {
3356 name: "id".into(),
3357 value: Some(id.into()),
3358 });
3359 }
3360 cursor = next;
3361 continue;
3362 }
3363
3364 if input.as_bytes().get(cursor) == Some(&b'.') {
3365 let (class, next) = parse_attribute_token(input, cursor + 1);
3366 if !class.is_empty() {
3367 attributes.push(DirectiveAttribute {
3368 name: "class".into(),
3369 value: Some(class.into()),
3370 });
3371 }
3372 cursor = next;
3373 continue;
3374 }
3375
3376 let token_start = cursor;
3377 let (name, next) = parse_attribute_name(input, cursor);
3378 if name.is_empty() {
3379 dropped.push((token_start, input.trim_end().len().max(token_start)));
3380 break;
3381 }
3382 let mut token_end = next;
3383 cursor = skip_spaces(input, next);
3384 let value = if input.as_bytes().get(cursor) == Some(&b'=') {
3385 cursor = skip_spaces(input, cursor + 1);
3386 token_end = cursor;
3387 if let Some((value, next)) = parse_attribute_value(input, cursor) {
3388 cursor = next;
3389 token_end = next;
3390 Some(value)
3391 } else {
3392 Some(String::new())
3393 }
3394 } else {
3395 None
3396 };
3397 if crate::validate::is_attribute_name(name) {
3400 attributes.push(DirectiveAttribute {
3401 name: name.into(),
3402 value,
3403 });
3404 } else {
3405 dropped.push((token_start, token_end));
3406 }
3407 }
3408 (attributes, dropped)
3409}
3410
3411fn parse_attribute_token(input: &str, index: usize) -> (&str, usize) {
3412 let mut cursor = index;
3413 while let Some((next, char)) = next_char(input, cursor) {
3414 if char.is_whitespace() {
3415 break;
3416 }
3417 cursor = next;
3418 }
3419 (&input[index..cursor], cursor)
3420}
3421
3422fn parse_attribute_name(input: &str, index: usize) -> (&str, usize) {
3423 let mut cursor = index;
3424 while let Some((next, char)) = next_char(input, cursor) {
3425 if char.is_whitespace() || char == '=' {
3426 break;
3427 }
3428 cursor = next;
3429 }
3430 (&input[index..cursor], cursor)
3431}
3432
3433fn parse_attribute_value(input: &str, index: usize) -> Option<(String, usize)> {
3434 let quote = input.as_bytes().get(index).copied();
3435 if matches!(quote, Some(b'"' | b'\'')) {
3436 let quote = quote?;
3437 let mut cursor = index + 1;
3438 while cursor < input.len() {
3439 let (next, char) = next_char(input, cursor)?;
3440 if char as u8 == quote && !is_escaped_at(input, cursor) {
3441 return Some((
3442 decode_escapes_and_references(&input[index + 1..cursor]),
3443 next,
3444 ));
3445 }
3446 cursor = next;
3447 }
3448 return None;
3449 }
3450
3451 let (value, next) = parse_attribute_token(input, index);
3452 Some((
3453 decode_selected_escapes_and_references(value, |char| matches!(char, '\\' | '&')),
3454 next,
3455 ))
3456}
3457
3458fn parse_code_span_with(
3460 lookups: &mut impl Lookups,
3461 input: &str,
3462 index: usize,
3463) -> Option<(usize, String)> {
3464 let len = backtick_run_len(input, index);
3465 let search_start = index + len;
3466 let close = lookups.code_span_close(search_start, len)?;
3467 Some((
3468 close + len,
3469 normalize_code_span(&input[search_start..close]),
3470 ))
3471}
3472
3473fn code_span_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
3475 let len = backtick_run_len(input, index);
3476 Some(lookups.code_span_close(index + len, len)? + len)
3477}
3478
3479fn find_code_span_close(input: &str, start: usize, marker_len: usize) -> Option<usize> {
3480 let bytes = input.as_bytes();
3481 let mut cursor = start;
3482 while cursor < bytes.len() {
3483 if bytes[cursor] != b'`' {
3484 cursor = next_char(input, cursor)
3485 .map(|(next, _)| next)
3486 .unwrap_or(bytes.len());
3487 continue;
3488 }
3489 let run_len = bytes[cursor..]
3490 .iter()
3491 .take_while(|byte| **byte == b'`')
3492 .count();
3493 if run_len == marker_len {
3494 return Some(cursor);
3495 }
3496 cursor += run_len;
3497 }
3498 None
3499}
3500
3501fn normalize_code_span(input: &str) -> String {
3502 let mut normalized = String::new();
3503 let mut cursor = 0;
3504 while cursor < input.len() {
3505 let (next, char) = next_char(input, cursor).expect("valid UTF-8 byte index");
3506 if char == '\r' {
3507 if input.as_bytes().get(next) == Some(&b'\n') {
3508 cursor = next + 1;
3509 } else {
3510 cursor = next;
3511 }
3512 normalized.push(' ');
3513 continue;
3514 }
3515 if char == '\n' {
3516 normalized.push(' ');
3517 cursor = next;
3518 continue;
3519 }
3520 normalized.push(char);
3521 cursor = next;
3522 }
3523
3524 if normalized.starts_with(' ')
3525 && normalized.ends_with(' ')
3526 && normalized.chars().any(|char| char != ' ')
3527 {
3528 normalized[1..normalized.len() - 1].into()
3529 } else {
3530 normalized
3531 }
3532}
3533
3534fn can_open_delimited(input: &str, index: usize, marker_len: usize) -> bool {
3535 delimiter_flanking(input, index, marker_len).left
3536}
3537
3538fn can_close_delimited(input: &str, index: usize, marker_len: usize) -> bool {
3539 delimiter_flanking(input, index, marker_len).right
3540}
3541
3542fn starts_delimiter_run(input: &str, index: usize, marker: u8) -> bool {
3546 index == 0 || input.as_bytes()[index - 1] != marker || is_escaped_at(input, index - 1)
3547}
3548
3549fn delimiter_byte_run_len(input: &str, index: usize, marker: u8) -> usize {
3550 let bytes = input.as_bytes();
3551 let mut cursor = index;
3552 while bytes.get(cursor) == Some(&marker) {
3553 cursor += 1;
3554 }
3555 cursor - index
3556}
3557
3558#[derive(Clone, Copy)]
3559struct DelimiterFlanking {
3560 left: bool,
3561 right: bool,
3562 previous: Option<char>,
3563 next: Option<char>,
3564}
3565
3566fn delimiter_flanking(input: &str, index: usize, marker_len: usize) -> DelimiterFlanking {
3567 delimiter_flanking_within(input, index, marker_len, false, false)
3568}
3569
3570fn delimiter_flanking_within(
3574 input: &str,
3575 index: usize,
3576 marker_len: usize,
3577 bounded_before: bool,
3578 bounded_after: bool,
3579) -> DelimiterFlanking {
3580 let previous = if bounded_before {
3581 None
3582 } else {
3583 input[..index].chars().next_back().map(source_char)
3584 };
3585 let next = if bounded_after {
3586 None
3587 } else {
3588 input[index + marker_len..].chars().next().map(source_char)
3589 };
3590
3591 let previous_whitespace = previous.is_none_or(char::is_whitespace);
3592 let next_whitespace = next.is_none_or(char::is_whitespace);
3593 let previous_punctuation = previous.is_some_and(is_flanking_punctuation);
3594 let next_punctuation = next.is_some_and(is_flanking_punctuation);
3595
3596 let left = next.is_some()
3597 && !next_whitespace
3598 && !(next_punctuation && !previous_whitespace && !previous_punctuation);
3599 let right = previous.is_some()
3600 && !previous_whitespace
3601 && !(previous_punctuation && !next_whitespace && !next_punctuation);
3602
3603 DelimiterFlanking {
3604 left,
3605 right,
3606 previous,
3607 next,
3608 }
3609}
3610
3611fn parse_math_inline(
3632 lookups: &mut impl Lookups,
3633 input: &str,
3634 index: usize,
3635) -> Option<(usize, String, MathInlineKind)> {
3636 if let Some((end, value)) = parse_math_code_inline(lookups, input, index) {
3637 return Some((end, value, MathInlineKind::Code));
3638 }
3639
3640 let bytes = input.as_bytes();
3641 let open_dollars = bytes[index..]
3642 .iter()
3643 .take_while(|byte| **byte == b'$')
3644 .count();
3645 if open_dollars == 0 || open_dollars > 2 {
3648 return None;
3649 }
3650
3651 let content_start = index + open_dollars;
3652 let close = scan_to_closing_dollar(lookups, input, content_start, open_dollars)?;
3653 let content_end = close - open_dollars;
3654 if content_end <= content_start {
3657 return None;
3658 }
3659
3660 let raw = &input[content_start..content_end];
3661 let value = if open_dollars == 1 {
3662 normalize_math_text(raw)
3663 } else {
3664 raw.into()
3665 };
3666 let dollars = u8::try_from(open_dollars).unwrap_or(u8::MAX);
3667 Some((close, value, MathInlineKind::Dollar { dollars }))
3668}
3669
3670pub(crate) fn reads_as_inline_math(written: &str, value: &str, kind: MathInlineKind) -> bool {
3673 parse_math_inline(&mut DirectLookups { input: written }, written, 0).is_some_and(
3674 |(end, read, read_kind)| end == written.len() && read == value && read_kind == kind,
3675 )
3676}
3677
3678fn scan_to_closing_dollar(
3682 lookups: &mut impl Lookups,
3683 input: &str,
3684 start: usize,
3685 open_dollars: usize,
3686) -> Option<usize> {
3687 let bytes = input.as_bytes();
3688 if open_dollars == 1 {
3689 if bytes.get(start).is_some_and(|byte| is_math_space(*byte)) {
3691 return None;
3692 }
3693 let close = lookups.unescaped_dollar(start)?;
3698 if is_math_space(bytes[close - 1]) || bytes.get(close + 1).is_some_and(u8::is_ascii_digit) {
3699 return None;
3700 }
3701 return Some(close + 1);
3702 }
3703 lookups.find("$$", start).map(|close| close + 2)
3706}
3707
3708fn is_math_space(byte: u8) -> bool {
3710 matches!(byte, b'\t' | b'\n' | b'\r' | b' ')
3711}
3712
3713fn normalize_math_text(input: &str) -> String {
3717 let mut normalized = String::new();
3718 let mut cursor = 0;
3719 while cursor < input.len() {
3720 let (next, char) = next_char(input, cursor).expect("valid UTF-8 byte index");
3721 if char == '\r' {
3722 if input.as_bytes().get(next) == Some(&b'\n') {
3723 cursor = next + 1;
3724 } else {
3725 cursor = next;
3726 }
3727 normalized.push(' ');
3728 continue;
3729 }
3730 if char == '\n' {
3731 normalized.push(' ');
3732 cursor = next;
3733 continue;
3734 }
3735 normalized.push(char);
3736 cursor = next;
3737 }
3738
3739 if normalized.starts_with(' ')
3740 && normalized.ends_with(' ')
3741 && normalized.chars().any(|char| char != ' ')
3742 {
3743 normalized[1..normalized.len() - 1].into()
3744 } else {
3745 normalized
3746 }
3747}
3748
3749fn parse_math_code_inline(
3750 lookups: &mut impl Lookups,
3751 input: &str,
3752 index: usize,
3753) -> Option<(usize, String)> {
3754 if !input[index..].starts_with("$`") {
3755 return None;
3756 }
3757
3758 let search_start = index + 2;
3759 let close = lookups.find("`$", search_start)?;
3760 if close == search_start {
3761 return None;
3762 }
3763
3764 Some((close + 2, input[search_start..close].into()))
3765}
3766
3767fn parse_link_resource(
3768 lookups: &mut impl Lookups,
3769 input: &str,
3770 open: usize,
3771) -> Option<(usize, ParsedLinkResource)> {
3772 let bytes = input.as_bytes();
3773 if bytes.get(open) != Some(&b'(') {
3774 return None;
3775 }
3776 let (mut cursor, initial_space) = skip_link_resource_space_with_info(input, open + 1)?;
3777 if bytes.get(cursor) == Some(&b')') {
3778 return Some((
3779 cursor + 1,
3780 ParsedLinkResource {
3781 destination: String::new(),
3782 destination_kind: LinkDestinationKind::Omitted,
3783 title: None,
3784 },
3785 ));
3786 }
3787 if initial_space && matches!(bytes.get(cursor), Some(b'"' | b'\'' | b'(')) {
3788 let (title, next) = parse_link_title(input, cursor)?;
3789 cursor = skip_link_resource_space(input, next)?;
3790 if bytes.get(cursor) == Some(&b')') {
3791 return Some((
3792 cursor + 1,
3793 ParsedLinkResource {
3794 destination: String::new(),
3795 destination_kind: LinkDestinationKind::Omitted,
3796 title: Some(title),
3797 },
3798 ));
3799 }
3800 return None;
3801 }
3802 let (destination, destination_kind, next) = parse_link_destination(lookups, input, cursor)?;
3803 let (after_destination, had_space) = skip_link_resource_space_with_info(input, next)?;
3804 cursor = after_destination;
3805 if bytes.get(cursor) == Some(&b')') {
3806 return Some((
3807 cursor + 1,
3808 ParsedLinkResource {
3809 destination,
3810 destination_kind,
3811 title: None,
3812 },
3813 ));
3814 }
3815 if !had_space {
3816 return None;
3817 }
3818
3819 let (title, next) = parse_link_title(input, cursor)?;
3820 cursor = skip_link_resource_space(input, next)?;
3821 if bytes.get(cursor) == Some(&b')') {
3822 Some((
3823 cursor + 1,
3824 ParsedLinkResource {
3825 destination,
3826 destination_kind,
3827 title: Some(title),
3828 },
3829 ))
3830 } else {
3831 None
3832 }
3833}
3834
3835fn parse_link_destination(
3836 lookups: &mut impl Lookups,
3837 input: &str,
3838 index: usize,
3839) -> Option<(String, LinkDestinationKind, usize)> {
3840 if input.as_bytes().get(index) == Some(&b'<') {
3841 let stop = lookups.angle_destination_stop(index + 1)?;
3842 if input.as_bytes()[stop] != b'>' {
3843 return None;
3844 }
3845 return Some((
3846 decode_escapes_and_references(&input[index + 1..stop]),
3847 LinkDestinationKind::Angle,
3848 stop + 1,
3849 ));
3850 }
3851
3852 let mut cursor = index;
3853 let mut depth = 0usize;
3854 while cursor < input.len() {
3855 let (next, char) = next_char(input, cursor)?;
3856 if char == ' ' || source_char(char).is_ascii_control() {
3861 break;
3862 }
3863 if char == '(' && !is_escaped_at(input, cursor) {
3864 depth += 1;
3865 if depth > 32 {
3867 return None;
3868 }
3869 } else if char == ')' && !is_escaped_at(input, cursor) {
3870 if depth == 0 {
3871 break;
3872 }
3873 depth -= 1;
3874 }
3875 cursor = next;
3876 }
3877
3878 if cursor == index || depth > 0 {
3879 None
3880 } else {
3881 Some((
3882 decode_escapes_and_references(&input[index..cursor]),
3883 LinkDestinationKind::Bare,
3884 cursor,
3885 ))
3886 }
3887}
3888
3889fn parse_link_title(input: &str, index: usize) -> Option<(Title, usize)> {
3890 let opener = input.as_bytes().get(index).copied()?;
3891 let (closer, title_kind) = match opener {
3892 b'"' => ('"', LinkTitleKind::DoubleQuote),
3893 b'\'' => ('\'', LinkTitleKind::SingleQuote),
3894 b'(' => (')', LinkTitleKind::Paren),
3895 _ => return None,
3896 };
3897 let mut cursor = index + 1;
3898 while cursor < input.len() {
3899 let (next, char) = next_char(input, cursor)?;
3900 if char == closer && !is_escaped_at(input, cursor) {
3901 if contains_blank_line(&input[index + 1..cursor]) {
3902 return None;
3903 }
3904 let value = decode_escapes_and_references(&input[index + 1..cursor]);
3905 return Some((Title::new(value, title_kind), next));
3906 }
3907 if opener == b'(' && char == '(' && !is_escaped_at(input, cursor) {
3908 return None;
3909 }
3910 cursor = next;
3911 }
3912 None
3913}
3914
3915fn contains_blank_line(input: &str) -> bool {
3916 if !input.bytes().any(|byte| matches!(byte, b'\n' | b'\r')) {
3917 return false;
3918 }
3919 let map = SourceMap::verbatim(input.len(), 0);
3924 let lines = collect_lines(input, &map);
3925 let interior = lines.len().saturating_sub(1);
3926 lines
3927 .iter()
3928 .take(interior)
3929 .skip(1)
3930 .any(|line| is_blank(line.text))
3931}
3932
3933fn skip_link_resource_space(input: &str, index: usize) -> Option<usize> {
3934 skip_link_resource_space_with_info(input, index).map(|(index, _)| index)
3935}
3936
3937fn skip_link_resource_space_with_info(input: &str, mut index: usize) -> Option<(usize, bool)> {
3938 let mut line_breaks = 0usize;
3939 let mut had_space = false;
3940 while input
3941 .as_bytes()
3942 .get(index)
3943 .is_some_and(|byte| matches!(*byte, b' ' | b'\t' | b'\n' | b'\r'))
3944 {
3945 had_space = true;
3946 match input.as_bytes()[index] {
3947 b'\n' => {
3948 line_breaks += 1;
3949 if line_breaks > 1 {
3950 return None;
3951 }
3952 index += 1;
3953 }
3954 b'\r' => {
3955 line_breaks += 1;
3956 if line_breaks > 1 {
3957 return None;
3958 }
3959 if input.as_bytes().get(index + 1) == Some(&b'\n') {
3960 index += 2;
3961 } else {
3962 index += 1;
3963 }
3964 }
3965 _ => index += 1,
3966 }
3967 }
3968 Some((index, had_space))
3969}
3970
3971pub(crate) fn is_escaped_at(input: &str, index: usize) -> bool {
3972 let bytes = input.as_bytes();
3973 let mut cursor = index;
3974 let mut count = 0;
3975 while cursor > 0 && bytes[cursor - 1] == b'\\' {
3976 count += 1;
3977 cursor -= 1;
3978 }
3979 count % 2 == 1
3980}
3981
3982fn parse_definition_destination_title(input: &str) -> Option<ParsedLinkResource> {
3983 let (mut cursor, _) = skip_link_resource_space_with_info(input, 0)?;
3984 let (destination, destination_kind, next) =
3985 parse_link_destination(&mut DirectLookups { input }, input, cursor)?;
3986 cursor = next;
3987
3988 let (next, had_space) = skip_link_resource_space_with_info(input, cursor)?;
3989 cursor = next;
3990 if cursor >= input.len() {
3991 return Some(ParsedLinkResource {
3992 destination,
3993 destination_kind,
3994 title: None,
3995 });
3996 }
3997 if !had_space {
3998 return None;
3999 }
4000
4001 let (title, next) = parse_link_title(input, cursor)?;
4002 let after_title = skip_link_resource_space(input, next)?;
4003 (after_title == input.len()).then_some(ParsedLinkResource {
4004 destination,
4005 destination_kind,
4006 title: Some(title),
4007 })
4008}
4009
4010fn open_definition_title(source: &str) -> Option<char> {
4013 let (cursor, _) = skip_link_resource_space_with_info(source, 0)?;
4014 let (_, _, next) =
4015 parse_link_destination(&mut DirectLookups { input: source }, source, cursor)?;
4016 let (cursor, had_space) = skip_link_resource_space_with_info(source, next)?;
4017 if !had_space {
4018 return None;
4019 }
4020 let closer = match source.as_bytes().get(cursor)? {
4021 b'"' => '"',
4022 b'\'' => '\'',
4023 b'(' => ')',
4024 _ => return None,
4025 };
4026 (!line_may_close_title(&source[cursor + 1..], closer)).then_some(closer)
4027}
4028
4029fn line_may_close_title(line: &str, closer: char) -> bool {
4033 line.char_indices().any(|(index, char)| {
4034 (char == closer || (closer == ')' && char == '(')) && !is_escaped_at(line, index)
4035 })
4036}
4037
4038fn line_can_start_definition_title(input: &str) -> bool {
4039 let trimmed = trim_ascii_start(input);
4040 matches!(trimmed.as_bytes().first(), Some(b'"' | b'\'' | b'('))
4041}
4042
4043fn push_line(output: &mut String, line: &str) {
4044 if !output.is_empty() {
4045 output.push('\n');
4046 }
4047 output.push_str(line);
4048}
4049
4050fn end_last_line(value: &mut String) {
4054 if value.is_empty() || ends_with_line_ending(value) {
4055 return;
4056 }
4057 value.push_str(value_line_ending(value));
4058}
4059
4060fn value_line_ending(value: &str) -> &'static str {
4063 match value.find(['\r', '\n']) {
4064 Some(index) if value[index..].starts_with("\r\n") => "\r\n",
4065 Some(index) if value[index..].starts_with('\r') => "\r",
4066 _ => "\n",
4067 }
4068}
4069
4070fn ends_with_line_ending(input: &str) -> bool {
4071 input.ends_with('\n') || input.ends_with('\r')
4072}
4073
4074fn flush_text(nodes: &mut Vec<Inline>, text: &mut String, text_start: usize, end: usize) {
4075 if !text.is_empty() {
4076 nodes.push(Inline::Text(Text {
4077 meta: NodeMeta::new(Some(Span::new(text_start, end))),
4078 value: core::mem::take(text),
4079 }));
4080 }
4081}
4082
4083fn next_char(input: &str, index: usize) -> Option<(usize, char)> {
4084 let char = input[index..].chars().next()?;
4085 Some((index + char.len_utf8(), char))
4086}
4087
4088fn is_flanking_punctuation(value: char) -> bool {
4093 value.is_ascii_punctuation() || crate::unicode_punctuation::is_unicode_punctuation(value)
4094}
4095
4096pub(crate) fn normalize_label(label: &str) -> String {
4106 label
4107 .replace('ẞ', "ss")
4113 .replace('\0', "\u{FFFD}")
4114 .split([' ', '\t', '\n', '\r'])
4115 .filter(|word| !word.is_empty())
4116 .collect::<Vec<_>>()
4117 .join(" ")
4118 .to_uppercase()
4119 .to_lowercase()
4120}
4121
4122#[derive(Clone, Copy)]
4125pub(crate) struct Definitions<'a> {
4126 pub(crate) own: &'a [String],
4127}
4128
4129fn definition_exists(definitions: Option<Definitions<'_>>, label: &str) -> bool {
4130 if label.is_empty() || !reference_label_is_within_limit(label) {
4131 return false;
4132 }
4133 let Some(definitions) = definitions else {
4134 return false;
4135 };
4136 let identifier = normalize_label(label);
4137 definitions.own.binary_search(&identifier).is_ok()
4138}
4139
4140fn reference_label_is_within_limit(label: &str) -> bool {
4141 label.chars().take(REFERENCE_LABEL_MAX_CHARS + 1).count() <= REFERENCE_LABEL_MAX_CHARS
4142}
4143
4144pub(crate) fn is_reference_label(label: &str) -> bool {
4149 let written = alloc::format!("[{label}]");
4150 path_walk(1, |cursor| reference_label_step(&written, cursor)) == Some(written.len() - 1)
4151 && reference_label_is_within_limit(label)
4152 && !normalize_label(label).is_empty()
4153 && !holds_blank_line(label)
4154}
4155
4156pub(crate) fn is_written_footnote_label(label: &str) -> bool {
4159 is_footnote_label(label) && !is_escaped_at(&alloc::format!("{label}]"), label.len())
4160}
4161
4162fn holds_blank_line(text: &str) -> bool {
4165 let text = text.replace("\r\n", "\n");
4166 let lines: Vec<&str> = text.split(['\n', '\r']).collect();
4167 lines.len() > 2 && lines[1..lines.len() - 1].iter().any(|line| is_blank(line))
4168}
4169
4170fn trim_up_to_three_spaces(input: &str) -> Option<&str> {
4171 let (columns, bytes) = leading_indent(input);
4172 if columns <= 3 {
4173 Some(&input[bytes..])
4174 } else {
4175 None
4176 }
4177}
4178
4179fn fence_start(input: &str) -> Option<(FenceMarker, usize)> {
4182 let marker = match input.as_bytes().first()? {
4183 b'`' => FenceMarker::Backtick,
4184 b'~' => FenceMarker::Tilde,
4185 _ => return None,
4186 };
4187 let byte = match marker {
4188 FenceMarker::Backtick => b'`',
4189 FenceMarker::Tilde => b'~',
4190 };
4191 let length = input
4192 .as_bytes()
4193 .iter()
4194 .take_while(|item| **item == byte)
4195 .count();
4196 let opens = length >= 3 && (byte == b'~' || !input[length..].contains('`'));
4197 opens.then_some((marker, length))
4198}
4199
4200fn fence_close(input: &str, marker: FenceMarker, length: usize) -> bool {
4201 let byte = match marker {
4202 FenceMarker::Backtick => b'`',
4203 FenceMarker::Tilde => b'~',
4204 };
4205 let count = input
4206 .as_bytes()
4207 .iter()
4208 .take_while(|item| **item == byte)
4209 .count();
4210 count >= length && is_blank(&input[count..])
4211}
4212
4213fn trim_closing_hashes(input: &str) -> &str {
4214 let input = input.trim_end_matches([' ', '\t']);
4215 let hash_start = input.trim_end_matches('#').len();
4216 if hash_start == input.len() {
4217 return input;
4218 }
4219 if hash_start == 0 {
4220 return "";
4221 }
4222
4223 let before = &input[..hash_start];
4224 if before.ends_with(' ') || before.ends_with('\t') {
4225 before.trim_end_matches([' ', '\t'])
4226 } else {
4227 input
4228 }
4229}
4230
4231fn opens_list_item(input: &str) -> bool {
4233 trim_up_to_three_spaces(input).is_some_and(|rest| blocks::list_marker_head(rest).is_some())
4234}
4235
4236fn slice_offset_in(text: &str, slice: &str) -> Option<usize> {
4238 let offset = (slice.as_ptr() as usize).checked_sub(text.as_ptr() as usize)?;
4239 (offset + slice.len() <= text.len()).then_some(offset)
4240}
4241
4242fn leading_indent(input: &str) -> (usize, usize) {
4243 leading_indent_at(input, 0)
4244}
4245
4246fn leading_indent_at(input: &str, start: usize) -> (usize, usize) {
4249 let mut column = start;
4250 let mut bytes = 0usize;
4251 for byte in input.as_bytes() {
4252 match *byte {
4253 b' ' => column += 1,
4254 b'\t' => column += 4 - (column % 4),
4255 _ => break,
4256 }
4257 bytes += 1;
4258 }
4259 (column - start, bytes)
4260}
4261
4262fn task_marker_checked(input: &str) -> Option<bool> {
4263 if input.starts_with("[ ]") {
4264 Some(false)
4265 } else if input.starts_with("[x]") || input.starts_with("[X]") {
4266 Some(true)
4267 } else {
4268 None
4269 }
4270}
4271
4272pub(crate) fn is_blank(text: &str) -> bool {
4274 text.bytes().all(|byte| matches!(byte, b' ' | b'\t'))
4275}
4276
4277fn trim_ascii_start(input: &str) -> &str {
4278 input.trim_start_matches(|char| matches!(char, ' ' | '\t'))
4279}
4280
4281fn leading_trim_bytes(input: &str) -> usize {
4282 input.len() - trim_ascii_start(input).len()
4283}
4284
4285fn parse_table_delimiter(input: &str) -> Option<Vec<TableAlignment>> {
4286 if !input.contains('-')
4289 || !input
4290 .chars()
4291 .all(|char| matches!(char, '|' | '-' | ':' | ' ' | '\t'))
4292 {
4293 return None;
4294 }
4295 let cells = table_row_cell_ranges(input);
4296 if cells.is_empty() {
4297 return None;
4298 }
4299 let mut alignments = Vec::new();
4300 for (start, end) in cells {
4301 alignments.push(table_delimiter_alignment(
4302 input[start..end].trim_matches([' ', '\t']),
4303 )?);
4304 }
4305 Some(alignments)
4306}
4307
4308fn table_delimiter_alignment(cell: &str) -> Option<TableAlignment> {
4311 let bytes = cell.as_bytes();
4312 let mut cursor = 0;
4313 let left = bytes.first() == Some(&b':');
4314 if left {
4315 cursor += 1;
4316 }
4317 let dash_start = cursor;
4318 while bytes.get(cursor) == Some(&b'-') {
4319 cursor += 1;
4320 }
4321 if cursor == dash_start {
4322 return None;
4323 }
4324 let right = bytes.get(cursor) == Some(&b':');
4325 if right {
4326 cursor += 1;
4327 }
4328 if cursor != bytes.len() {
4329 return None;
4330 }
4331 Some(match (left, right) {
4332 (true, true) => TableAlignment::Center,
4333 (true, false) => TableAlignment::Left,
4334 (false, true) => TableAlignment::Right,
4335 (false, false) => TableAlignment::None,
4336 })
4337}
4338
4339fn table_row_delimiters(row: &str) -> Vec<usize> {
4343 let bytes = row.as_bytes();
4344 let mut delimiters = Vec::new();
4345 let mut cursor = 0;
4346 while cursor < bytes.len() {
4347 match bytes[cursor] {
4348 b'\\' => {
4349 let backslashes = delimiter_byte_run_len(row, cursor, b'\\');
4350 let pipe = cursor + backslashes;
4351 cursor = if bytes.get(pipe) == Some(&b'|') && backslashes % 2 == 1 {
4352 pipe + 1
4353 } else {
4354 pipe
4355 };
4356 }
4357 b'|' => {
4358 delimiters.push(cursor);
4359 cursor += 1;
4360 }
4361 _ => cursor += 1,
4362 }
4363 }
4364 delimiters
4365}
4366
4367fn table_row_cell_ranges(input: &str) -> Vec<(usize, usize)> {
4369 let trimmed = input.trim_matches([' ', '\t']);
4370 let offset = leading_trim_bytes(input);
4371 let delimiters = table_row_delimiters(trimmed);
4372 let mut cells = Vec::new();
4373 let mut start = 0;
4374 for &pipe in &delimiters {
4375 cells.push((offset + start, offset + pipe));
4376 start = pipe + 1;
4377 }
4378 cells.push((offset + start, offset + trimmed.len()));
4379
4380 if delimiters.first() == Some(&0) {
4383 cells.remove(0);
4384 }
4385 if delimiters
4386 .last()
4387 .is_some_and(|&pipe| is_blank(&trimmed[pipe + 1..]))
4388 {
4389 cells.pop();
4390 }
4391 cells
4392}
4393
4394struct TableCellSource {
4397 text: DerivedText,
4398 escaped_pipes: Vec<usize>,
4400 span: Span,
4401}
4402
4403fn table_row_cells(line: &Line<'_>, row: &str) -> Vec<TableCellSource> {
4406 let row_offset = slice_offset_in(line.text, row).unwrap_or(0);
4407 table_row_cell_ranges(row)
4408 .into_iter()
4409 .map(|(start, end)| {
4410 let raw = &row[start..end];
4411 let leading = leading_trim_bytes(raw);
4412 let content = raw.trim_matches([' ', '\t']);
4413 let offset = row_offset + start + leading;
4414 let span = Span::new(
4415 line.source_start(offset),
4416 line.source_end(offset + content.len()),
4417 );
4418 let mut text = DerivedText::default();
4419 let mut escaped_pipes = Vec::new();
4420 let bytes = content.as_bytes();
4425 let mut copied = 0;
4426 let mut cursor = 0;
4427 while cursor < bytes.len() {
4428 if bytes[cursor] == b'\\' {
4429 let pipe = cursor + delimiter_byte_run_len(content, cursor, b'\\');
4430 if bytes.get(pipe) == Some(&b'|') && (pipe - cursor) % 2 == 1 {
4431 text.append(line, &content[copied..pipe - 1]);
4432 escaped_pipes.push(text.text.len());
4433 text.append_replacing(
4434 "|",
4435 line.source_start(offset + pipe - 1),
4436 line.source_end(offset + pipe + 1),
4437 );
4438 copied = pipe + 1;
4439 }
4440 cursor = pipe;
4441 } else {
4442 cursor += 1;
4443 }
4444 }
4445 text.append(line, &content[copied..]);
4446 TableCellSource {
4447 text,
4448 escaped_pipes,
4449 span,
4450 }
4451 })
4452 .collect()
4453}
4454
4455fn table_has_separator(header: &str, delimiter: &str) -> bool {
4456 let Some(alignments) = parse_table_delimiter(delimiter) else {
4464 return true;
4465 };
4466 if alignments.len() == 1 {
4467 return contains_unescaped_pipe(header)
4468 || contains_unescaped_pipe(delimiter)
4469 || delimiter.contains(':');
4470 }
4471 true
4472}
4473
4474fn contains_unescaped_pipe(input: &str) -> bool {
4477 !table_row_delimiters(input).is_empty()
4478}
4479
4480fn autolink_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
4484 let end = lookups.find(">", index)? + 1;
4485 is_angle_autolink(&input[index + 1..end - 1]).then_some(end)
4486}
4487
4488fn html_inline_end(lookups: &mut impl Lookups, input: &str, index: usize) -> Option<usize> {
4491 let rest = &input[index..];
4492 if rest.starts_with("<!--") {
4493 return Some(lookups.find("-->", index)? + 3);
4494 }
4495 if rest.starts_with("<?") {
4497 return Some(lookups.find("?>", index + 2)? + 2);
4498 }
4499 if rest.starts_with("<![CDATA[") {
4500 return Some(lookups.find("]]>", index)? + 3);
4501 }
4502 if is_declaration_start(rest) {
4503 return Some(lookups.find(">", index)? + 1);
4504 }
4505 parse_html_tag_with(lookups, input, index).map(|(end, _)| end)
4506}
4507
4508fn parse_html_tag(input: &str, index: usize) -> Option<(usize, &str)> {
4509 parse_html_tag_with(&mut DirectLookups { input }, input, index)
4510}
4511
4512fn parse_html_tag_with<'a>(
4513 lookups: &mut impl Lookups,
4514 input: &'a str,
4515 index: usize,
4516) -> Option<(usize, &'a str)> {
4517 let bytes = input.as_bytes();
4518 if bytes.get(index) != Some(&b'<') {
4519 return None;
4520 }
4521
4522 let closing = bytes.get(index + 1) == Some(&b'/');
4523 let name_start = index + if closing { 2 } else { 1 };
4524 let first = *bytes.get(name_start)?;
4525 if !first.is_ascii_alphabetic() {
4526 return None;
4527 }
4528
4529 let mut cursor = name_start + 1;
4530 while bytes.get(cursor).is_some_and(|byte| html_name_byte(*byte)) {
4531 cursor += 1;
4532 }
4533 let name = &input[name_start..cursor];
4534
4535 if closing {
4536 cursor = skip_spaces(input, cursor);
4537 if bytes.get(cursor) == Some(&b'>') {
4538 return Some((cursor + 1, name));
4539 }
4540 return None;
4541 }
4542
4543 let mut needs_space = false;
4544 loop {
4545 let before_spaces = cursor;
4546 cursor = skip_spaces(input, cursor);
4547 let had_space = cursor > before_spaces;
4548 match bytes.get(cursor) {
4549 Some(b'>') => return Some((cursor + 1, name)),
4550 Some(b'/') if bytes.get(cursor + 1) == Some(&b'>') => return Some((cursor + 2, name)),
4551 Some(byte) if had_space && html_attribute_name_start(*byte) => {
4552 cursor += 1;
4553 while bytes
4554 .get(cursor)
4555 .is_some_and(|byte| html_attribute_name_byte(*byte))
4556 {
4557 cursor += 1;
4558 }
4559 let after_name = cursor;
4560 let after_spaces = skip_spaces(input, cursor);
4561 if bytes.get(after_spaces) == Some(&b'=') {
4562 cursor = skip_spaces(input, after_spaces + 1);
4563 cursor = parse_html_attribute_value(lookups, input, cursor)?;
4564 } else {
4565 cursor = after_name;
4566 }
4567 needs_space = true;
4568 }
4569 Some(_) if needs_space => return None,
4570 _ => return None,
4571 }
4572 }
4573}
4574
4575fn parse_html_attribute_value(
4576 lookups: &mut impl Lookups,
4577 input: &str,
4578 index: usize,
4579) -> Option<usize> {
4580 let bytes = input.as_bytes();
4581 match bytes.get(index)? {
4582 b'"' => Some(lookups.find("\"", index + 1)? + 1),
4583 b'\'' => Some(lookups.find("'", index + 1)? + 1),
4584 b'=' | b'<' | b'>' | b'`' => None,
4585 _ => {
4586 let mut cursor = index;
4587 while bytes.get(cursor).is_some_and(|byte| {
4588 !byte.is_ascii_whitespace()
4589 && !matches!(*byte, b'"' | b'\'' | b'=' | b'<' | b'>' | b'`')
4590 }) {
4591 cursor += 1;
4592 }
4593 if cursor == index {
4594 None
4595 } else {
4596 Some(cursor)
4597 }
4598 }
4599 }
4600}
4601
4602fn html_name_byte(byte: u8) -> bool {
4603 byte.is_ascii_alphanumeric() || byte == b'-'
4604}
4605
4606fn html_attribute_name_start(byte: u8) -> bool {
4607 byte.is_ascii_alphabetic() || byte == b'_' || byte == b':'
4608}
4609
4610fn html_attribute_name_byte(byte: u8) -> bool {
4611 byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b':' | b'.' | b'-')
4612}
4613
4614fn skip_spaces(input: &str, mut index: usize) -> usize {
4615 while input
4616 .as_bytes()
4617 .get(index)
4618 .is_some_and(|byte| matches!(*byte, b' ' | b'\t' | b'\n' | b'\r'))
4619 {
4620 index += 1;
4621 }
4622 index
4623}
4624
4625pub(crate) fn angle_autolink_destination(uri: &str) -> Option<String> {
4628 if is_uri_autolink(uri) {
4629 Some(String::from(uri))
4630 } else if is_email_autolink(uri) {
4631 Some(alloc::format!("mailto:{uri}"))
4632 } else {
4633 None
4634 }
4635}
4636
4637fn is_angle_autolink(uri: &str) -> bool {
4639 is_uri_autolink(uri) || is_email_autolink(uri)
4640}
4641
4642fn is_uri_autolink(input: &str) -> bool {
4643 let Some(colon) = input
4645 .as_bytes()
4646 .iter()
4647 .take(33)
4648 .position(|byte| *byte == b':')
4649 else {
4650 return false;
4651 };
4652 let scheme = &input[..colon];
4653 if scheme.len() < 2 || scheme.len() > 32 {
4654 return false;
4655 }
4656 let mut bytes = scheme.bytes();
4657 if !bytes.next().is_some_and(|byte| byte.is_ascii_alphabetic()) {
4658 return false;
4659 }
4660 if !bytes.all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'.' | b'-')) {
4661 return false;
4662 }
4663 input[colon + 1..]
4664 .chars()
4665 .map(source_char)
4666 .all(|char| !matches!(char, '<' | '>' | ' ') && !char.is_ascii_control())
4667}
4668
4669fn is_email_autolink(input: &str) -> bool {
4670 if input
4673 .chars()
4674 .any(|char| char.is_whitespace() || char == '<')
4675 {
4676 return false;
4677 }
4678 let Some(at) = input.find('@') else {
4679 return false;
4680 };
4681 if at == 0 || at + 1 >= input.len() {
4682 return false;
4683 }
4684 is_email_local_part(&input[..at]) && is_email_domain(&input[at + 1..], 1)
4687}
4688
4689pub(crate) fn literal_autolink_destination(text: &str) -> Option<String> {
4700 let mut input = String::from(text);
4701 input.push_str(".. ");
4702 let (end, prefix) =
4703 parse_literal_autolink(&input, 0, &mut LiteralAutolinkScan::default(), true)?;
4704 (end == text.len()).then(|| {
4705 let mut destination = String::from(prefix);
4706 destination.push_str(text);
4707 destination
4708 })
4709}
4710
4711fn parse_literal_autolink(
4724 input: &str,
4725 index: usize,
4726 scan: &mut LiteralAutolinkScan,
4727 urls: bool,
4728) -> Option<(usize, &'static str)> {
4729 let rest = &input[index..];
4730 let www = rest
4731 .as_bytes()
4732 .get(..4)
4733 .is_some_and(|prefix| prefix.eq_ignore_ascii_case(b"www."));
4734 if !urls {
4735 if www {
4738 return None;
4739 }
4740 return parse_literal_email(input, index, scan);
4741 }
4742
4743 if let Some(scheme_len) = rest
4746 .starts_with("http://")
4747 .then_some(7)
4748 .or_else(|| rest.starts_with("https://").then_some(8))
4749 {
4750 if !literal_scheme_prefix_ok(input, index) {
4751 return None;
4752 }
4753 if !http_literal_host_ok(&input[index + scheme_len..]) {
4756 return None;
4757 }
4758 let end = autolink_url_end(input, index + scheme_len);
4761 if end <= index + scheme_len {
4762 return None;
4763 }
4764 return Some((end, ""));
4765 }
4766
4767 if www {
4770 if !literal_www_prefix_ok(input, index) {
4771 return None;
4772 }
4773 if !check_domain(rest, false) {
4774 return None;
4775 }
4776 let end = autolink_url_end(input, index);
4777 if end <= index || (end <= index + 3 && !literal_starts_line(input, index)) {
4778 return None;
4779 }
4780 return Some((end, "http://"));
4781 }
4782
4783 parse_literal_email(input, index, scan)
4784}
4785
4786fn literal_scheme_prefix_ok(input: &str, index: usize) -> bool {
4790 if index == 0 {
4791 return true;
4792 }
4793 let Some(previous) = input[..index].chars().next_back() else {
4794 return true;
4795 };
4796 !previous.is_ascii_alphabetic()
4797}
4798
4799fn literal_www_prefix_ok(input: &str, index: usize) -> bool {
4803 if index == 0 {
4804 return true;
4805 }
4806 let Some(previous) = input[..index].chars().next_back() else {
4807 return true;
4808 };
4809 if matches!(previous, '*' | '_' | '~' | '(' | '[' | ']') {
4810 return true;
4811 }
4812 matches!(previous, ' ' | '\t' | '\n' | '\r')
4813}
4814
4815fn literal_starts_line(input: &str, index: usize) -> bool {
4816 index == 0
4817 || input
4818 .as_bytes()
4819 .get(index - 1)
4820 .is_some_and(|byte| matches!(byte, b'\n' | b'\r'))
4821}
4822
4823fn http_literal_host_ok(host: &str) -> bool {
4824 match host.chars().next() {
4825 Some(char) if char.is_ascii() && char.is_ascii_alphanumeric() => check_domain(host, true),
4826 Some(char) if !char.is_ascii() && is_valid_hostchar(source_char(char)) => {
4827 check_domain(host, true)
4828 }
4829 _ => false,
4830 }
4831}
4832
4833fn is_valid_hostchar(char: char) -> bool {
4836 !char.is_whitespace() && !crate::unicode_punctuation::is_unicode_punctuation(char)
4837}
4838
4839fn check_domain(data: &str, allow_short: bool) -> bool {
4852 let mut np = 0usize;
4853 let mut uscore1 = 0usize;
4854 let mut uscore2 = 0usize;
4855
4856 let mut chars = data.char_indices().peekable();
4857 while let Some((offset, char)) = chars.next() {
4858 let account = offset != 0 && chars.peek().is_some();
4863 match char {
4864 '\\' => {
4865 chars.next();
4867 }
4868 '_' if account => uscore2 += 1,
4869 '.' if account => {
4870 uscore1 = uscore2;
4871 uscore2 = 0;
4872 np += 1;
4873 if np > 10 {
4874 return true;
4875 }
4876 }
4877 '_' | '.' | '-' => {}
4878 _ => {
4879 if !is_valid_hostchar(source_char(char)) {
4880 break;
4881 }
4882 }
4883 }
4884 }
4885
4886 if (uscore1 > 0 || uscore2 > 0) && np <= 10 {
4887 return false;
4888 }
4889
4890 allow_short || np > 0
4891}
4892
4893fn autolink_url_end(input: &str, start: usize) -> usize {
4900 let bytes = input.as_bytes();
4901 let mut end = start;
4902 let mut strict_has_open_bracket = false;
4905 let mut strict_inside_backticks = false;
4906 for (offset, char) in input[start..].char_indices() {
4907 if char.is_whitespace()
4908 || char == '<'
4909 || is_autolink_terminating_control(char)
4910 || (!char.is_ascii()
4911 && char != '\u{FFFD}'
4912 && crate::unicode_punctuation::is_unicode_punctuation(char))
4913 || input[start + offset..].starts_with("[[")
4914 {
4915 break;
4916 }
4917 match char {
4918 '[' => strict_has_open_bracket = true,
4919 '`' => strict_inside_backticks = !strict_inside_backticks,
4920 ']' if !strict_has_open_bracket && !strict_inside_backticks => break,
4921 _ => {}
4922 }
4923 if char == '\\' {
4930 if let Some(&next) = bytes.get(start + offset + 1) {
4931 if next.is_ascii_punctuation() && next != b'.' {
4932 break;
4933 }
4934 }
4935 }
4936 end = start + offset + char.len_utf8();
4937 }
4938 autolink_delim(input, start, end)
4939}
4940
4941fn is_autolink_terminating_control(char: char) -> bool {
4942 matches!(char, '\u{2066}'..='\u{2069}')
4943}
4944
4945fn autolink_delim(input: &str, start: usize, mut end: usize) -> usize {
4950 let bytes = input.as_bytes();
4951 let mut opening = 0usize;
4952 let mut closing = 0usize;
4953 for &byte in &bytes[start..end] {
4954 match byte {
4955 b'(' => opening += 1,
4956 b')' => closing += 1,
4957 _ => {}
4958 }
4959 }
4960
4961 while end > start {
4962 match bytes[end - 1] {
4963 b')' => {
4964 if closing <= opening {
4965 break;
4966 }
4967 closing -= 1;
4968 end -= 1;
4969 }
4970 b'?' | b'!' | b'.' | b',' | b':' | b'*' | b'_' | b'~' | b'\'' | b'"' => {
4971 end -= 1;
4972 }
4973 b';' => {
4974 let mut new_end = end - 1;
4977 while new_end > start && bytes[new_end - 1].is_ascii_alphanumeric() {
4978 new_end -= 1;
4979 }
4980 if new_end > start && new_end < end - 1 && bytes[new_end - 1] == b'&' {
4981 end = new_end - 1;
4982 } else {
4983 end -= 1;
4984 }
4985 }
4986 _ => break,
4987 }
4988 }
4989 end
4990}
4991
4992#[derive(Default)]
4996struct LiteralAutolinkScan {
4997 email_local: ByteRun,
4999 email_at: EmailAt,
5001 email_prefix: EmailPrefix,
5003}
5004
5005#[derive(Default)]
5009struct EmailAt {
5010 at: Option<usize>,
5011 from: usize,
5012 last_colon: Option<usize>,
5013 domain_end: [Option<Option<usize>>; 2],
5014}
5015
5016impl EmailAt {
5017 fn update(&mut self, input: &str, index: usize, at: usize) {
5018 if self.at != Some(at) {
5019 *self = Self {
5020 at: Some(at),
5021 from: usize::MAX,
5022 ..Self::default()
5023 };
5024 }
5025 if index < self.from {
5026 self.last_colon = input[index..at].rfind(':').map(|offset| index + offset);
5027 self.from = index;
5028 }
5029 }
5030
5031 fn domain_end(&mut self, input: &str, is_xmpp: bool) -> Option<usize> {
5033 let at = self.at.expect("updated before use");
5034 *self.domain_end[usize::from(is_xmpp)].get_or_insert_with(|| {
5035 let start = at + 1;
5036 let end = literal_email_domain_end(input, start, is_xmpp)?;
5037 let trimmed = autolink_delim(input, start, end);
5038 (trimmed > start && is_gfm_email_domain(&input[start..trimmed], is_xmpp))
5039 .then_some(trimmed)
5040 })
5041 }
5042}
5043
5044#[derive(Default)]
5050struct EmailPrefix {
5051 cursor: usize,
5052 segment: usize,
5053 non_local: Option<usize>,
5054 at: Option<usize>,
5055 domain_bad: Option<usize>,
5056 domain_dot: Option<usize>,
5057 domain_empty_label: Option<usize>,
5058}
5059
5060impl EmailPrefix {
5061 fn advance(&mut self, input: &str, end: usize) {
5062 if end < self.cursor {
5063 *self = Self::default();
5064 }
5065 let bytes = input.as_bytes();
5066 let from = self.cursor;
5067 for (offset, char) in input[from..end].char_indices() {
5068 let position = from + offset;
5069 if char.is_whitespace() {
5070 *self = Self {
5071 segment: position + char.len_utf8(),
5072 ..Self::default()
5073 };
5074 continue;
5075 }
5076 let byte = char.is_ascii().then_some(char as u8);
5077 if self.non_local.is_none() && !byte.is_some_and(is_gfm_email_local_byte) {
5078 self.non_local = Some(position);
5079 }
5080 if char == '@' {
5081 self.at = Some(position);
5082 self.domain_bad = None;
5083 self.domain_dot = None;
5084 self.domain_empty_label = None;
5085 continue;
5086 }
5087 let Some(at) = self.at else {
5088 continue;
5089 };
5090 match byte {
5091 Some(b'.') => {
5092 self.domain_dot.get_or_insert(position);
5093 if position == at + 1 || bytes[position - 1] == b'.' {
5094 self.domain_empty_label.get_or_insert(position);
5095 }
5096 }
5097 Some(byte) if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_') => {}
5098 _ => {
5099 self.domain_bad.get_or_insert(position);
5100 }
5101 }
5102 }
5103 self.cursor = end;
5104 }
5105
5106 fn ends_with_email(&mut self, input: &str, end: usize) -> bool {
5109 self.advance(input, end);
5110 let Some(at) = self.at else {
5111 return false;
5112 };
5113 let before = |position: Option<usize>| position.is_some_and(|position| position < end);
5114 at > self.segment
5115 && !self.non_local.is_some_and(|position| position < at)
5116 && end > at + 1
5117 && !before(self.domain_bad)
5118 && before(self.domain_dot)
5119 && !before(self.domain_empty_label)
5120 && !matches!(input.as_bytes()[end - 1], b'.' | b'-' | b'_')
5121 }
5122}
5123
5124#[derive(Default)]
5128struct ByteRun {
5129 start: usize,
5130 end: Option<usize>,
5131}
5132
5133impl ByteRun {
5134 fn end(&mut self, input: &str, index: usize, in_run: fn(u8) -> bool) -> usize {
5137 if let Some(end) = self.end {
5138 if self.start <= index && index <= end {
5139 return end;
5140 }
5141 }
5142 let end = input.as_bytes()[index..]
5143 .iter()
5144 .position(|byte| !in_run(*byte))
5145 .map_or(input.len(), |offset| index + offset);
5146 self.start = index;
5147 self.end = Some(end);
5148 end
5149 }
5150}
5151
5152fn is_email_local_or_scheme_byte(byte: u8) -> bool {
5153 is_gfm_email_local_byte(byte) || byte == b':'
5154}
5155
5156fn parse_literal_email(
5162 input: &str,
5163 index: usize,
5164 scan: &mut LiteralAutolinkScan,
5165) -> Option<(usize, &'static str)> {
5166 let at_index = scan
5169 .email_local
5170 .end(input, index, is_email_local_or_scheme_byte);
5171 if input.as_bytes().get(at_index) != Some(&b'@') || at_index == index {
5172 return None;
5173 }
5174
5175 let (auto_mailto, is_xmpp) = classify_email_local(&input[index..at_index]);
5179
5180 if !email_left_boundary_ok(input, index, auto_mailto, |end| {
5185 scan.email_prefix.ends_with_email(input, end)
5186 }) {
5187 return None;
5188 }
5189
5190 let body_start = if auto_mailto {
5195 index
5196 } else if is_xmpp {
5197 index + "xmpp:".len()
5198 } else {
5199 index + "mailto:".len()
5200 };
5201 scan.email_at.update(input, index, at_index);
5202 if body_start >= at_index
5203 || scan
5204 .email_at
5205 .last_colon
5206 .is_some_and(|colon| colon >= body_start)
5207 {
5208 return None;
5209 }
5210 let end = scan.email_at.domain_end(input, is_xmpp)?;
5211 Some((end, if auto_mailto { "mailto:" } else { "" }))
5212}
5213
5214fn classify_email_local(local: &str) -> (bool, bool) {
5219 if let Some(rest) = strip_ci_prefix(local, "mailto:") {
5220 if !rest.is_empty() {
5221 return (false, false);
5222 }
5223 }
5224 if let Some(rest) = strip_ci_prefix(local, "xmpp:") {
5225 if !rest.is_empty() {
5226 return (false, true);
5227 }
5228 }
5229 (true, false)
5230}
5231
5232fn strip_ci_prefix<'a>(input: &'a str, prefix: &str) -> Option<&'a str> {
5233 let bytes = input.as_bytes();
5234 let plen = prefix.len();
5235 if bytes.len() >= plen && bytes[..plen].eq_ignore_ascii_case(prefix.as_bytes()) {
5236 Some(&input[plen..])
5237 } else {
5238 None
5239 }
5240}
5241
5242fn email_left_boundary_ok(
5249 input: &str,
5250 index: usize,
5251 auto_mailto: bool,
5252 prefix_ends_with_email: impl FnOnce(usize) -> bool,
5253) -> bool {
5254 if index == 0 {
5255 return true;
5256 }
5257 let Some(previous) = input[..index].chars().next_back() else {
5258 return true;
5259 };
5260 if previous.is_ascii_alphanumeric() {
5261 if auto_mailto && input[index..].starts_with('+') && prefix_ends_with_email(index) {
5262 return true;
5263 }
5264 return false;
5265 }
5266 if auto_mailto && previous == '/' {
5267 return false;
5268 }
5269 true
5270}
5271
5272#[cfg(test)]
5273fn prefix_ends_with_gfm_email(input: &str, end: usize) -> bool {
5274 let start = input[..end]
5275 .char_indices()
5276 .rev()
5277 .find(|(_, char)| char.is_whitespace())
5278 .map_or(0, |(offset, char)| offset + char.len_utf8());
5279 let candidate = &input[start..end];
5280 let Some(at) = candidate.rfind('@') else {
5281 return false;
5282 };
5283 email_local_is_valid(&candidate[..at], true) && is_gfm_email_domain(&candidate[at + 1..], false)
5284}
5285
5286#[cfg(test)]
5290fn email_local_is_valid(local: &str, auto_mailto: bool) -> bool {
5291 let body = if auto_mailto {
5292 local
5293 } else if let Some(rest) = strip_ci_prefix(local, "mailto:") {
5294 rest
5295 } else if let Some(rest) = strip_ci_prefix(local, "xmpp:") {
5296 rest
5297 } else {
5298 local
5299 };
5300 !body.is_empty() && body.bytes().all(is_gfm_email_local_byte)
5301}
5302
5303fn is_gfm_email_local_byte(byte: u8) -> bool {
5306 byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'+' | b'_' | b'-')
5307}
5308
5309fn is_email_local_part(input: &str) -> bool {
5310 !input.is_empty()
5311 && input
5312 .split('.')
5313 .all(|segment| !segment.is_empty() && segment.bytes().all(is_email_atext))
5314}
5315
5316fn is_email_atext(byte: u8) -> bool {
5317 byte.is_ascii_alphanumeric()
5318 || matches!(
5319 byte,
5320 b'!' | b'#'
5321 | b'$'
5322 | b'%'
5323 | b'&'
5324 | b'\''
5325 | b'*'
5326 | b'+'
5327 | b'/'
5328 | b'='
5329 | b'?'
5330 | b'^'
5331 | b'_'
5332 | b'`'
5333 | b'{'
5334 | b'|'
5335 | b'}'
5336 | b'~'
5337 | b'-'
5338 )
5339}
5340
5341fn literal_email_domain_end(input: &str, index: usize, is_xmpp: bool) -> Option<usize> {
5349 let bytes = input.as_bytes();
5350 let mut end = index;
5351 let mut np = 0usize;
5352 while end < bytes.len() {
5353 let byte = bytes[end];
5354 if byte.is_ascii_alphanumeric() {
5355 end += 1;
5356 } else if byte == b'.' && end + 1 < bytes.len() && bytes[end + 1].is_ascii_alphanumeric() {
5357 np += 1;
5358 end += 1;
5359 } else if byte == b'-' || byte == b'_' || (byte == b'/' && is_xmpp) {
5360 end += 1;
5363 } else {
5364 break;
5365 }
5366 }
5367 if end <= index {
5368 return None;
5369 }
5370 let len = end - index;
5371 let last = bytes[end - 1];
5372 if len < 1 || np == 0 || !(last.is_ascii_alphabetic() || last == b'.') {
5373 return None;
5374 }
5375 Some(end)
5376}
5377
5378fn is_gfm_email_domain(input: &str, is_xmpp: bool) -> bool {
5383 if input.is_empty() {
5384 return false;
5385 }
5386 let host = if is_xmpp {
5389 input.split('/').next().unwrap_or(input)
5390 } else {
5391 input
5392 };
5393 if !host.contains('.') {
5394 return false;
5395 }
5396 let last = host.as_bytes()[host.len() - 1];
5397 if matches!(last, b'-' | b'_') {
5400 return false;
5401 }
5402 host.split('.').all(|label| {
5403 !label.is_empty()
5404 && label
5405 .bytes()
5406 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_'))
5407 })
5408}
5409
5410fn is_email_domain(input: &str, min_labels: usize) -> bool {
5411 let mut label_count = 0usize;
5412 for label in input.split('.') {
5413 label_count += 1;
5414 let bytes = label.as_bytes();
5415 if bytes.is_empty()
5416 || bytes.len() > 63
5417 || !bytes
5418 .first()
5419 .is_some_and(|byte| byte.is_ascii_alphanumeric())
5420 || !bytes
5421 .last()
5422 .is_some_and(|byte| byte.is_ascii_alphanumeric())
5423 || !bytes
5424 .iter()
5425 .all(|byte| byte.is_ascii_alphanumeric() || *byte == b'-')
5426 {
5427 return false;
5428 }
5429 }
5430 label_count >= min_labels
5431}
5432
5433pub(crate) fn is_footnote_label(label: &str) -> bool {
5436 !label.is_empty()
5437 && reference_label_is_within_limit(label)
5438 && !label.contains([' ', '\t', '\n', '\r'])
5439 && !label
5440 .match_indices(['[', ']'])
5441 .any(|(index, _)| !is_escaped_at(label, index))
5442}
5443
5444fn find_footnote_definition_label_end(input: &str) -> Option<usize> {
5445 let close = find_footnote_reference_label_end(input, 2)?;
5446 if input.as_bytes().get(close + 1) == Some(&b':') {
5447 Some(close)
5448 } else {
5449 None
5450 }
5451}
5452
5453fn find_footnote_reference_label_end(input: &str, mut cursor: usize) -> Option<usize> {
5454 while cursor < input.len() {
5455 let (next, char) = next_char(input, cursor)?;
5456 if char == ']' && !is_escaped_at(input, cursor) {
5457 return Some(cursor);
5458 }
5459 cursor = next;
5460 }
5461 None
5462}