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