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, ReferenceKind, Table, TextDirective,
13 },
14 diagnostic::Diagnostic,
15 parse::{
16 alert_title, frontmatter_fence_kind, is_blank, is_footnote_label, is_reference_label,
17 is_written_footnote_label, normalize_label, reads_as_inline_math, MAX_BLOCK_NESTING,
18 },
19 serialize::{write_inline_math, written_after_paragraph_interrupts, written_marker},
20 span::Span,
21};
22
23impl Document {
24 pub fn validate(&self) -> Vec<Diagnostic> {
27 validate_document(self)
28 }
29}
30
31pub(crate) fn validate_document(document: &Document) -> Vec<Diagnostic> {
32 let mut diagnostics = Vec::new();
33 validate_blocks(&document.children, 0, &mut diagnostics);
34 diagnostics
35}
36
37fn validate_blocks(blocks: &[Block], depth: usize, diagnostics: &mut Vec<Diagnostic>) {
43 let mut before: Option<&Block> = None;
46 for block in blocks {
47 if let (Some(Block::List(before)), Block::List(list)) = (before, block) {
48 if written_marker(before) == written_marker(list) {
49 diagnostics.push(Diagnostic::invalid(
50 list.meta.span,
51 "adjacent lists cannot use the same marker",
52 ));
53 }
54 }
55 if let Block::Definition(definition) = block {
56 let indented = match before {
60 Some(before) => matches!(before, Block::Definition(_)),
61 None => depth > 0,
62 };
63 validate_definition(definition, depth, indented, diagnostics);
64 }
65 validate_block(block, depth, diagnostics);
66 if !matches!(block, Block::BlankLine(_)) {
67 before = Some(block);
68 }
69 }
70}
71
72fn validate_block(block: &Block, depth: usize, diagnostics: &mut Vec<Diagnostic>) {
73 match block {
74 Block::Paragraph(paragraph) => {
75 if paragraph.children.iter().all(is_empty_text) {
77 diagnostics.push(Diagnostic::invalid(
78 paragraph.meta.span,
79 "paragraph cannot be empty",
80 ));
81 }
82 validate_inlines(¶graph.children, diagnostics);
83 }
84 Block::Heading(heading) => validate_heading(heading, diagnostics),
85 Block::BlockQuote(block_quote) => {
86 validate_blocks(&block_quote.children, depth + 1, diagnostics);
87 }
88 Block::Alert(alert) => validate_alert(alert, depth, diagnostics),
89 Block::List(list) => validate_list(list, depth, diagnostics),
90 Block::Table(table) => validate_table(table, diagnostics),
91 Block::FootnoteDefinition(definition) => {
92 if definition.identifier.is_empty() {
93 diagnostics.push(Diagnostic::invalid(
94 definition.meta.span,
95 "footnote definition identifier cannot be empty",
96 ));
97 }
98 validate_footnote_label(
99 definition.meta.span,
100 &definition.label,
101 &definition.identifier,
102 diagnostics,
103 );
104 let edges = [definition.children.first(), definition.children.last()];
107 if let Some(Block::BlankLine(blank)) = edges
108 .into_iter()
109 .flatten()
110 .find(|block| matches!(block, Block::BlankLine(_)))
111 {
112 diagnostics.push(Diagnostic::invalid(
113 blank.meta.span,
114 "a footnote definition cannot open or end with a blank line",
115 ));
116 }
117 validate_blocks(&definition.children, depth + 1, diagnostics);
118 }
119 Block::Definition(_) => {}
121 Block::LeafDirective(directive) => validate_leaf_directive(directive, diagnostics),
122 Block::ContainerDirective(directive) => {
123 validate_container_directive(directive, depth, diagnostics)
124 }
125 Block::HtmlContainer(container) => validate_html_container(container, depth, diagnostics),
126 Block::CodeBlock(code) => validate_code_block(code, diagnostics),
127 Block::Frontmatter(frontmatter) => validate_frontmatter(frontmatter, depth, diagnostics),
128 Block::ThematicBreak(_)
129 | Block::HtmlBlock(_)
130 | Block::MathBlock(_)
131 | Block::BlankLine(_) => {}
132 }
133}
134
135fn validate_html_container(
136 container: &HtmlContainer,
137 depth: usize,
138 diagnostics: &mut Vec<Diagnostic>,
139) {
140 if container.opening.name.is_empty() {
141 diagnostics.push(Diagnostic::invalid(
142 container.opening.meta.span,
143 "HTML container opening tag name cannot be empty",
144 ));
145 }
146 if container.closing.name.is_empty() {
147 diagnostics.push(Diagnostic::invalid(
148 container.closing.meta.span,
149 "HTML container closing tag name cannot be empty",
150 ));
151 }
152 if container.opening.name != container.closing.name {
153 diagnostics.push(Diagnostic::invalid(
154 container.meta.span,
155 "HTML container opening and closing tag names must match",
156 ));
157 }
158 if container.opening.raw.is_empty() {
159 diagnostics.push(Diagnostic::invalid(
160 container.opening.meta.span,
161 "HTML container opening tag source cannot be empty",
162 ));
163 }
164 if container.closing.raw.is_empty() {
165 diagnostics.push(Diagnostic::invalid(
166 container.closing.meta.span,
167 "HTML container closing tag source cannot be empty",
168 ));
169 }
170
171 match &container.content {
172 HtmlContainerContent::Blocks(children) => {
173 validate_blocks(children, depth + 1, diagnostics);
174 }
175 HtmlContainerContent::Inlines(children) => validate_inlines(children, diagnostics),
176 }
177}
178
179fn validate_heading(heading: &Heading, diagnostics: &mut Vec<Diagnostic>) {
180 if heading.depth == 0 || heading.depth > 6 {
181 diagnostics.push(Diagnostic::invalid(
182 heading.meta.span,
183 "heading depth must be in the range 1..=6",
184 ));
185 }
186 validate_inlines(&heading.children, diagnostics);
187}
188
189fn validate_alert(alert: &Alert, depth: usize, diagnostics: &mut Vec<Diagnostic>) {
192 if let Some(title) = &alert.title {
193 if alert_title(title).as_deref() != Some(title.as_str()) {
194 diagnostics.push(Diagnostic::invalid(
195 alert.meta.span,
196 "alert title cannot be empty or start or end with a space or a tab",
197 ));
198 }
199 }
200 validate_blocks(&alert.children, depth + 1, diagnostics);
201}
202
203fn validate_list(list: &List, depth: usize, diagnostics: &mut Vec<Diagnostic>) {
204 if list.children.is_empty() {
205 diagnostics.push(Diagnostic::invalid(
206 list.meta.span,
207 "list must hold at least one item",
208 ));
209 }
210 if !list.tight
213 && matches!(list.children.as_slice(), [item] if item
214 .children
215 .iter()
216 .filter(|block| !matches!(block, Block::BlankLine(_)))
217 .count()
218 <= 1)
219 {
220 diagnostics.push(Diagnostic::invalid(
221 list.meta.span,
222 "a loose list must hold two items or an item of two blocks",
223 ));
224 }
225 for (index, item) in list.children.iter().enumerate() {
226 validate_item_number(list, item, diagnostics);
227 validate_item_blank_lines(list, index, item, diagnostics);
228 validate_task_item(item, diagnostics);
229 if list.tight {
230 validate_tight_item(item, diagnostics);
231 }
232 validate_blocks(&item.children, depth + 1, diagnostics);
233 }
234}
235
236fn validate_task_item(item: &ListItem, diagnostics: &mut Vec<Diagnostic>) {
239 if item.checked.is_none() {
240 return;
241 }
242 let first = item
243 .children
244 .iter()
245 .find(|block| !matches!(block, Block::Definition(_)));
246 if !matches!(first, Some(Block::Paragraph(paragraph)) if !paragraph.children.is_empty()) {
247 diagnostics.push(Diagnostic::invalid(
248 item.meta.span,
249 "task list item must hold a non-empty paragraph after its leading definitions",
250 ));
251 }
252}
253
254fn validate_tight_item(item: &ListItem, diagnostics: &mut Vec<Diagnostic>) {
257 for pair in item.children.windows(2) {
258 let [Block::Paragraph(_), block] = pair else {
259 continue;
260 };
261 if !written_after_paragraph_interrupts(block) {
262 diagnostics.push(Diagnostic::invalid(
263 block.span(),
264 "in a tight list item, a block after a paragraph must be able to interrupt it",
265 ));
266 }
267 }
268}
269
270fn validate_table(table: &Table, diagnostics: &mut Vec<Diagnostic>) {
271 if table.rows.is_empty() {
272 diagnostics.push(Diagnostic::invalid(
273 table.meta.span,
274 "table must contain at least a header row",
275 ));
276 return;
277 }
278
279 let width = table.rows[0].cells.len();
280 if width == 0 {
281 diagnostics.push(Diagnostic::invalid(
282 table.meta.span,
283 "table header row must contain at least one cell",
284 ));
285 }
286
287 if table.alignments.len() != width {
288 diagnostics.push(Diagnostic::invalid(
289 table.meta.span,
290 "table alignment count must match header width",
291 ));
292 }
293
294 for row in &table.rows {
295 if row.cells.len() != width {
296 diagnostics.push(Diagnostic::invalid(
297 row.meta.span,
298 "table row width must match header width",
299 ));
300 }
301 for cell in &row.cells {
302 validate_inlines(&cell.children, diagnostics);
303 validate_cell_values(&cell.children, diagnostics);
304 validate_one_line(
305 &cell.children,
306 "a table cell cannot hold a line break",
307 diagnostics,
308 );
309 }
310 }
311}
312
313fn validate_cell_values(inlines: &[Inline], diagnostics: &mut Vec<Diagnostic>) {
318 for inline in inlines {
319 let unwritable = match inline {
320 Inline::Code(node) => has_odd_escaped_pipe(&node.value),
321 Inline::Math(node) => has_odd_escaped_pipe(&node.value),
322 Inline::Html(node) => has_odd_escaped_pipe(&node.value),
323 Inline::Autolink(node) => has_odd_escaped_pipe(&node.text),
324 Inline::LinkReference(node) => has_odd_escaped_pipe(&node.label),
325 Inline::ImageReference(node) => has_odd_escaped_pipe(&node.label),
326 Inline::FootnoteReference(node) => has_odd_escaped_pipe(&node.label),
327 Inline::WikiLink(node) => {
328 has_odd_escaped_pipe(&node.target) || has_odd_escaped_pipe(&node.label)
329 }
330 _ => false,
331 };
332 if unwritable {
333 diagnostics.push(Diagnostic::invalid(
334 inline.span(),
335 "a value in a table cell cannot hold a `|` after an odd run of backslashes",
336 ));
337 }
338 validate_cell_values(inline.children(), diagnostics);
339 }
340}
341
342fn has_odd_escaped_pipe(value: &str) -> bool {
344 let mut backslashes = 0usize;
345 for byte in value.bytes() {
346 if byte == b'|' && backslashes % 2 == 1 {
347 return true;
348 }
349 backslashes = if byte == b'\\' { backslashes + 1 } else { 0 };
350 }
351 false
352}
353
354fn validate_one_line(inlines: &[Inline], message: &'static str, diagnostics: &mut Vec<Diagnostic>) {
357 for inline in inlines {
358 if matches!(inline, Inline::SoftBreak(_) | Inline::LineBreak(_)) {
359 diagnostics.push(Diagnostic::invalid(inline.span(), message));
360 }
361 validate_one_line(inline.children(), message, diagnostics);
362 }
363}
364
365fn validate_leaf_directive(directive: &LeafDirective, diagnostics: &mut Vec<Diagnostic>) {
366 validate_directive_name(directive.meta.span, &directive.name, diagnostics);
367 validate_directive_attributes(&directive.attributes, diagnostics);
368 validate_inlines(&directive.label, diagnostics);
369 validate_one_line(
370 &directive.label,
371 "a leaf directive label cannot hold a line break",
372 diagnostics,
373 );
374}
375
376fn validate_container_directive(
377 directive: &ContainerDirective,
378 depth: usize,
379 diagnostics: &mut Vec<Diagnostic>,
380) {
381 validate_directive_name(directive.meta.span, &directive.name, diagnostics);
382 validate_directive_attributes(&directive.attributes, diagnostics);
383 validate_inlines(&directive.label, diagnostics);
384 validate_one_line(
385 &directive.label,
386 "a container directive label cannot hold a line break",
387 diagnostics,
388 );
389 validate_blocks(&directive.children, depth + 1, diagnostics);
390}
391
392fn validate_definition(
398 definition: &Definition,
399 depth: usize,
400 indented: bool,
401 diagnostics: &mut Vec<Diagnostic>,
402) {
403 if definition
404 .identifier
405 .trim_matches([' ', '\t', '\n', '\r'])
406 .is_empty()
407 {
408 diagnostics.push(Diagnostic::invalid(
409 definition.meta.span,
410 "definition identifier cannot be empty",
411 ));
412 }
413 validate_reference_label(
414 definition.meta.span,
415 &definition.label,
416 &definition.identifier,
417 true,
418 diagnostics,
419 );
420 let footnote = definition
421 .label
422 .strip_prefix('^')
423 .is_some_and(is_footnote_label);
424 if footnote && depth < MAX_BLOCK_NESTING && !indented {
425 diagnostics.push(Diagnostic::invalid(
426 definition.meta.span,
427 "definition label cannot be `^` followed by a footnote label",
428 ));
429 }
430 validate_destination(
431 definition.meta.span,
432 &definition.destination,
433 definition.destination_kind,
434 diagnostics,
435 );
436}
437
438fn validate_reference_label(
442 span: Option<Span>,
443 label: &str,
444 identifier: &str,
445 written: bool,
446 diagnostics: &mut Vec<Diagnostic>,
447) {
448 if normalize_label(label) != identifier {
449 diagnostics.push(Diagnostic::invalid(
450 span,
451 "reference identifier must be the normalized label",
452 ));
453 }
454 if written && !is_reference_label(label) {
455 diagnostics.push(Diagnostic::invalid(
456 span,
457 "reference label must be one the parser reads as a label",
458 ));
459 }
460}
461
462fn validate_footnote_label(
465 span: Option<Span>,
466 label: &str,
467 identifier: &str,
468 diagnostics: &mut Vec<Diagnostic>,
469) {
470 if normalize_label(label) != identifier {
471 diagnostics.push(Diagnostic::invalid(
472 span,
473 "footnote identifier must be the normalized label",
474 ));
475 }
476 if !is_written_footnote_label(label) {
477 diagnostics.push(Diagnostic::invalid(
478 span,
479 "footnote label must be one the parser reads as a footnote label",
480 ));
481 }
482}
483
484fn validate_destination(
487 span: Option<Span>,
488 destination: &str,
489 kind: LinkDestinationKind,
490 diagnostics: &mut Vec<Diagnostic>,
491) {
492 if kind == LinkDestinationKind::Bare && destination.is_empty() {
493 diagnostics.push(Diagnostic::invalid(
494 span,
495 "a bare destination cannot be empty",
496 ));
497 }
498}
499
500fn validate_code_block(code: &CodeBlock, diagnostics: &mut Vec<Diagnostic>) {
504 if code.info.as_deref() == Some("") {
505 diagnostics.push(Diagnostic::invalid(
506 code.meta.span,
507 "code block info string cannot be empty",
508 ));
509 }
510 if code.kind == CodeBlockKind::Indented {
511 let value = code.value.replace("\r\n", "\n");
514 let body = value.strip_suffix(['\n', '\r']).unwrap_or(&value);
515 let mut lines = body.split(['\n', '\r']);
516 let first = lines.next().unwrap_or_default();
517 let last = lines.next_back().unwrap_or(first);
518 if is_blank(first) || is_blank(last) {
519 diagnostics.push(Diagnostic::invalid(
520 code.meta.span,
521 "indented code cannot be empty or start or end with a blank line",
522 ));
523 }
524 }
525}
526
527fn validate_frontmatter(
530 frontmatter: &Frontmatter,
531 depth: usize,
532 diagnostics: &mut Vec<Diagnostic>,
533) {
534 if depth > 0 {
535 diagnostics.push(Diagnostic::invalid(
536 frontmatter.meta.span,
537 "frontmatter cannot be inside a container",
538 ));
539 }
540 if frontmatter
541 .value
542 .replace("\r\n", "\n")
543 .split(['\n', '\r'])
544 .any(|line| frontmatter_fence_kind(line) == Some(frontmatter.kind))
545 {
546 diagnostics.push(Diagnostic::invalid(
547 frontmatter.meta.span,
548 "frontmatter value cannot hold its own fence line",
549 ));
550 }
551}
552
553fn validate_inlines(inlines: &[Inline], diagnostics: &mut Vec<Diagnostic>) {
557 if let Some(Inline::LineBreak(node)) = inlines.last() {
558 diagnostics.push(Diagnostic::invalid(
559 node.meta.span,
560 "hard line break cannot be the final inline of its container",
561 ));
562 }
563 validate_inline_nodes(inlines, diagnostics);
564}
565
566fn validate_inline_nodes(inlines: &[Inline], diagnostics: &mut Vec<Diagnostic>) {
569 for (index, inline) in inlines.iter().enumerate() {
570 if let Some(before) = index.checked_sub(1).map(|before| &inlines[before]) {
571 validate_siblings(before, inline, diagnostics);
572 }
573 match inline {
574 Inline::Text(node) => {
575 if node.value.contains(['\n', '\r']) {
576 diagnostics.push(Diagnostic::invalid(
577 node.meta.span,
578 "text cannot hold a line ending; a line ends with a soft or hard break",
579 ));
580 }
581 }
582 Inline::Emphasis(_) | Inline::Strong(_) | Inline::Delete(_) | Inline::Mark(_) => {
583 validate_emphasis_container(inline, diagnostics)
584 }
585 Inline::Shortcode(node) => {
586 if node.glyph().is_none() {
587 diagnostics.push(Diagnostic::invalid(
588 node.meta.span,
589 "shortcode name must be a gemoji name",
590 ));
591 }
592 }
593 Inline::Tag(node) => {
594 if !crate::unicode_letters::is_tag_name(&node.name) {
595 diagnostics.push(Diagnostic::invalid(
596 node.meta.span,
597 "tag name must be a Unicode letter followed by letters, numbers, `/`, `_`, or `-`",
598 ));
599 }
600 }
601 Inline::Link(node) => {
602 validate_destination(
603 node.meta.span,
604 &node.destination,
605 node.destination_kind,
606 diagnostics,
607 );
608 validate_link_text(&node.children, diagnostics);
609 validate_inline_nodes(&node.children, diagnostics);
610 }
611 Inline::Autolink(node) => {
612 if node.destination().is_none() {
613 diagnostics.push(Diagnostic::invalid(
614 node.meta.span,
615 "autolink text must be exactly one autolink of its form",
616 ));
617 }
618 }
619 Inline::Image(node) => {
620 validate_destination(
621 node.meta.span,
622 &node.destination,
623 node.destination_kind,
624 diagnostics,
625 );
626 validate_inline_nodes(&node.alt, diagnostics);
627 }
628 Inline::LinkReference(node) => {
629 if node.identifier.is_empty() {
630 diagnostics.push(Diagnostic::invalid(
631 node.meta.span,
632 "link reference identifier cannot be empty",
633 ));
634 }
635 validate_reference_label(
636 node.meta.span,
637 &node.label,
638 &node.identifier,
639 node.kind == ReferenceKind::Full,
640 diagnostics,
641 );
642 validate_link_text(&node.children, diagnostics);
643 validate_inline_nodes(&node.children, diagnostics);
644 }
645 Inline::ImageReference(node) => {
646 if node.identifier.is_empty() {
647 diagnostics.push(Diagnostic::invalid(
648 node.meta.span,
649 "image reference identifier cannot be empty",
650 ));
651 }
652 validate_reference_label(
653 node.meta.span,
654 &node.label,
655 &node.identifier,
656 node.kind == ReferenceKind::Full,
657 diagnostics,
658 );
659 validate_inline_nodes(&node.alt, diagnostics);
660 }
661 Inline::Escape(node) => validate_escape(node, diagnostics),
662 Inline::CharacterReference(node) => {
663 if node.value().is_none() {
664 diagnostics.push(Diagnostic::invalid(
665 node.meta.span,
666 "character reference must be exactly one character reference",
667 ));
668 }
669 }
670 Inline::TextDirective(node) => validate_text_directive(node, diagnostics),
671 Inline::FootnoteReference(node) => {
672 if node.identifier.is_empty() {
673 diagnostics.push(Diagnostic::invalid(
674 node.meta.span,
675 "footnote reference identifier cannot be empty",
676 ));
677 }
678 validate_footnote_label(node.meta.span, &node.label, &node.identifier, diagnostics);
679 }
680 Inline::InlineFootnote(node) => {
681 if node.children.is_empty() {
682 diagnostics.push(Diagnostic::invalid(
683 node.meta.span,
684 "inline footnote cannot be empty",
685 ));
686 }
687 validate_inline_nodes(&node.children, diagnostics)
688 }
689 Inline::WikiLink(node) => {
690 if node.target.is_empty() {
691 diagnostics.push(Diagnostic::invalid(
692 node.meta.span,
693 "wikilink target cannot be empty",
694 ));
695 }
696 }
697 Inline::Code(node) => validate_code_inline(node, diagnostics),
698 Inline::Math(node) => validate_math_inline(node, diagnostics),
699 Inline::Html(_) | Inline::SoftBreak(_) | Inline::LineBreak(_) => {}
700 }
701 }
702}
703
704fn validate_siblings(before: &Inline, inline: &Inline, diagnostics: &mut Vec<Diagnostic>) {
713 let after_break = matches!(before, Inline::SoftBreak(_) | Inline::LineBreak(_));
714 let blank_line = match inline {
715 Inline::SoftBreak(_) => true,
716 Inline::LineBreak(node) => node.kind == LineBreakKind::Spaces,
717 _ => false,
718 };
719 if after_break && blank_line {
720 diagnostics.push(Diagnostic::invalid(
721 inline.span(),
722 "a soft break or a spaces hard break cannot follow a line break",
723 ));
724 }
725 if matches!((before, inline), (Inline::Delete(_), Inline::Delete(_))) {
726 diagnostics.push(Diagnostic::invalid(
727 inline.span(),
728 "adjacent strikethroughs cannot be written",
729 ));
730 }
731}
732
733fn validate_emphasis_container(node: &Inline, diagnostics: &mut Vec<Diagnostic>) {
741 let children = node.children();
742 let span = node.span();
743 if children.iter().all(is_empty_text) {
744 diagnostics.push(Diagnostic::invalid(
745 span,
746 "emphasis-like inline container cannot have empty children",
747 ));
748 } else if starts_with_whitespace(children) || ends_with_whitespace(children) {
749 diagnostics.push(Diagnostic::invalid(
750 span,
751 "emphasis-like inline content cannot start or end with whitespace",
752 ));
753 }
754 let joined = match (node, children) {
755 (Inline::Emphasis(outer), [Inline::Emphasis(inner)]) => outer.delimiter == inner.delimiter,
756 (Inline::Strong(outer), [Inline::Emphasis(inner)]) => outer.delimiter == inner.delimiter,
757 (Inline::Delete(_), [Inline::Delete(_)]) | (Inline::Mark(_), [Inline::Mark(_)]) => true,
758 _ => false,
759 };
760 if joined {
761 diagnostics.push(Diagnostic::invalid(
762 span,
763 "emphasis-like span cannot hold only a span written with the same delimiter",
764 ));
765 }
766 validate_inlines(children, diagnostics);
767}
768
769fn starts_with_whitespace(inlines: &[Inline]) -> bool {
774 match inlines.iter().find(|inline| !is_empty_text(inline)) {
775 Some(Inline::Text(text)) => text.value.starts_with(char::is_whitespace),
776 Some(Inline::SoftBreak(_)) => true,
777 Some(Inline::LineBreak(node)) => node.kind == LineBreakKind::Spaces,
778 _ => false,
779 }
780}
781
782fn ends_with_whitespace(inlines: &[Inline]) -> bool {
785 match inlines.iter().rev().find(|inline| !is_empty_text(inline)) {
786 Some(Inline::Text(text)) => text.value.ends_with(char::is_whitespace),
787 Some(Inline::SoftBreak(_) | Inline::LineBreak(_)) => true,
788 _ => false,
789 }
790}
791
792fn is_empty_text(inline: &Inline) -> bool {
793 matches!(inline, Inline::Text(text) if text.value.is_empty())
794}
795
796fn validate_link_text(inlines: &[Inline], diagnostics: &mut Vec<Diagnostic>) {
798 for inline in inlines {
799 match inline {
800 Inline::Link(_)
801 | Inline::Autolink(_)
802 | Inline::LinkReference(_)
803 | Inline::WikiLink(_) => {
804 diagnostics.push(Diagnostic::invalid(
805 inline.span(),
806 "link text cannot hold a link",
807 ));
808 }
809 _ => validate_link_text(inline.children(), diagnostics),
810 }
811 }
812}
813
814fn validate_escape(escape: &Escape, diagnostics: &mut Vec<Diagnostic>) {
815 if !escape.value.is_ascii_punctuation() {
816 diagnostics.push(Diagnostic::invalid(
817 escape.meta.span,
818 "escaped value must be an ASCII punctuation character",
819 ));
820 }
821}
822
823fn validate_code_inline(code: &CodeInline, diagnostics: &mut Vec<Diagnostic>) {
826 if code.value.is_empty() {
827 diagnostics.push(Diagnostic::invalid(
828 code.meta.span,
829 "inline code value cannot be empty",
830 ));
831 }
832 if code.value.contains(['\n', '\r']) {
833 diagnostics.push(Diagnostic::invalid(
834 code.meta.span,
835 "inline code value cannot hold a line ending",
836 ));
837 }
838}
839
840fn validate_math_inline(math: &MathInline, diagnostics: &mut Vec<Diagnostic>) {
844 if math.value.is_empty() {
845 diagnostics.push(Diagnostic::invalid(
846 math.meta.span,
847 "inline math value cannot be empty",
848 ));
849 }
850 match math.kind {
851 MathInlineKind::Dollar { dollars: 0 } => diagnostics.push(Diagnostic::invalid(
852 math.meta.span,
853 "dollar-fenced inline math must have a fence length of at least 1",
854 )),
855 MathInlineKind::Code if math.value.contains("`$") => diagnostics.push(Diagnostic::invalid(
856 math.meta.span,
857 "code-form inline math value cannot contain a backtick followed by `$`",
858 )),
859 MathInlineKind::Dollar { .. }
863 if !math.value.is_empty()
864 && !reads_as_inline_math(&write_inline_math(math), &math.value, math.kind) =>
865 {
866 diagnostics.push(Diagnostic::invalid(
867 math.meta.span,
868 "dollar-fenced inline math must read back as written",
869 ));
870 }
871 _ => {}
872 }
873}
874
875fn validate_item_number(list: &List, item: &ListItem, diagnostics: &mut Vec<Diagnostic>) {
876 let Some(number) = item.number else {
877 return;
878 };
879 if !list.ordered {
880 diagnostics.push(Diagnostic::invalid(
881 item.meta.span,
882 "a bullet list item cannot hold a number",
883 ));
884 } else if number > crate::parse::MAX_ORDERED_NUMBER {
885 diagnostics.push(Diagnostic::invalid(
886 item.meta.span,
887 "ordered list item number must be representable in at most 9 digits",
888 ));
889 }
890}
891
892fn validate_item_blank_lines(
896 list: &List,
897 index: usize,
898 item: &ListItem,
899 diagnostics: &mut Vec<Diagnostic>,
900) {
901 let last_item = index + 1 == list.children.len();
902 for (at, block) in item.children.iter().enumerate() {
903 let Block::BlankLine(blank) = block else {
904 continue;
905 };
906 let message = if list.tight {
907 "a tight list cannot hold a blank line"
908 } else if at == 0 {
909 "a list item cannot open with a blank line"
910 } else if last_item && at + 1 == item.children.len() {
911 "the last item of a list cannot end with a blank line"
912 } else {
913 continue;
914 };
915 diagnostics.push(Diagnostic::invalid(blank.meta.span, message));
916 }
917}
918
919fn validate_text_directive(directive: &TextDirective, diagnostics: &mut Vec<Diagnostic>) {
920 validate_directive_name(directive.meta.span, &directive.name, diagnostics);
921 validate_directive_attributes(&directive.attributes, diagnostics);
922 validate_inline_nodes(&directive.label, diagnostics);
923}
924
925fn validate_directive_name(span: Option<Span>, name: &str, diagnostics: &mut Vec<Diagnostic>) {
926 if !is_directive_name(name) {
927 diagnostics.push(Diagnostic::invalid(
928 span,
929 "directive name must be runs of ASCII letters joined by single `-`",
930 ));
931 }
932}
933
934fn validate_directive_attributes(
935 attributes: &[DirectiveAttribute],
936 diagnostics: &mut Vec<Diagnostic>,
937) {
938 for attribute in attributes {
939 if !is_attribute_name(&attribute.name) {
940 diagnostics.push(Diagnostic::invalid(
941 None,
942 "directive attribute name must start with a letter, `_`, or `-`",
943 ));
944 }
945 }
946}
947
948pub(crate) fn is_directive_name(name: &str) -> bool {
950 name.split('-')
951 .all(|run| !run.is_empty() && run.bytes().all(|byte| byte.is_ascii_alphabetic()))
952}
953
954pub(crate) fn is_attribute_name(name: &str) -> bool {
955 let mut chars = name.chars();
956 let Some(first) = chars.next() else {
957 return false;
958 };
959 if !(first.is_ascii_alphabetic() || first == '_' || first == '-') {
960 return false;
961 }
962 chars.all(|char| char.is_ascii_alphanumeric() || char == '_' || char == '-' || char == ':')
963}