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