1use alloc::vec::Vec;
6
7use crate::{
8 ast::{
9 Autolink, AutolinkKind, Block, CodeInline, ContainerDirective, DirectiveAttribute,
10 Document, Escape, Heading, HtmlContainer, HtmlContainerContent, Inline, LeafDirective,
11 List, MathInlineKind, Table, TextDirective,
12 },
13 diagnostic::Diagnostic,
14 span::Span,
15};
16
17impl Document {
18 pub fn validate(&self) -> Vec<Diagnostic> {
21 validate_document(self)
22 }
23}
24
25pub(crate) fn validate_document(document: &Document) -> Vec<Diagnostic> {
26 let mut diagnostics = Vec::new();
27 for block in &document.children {
28 validate_block(block, &mut diagnostics);
29 }
30 diagnostics
31}
32
33fn validate_block(block: &Block, diagnostics: &mut Vec<Diagnostic>) {
34 match block {
35 Block::Paragraph(paragraph) => validate_inlines(¶graph.children, diagnostics),
36 Block::Heading(heading) => validate_heading(heading, diagnostics),
37 Block::BlockQuote(block_quote) => {
38 for child in &block_quote.children {
39 validate_block(child, diagnostics);
40 }
41 }
42 Block::Alert(alert) => {
43 for child in &alert.children {
44 validate_block(child, diagnostics);
45 }
46 }
47 Block::List(list) => {
48 validate_list_start(list, diagnostics);
49 for item in &list.children {
50 for child in &item.children {
51 validate_block(child, diagnostics);
52 }
53 }
54 }
55 Block::DescriptionList(list) => {
56 for item in &list.children {
57 validate_inlines(&item.term, diagnostics);
58 if item.details.is_empty() {
59 diagnostics.push(Diagnostic::invalid(
60 item.meta.span,
61 "description item must contain at least one details block",
62 ));
63 }
64 for details in &item.details {
65 for child in &details.children {
66 validate_block(child, diagnostics);
67 }
68 }
69 }
70 }
71 Block::Table(table) => validate_table(table, diagnostics),
72 Block::FootnoteDefinition(definition) => {
73 if definition.identifier.is_empty() {
74 diagnostics.push(Diagnostic::invalid(
75 definition.meta.span,
76 "footnote definition identifier cannot be empty",
77 ));
78 }
79 for child in &definition.children {
80 validate_block(child, diagnostics);
81 }
82 }
83 Block::Definition(definition) => {
84 if definition.identifier.trim().is_empty() {
85 diagnostics.push(Diagnostic::invalid(
86 definition.meta.span,
87 "definition identifier cannot be empty",
88 ));
89 }
90 }
91 Block::LeafDirective(directive) => validate_leaf_directive(directive, diagnostics),
92 Block::ContainerDirective(directive) => {
93 validate_container_directive(directive, diagnostics)
94 }
95 Block::HtmlContainer(container) => validate_html_container(container, diagnostics),
96 Block::ThematicBreak(_)
97 | Block::CodeBlock(_)
98 | Block::HtmlBlock(_)
99 | Block::MathBlock(_)
100 | Block::Frontmatter(_)
101 | Block::MdxEsm(_)
102 | Block::MdxExpression(_)
103 | Block::MdxJsx(_) => {}
104 }
105}
106
107fn validate_html_container(container: &HtmlContainer, diagnostics: &mut Vec<Diagnostic>) {
108 if container.opening.name.is_empty() {
109 diagnostics.push(Diagnostic::invalid(
110 container.opening.meta.span,
111 "HTML container opening tag name cannot be empty",
112 ));
113 }
114 if container.closing.name.is_empty() {
115 diagnostics.push(Diagnostic::invalid(
116 container.closing.meta.span,
117 "HTML container closing tag name cannot be empty",
118 ));
119 }
120 if container.opening.name != container.closing.name {
121 diagnostics.push(Diagnostic::invalid(
122 container.meta.span,
123 "HTML container opening and closing tag names must match",
124 ));
125 }
126 if container.opening.raw.is_empty() {
127 diagnostics.push(Diagnostic::invalid(
128 container.opening.meta.span,
129 "HTML container opening tag source cannot be empty",
130 ));
131 }
132 if container.closing.raw.is_empty() {
133 diagnostics.push(Diagnostic::invalid(
134 container.closing.meta.span,
135 "HTML container closing tag source cannot be empty",
136 ));
137 }
138
139 match &container.content {
140 HtmlContainerContent::Blocks(children) => {
141 for child in children {
142 validate_block(child, diagnostics);
143 }
144 }
145 HtmlContainerContent::Inlines(children) => validate_inlines(children, diagnostics),
146 }
147}
148
149fn validate_heading(heading: &Heading, diagnostics: &mut Vec<Diagnostic>) {
150 if heading.depth == 0 || heading.depth > 6 {
151 diagnostics.push(Diagnostic::invalid(
152 heading.meta.span,
153 "heading depth must be in the range 1..=6",
154 ));
155 }
156 validate_inlines(&heading.children, diagnostics);
157}
158
159fn validate_table(table: &Table, diagnostics: &mut Vec<Diagnostic>) {
160 if table.rows.is_empty() {
161 diagnostics.push(Diagnostic::invalid(
162 table.meta.span,
163 "table must contain at least a header row",
164 ));
165 return;
166 }
167
168 let width = table.rows[0].cells.len();
169 if width == 0 {
170 diagnostics.push(Diagnostic::invalid(
171 table.meta.span,
172 "table header row must contain at least one cell",
173 ));
174 }
175
176 if table.alignments.len() != width {
177 diagnostics.push(Diagnostic::invalid(
178 table.meta.span,
179 "table alignment count must match header width",
180 ));
181 }
182
183 for row in &table.rows {
184 if row.cells.len() != width {
185 diagnostics.push(Diagnostic::invalid(
186 row.meta.span,
187 "table row width must match header width",
188 ));
189 }
190 for cell in &row.cells {
191 validate_inlines(&cell.children, diagnostics);
192 }
193 }
194}
195
196fn validate_leaf_directive(directive: &LeafDirective, diagnostics: &mut Vec<Diagnostic>) {
197 validate_directive_name(directive.meta.span, &directive.name, diagnostics);
198 validate_directive_attributes(&directive.attributes, diagnostics);
199 validate_inlines(&directive.label, diagnostics);
200}
201
202fn validate_container_directive(directive: &ContainerDirective, diagnostics: &mut Vec<Diagnostic>) {
203 validate_directive_name(directive.meta.span, &directive.name, diagnostics);
204 validate_directive_attributes(&directive.attributes, diagnostics);
205 validate_inlines(&directive.label, diagnostics);
206 for child in &directive.children {
207 validate_block(child, diagnostics);
208 }
209}
210
211fn validate_inlines(inlines: &[Inline], diagnostics: &mut Vec<Diagnostic>) {
215 if let Some(Inline::LineBreak(node)) = inlines.last() {
216 diagnostics.push(Diagnostic::invalid(
217 node.meta.span,
218 "hard line break cannot be the final inline of its container",
219 ));
220 }
221 validate_inline_nodes(inlines, diagnostics);
222}
223
224fn validate_inline_nodes(inlines: &[Inline], diagnostics: &mut Vec<Diagnostic>) {
227 for inline in inlines {
228 match inline {
229 Inline::Emphasis(node) => {
230 validate_emphasis_container(&node.children, node.meta.span, diagnostics)
231 }
232 Inline::Strong(node) => {
233 validate_emphasis_container(&node.children, node.meta.span, diagnostics)
234 }
235 Inline::Underline(node) => {
236 validate_emphasis_container(&node.children, node.meta.span, diagnostics)
237 }
238 Inline::Delete(node) => {
239 validate_emphasis_container(&node.children, node.meta.span, diagnostics)
240 }
241 Inline::Insert(node) => {
242 validate_emphasis_container(&node.children, node.meta.span, diagnostics)
243 }
244 Inline::Mark(node) => {
245 validate_emphasis_container(&node.children, node.meta.span, diagnostics)
246 }
247 Inline::Subscript(node) => {
248 validate_emphasis_container(&node.children, node.meta.span, diagnostics)
249 }
250 Inline::Superscript(node) => {
251 validate_emphasis_container(&node.children, node.meta.span, diagnostics)
252 }
253 Inline::Spoiler(node) => {
254 validate_emphasis_container(&node.children, node.meta.span, diagnostics)
255 }
256 Inline::Shortcode(node) => {
257 if node.name.is_empty() {
258 diagnostics.push(Diagnostic::invalid(
259 node.meta.span,
260 "shortcode name cannot be empty",
261 ));
262 }
263 }
264 Inline::Link(node) => validate_inline_nodes(&node.children, diagnostics),
265 Inline::Image(node) => validate_inline_nodes(&node.alt, diagnostics),
266 Inline::LinkReference(node) => {
267 if node.identifier.is_empty() {
268 diagnostics.push(Diagnostic::invalid(
269 node.meta.span,
270 "link reference identifier cannot be empty",
271 ));
272 }
273 validate_inline_nodes(&node.children, diagnostics);
274 }
275 Inline::ImageReference(node) => {
276 if node.identifier.is_empty() {
277 diagnostics.push(Diagnostic::invalid(
278 node.meta.span,
279 "image reference identifier cannot be empty",
280 ));
281 }
282 validate_inline_nodes(&node.alt, diagnostics);
283 }
284 Inline::Escape(node) => validate_escape(node, diagnostics),
285 Inline::CharacterReference(node) => {
286 if node.reference.is_empty() {
287 diagnostics.push(Diagnostic::invalid(
288 node.meta.span,
289 "character reference source cannot be empty",
290 ));
291 }
292 if node.value.is_empty() {
293 diagnostics.push(Diagnostic::invalid(
294 node.meta.span,
295 "character reference value cannot be empty",
296 ));
297 }
298 }
299 Inline::TextDirective(node) => validate_text_directive(node, diagnostics),
300 Inline::FootnoteReference(node) => {
301 if node.identifier.is_empty() {
302 diagnostics.push(Diagnostic::invalid(
303 node.meta.span,
304 "footnote reference identifier cannot be empty",
305 ));
306 }
307 }
308 Inline::InlineFootnote(node) => validate_inline_nodes(&node.children, diagnostics),
309 Inline::WikiLink(node) => {
310 if node.target.is_empty() {
311 diagnostics.push(Diagnostic::invalid(
312 node.meta.span,
313 "wikilink target cannot be empty",
314 ));
315 }
316 }
317 Inline::Code(node) => validate_code_inline(node, diagnostics),
318 Inline::Autolink(node) => validate_autolink(node, diagnostics),
319 Inline::Math(node) => {
320 if let MathInlineKind::Dollar { dollars: 0 } = node.kind {
321 diagnostics.push(Diagnostic::invalid(
322 node.meta.span,
323 "dollar-fenced inline math must have a fence length of at least 1",
324 ));
325 }
326 }
327 Inline::Text(_)
328 | Inline::Html(_)
329 | Inline::SoftBreak(_)
330 | Inline::LineBreak(_)
331 | Inline::MdxExpression(_)
332 | Inline::MdxJsx(_) => {}
333 }
334 }
335}
336
337fn validate_emphasis_container(
338 children: &[Inline],
339 span: Option<Span>,
340 diagnostics: &mut Vec<Diagnostic>,
341) {
342 if children.is_empty() {
343 diagnostics.push(Diagnostic::invalid(
344 span,
345 "emphasis-like inline container cannot have empty children",
346 ));
347 }
348 validate_inlines(children, diagnostics);
349}
350
351fn validate_escape(escape: &Escape, diagnostics: &mut Vec<Diagnostic>) {
352 if !escape.value.is_ascii_punctuation() {
353 diagnostics.push(Diagnostic::invalid(
354 escape.meta.span,
355 "escaped value must be an ASCII punctuation character",
356 ));
357 }
358}
359
360fn validate_autolink(autolink: &Autolink, diagnostics: &mut Vec<Diagnostic>) {
361 if matches!(autolink.kind, AutolinkKind::GfmLiteral { .. }) {
365 return;
366 }
367 if autolink
368 .destination
369 .chars()
370 .any(|char| char.is_whitespace() || char == '<' || char == '>')
371 {
372 diagnostics.push(Diagnostic::invalid(
373 autolink.meta.span,
374 "autolink destination cannot contain whitespace, `<`, or `>`",
375 ));
376 }
377}
378
379fn validate_code_inline(code: &CodeInline, diagnostics: &mut Vec<Diagnostic>) {
380 if code.fence_length == 0 {
381 return;
382 }
383 if raw_has_backtick_run(&code.raw, code.fence_length) {
387 diagnostics.push(Diagnostic::invalid(
388 code.meta.span,
389 "inline code raw passthrough contains a backtick run equal to its fence length",
390 ));
391 }
392}
393
394fn raw_has_backtick_run(input: &str, length: usize) -> bool {
395 let mut current = 0;
396 for byte in input.bytes() {
397 if byte == b'`' {
398 current += 1;
399 } else {
400 if current == length {
401 return true;
402 }
403 current = 0;
404 }
405 }
406 current == length
407}
408
409fn validate_list_start(list: &List, diagnostics: &mut Vec<Diagnostic>) {
410 if !list.ordered {
411 return;
412 }
413 let Some(start) = list.start else {
414 return;
415 };
416 if start > 999_999_999 {
417 diagnostics.push(Diagnostic::invalid(
418 list.meta.span,
419 "ordered list start must be representable in at most 9 digits",
420 ));
421 }
422}
423
424fn validate_text_directive(directive: &TextDirective, diagnostics: &mut Vec<Diagnostic>) {
425 validate_directive_name(directive.meta.span, &directive.name, diagnostics);
426 validate_directive_attributes(&directive.attributes, diagnostics);
427 validate_inline_nodes(&directive.label, diagnostics);
428}
429
430fn validate_directive_name(span: Option<Span>, name: &str, diagnostics: &mut Vec<Diagnostic>) {
431 if !is_directive_name(name) {
432 diagnostics.push(Diagnostic::invalid(
433 span,
434 "directive name must start with a letter and contain letters, digits, `_`, or `-`",
435 ));
436 }
437}
438
439fn validate_directive_attributes(
440 attributes: &[DirectiveAttribute],
441 diagnostics: &mut Vec<Diagnostic>,
442) {
443 for attribute in attributes {
444 if !is_attribute_name(&attribute.name) {
445 diagnostics.push(Diagnostic::invalid(
446 None,
447 "directive attribute name must start with a letter, `_`, or `-`",
448 ));
449 }
450 }
451}
452
453pub(crate) fn is_directive_name(name: &str) -> bool {
454 let mut chars = name.chars();
455 let Some(first) = chars.next() else {
456 return false;
457 };
458 if !first.is_ascii_alphabetic() {
459 return false;
460 }
461 chars.all(|char| char.is_ascii_alphanumeric() || char == '_' || char == '-')
462}
463
464pub(crate) fn is_attribute_name(name: &str) -> bool {
465 let mut chars = name.chars();
466 let Some(first) = chars.next() else {
467 return false;
468 };
469 if !(first.is_ascii_alphabetic() || first == '_' || first == '-') {
470 return false;
471 }
472 chars.all(|char| char.is_ascii_alphanumeric() || char == '_' || char == '-' || char == ':')
473}