1use alloc::vec::Vec;
6
7use crate::{
8 ast::{
9 Alert, Block, CodeBlock, CodeBlockKind, CodeInline, ContainerDirective, Definition,
10 DirectiveAttribute, Document, Escape, Frontmatter, Heading, HtmlContainer,
11 HtmlContainerContent, Inline, LeafDirective, LineBreakKind, LinkDestinationKind, List,
12 ListItem, MathInline, MathInlineKind, NodePath, ReferenceKind, Table, TextDirective,
13 },
14 diagnostic::Diagnostic,
15 parse::{
16 alert_title, frontmatter_fence_kind, html_block_ends_at_blank_line, html_block_left_open,
17 html_block_opens, interrupts_paragraph, is_blank, is_footnote_label, is_reference_label,
18 is_written_footnote_label, normalize_label, reads_as_inline_math, MAX_BLOCK_NESTING,
19 },
20 serialize::{
21 item_misreads_after_marker, write_inline_math, written_marker, written_opens_fence_like,
22 },
23 span::Span,
24};
25
26impl Document {
27 pub fn validate(&self) -> Vec<Diagnostic> {
30 validate_document(self)
31 }
32}
33
34pub(crate) fn validate_document(document: &Document) -> Vec<Diagnostic> {
35 let mut report = Report::default();
36 validate_blocks(&document.children, 0, false, &mut report);
37 report.diagnostics
38}
39
40#[derive(Default)]
43struct Report {
44 diagnostics: Vec<Diagnostic>,
45 path: Vec<usize>,
46}
47
48impl Report {
49 fn push(&mut self, mut diagnostic: Diagnostic) {
50 diagnostic.path = Some(NodePath(self.path.clone()));
51 self.diagnostics.push(diagnostic);
52 }
53
54 fn at(&mut self, index: usize, check: impl FnOnce(&mut Self)) {
56 self.path.push(index);
57 check(self);
58 self.path.pop();
59 }
60}
61
62fn validate_blocks(blocks: &[Block], depth: usize, tight: bool, diagnostics: &mut Report) {
68 let mut before: Option<&Block> = None;
71 for (index, block) in blocks.iter().enumerate() {
72 diagnostics.at(index, |diagnostics| {
73 if let (Some(Block::List(before)), Block::List(list)) = (before, block) {
74 if written_marker(before) == written_marker(list) {
75 diagnostics.push(Diagnostic::invalid(
76 list.meta.span,
77 "adjacent lists cannot use the same marker",
78 ));
79 }
80 }
81 if let Some(before) = before {
82 validate_taken_block(before, block, tight, diagnostics);
83 }
84 if let Block::Definition(definition) = block {
85 let indented = match before {
89 Some(before) => matches!(before, Block::Definition(_)),
90 None => depth > 0,
91 };
92 validate_definition(definition, depth, indented, diagnostics);
93 }
94 validate_block(block, depth, diagnostics);
95 });
96 if !matches!(block, Block::BlankLine(_)) {
97 before = Some(block);
98 }
99 }
100}
101
102fn validate_block(block: &Block, depth: usize, diagnostics: &mut Report) {
103 match block {
104 Block::Paragraph(paragraph) => {
105 if paragraph.children.iter().all(is_empty_text) {
107 diagnostics.push(Diagnostic::invalid(
108 paragraph.meta.span,
109 "paragraph cannot be empty",
110 ));
111 }
112 validate_inlines(¶graph.children, diagnostics);
113 }
114 Block::Heading(heading) => validate_heading(heading, diagnostics),
115 Block::BlockQuote(block_quote) => {
116 validate_blocks(&block_quote.children, depth + 1, false, diagnostics);
117 }
118 Block::Alert(alert) => validate_alert(alert, depth, diagnostics),
119 Block::List(list) => validate_list(list, depth, diagnostics),
120 Block::Table(table) => validate_table(table, diagnostics),
121 Block::FootnoteDefinition(definition) => {
122 if definition.identifier.is_empty() {
123 diagnostics.push(Diagnostic::invalid(
124 definition.meta.span,
125 "footnote definition identifier cannot be empty",
126 ));
127 }
128 validate_footnote_label(
129 definition.meta.span,
130 &definition.label,
131 &definition.identifier,
132 diagnostics,
133 );
134 let last = definition.children.len().saturating_sub(1);
137 if let Some((at, Block::BlankLine(blank))) = [0, last]
138 .into_iter()
139 .filter_map(|at| definition.children.get(at).map(|block| (at, block)))
140 .find(|(_, block)| matches!(block, Block::BlankLine(_)))
141 {
142 diagnostics.at(at, |diagnostics| {
143 diagnostics.push(Diagnostic::invalid(
144 blank.meta.span,
145 "a footnote definition cannot open or end with a blank line",
146 ))
147 });
148 }
149 validate_blocks(&definition.children, depth + 1, false, diagnostics);
150 }
151 Block::Definition(_) => {}
153 Block::LeafDirective(directive) => validate_leaf_directive(directive, diagnostics),
154 Block::ContainerDirective(directive) => {
155 validate_container_directive(directive, depth, diagnostics)
156 }
157 Block::HtmlContainer(container) => validate_html_container(container, depth, diagnostics),
158 Block::CodeBlock(code) => validate_code_block(code, diagnostics),
159 Block::Frontmatter(frontmatter) => validate_frontmatter(frontmatter, depth, diagnostics),
160 Block::ThematicBreak(_)
161 | Block::HtmlBlock(_)
162 | Block::MathBlock(_)
163 | Block::BlankLine(_) => {}
164 }
165}
166
167fn validate_html_container(container: &HtmlContainer, depth: usize, diagnostics: &mut Report) {
168 if container.opening.name.is_empty() {
169 diagnostics.push(Diagnostic::invalid(
170 container.opening.meta.span,
171 "HTML container opening tag name cannot be empty",
172 ));
173 }
174 if container.closing.name.is_empty() {
175 diagnostics.push(Diagnostic::invalid(
176 container.closing.meta.span,
177 "HTML container closing tag name cannot be empty",
178 ));
179 }
180 if container.opening.name != container.closing.name {
181 diagnostics.push(Diagnostic::invalid(
182 container.meta.span,
183 "HTML container opening and closing tag names must match",
184 ));
185 }
186 if container.opening.raw.is_empty() {
187 diagnostics.push(Diagnostic::invalid(
188 container.opening.meta.span,
189 "HTML container opening tag source cannot be empty",
190 ));
191 }
192 if container.closing.raw.is_empty() {
193 diagnostics.push(Diagnostic::invalid(
194 container.closing.meta.span,
195 "HTML container closing tag source cannot be empty",
196 ));
197 }
198
199 match &container.content {
200 HtmlContainerContent::Blocks(children) => {
201 validate_blocks(children, depth + 1, false, diagnostics);
202 }
203 HtmlContainerContent::Inlines(children) => validate_inlines(children, diagnostics),
204 }
205}
206
207fn validate_heading(heading: &Heading, diagnostics: &mut Report) {
208 if heading.depth == 0 || heading.depth > 6 {
209 diagnostics.push(Diagnostic::invalid(
210 heading.meta.span,
211 "heading depth must be in the range 1..=6",
212 ));
213 }
214 validate_inlines(&heading.children, diagnostics);
215}
216
217fn validate_alert(alert: &Alert, depth: usize, diagnostics: &mut Report) {
220 if let Some(title) = &alert.title {
221 if alert_title(title).as_deref() != Some(title.as_str()) {
222 diagnostics.push(Diagnostic::invalid(
223 alert.meta.span,
224 "alert title cannot be empty or start or end with a space or a tab",
225 ));
226 }
227 }
228 validate_blocks(&alert.children, depth + 1, false, diagnostics);
229}
230
231fn validate_list(list: &List, depth: usize, diagnostics: &mut Report) {
232 if list.children.is_empty() {
233 diagnostics.push(Diagnostic::invalid(
234 list.meta.span,
235 "list must hold at least one item",
236 ));
237 }
238 if !list.tight
241 && matches!(list.children.as_slice(), [item] if item
242 .children
243 .iter()
244 .filter(|block| !matches!(block, Block::BlankLine(_)))
245 .count()
246 <= 1)
247 {
248 diagnostics.push(Diagnostic::invalid(
249 list.meta.span,
250 "a loose list must hold two items or an item of two blocks",
251 ));
252 }
253 for (index, item) in list.children.iter().enumerate() {
254 diagnostics.at(index, |diagnostics| {
255 validate_item_number(list, item, diagnostics);
256 validate_item_first_line(list, item, diagnostics);
257 validate_item_blank_lines(list, index, item, diagnostics);
258 validate_task_item(item, diagnostics);
259 validate_blocks(&item.children, depth + 1, list.tight, diagnostics);
260 });
261 }
262}
263
264fn validate_task_item(item: &ListItem, diagnostics: &mut Report) {
267 if item.checked.is_none() {
268 return;
269 }
270 let first = item
271 .children
272 .iter()
273 .find(|block| !matches!(block, Block::Definition(_)));
274 if !matches!(first, Some(Block::Paragraph(paragraph)) if !paragraph.children.is_empty()) {
275 diagnostics.push(Diagnostic::invalid(
276 item.meta.span,
277 "task list item must hold a non-empty paragraph after its leading definitions",
278 ));
279 }
280}
281
282fn validate_taken_block(left: &Block, right: &Block, tight: bool, diagnostics: &mut Report) {
288 let message = if matches!(left, Block::HtmlBlock(node) if html_block_left_open(node)) {
289 "a block must not follow an HTML block that its value leaves open"
290 } else if matches!(right, Block::CodeBlock(code) if code.kind == CodeBlockKind::Indented)
291 && takes_indented_code(left)
292 {
293 "an indented code block must not be read as part of the block before it"
294 } else if tight && matches!(left, Block::Paragraph(_)) && takes_next_line(left, right) {
295 "in a tight list item, a block after a paragraph must be able to interrupt it"
296 } else if tight && takes_next_line(left, right) {
297 "in a tight list item, a block must not be read as part of the block before it"
298 } else {
299 return;
300 };
301 diagnostics.push(Diagnostic::invalid(right.span(), message));
302}
303
304fn takes_indented_code(left: &Block) -> bool {
311 match left {
312 Block::CodeBlock(code) => code.kind == CodeBlockKind::Indented,
313 Block::FootnoteDefinition(_) => true,
314 _ => false,
315 }
316}
317
318fn takes_next_line(left: &Block, right: &Block) -> bool {
327 match left {
328 Block::Paragraph(_) => !interrupts_paragraph(right),
329 Block::Definition(_) => {
333 !interrupts_paragraph(right)
334 && !matches!(right, Block::Paragraph(_) | Block::Definition(_))
335 }
336 Block::Table(_) => !starts_outside_paragraph(right, false),
337 Block::HtmlBlock(node) => html_block_ends_at_blank_line(node),
338 Block::BlockQuote(_) | Block::Alert(_)
339 if matches!(right, Block::BlockQuote(_) | Block::Alert(_)) =>
340 {
341 true
342 }
343 _ => match ends_in_open_paragraph(left) {
344 Some(through_quote) => {
347 !starts_outside_paragraph(right, true)
348 && !(through_quote && written_opens_fence_like(right))
349 }
350 None => false,
351 },
352 }
353}
354
355fn starts_outside_paragraph(block: &Block, lazy: bool) -> bool {
362 match block {
363 Block::Paragraph(_) | Block::Definition(_) | Block::Table(_) => false,
364 Block::HtmlBlock(node) => html_block_opens(node),
365 Block::CodeBlock(code) => !(lazy && code.kind == CodeBlockKind::Indented),
366 _ => true,
367 }
368}
369
370fn ends_in_open_paragraph(block: &Block) -> Option<bool> {
378 let mut block = block;
379 let mut nested = false;
380 let mut through_quote = false;
381 loop {
382 let children = match block {
383 Block::Paragraph(_) | Block::Definition(_) => return nested.then_some(through_quote),
384 Block::Alert(alert) if alert.children.is_empty() => return Some(true),
385 Block::BlockQuote(quote) => "e.children,
386 Block::Alert(alert) => &alert.children,
387 Block::FootnoteDefinition(footnote) => &footnote.children,
388 Block::List(list) => &list.children.last()?.children,
389 _ => return None,
390 };
391 through_quote |= matches!(block, Block::BlockQuote(_) | Block::Alert(_));
392 block = children.last()?;
393 nested = true;
394 }
395}
396
397fn validate_table(table: &Table, diagnostics: &mut Report) {
398 if table.rows.is_empty() {
399 diagnostics.push(Diagnostic::invalid(
400 table.meta.span,
401 "table must contain at least a header row",
402 ));
403 return;
404 }
405
406 let width = table.rows[0].cells.len();
407 if width == 0 {
408 diagnostics.push(Diagnostic::invalid(
409 table.meta.span,
410 "table header row must contain at least one cell",
411 ));
412 }
413
414 if table.alignments.len() != width {
415 diagnostics.push(Diagnostic::invalid(
416 table.meta.span,
417 "table alignment count must match header width",
418 ));
419 }
420
421 for (index, row) in table.rows.iter().enumerate() {
422 diagnostics.at(index, |diagnostics| {
423 if row.cells.len() != width {
424 diagnostics.push(Diagnostic::invalid(
425 row.meta.span,
426 "table row width must match header width",
427 ));
428 }
429 for (index, cell) in row.cells.iter().enumerate() {
430 diagnostics.at(index, |diagnostics| {
431 validate_inlines(&cell.children, diagnostics);
432 validate_cell_values(&cell.children, diagnostics);
433 validate_one_line(
434 &cell.children,
435 "a table cell cannot hold a line break",
436 diagnostics,
437 );
438 });
439 }
440 });
441 }
442}
443
444fn validate_cell_values(inlines: &[Inline], diagnostics: &mut Report) {
449 for inline in inlines {
450 let unwritable = match inline {
451 Inline::Code(node) => has_odd_escaped_pipe(&node.value),
452 Inline::Math(node) => has_odd_escaped_pipe(&node.value),
453 Inline::Html(node) => has_odd_escaped_pipe(&node.value),
454 Inline::Autolink(node) => has_odd_escaped_pipe(&node.text),
455 Inline::LinkReference(node) => has_odd_escaped_pipe(&node.label),
456 Inline::ImageReference(node) => has_odd_escaped_pipe(&node.label),
457 Inline::FootnoteReference(node) => has_odd_escaped_pipe(&node.label),
458 Inline::WikiLink(node) => {
459 has_odd_escaped_pipe(&node.target) || has_odd_escaped_pipe(&node.label)
460 }
461 _ => false,
462 };
463 if unwritable {
464 diagnostics.push(Diagnostic::invalid(
465 inline.span(),
466 "a value in a table cell cannot hold a `|` after an odd run of backslashes",
467 ));
468 }
469 validate_cell_values(inline.children(), diagnostics);
470 }
471}
472
473fn has_odd_escaped_pipe(value: &str) -> bool {
475 let mut backslashes = 0usize;
476 for byte in value.bytes() {
477 if byte == b'|' && backslashes % 2 == 1 {
478 return true;
479 }
480 backslashes = if byte == b'\\' { backslashes + 1 } else { 0 };
481 }
482 false
483}
484
485fn validate_one_line(inlines: &[Inline], message: &'static str, diagnostics: &mut Report) {
488 for inline in inlines {
489 if matches!(inline, Inline::SoftBreak(_) | Inline::LineBreak(_)) {
490 diagnostics.push(Diagnostic::invalid(inline.span(), message));
491 }
492 validate_one_line(inline.children(), message, diagnostics);
493 }
494}
495
496fn validate_leaf_directive(directive: &LeafDirective, diagnostics: &mut Report) {
497 validate_directive_name(directive.meta.span, &directive.name, diagnostics);
498 validate_directive_attributes(&directive.attributes, diagnostics);
499 validate_inlines(&directive.label, diagnostics);
500 validate_one_line(
501 &directive.label,
502 "a leaf directive label cannot hold a line break",
503 diagnostics,
504 );
505}
506
507fn validate_container_directive(
508 directive: &ContainerDirective,
509 depth: usize,
510 diagnostics: &mut Report,
511) {
512 validate_directive_name(directive.meta.span, &directive.name, diagnostics);
513 validate_directive_attributes(&directive.attributes, diagnostics);
514 validate_inlines(&directive.label, diagnostics);
515 validate_one_line(
516 &directive.label,
517 "a container directive label cannot hold a line break",
518 diagnostics,
519 );
520 validate_blocks(&directive.children, depth + 1, false, diagnostics);
521}
522
523fn validate_definition(
529 definition: &Definition,
530 depth: usize,
531 indented: bool,
532 diagnostics: &mut Report,
533) {
534 if definition
535 .identifier
536 .trim_matches([' ', '\t', '\n', '\r'])
537 .is_empty()
538 {
539 diagnostics.push(Diagnostic::invalid(
540 definition.meta.span,
541 "definition identifier cannot be empty",
542 ));
543 }
544 validate_reference_label(
545 definition.meta.span,
546 &definition.label,
547 &definition.identifier,
548 true,
549 diagnostics,
550 );
551 let footnote = definition
552 .label
553 .strip_prefix('^')
554 .is_some_and(is_footnote_label);
555 if footnote && depth < MAX_BLOCK_NESTING && !indented {
556 diagnostics.push(Diagnostic::invalid(
557 definition.meta.span,
558 "definition label cannot be `^` followed by a footnote label",
559 ));
560 }
561 validate_destination(
562 definition.meta.span,
563 &definition.destination,
564 definition.destination_kind,
565 diagnostics,
566 );
567}
568
569fn validate_reference_label(
573 span: Option<Span>,
574 label: &str,
575 identifier: &str,
576 written: bool,
577 diagnostics: &mut Report,
578) {
579 if normalize_label(label) != identifier {
580 diagnostics.push(Diagnostic::invalid(
581 span,
582 "reference identifier must be the normalized label",
583 ));
584 }
585 if written && !is_reference_label(label) {
586 diagnostics.push(Diagnostic::invalid(
587 span,
588 "reference label must be one the parser reads as a label",
589 ));
590 }
591}
592
593fn validate_footnote_label(
596 span: Option<Span>,
597 label: &str,
598 identifier: &str,
599 diagnostics: &mut Report,
600) {
601 if normalize_label(label) != identifier {
602 diagnostics.push(Diagnostic::invalid(
603 span,
604 "footnote identifier must be the normalized label",
605 ));
606 }
607 if !is_written_footnote_label(label) {
608 diagnostics.push(Diagnostic::invalid(
609 span,
610 "footnote label must be one the parser reads as a footnote label",
611 ));
612 }
613}
614
615fn validate_destination(
618 span: Option<Span>,
619 destination: &str,
620 kind: LinkDestinationKind,
621 diagnostics: &mut Report,
622) {
623 if kind == LinkDestinationKind::Bare && destination.is_empty() {
624 diagnostics.push(Diagnostic::invalid(
625 span,
626 "a bare destination cannot be empty",
627 ));
628 }
629}
630
631fn validate_code_block(code: &CodeBlock, diagnostics: &mut Report) {
635 if code.info.as_deref() == Some("") {
636 diagnostics.push(Diagnostic::invalid(
637 code.meta.span,
638 "code block info string cannot be empty",
639 ));
640 }
641 if code.kind == CodeBlockKind::Indented {
642 let value = code.value.replace("\r\n", "\n");
645 let body = value.strip_suffix(['\n', '\r']).unwrap_or(&value);
646 let mut lines = body.split(['\n', '\r']);
647 let first = lines.next().unwrap_or_default();
648 let last = lines.next_back().unwrap_or(first);
649 if is_blank(first) || is_blank(last) {
650 diagnostics.push(Diagnostic::invalid(
651 code.meta.span,
652 "indented code cannot be empty or start or end with a blank line",
653 ));
654 }
655 }
656}
657
658fn validate_frontmatter(frontmatter: &Frontmatter, depth: usize, diagnostics: &mut Report) {
661 if depth > 0 {
662 diagnostics.push(Diagnostic::invalid(
663 frontmatter.meta.span,
664 "frontmatter cannot be inside a container",
665 ));
666 }
667 if frontmatter
668 .value
669 .replace("\r\n", "\n")
670 .split(['\n', '\r'])
671 .any(|line| frontmatter_fence_kind(line) == Some(frontmatter.kind))
672 {
673 diagnostics.push(Diagnostic::invalid(
674 frontmatter.meta.span,
675 "frontmatter value cannot hold its own fence line",
676 ));
677 }
678}
679
680fn validate_inlines(inlines: &[Inline], diagnostics: &mut Report) {
684 if let Some(Inline::LineBreak(node)) = inlines.last() {
685 diagnostics.push(Diagnostic::invalid(
686 node.meta.span,
687 "hard line break cannot be the final inline of its container",
688 ));
689 }
690 validate_inline_nodes(inlines, diagnostics);
691}
692
693fn validate_inline_nodes(inlines: &[Inline], diagnostics: &mut Report) {
696 for (index, inline) in inlines.iter().enumerate() {
697 if let Some(before) = index.checked_sub(1).map(|before| &inlines[before]) {
698 validate_siblings(before, inline, diagnostics);
699 }
700 match inline {
701 Inline::Text(node) => {
702 if node.value.contains(['\n', '\r']) {
703 diagnostics.push(Diagnostic::invalid(
704 node.meta.span,
705 "text cannot hold a line ending; a line ends with a soft or hard break",
706 ));
707 }
708 }
709 Inline::Emphasis(_) | Inline::Strong(_) | Inline::Delete(_) | Inline::Mark(_) => {
710 validate_emphasis_container(inline, diagnostics)
711 }
712 Inline::Shortcode(node) => {
713 if node.glyph().is_none() {
714 diagnostics.push(Diagnostic::invalid(
715 node.meta.span,
716 "shortcode name must be a gemoji name",
717 ));
718 }
719 }
720 Inline::Tag(node) => {
721 if !crate::unicode_letters::is_tag_name(&node.name) {
722 diagnostics.push(Diagnostic::invalid(
723 node.meta.span,
724 "tag name must be a Unicode letter followed by letters, numbers, `/`, `_`, or `-`",
725 ));
726 }
727 }
728 Inline::Link(node) => {
729 validate_destination(
730 node.meta.span,
731 &node.destination,
732 node.destination_kind,
733 diagnostics,
734 );
735 validate_link_text(&node.children, diagnostics);
736 validate_inline_nodes(&node.children, diagnostics);
737 }
738 Inline::Autolink(node) => {
739 if node.destination().is_none() {
740 diagnostics.push(Diagnostic::invalid(
741 node.meta.span,
742 "autolink text must be exactly one autolink of its form",
743 ));
744 }
745 }
746 Inline::Image(node) => {
747 validate_destination(
748 node.meta.span,
749 &node.destination,
750 node.destination_kind,
751 diagnostics,
752 );
753 validate_inline_nodes(&node.alt, diagnostics);
754 }
755 Inline::LinkReference(node) => {
756 if node.identifier.is_empty() {
757 diagnostics.push(Diagnostic::invalid(
758 node.meta.span,
759 "link reference identifier cannot be empty",
760 ));
761 }
762 validate_reference_label(
763 node.meta.span,
764 &node.label,
765 &node.identifier,
766 node.kind == ReferenceKind::Full,
767 diagnostics,
768 );
769 validate_link_text(&node.children, diagnostics);
770 validate_inline_nodes(&node.children, diagnostics);
771 }
772 Inline::ImageReference(node) => {
773 if node.identifier.is_empty() {
774 diagnostics.push(Diagnostic::invalid(
775 node.meta.span,
776 "image reference identifier cannot be empty",
777 ));
778 }
779 validate_reference_label(
780 node.meta.span,
781 &node.label,
782 &node.identifier,
783 node.kind == ReferenceKind::Full,
784 diagnostics,
785 );
786 validate_inline_nodes(&node.alt, diagnostics);
787 }
788 Inline::Escape(node) => validate_escape(node, diagnostics),
789 Inline::CharacterReference(node) => {
790 if node.value().is_none() {
791 diagnostics.push(Diagnostic::invalid(
792 node.meta.span,
793 "character reference must be exactly one character reference",
794 ));
795 }
796 }
797 Inline::TextDirective(node) => validate_text_directive(node, diagnostics),
798 Inline::FootnoteReference(node) => {
799 if node.identifier.is_empty() {
800 diagnostics.push(Diagnostic::invalid(
801 node.meta.span,
802 "footnote reference identifier cannot be empty",
803 ));
804 }
805 validate_footnote_label(node.meta.span, &node.label, &node.identifier, diagnostics);
806 }
807 Inline::InlineFootnote(node) => {
808 if node.children.is_empty() {
809 diagnostics.push(Diagnostic::invalid(
810 node.meta.span,
811 "inline footnote cannot be empty",
812 ));
813 }
814 validate_inline_nodes(&node.children, diagnostics)
815 }
816 Inline::WikiLink(node) => {
817 if node.target.is_empty() {
818 diagnostics.push(Diagnostic::invalid(
819 node.meta.span,
820 "wikilink target cannot be empty",
821 ));
822 }
823 }
824 Inline::Code(node) => validate_code_inline(node, diagnostics),
825 Inline::Math(node) => validate_math_inline(node, diagnostics),
826 Inline::Html(_) | Inline::SoftBreak(_) | Inline::LineBreak(_) => {}
827 }
828 }
829}
830
831fn validate_siblings(before: &Inline, inline: &Inline, diagnostics: &mut Report) {
840 let after_break = matches!(before, Inline::SoftBreak(_) | Inline::LineBreak(_));
841 let blank_line = match inline {
842 Inline::SoftBreak(_) => true,
843 Inline::LineBreak(node) => node.kind == LineBreakKind::Spaces,
844 _ => false,
845 };
846 if after_break && blank_line {
847 diagnostics.push(Diagnostic::invalid(
848 inline.span(),
849 "a soft break or a spaces hard break cannot follow a line break",
850 ));
851 }
852 if matches!((before, inline), (Inline::Delete(_), Inline::Delete(_))) {
853 diagnostics.push(Diagnostic::invalid(
854 inline.span(),
855 "adjacent strikethroughs cannot be written",
856 ));
857 }
858}
859
860fn validate_emphasis_container(node: &Inline, diagnostics: &mut Report) {
868 let children = node.children();
869 let span = node.span();
870 if children.iter().all(is_empty_text) {
871 diagnostics.push(Diagnostic::invalid(
872 span,
873 "emphasis-like inline container cannot have empty children",
874 ));
875 } else if starts_with_whitespace(children) || ends_with_whitespace(children) {
876 diagnostics.push(Diagnostic::invalid(
877 span,
878 "emphasis-like inline content cannot start or end with whitespace",
879 ));
880 }
881 let joined = match (node, children) {
882 (Inline::Emphasis(outer), [Inline::Emphasis(inner)]) => outer.delimiter == inner.delimiter,
883 (Inline::Strong(outer), [Inline::Emphasis(inner)]) => outer.delimiter == inner.delimiter,
884 (Inline::Delete(_), [Inline::Delete(_)]) | (Inline::Mark(_), [Inline::Mark(_)]) => true,
885 _ => false,
886 };
887 if joined {
888 diagnostics.push(Diagnostic::invalid(
889 span,
890 "emphasis-like span cannot hold only a span written with the same delimiter",
891 ));
892 }
893 validate_inlines(children, diagnostics);
894}
895
896fn starts_with_whitespace(inlines: &[Inline]) -> bool {
901 match inlines.iter().find(|inline| !is_empty_text(inline)) {
902 Some(Inline::Text(text)) => text.value.starts_with(char::is_whitespace),
903 Some(Inline::SoftBreak(_)) => true,
904 Some(Inline::LineBreak(node)) => node.kind == LineBreakKind::Spaces,
905 _ => false,
906 }
907}
908
909fn ends_with_whitespace(inlines: &[Inline]) -> bool {
912 match inlines.iter().rev().find(|inline| !is_empty_text(inline)) {
913 Some(Inline::Text(text)) => text.value.ends_with(char::is_whitespace),
914 Some(Inline::SoftBreak(_) | Inline::LineBreak(_)) => true,
915 _ => false,
916 }
917}
918
919fn is_empty_text(inline: &Inline) -> bool {
920 matches!(inline, Inline::Text(text) if text.value.is_empty())
921}
922
923fn validate_link_text(inlines: &[Inline], diagnostics: &mut Report) {
925 for inline in inlines {
926 match inline {
927 Inline::Link(_)
928 | Inline::Autolink(_)
929 | Inline::LinkReference(_)
930 | Inline::WikiLink(_) => {
931 diagnostics.push(Diagnostic::invalid(
932 inline.span(),
933 "link text cannot hold a link",
934 ));
935 }
936 _ => validate_link_text(inline.children(), diagnostics),
937 }
938 }
939}
940
941fn validate_escape(escape: &Escape, diagnostics: &mut Report) {
942 if !escape.value.is_ascii_punctuation() {
943 diagnostics.push(Diagnostic::invalid(
944 escape.meta.span,
945 "escaped value must be an ASCII punctuation character",
946 ));
947 }
948}
949
950fn validate_code_inline(code: &CodeInline, diagnostics: &mut Report) {
953 if code.value.is_empty() {
954 diagnostics.push(Diagnostic::invalid(
955 code.meta.span,
956 "inline code value cannot be empty",
957 ));
958 }
959 if code.value.contains(['\n', '\r']) {
960 diagnostics.push(Diagnostic::invalid(
961 code.meta.span,
962 "inline code value cannot hold a line ending",
963 ));
964 }
965}
966
967fn validate_math_inline(math: &MathInline, diagnostics: &mut Report) {
971 if math.value.is_empty() {
972 diagnostics.push(Diagnostic::invalid(
973 math.meta.span,
974 "inline math value cannot be empty",
975 ));
976 }
977 match math.kind {
978 MathInlineKind::Dollar { dollars: 0 } => diagnostics.push(Diagnostic::invalid(
979 math.meta.span,
980 "dollar-fenced inline math must have a fence length of at least 1",
981 )),
982 MathInlineKind::Code if math.value.contains("`$") => diagnostics.push(Diagnostic::invalid(
983 math.meta.span,
984 "code-form inline math value cannot contain a backtick followed by `$`",
985 )),
986 MathInlineKind::Dollar { .. }
990 if !math.value.is_empty()
991 && !reads_as_inline_math(&write_inline_math(math), &math.value, math.kind) =>
992 {
993 diagnostics.push(Diagnostic::invalid(
994 math.meta.span,
995 "dollar-fenced inline math must read back as written",
996 ));
997 }
998 _ => {}
999 }
1000}
1001
1002fn validate_item_number(list: &List, item: &ListItem, diagnostics: &mut Report) {
1003 let Some(number) = item.number else {
1004 return;
1005 };
1006 if !list.ordered {
1007 diagnostics.push(Diagnostic::invalid(
1008 item.meta.span,
1009 "a bullet list item cannot hold a number",
1010 ));
1011 } else if number > crate::parse::MAX_ORDERED_NUMBER {
1012 diagnostics.push(Diagnostic::invalid(
1013 item.meta.span,
1014 "ordered list item number must be representable in at most 9 digits",
1015 ));
1016 }
1017}
1018
1019fn validate_item_first_line(list: &List, item: &ListItem, diagnostics: &mut Report) {
1023 if item_misreads_after_marker(list, item) {
1024 diagnostics.push(Diagnostic::invalid(
1025 item.meta.span,
1026 "a list item's first line must not read as padding after its marker or as a thematic break with it",
1027 ));
1028 }
1029}
1030
1031fn validate_item_blank_lines(list: &List, index: usize, item: &ListItem, diagnostics: &mut Report) {
1035 let last_item = index + 1 == list.children.len();
1036 for (at, block) in item.children.iter().enumerate() {
1037 let Block::BlankLine(blank) = block else {
1038 continue;
1039 };
1040 let message = if list.tight {
1041 "a tight list cannot hold a blank line"
1042 } else if at == 0 {
1043 "a list item cannot open with a blank line"
1044 } else if last_item && at + 1 == item.children.len() {
1045 "the last item of a list cannot end with a blank line"
1046 } else {
1047 continue;
1048 };
1049 diagnostics.at(at, |diagnostics| {
1050 diagnostics.push(Diagnostic::invalid(blank.meta.span, message))
1051 });
1052 }
1053}
1054
1055fn validate_text_directive(directive: &TextDirective, diagnostics: &mut Report) {
1056 validate_directive_name(directive.meta.span, &directive.name, diagnostics);
1057 validate_directive_attributes(&directive.attributes, diagnostics);
1058 validate_inline_nodes(&directive.label, diagnostics);
1059}
1060
1061fn validate_directive_name(span: Option<Span>, name: &str, diagnostics: &mut Report) {
1062 if !is_directive_name(name) {
1063 diagnostics.push(Diagnostic::invalid(
1064 span,
1065 "directive name must be runs of ASCII letters joined by single `-`",
1066 ));
1067 }
1068}
1069
1070fn validate_directive_attributes(attributes: &[DirectiveAttribute], diagnostics: &mut Report) {
1071 for attribute in attributes {
1072 if !is_attribute_name(&attribute.name) {
1073 diagnostics.push(Diagnostic::invalid(
1074 None,
1075 "directive attribute name must start with a letter, `_`, or `-`",
1076 ));
1077 }
1078 }
1079}
1080
1081pub(crate) fn is_directive_name(name: &str) -> bool {
1083 name.split('-')
1084 .all(|run| !run.is_empty() && run.bytes().all(|byte| byte.is_ascii_alphabetic()))
1085}
1086
1087pub(crate) fn is_attribute_name(name: &str) -> bool {
1088 let mut chars = name.chars();
1089 let Some(first) = chars.next() else {
1090 return false;
1091 };
1092 if !(first.is_ascii_alphabetic() || first == '_' || first == '-') {
1093 return false;
1094 }
1095 chars.all(|char| char.is_ascii_alphanumeric() || char == '_' || char == '-' || char == ':')
1096}