1use crate::ast::{Block, CodeAttrs, Document, Inline, ListItem, Row, ShortArgs, TaskState};
2use crate::diag::{Code, Diagnostic};
3use crate::inline::{parse_args, parse_inline};
4use crate::span::{SourceMap, Span};
5use crate::token::{Token, TokenKind};
6
7pub fn parse(tokens: Vec<Token>, src: &SourceMap) -> (Document, Vec<Diagnostic>) {
8 let (metadata, fm_consumed, fm_diags) = parse_frontmatter(&tokens, src);
9 let mut p = Parser {
10 src,
11 toks: tokens,
12 pos: fm_consumed,
13 diags: fm_diags,
14 depth: 0,
15 };
16 let mut blocks = p.parse_blocks(0, None);
17 while !p.at_eof() {
19 let tok = *p.peek();
20 match tok.kind {
21 TokenKind::Eof => break,
22 TokenKind::Blank => {
23 p.pos += 1;
24 }
25 TokenKind::Line if p.line(&tok).trim() == "@end" => {
26 p.diags.push(Diagnostic::new(Code::StrayEnd, tok.span));
27 p.pos += 1;
28 }
29 _ => {
30 blocks.append(&mut p.parse_blocks(0, None));
31 }
32 }
33 }
34 (
35 Document {
36 blocks,
37 metadata,
38 resolved_refs: Default::default(),
39 },
40 p.diags,
41 )
42}
43
44fn parse_frontmatter(
45 toks: &[Token],
46 src: &SourceMap,
47) -> (Option<toml::Table>, usize, Vec<Diagnostic>) {
48 if toks.is_empty() {
56 return (None, 0, Vec::new());
57 }
58 let first = &toks[0];
59 let opens = match first.kind {
60 TokenKind::Line => {
61 first.indent == 0 && first.span.start == 0 && &src.source[first.span.range()] == "+++"
62 }
63 _ => false,
64 };
65 if !opens {
66 return (None, 0, Vec::new());
67 }
68
69 let mut idx = 1usize;
72 let body_start = toks
73 .get(idx)
74 .map(|t| t.span.start as usize)
75 .unwrap_or(src.source.len());
76
77 let mut diags = Vec::new();
78 while idx < toks.len() {
79 match toks[idx].kind {
80 TokenKind::Eof => {
81 diags.push(
82 Diagnostic::new(Code::UnterminatedFrontmatter, first.span)
83 .label("frontmatter opened with `+++` is never closed"),
84 );
85 return (None, idx, diags);
86 }
87 TokenKind::Line
88 if toks[idx].indent == 0 && &src.source[toks[idx].span.range()] == "+++" =>
89 {
90 let close = &toks[idx];
91 let body_end = close.span.start as usize;
92 let body = &src.source[body_start..body_end];
93 idx += 1; match toml::from_str::<toml::Table>(body) {
95 Ok(t) => return (Some(t), idx, diags),
96 Err(e) => {
97 let (off, len) = match e.span() {
98 Some(r) => (body_start + r.start, r.end - r.start),
99 None => (body_start, body.len()),
100 };
101 let span = Span::new(off, len.max(1));
104 diags.push(
105 Diagnostic::new(Code::FrontmatterToml, span).label(e.to_string()),
106 );
107 return (None, idx, diags);
108 }
109 }
110 }
111 _ => {
112 idx += 1;
113 }
114 }
115 }
116 diags.push(
117 Diagnostic::new(Code::UnterminatedFrontmatter, first.span)
118 .label("frontmatter opened with `+++` is never closed"),
119 );
120 (None, idx, diags)
121}
122
123const MAX_NESTING_DEPTH: usize = 64;
128
129struct Parser<'a> {
130 src: &'a SourceMap,
131 toks: Vec<Token>,
132 pos: usize,
133 diags: Vec<Diagnostic>,
134 depth: usize,
135}
136
137impl<'a> Parser<'a> {
138 fn peek(&self) -> &Token {
139 &self.toks[self.pos]
140 }
141 fn line(&self, tok: &Token) -> &'a str {
145 &self.src.source[tok.span.range()]
146 }
147 fn at_eof(&self) -> bool {
148 matches!(self.peek().kind, TokenKind::Eof)
149 }
150
151 fn parse_blocks(&mut self, base_indent: u16, end_at_indent_below: Option<u16>) -> Vec<Block> {
152 if self.depth >= MAX_NESTING_DEPTH {
153 self.diags.push(
154 Diagnostic::new(Code::NestingTooDeep, self.peek().span).label(format!(
155 "blocks nest deeper than {} levels",
156 MAX_NESTING_DEPTH
157 )),
158 );
159 self.skip_overdeep_region(base_indent, end_at_indent_below);
160 return Vec::new();
161 }
162 self.depth += 1;
163 let out = self.parse_blocks_inner(base_indent, end_at_indent_below);
164 self.depth -= 1;
165 out
166 }
167
168 fn skip_overdeep_region(&mut self, base_indent: u16, end_at_indent_below: Option<u16>) {
171 while !self.at_eof() {
172 let tok = *self.peek();
173 match tok.kind {
174 TokenKind::Eof => break,
175 TokenKind::Blank => {
176 self.pos += 1;
177 }
178 TokenKind::Line => {
179 let indent = tok.indent;
180 if indent < base_indent {
181 break;
182 }
183 if let Some(min) = end_at_indent_below {
184 if indent < min {
185 break;
186 }
187 }
188 if self.line(&tok)[indent as usize..].trim() == "@end" {
189 break;
190 }
191 self.pos += 1;
192 }
193 }
194 }
195 }
196
197 fn parse_blocks_inner(
198 &mut self,
199 base_indent: u16,
200 end_at_indent_below: Option<u16>,
201 ) -> Vec<Block> {
202 let mut out = Vec::new();
203 loop {
204 if self.at_eof() {
205 break;
206 }
207 match &self.peek().kind {
208 TokenKind::Eof => break,
209 TokenKind::Blank => {
210 self.pos += 1;
211 continue;
212 }
213 TokenKind::Line => {
214 let tok = *self.peek();
215 let indent = tok.indent;
216 if indent < base_indent {
217 break;
218 }
219 if let Some(min) = end_at_indent_below {
220 if indent < min {
221 break;
222 }
223 }
224 let trimmed = &self.line(&tok)[indent as usize..];
225
226 if trimmed.trim() == "@end" {
230 break;
231 }
232 if trimmed.starts_with("//") {
233 self.pos += 1;
234 continue;
235 }
236 if trimmed.starts_with("/*") {
237 self.consume_block_comment(trimmed);
238 continue;
239 }
240 if let Some(b) = self.try_block_at(trimmed, indent) {
241 out.push(b);
242 } else {
243 out.push(self.parse_paragraph(indent));
244 }
245 }
246 }
247 }
248 out
249 }
250
251 fn try_block_at(&mut self, trimmed: &str, indent: u16) -> Option<Block> {
252 if trimmed.starts_with('#') {
253 return Some(self.parse_heading());
254 }
255 if trimmed == "---" {
256 return Some(self.parse_hr());
257 }
258 if trimmed.len() >= 2 && trimmed.bytes().all(|b| b == b'-') {
262 let span = self.peek().span;
263 self.diags.push(
264 Diagnostic::new(Code::BadHorizontalRule, span).label(format!(
265 "horizontal rule is exactly `---`, got {} dashes",
266 trimmed.len()
267 )),
268 );
269 self.pos += 1;
270 return Some(Block::Paragraph {
271 content: vec![],
272 span,
273 });
274 }
275 if trimmed.starts_with("```") {
276 return Some(self.parse_code_fence());
277 }
278 if trimmed.starts_with("- ") {
279 return Some(self.parse_unordered_list(indent));
280 }
281 if leading_ordered_marker(trimmed).is_some() {
282 return Some(self.parse_ordered_list(indent));
283 }
284 if trimmed.starts_with('>') {
285 return Some(self.parse_blockquote(indent));
286 }
287 if trimmed == "@t" || trimmed.starts_with("@t ") || trimmed.starts_with("@t(") {
288 return Some(self.parse_table(indent));
289 }
290 if trimmed == "@dl" || trimmed.starts_with("@dl ") || trimmed.starts_with("@dl(") {
291 return Some(self.parse_definition_list(indent));
292 }
293 if trimmed.starts_with('@') {
294 return self.parse_block_shortcode_or_inline(indent);
295 }
296 if trimmed.starts_with('|') {
297 let span = self.peek().span;
298 self.diags.push(
299 Diagnostic::new(Code::StrayContent, span)
300 .label("`|` only appears inside a `@t` table"),
301 );
302 self.pos += 1;
303 return Some(Block::Paragraph {
304 content: vec![],
305 span,
306 });
307 }
308 None
309 }
310
311 fn consume_block_comment(&mut self, trimmed: &str) {
312 if trimmed.ends_with("*/") && trimmed.len() >= 4 {
313 self.pos += 1;
314 return;
315 }
316 self.pos += 1;
317 loop {
318 let tok = *self.peek();
319 match tok.kind {
320 TokenKind::Eof => {
321 self.diags.push(
322 Diagnostic::new(Code::UnterminatedBlock, tok.span)
323 .label("unterminated /* */ comment"),
324 );
325 return;
326 }
327 TokenKind::Blank => {
328 self.pos += 1;
329 }
330 TokenKind::Line => {
331 let s = self.line(&tok);
332 self.pos += 1;
333 if s.trim_end().ends_with("*/") {
334 return;
335 }
336 }
337 }
338 }
339 }
340
341 fn parse_heading(&mut self) -> Block {
342 let tok = *self.peek();
343 let line = self.line(&tok);
344 self.pos += 1;
345 let indent = tok.indent as usize;
346 let s = &line[indent..];
347 let mut level = 0u8;
348 let bytes = s.as_bytes();
349 while (level as usize) < bytes.len() && bytes[level as usize] == b'#' {
350 level += 1;
351 if level > 6 {
352 break;
353 }
354 }
355 let mut hash_count = level as usize;
356 while hash_count < bytes.len() && bytes[hash_count] == b'#' {
357 hash_count += 1;
358 }
359 if hash_count > 6 {
360 let span = Span::new(tok.span.start as usize + indent, hash_count);
361 self.diags.push(
362 Diagnostic::new(Code::HeadingTooDeep, span)
363 .label("Brief supports heading levels 1-6 only"),
364 );
365 return Block::Paragraph {
366 content: vec![],
367 span: tok.span,
368 };
369 }
370 if bytes.get(level as usize) != Some(&b' ') {
371 self.diags.push(
372 Diagnostic::new(Code::HeadingNoSpace, tok.span)
373 .help("write `# heading` with exactly one space after the `#`s"),
374 );
375 return Block::Paragraph {
376 content: vec![],
377 span: tok.span,
378 };
379 }
380 if bytes.get(level as usize + 1) == Some(&b' ') {
381 self.diags.push(
382 Diagnostic::new(Code::HeadingNoSpace, tok.span)
383 .label("multiple spaces after heading marker"),
384 );
385 }
386 let text_offset = indent + level as usize + 1;
387 let raw_text = &line[text_offset..];
388
389 let (heading_text, anchor) = parse_heading_anchor(
392 raw_text,
393 tok.span.start + text_offset as u32,
394 &mut self.diags,
395 );
396
397 let (content, idiags) = parse_inline(heading_text, tok.span.start + text_offset as u32);
398 self.diags.extend(idiags);
399 Block::Heading {
400 level,
401 content,
402 anchor,
403 span: tok.span,
404 }
405 }
406
407 fn parse_paragraph(&mut self, indent: u16) -> Block {
408 let first = *self.peek();
409 let mut span = first.span;
410 let mut text = String::new();
411 let mut single: Option<&'a str> = None;
416 let mut hard_break_indices: Vec<usize> = Vec::new();
417 let mut first_line = true;
418 loop {
419 let tok = *self.peek();
420 match tok.kind {
421 TokenKind::Line => {
422 if tok.indent != indent {
423 break;
424 }
425 let trimmed = &self.line(&tok)[indent as usize..];
426 if !first_line && leading_block_sigil(trimmed) {
430 break;
431 }
432 let (line_text, hard) = match trimmed.strip_suffix('\\') {
433 Some(rest) => (rest, true),
434 None => (trimmed, false),
435 };
436 if first_line && !hard {
437 single = Some(line_text);
438 } else {
439 if let Some(prev) = single.take() {
440 text.push_str(prev);
441 }
442 if !text.is_empty() {
443 text.push(' ');
444 }
445 if hard {
446 hard_break_indices.push(text.len() + line_text.len());
447 }
448 text.push_str(line_text);
449 }
450 first_line = false;
451 span = span.join(tok.span);
452 self.pos += 1;
453 }
454 _ => break,
455 }
456 }
457 if let Some(s) = single {
458 let base = first.span.start + first.indent as u32;
459 let (content, d) = parse_inline(s, base);
460 self.diags.extend(d);
461 return Block::Paragraph { content, span };
462 }
463 let mut content: Vec<Inline> = Vec::new();
464 let mut cursor = 0usize;
465 let base = first.span.start + first.indent as u32;
466 for hb in &hard_break_indices {
467 let chunk = &text[cursor..*hb];
468 let (mut inl, d) = parse_inline(chunk, base + cursor as u32);
469 self.diags.extend(d);
470 content.append(&mut inl);
471 content.push(Inline::HardBreak {
472 span: Span::new(base as usize + *hb, 1),
473 });
474 cursor = *hb;
475 }
476 let chunk = &text[cursor..];
477 let (mut inl, d) = parse_inline(chunk, base + cursor as u32);
478 self.diags.extend(d);
479 content.append(&mut inl);
480 Block::Paragraph { content, span }
481 }
482
483 fn parse_hr(&mut self) -> Block {
484 let tok = *self.peek();
485 self.pos += 1;
486 Block::HorizontalRule { span: tok.span }
487 }
488
489 fn parse_code_fence(&mut self) -> Block {
490 let open = *self.peek();
491 let line = self.line(&open);
492 self.pos += 1;
493 let indent = open.indent as usize;
494 let after = &line[indent + 3..];
495 if after.starts_with('`') {
496 self.diags.push(
497 Diagnostic::new(Code::UnterminatedFence, open.span)
498 .label("opening fence must be exactly three backticks"),
499 );
500 }
501 let info_offset = open.span.start as usize + indent + 3;
502 let (lang, attrs) = parse_fence_info(after, info_offset as u32, open.span, &mut self.diags);
503 let mut body = String::new();
504 let mut span = open.span;
505 loop {
506 let tok = *self.peek();
507 match tok.kind {
508 TokenKind::Eof => {
509 self.diags.push(
510 Diagnostic::new(Code::UnterminatedFence, open.span)
511 .label("fence opened here is never closed"),
512 );
513 break;
514 }
515 TokenKind::Blank => {
516 body.push('\n');
517 span = span.join(tok.span);
518 self.pos += 1;
519 }
520 TokenKind::Line => {
521 let s = self.line(&tok);
522 if s.trim() == "```" {
523 span = span.join(tok.span);
524 self.pos += 1;
525 break;
526 }
527 body.push_str(s);
528 body.push('\n');
529 span = span.join(tok.span);
530 self.pos += 1;
531 }
532 }
533 }
534 if body.ends_with('\n') {
535 body.pop();
536 }
537 Block::CodeBlock {
538 lang,
539 body,
540 attrs,
541 span,
542 }
543 }
544
545 fn parse_unordered_list(&mut self, indent: u16) -> Block {
546 let start_span = self.peek().span;
547 let mut items: Vec<ListItem> = Vec::new();
548 loop {
549 if self.at_eof() {
550 break;
551 }
552 let tok = *self.peek();
553 if !matches!(tok.kind, TokenKind::Line) {
554 break;
555 }
556 if tok.indent != indent {
557 break;
558 }
559 let trimmed = &self.line(&tok)[indent as usize..];
560 if !trimmed.starts_with("- ") {
561 break;
562 }
563 let after_marker = &trimmed[2..];
564 let (task, item_text, content_offset) =
568 if let Some(rest) = after_marker.strip_prefix("[x] ") {
569 (Some(TaskState::Done), rest, 4u32)
570 } else if let Some(rest) = after_marker.strip_prefix("[ ] ") {
571 (Some(TaskState::Todo), rest, 4u32)
572 } else {
573 (None, after_marker, 0u32)
574 };
575 let (content, d) = parse_inline(
576 item_text,
577 tok.span.start + indent as u32 + 2 + content_offset,
578 );
579 self.diags.extend(d);
580 self.pos += 1;
581 let mut children: Vec<Block> = Vec::new();
582 self.skip_blanks();
583 if matches!(self.peek().kind, TokenKind::Line) && self.peek().indent >= indent + 2 {
584 children = self.parse_blocks(indent + 2, Some(indent + 2));
585 }
586 items.push(ListItem {
587 content,
588 children,
589 task,
590 span: tok.span,
591 });
592 }
593 let span = items.iter().fold(start_span, |a, it| a.join(it.span));
594 Block::List {
595 ordered: false,
596 items,
597 span,
598 }
599 }
600
601 fn parse_ordered_list(&mut self, indent: u16) -> Block {
602 let start_span = self.peek().span;
603 let mut items: Vec<ListItem> = Vec::new();
604 let mut expected: u32 = 1;
605 loop {
606 if self.at_eof() {
607 break;
608 }
609 let tok = *self.peek();
610 if !matches!(tok.kind, TokenKind::Line) {
611 break;
612 }
613 if tok.indent != indent {
614 break;
615 }
616 let trimmed = &self.line(&tok)[indent as usize..];
617 let Some((num, marker_len)) = leading_ordered_marker(trimmed) else {
618 break;
619 };
620 if num != expected {
621 let span = Span::new(tok.span.start as usize + indent as usize, marker_len);
622 self.diags.push(
623 Diagnostic::new(Code::OrderedListSequence, span)
624 .label(format!("got `{}.`, expected `{}.`", num, expected))
625 .help("ordered lists must number sequentially starting from 1"),
626 );
627 }
628 expected = expected.saturating_add(1);
629 let after_marker = &trimmed[marker_len..];
630 let (task, item_text, content_offset) =
634 if let Some(rest) = after_marker.strip_prefix("[x] ") {
635 (Some(TaskState::Done), rest, 4u32)
636 } else if let Some(rest) = after_marker.strip_prefix("[ ] ") {
637 (Some(TaskState::Todo), rest, 4u32)
638 } else {
639 (None, after_marker, 0u32)
640 };
641 let (content, d) = parse_inline(
642 item_text,
643 tok.span.start + indent as u32 + marker_len as u32 + content_offset,
644 );
645 self.diags.extend(d);
646 self.pos += 1;
647 let mut children: Vec<Block> = Vec::new();
648 self.skip_blanks();
649 if matches!(self.peek().kind, TokenKind::Line) && self.peek().indent >= indent + 2 {
650 children = self.parse_blocks(indent + 2, Some(indent + 2));
651 }
652 items.push(ListItem {
653 content,
654 children,
655 task,
656 span: tok.span,
657 });
658 }
659 let span = items.iter().fold(start_span, |a, it| a.join(it.span));
660 Block::List {
661 ordered: true,
662 items,
663 span,
664 }
665 }
666
667 fn parse_blockquote(&mut self, indent: u16) -> Block {
668 let mut lines: Vec<(usize, &'a str, Span)> = Vec::new();
669 let start = self.peek().span;
670 loop {
671 if self.at_eof() {
672 break;
673 }
674 let tok = *self.peek();
675 if !matches!(tok.kind, TokenKind::Line) {
676 break;
677 }
678 if tok.indent != indent {
679 break;
680 }
681 let trimmed = &self.line(&tok)[indent as usize..];
682 let mut depth: usize = 0;
683 let mut idx = 0usize;
684 let bytes = trimmed.as_bytes();
685 while idx < bytes.len() && bytes[idx] == b'>' {
686 depth += 1;
687 idx += 1;
688 }
689 if depth == 0 {
690 break;
691 }
692 if depth > MAX_NESTING_DEPTH {
693 self.diags.push(
694 Diagnostic::new(Code::NestingTooDeep, tok.span).label(format!(
695 "blockquote nests deeper than {} levels",
696 MAX_NESTING_DEPTH
697 )),
698 );
699 self.pos += 1;
700 break;
701 }
702 if bytes.get(idx) != Some(&b' ') {
703 self.diags.push(
704 Diagnostic::new(Code::BadBlockquote, tok.span)
705 .label("expected one space after `>`"),
706 );
707 self.pos += 1;
708 break;
709 }
710 let body = &trimmed[idx + 1..];
711 lines.push((depth, body, tok.span));
712 self.pos += 1;
713 }
714 let (children, span) = build_blockquote(&lines, 1);
715 Block::Blockquote {
716 children,
717 span: if span == Span::DUMMY { start } else { span },
718 }
719 }
720
721 fn parse_table(&mut self, indent: u16) -> Block {
722 let directive = *self.peek();
723 let line = self.line(&directive);
724 self.pos += 1;
725 let trimmed = &line[indent as usize..];
726 let mut cursor = 2usize;
727 let args = if trimmed.as_bytes().get(cursor) == Some(&b'(') {
728 match parse_args(trimmed, &mut cursor) {
729 Ok(a) => a,
730 Err(d) => {
731 self.diags.push(d);
732 ShortArgs::default()
733 }
734 }
735 } else {
736 ShortArgs::default()
737 };
738 let mut rows: Vec<Row> = Vec::new();
739 loop {
740 if self.at_eof() {
741 break;
742 }
743 let tok = *self.peek();
744 if !matches!(tok.kind, TokenKind::Line) {
745 break;
746 }
747 let row_line = self.line(&tok);
748 let trimmed = row_line.trim_start();
749 if !trimmed.starts_with('|') {
750 break;
751 }
752 let split = split_cells(trimmed);
753 if let Some(rel) = split.unclosed_backtick_at {
754 let leading_ws = row_line.len() - trimmed.len();
758 debug_assert!(
759 rel < trimmed.len(),
760 "unclosed_backtick_at {} out of bounds for trimmed (len {})",
761 rel,
762 trimmed.len()
763 );
764 let abs = tok.span.start as usize + leading_ws + rel;
765 self.diags.push(
766 Diagnostic::new(Code::UnterminatedCode, Span::new(abs, 1))
767 .label("inline code span never closed inside a table row"),
768 );
769 self.pos += 1;
770 continue;
771 }
772 let cells = split.cells;
773 let mut parsed_cells: Vec<Vec<Inline>> = Vec::new();
774 for c in cells {
775 let (inl, d) = parse_inline(c.trim(), tok.span.start);
776 self.diags.extend(d);
777 parsed_cells.push(inl);
778 }
779 rows.push(Row {
780 cells: parsed_cells,
781 span: tok.span,
782 });
783 self.pos += 1;
784 }
785 if rows.is_empty() {
786 self.diags.push(
787 Diagnostic::new(Code::StrayContent, directive.span)
788 .label("`@t` must be followed by at least a header row"),
789 );
790 return Block::Paragraph {
791 content: vec![],
792 span: directive.span,
793 };
794 }
795 let header = rows.remove(0);
796 let cols = header.cells.len();
797 for r in &rows {
798 if r.cells.len() != cols {
799 self.diags.push(
800 Diagnostic::new(Code::TableColumnMismatch, r.span).label(format!(
801 "table row has {} cells, expected {}",
802 r.cells.len(),
803 cols
804 )),
805 );
806 }
807 }
808 if let Some(crate::shortcode::ArgValue::Array(a)) = args.keyword.get("align") {
809 if a.len() != cols {
810 self.diags.push(
811 Diagnostic::new(Code::AlignArrayLength, directive.span).label(format!(
812 "`align` has {} entries but table has {} columns",
813 a.len(),
814 cols
815 )),
816 );
817 }
818 for v in a {
823 let ok = matches!(v.as_str(), Some("left") | Some("right") | Some("center"));
824 if !ok {
825 self.diags
826 .push(
827 Diagnostic::new(Code::BadEnumValue, directive.span).label(format!(
828 "`align` entries must be `left`, `right`, or `center`, got `{}`",
829 v.as_str().unwrap_or(v.type_name())
830 )),
831 );
832 }
833 }
834 }
835 let span = rows
836 .iter()
837 .fold(directive.span.join(header.span), |a, r| a.join(r.span));
838 Block::Table {
839 args,
840 header,
841 rows,
842 span,
843 }
844 }
845
846 fn parse_definition_list(&mut self, indent: u16) -> Block {
847 use crate::ast::DefinitionItem;
848 let directive = *self.peek();
849 let line = self.line(&directive);
850 self.pos += 1;
851 let trimmed = &line[indent as usize..];
852 let mut cursor = 3usize;
853 let args = if trimmed.as_bytes().get(cursor) == Some(&b'(') {
854 match parse_args(trimmed, &mut cursor) {
855 Ok(a) => a,
856 Err(d) => {
857 self.diags.push(d);
858 ShortArgs::default()
859 }
860 }
861 } else {
862 ShortArgs::default()
863 };
864
865 let mut items: Vec<DefinitionItem> = Vec::new();
866 let mut pending_term: Option<(Vec<Inline>, Span)> = None;
867 let mut pending_def: Option<(String, u32, Span)> = None;
870 let cont_indent = indent + 2;
871 let mut end_span = directive.span;
872
873 let finalize_def = |items: &mut Vec<DefinitionItem>,
875 pending_term: &mut Option<(Vec<Inline>, Span)>,
876 pending_def: &mut Option<(String, u32, Span)>,
877 diags: &mut Vec<Diagnostic>| {
878 if let Some((text, base, span)) = pending_def.take() {
879 let term_pair = pending_term.take();
880 let (def_inl, dd) = parse_inline(&text, base);
881 diags.extend(dd);
882 if let Some((term, t_span)) = term_pair {
883 let pair_span = t_span.join(span);
884 items.push(DefinitionItem {
885 term,
886 definition: def_inl,
887 span: pair_span,
888 });
889 } else {
890 }
893 }
894 };
895
896 loop {
897 if self.at_eof() {
898 self.diags.push(
899 Diagnostic::new(Code::UnterminatedBlock, directive.span)
900 .label("`@dl` block was never closed with `@end`"),
901 );
902 break;
903 }
904 let tok = *self.peek();
905 match tok.kind {
906 TokenKind::Eof => {
907 self.diags.push(
908 Diagnostic::new(Code::UnterminatedBlock, directive.span)
909 .label("`@dl` block was never closed with `@end`"),
910 );
911 break;
912 }
913 TokenKind::Blank => {
914 finalize_def(
915 &mut items,
916 &mut pending_term,
917 &mut pending_def,
918 &mut self.diags,
919 );
920 self.pos += 1;
921 continue;
922 }
923 TokenKind::Line => {
924 let s = self.line(&tok);
925 if let Some(pd) = pending_def.as_mut()
926 && tok.indent == cont_indent
927 {
928 let body = &s[cont_indent as usize..];
930 pd.0.push(' ');
931 pd.0.push_str(body);
932 pd.2 = pd.2.join(tok.span);
933 self.pos += 1;
934 continue;
935 }
936 if tok.indent != indent {
937 self.pos += 1;
941 continue;
942 }
943 let body = &s[indent as usize..];
944 if body.trim() == "@end" {
945 finalize_def(
946 &mut items,
947 &mut pending_term,
948 &mut pending_def,
949 &mut self.diags,
950 );
951 end_span = tok.span;
952 self.pos += 1;
953 break;
954 }
955 if let Some(rest) = body.strip_prefix(": ") {
956 if rest.starts_with(' ') {
960 self.diags.push(
961 Diagnostic::new(Code::BadDefinitionList, tok.span)
962 .label("`:` must be followed by exactly one space"),
963 );
964 self.pos += 1;
965 continue;
966 }
967 if pending_term.is_none() && pending_def.is_none() {
968 self.diags.push(
969 Diagnostic::new(Code::BadDefinitionList, tok.span)
970 .label("definition without a term"),
971 );
972 self.pos += 1;
973 continue;
974 }
975 if pending_def.is_some() {
976 self.diags.push(
977 Diagnostic::new(Code::BadDefinitionList, tok.span).label(
978 "multiple definitions per term are not supported in v0.4",
979 ),
980 );
981 self.pos += 1;
984 continue;
985 }
986 let base = tok.span.start + indent as u32 + 2;
987 pending_def = Some((rest.to_string(), base, tok.span));
988 self.pos += 1;
989 } else if body.starts_with(':') {
990 self.diags.push(
991 Diagnostic::new(Code::BadDefinitionList, tok.span)
992 .label("`:` must be followed by exactly one space"),
993 );
994 self.pos += 1;
995 } else {
996 finalize_def(
998 &mut items,
999 &mut pending_term,
1000 &mut pending_def,
1001 &mut self.diags,
1002 );
1003 if let Some((_t, t_span)) = pending_term.take() {
1004 self.diags.push(
1005 Diagnostic::new(Code::BadDefinitionList, t_span)
1006 .label("term without a definition"),
1007 );
1008 }
1009 let base = tok.span.start + indent as u32;
1010 let (term, td) = parse_inline(body, base);
1011 self.diags.extend(td);
1012 pending_term = Some((term, tok.span));
1013 self.pos += 1;
1014 }
1015 }
1016 }
1017 }
1018
1019 finalize_def(
1021 &mut items,
1022 &mut pending_term,
1023 &mut pending_def,
1024 &mut self.diags,
1025 );
1026 if let Some((_t, t_span)) = pending_term {
1027 self.diags.push(
1028 Diagnostic::new(Code::BadDefinitionList, t_span).label("term without a definition"),
1029 );
1030 }
1031 if items.is_empty() && !self.diags.iter().any(|d| d.code == Code::BadDefinitionList) {
1032 self.diags.push(
1033 Diagnostic::new(Code::BadDefinitionList, directive.span)
1034 .label("`@dl` must contain at least one term/definition pair"),
1035 );
1036 }
1037
1038 Block::DefinitionList {
1039 args,
1040 items,
1041 span: directive.span.join(end_span),
1042 }
1043 }
1044
1045 fn parse_block_shortcode_or_inline(&mut self, indent: u16) -> Option<Block> {
1046 let tok = *self.peek();
1047 if !matches!(tok.kind, TokenKind::Line) {
1048 return None;
1049 }
1050 let trimmed = &self.line(&tok)[indent as usize..];
1051 let mut cursor = 1usize;
1052 let bytes = trimmed.as_bytes();
1053 if cursor >= bytes.len() || !bytes[cursor].is_ascii_alphabetic() {
1054 return None;
1055 }
1056 let name_start = cursor;
1057 while cursor < bytes.len()
1058 && (bytes[cursor].is_ascii_alphanumeric() || bytes[cursor] == b'-')
1059 {
1060 cursor += 1;
1061 }
1062 let name = trimmed[name_start..cursor].to_string();
1063 let mut args = ShortArgs::default();
1064 if bytes.get(cursor) == Some(&b'(') {
1065 match parse_args(trimmed, &mut cursor) {
1066 Ok(a) => args = a,
1067 Err(d) => self.diags.push(d),
1068 }
1069 }
1070 if !trimmed[cursor..].trim().is_empty() {
1071 return None;
1072 }
1073 self.pos += 1;
1074 if name == "code" {
1075 return Some(self.parse_code_shortcode_body(indent, &tok, args));
1076 }
1077 let children = self.parse_blocks(indent, Some(indent));
1078 let mut end_span = tok.span;
1079 let end_tok = *self.peek();
1080 match end_tok.kind {
1081 TokenKind::Line if end_tok.indent == indent && self.line(&end_tok).trim() == "@end" => {
1082 end_span = end_tok.span;
1083 self.pos += 1;
1084 }
1085 _ => {
1086 self.diags.push(
1087 Diagnostic::new(Code::UnterminatedBlock, tok.span)
1088 .label(format!("`@{}` block was never closed with `@end`", name)),
1089 );
1090 }
1091 }
1092 Some(Block::BlockShortcode {
1093 name,
1094 args,
1095 children,
1096 span: tok.span.join(end_span),
1097 })
1098 }
1099
1100 fn parse_code_shortcode_body(&mut self, indent: u16, open: &Token, args: ShortArgs) -> Block {
1106 let lang = self.code_shortcode_lang(args, open.span);
1107 let mut body = String::new();
1108 let mut span = open.span;
1109 loop {
1110 let tok = *self.peek();
1111 match tok.kind {
1112 TokenKind::Eof => {
1113 self.diags.push(
1114 Diagnostic::new(Code::UnterminatedBlock, open.span)
1115 .label("`@code` block was never closed with `@end`"),
1116 );
1117 break;
1118 }
1119 TokenKind::Blank => {
1120 body.push('\n');
1121 span = span.join(tok.span);
1122 self.pos += 1;
1123 }
1124 TokenKind::Line => {
1125 let s = self.line(&tok);
1126 if s.trim() == "@end" && tok.indent == indent {
1127 span = span.join(tok.span);
1128 self.pos += 1;
1129 break;
1130 }
1131 body.push_str(s);
1132 body.push('\n');
1133 span = span.join(tok.span);
1134 self.pos += 1;
1135 }
1136 }
1137 }
1138 if body.ends_with('\n') {
1139 body.pop();
1140 }
1141 Block::CodeBlock {
1142 lang,
1143 body,
1144 attrs: CodeAttrs::default(),
1145 span,
1146 }
1147 }
1148
1149 fn code_shortcode_lang(&mut self, args: ShortArgs, span: Span) -> Option<String> {
1153 let mut lang: Option<String> = None;
1154 for (i, v) in args.positional.iter().enumerate() {
1155 if i > 0 {
1156 self.diags.push(
1157 Diagnostic::new(Code::BadArgSyntax, span)
1158 .label("`@code` takes at most one positional argument (the language)"),
1159 );
1160 break;
1161 }
1162 match v.as_str() {
1163 Some(s) => lang = Some(s.to_string()),
1164 None => {
1165 self.diags
1166 .push(Diagnostic::new(Code::ArgTypeMismatch, span).label(format!(
1167 "argument `lang` has type {} but expected String",
1168 v.type_name()
1169 )));
1170 }
1171 }
1172 }
1173 for (kw, v) in &args.keyword {
1174 if kw != "lang" {
1175 self.diags.push(
1176 Diagnostic::new(Code::BadArgSyntax, span)
1177 .label(format!("`@code` has no argument named `{}`", kw)),
1178 );
1179 continue;
1180 }
1181 if lang.is_some() {
1182 self.diags.push(
1183 Diagnostic::new(Code::DuplicateKwarg, span)
1184 .label("`lang` given both positionally and by keyword"),
1185 );
1186 continue;
1187 }
1188 match v.as_str() {
1189 Some(s) => lang = Some(s.to_string()),
1190 None => {
1191 self.diags
1192 .push(Diagnostic::new(Code::ArgTypeMismatch, span).label(format!(
1193 "argument `lang` has type {} but expected String",
1194 v.type_name()
1195 )));
1196 }
1197 }
1198 }
1199 lang
1200 }
1201
1202 fn skip_blanks(&mut self) {
1203 while matches!(self.peek().kind, TokenKind::Blank) {
1204 self.pos += 1;
1205 }
1206 }
1207}
1208
1209fn build_blockquote(items: &[(usize, &str, Span)], depth: usize) -> (Vec<Block>, Span) {
1210 let mut paras: Vec<Block> = Vec::new();
1211 let mut full_span = Span::DUMMY;
1212 let mut i = 0;
1213 while i < items.len() {
1214 let (d, body, span) = &items[i];
1215 if *d < depth {
1216 break;
1217 }
1218 full_span = if full_span == Span::DUMMY {
1219 *span
1220 } else {
1221 full_span.join(*span)
1222 };
1223 if *d == depth {
1224 let (content, _) = parse_inline(body, span.start);
1225 paras.push(Block::Paragraph {
1226 content,
1227 span: *span,
1228 });
1229 i += 1;
1230 } else {
1231 let mut j = i;
1232 while j < items.len() && items[j].0 > depth {
1233 j += 1;
1234 }
1235 let (children, child_span) = build_blockquote(&items[i..j], depth + 1);
1236 paras.push(Block::Blockquote {
1237 children,
1238 span: child_span,
1239 });
1240 i = j;
1241 }
1242 }
1243 (paras, full_span)
1244}
1245
1246fn parse_fence_info(
1247 after: &str,
1248 base: u32,
1249 fence_span: Span,
1250 diags: &mut Vec<Diagnostic>,
1251) -> (Option<String>, CodeAttrs) {
1252 let mut attrs = CodeAttrs::default();
1256 let bytes = after.as_bytes();
1257 let mut i = 0usize;
1258 while i < bytes.len() && bytes[i] == b' ' {
1259 i += 1;
1260 }
1261 if i == bytes.len() {
1262 return (None, attrs);
1263 }
1264 let lang_start = i;
1265 while i < bytes.len() && bytes[i] != b' ' {
1266 i += 1;
1267 }
1268 let lang_tok = &after[lang_start..i];
1269 let lang = if lang_tok.is_empty() {
1270 None
1271 } else if lang_tok.starts_with('@') {
1272 i = lang_start;
1274 None
1275 } else {
1276 Some(lang_tok.to_string())
1277 };
1278
1279 while i < bytes.len() {
1280 while i < bytes.len() && bytes[i] == b' ' {
1281 i += 1;
1282 }
1283 if i >= bytes.len() {
1284 break;
1285 }
1286 let tok_start = i;
1287 while i < bytes.len() && bytes[i] != b' ' {
1288 i += 1;
1289 }
1290 let tok = &after[tok_start..i];
1291 if tok.is_empty() {
1292 continue;
1293 }
1294 let tok_span = Span::new(base as usize + tok_start, tok.len());
1295 if !tok.starts_with('@') {
1296 diags.push(
1297 Diagnostic::new(Code::UnknownCodeAttribute, tok_span)
1298 .label(format!("`{}` is not a valid code-fence attribute", tok))
1299 .help("attributes must be `@`-prefixed identifiers (e.g. `@nominify`)"),
1300 );
1301 continue;
1302 }
1303 let name = &tok[1..];
1304 match name {
1305 "nominify" => {
1306 if attrs.minify || attrs.keep_comments {
1307 diags.push(
1308 Diagnostic::new(Code::ConflictingCodeAttributes, fence_span)
1309 .label("`@nominify` conflicts with `@minify`/`@minify-keep-comments`"),
1310 );
1311 }
1312 attrs.nominify = true;
1313 }
1314 "minify" => {
1315 if attrs.nominify {
1316 diags.push(
1317 Diagnostic::new(Code::ConflictingCodeAttributes, fence_span)
1318 .label("`@nominify` and `@minify` cannot both be set"),
1319 );
1320 }
1321 attrs.minify = true;
1322 }
1323 "minify-keep-comments" => {
1324 if attrs.nominify {
1325 diags.push(
1326 Diagnostic::new(Code::ConflictingCodeAttributes, fence_span)
1327 .label("`@nominify` and `@minify-keep-comments` cannot both be set"),
1328 );
1329 }
1330 attrs.keep_comments = true;
1331 }
1332 _ => {
1333 diags.push(
1334 Diagnostic::new(Code::UnknownCodeAttribute, tok_span)
1335 .label(format!("unknown code-fence attribute `{}`", tok))
1336 .help("v0.4 supports `@nominify`, `@minify`, `@minify-keep-comments`"),
1337 );
1338 }
1339 }
1340 }
1341 (lang, attrs)
1342}
1343
1344fn parse_heading_anchor<'a>(
1362 text: &'a str,
1363 base: u32,
1364 diags: &mut Vec<Diagnostic>,
1365) -> (&'a str, Option<String>) {
1366 if !text.contains("{#") {
1368 return (text, None);
1369 }
1370
1371 let hash_open = match text.rfind("{#") {
1373 Some(i) => i,
1374 None => return (text, None),
1375 };
1376
1377 let after_hash = &text[hash_open..];
1383 let rbrace = match after_hash.find('}') {
1384 Some(i) => i,
1385 None => {
1386 return (text, None);
1388 }
1389 };
1390
1391 let candidate = &after_hash[..rbrace + 1]; let after_candidate = &after_hash[rbrace + 1..]; if !after_candidate.is_empty() {
1397 let anchor_span = Span::new(base as usize + hash_open, candidate.len());
1398 diags.push(
1399 Diagnostic::new(Code::BadHeadingAnchor, anchor_span)
1400 .label("anchor block must be `{#anchor}` with exactly one space before `{` and no content after `}`"),
1401 );
1402 return (text, None);
1404 }
1405
1406 let before = &text[..hash_open];
1409
1410 let malformed = if before.is_empty() {
1411 true
1413 } else {
1414 let last_ch = before.chars().last().unwrap();
1415 if last_ch != ' ' {
1416 true
1418 } else {
1419 let before_trim = &before[..before.len() - 1];
1421 before_trim.ends_with(' ')
1422 }
1423 };
1424
1425 let name_part = &candidate[2..candidate.len() - 1]; let anchor_span_start = base as usize + hash_open;
1429 let anchor_span = Span::new(anchor_span_start, candidate.len());
1430
1431 if malformed {
1432 diags.push(
1433 Diagnostic::new(Code::BadHeadingAnchor, anchor_span)
1434 .label("anchor block must be `{#anchor}` with exactly one space before `{` and no content after `}`"),
1435 );
1436 return (&text[..hash_open], None);
1438 }
1439
1440 let name_is_valid = !name_part.is_empty()
1442 && name_part
1443 .bytes()
1444 .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'-');
1445
1446 let stripped = &text[..hash_open - 1];
1449
1450 if !name_is_valid {
1451 let name_span = Span::new(anchor_span_start + 2, name_part.len().max(1));
1452 diags.push(
1453 Diagnostic::new(Code::BadHeadingAnchor, name_span)
1454 .label("anchor must match `[a-z0-9-]+`")
1455 .help("use lowercase letters, digits, and hyphens only"),
1456 );
1457 return (stripped, None);
1458 }
1459
1460 (stripped, Some(name_part.to_string()))
1461}
1462
1463fn leading_block_sigil(s: &str) -> bool {
1464 if s.is_empty() {
1465 return false;
1466 }
1467 let b = s.as_bytes()[0];
1468 if b == b'#' || b == b'>' || b == b'|' || b == b'`' {
1469 return true;
1470 }
1471 if s == "---" {
1472 return true;
1473 }
1474 if s.starts_with("- ") {
1475 return true;
1476 }
1477 if leading_ordered_marker(s).is_some() {
1478 return true;
1479 }
1480 if s.starts_with("//") || s.starts_with("/*") {
1481 return true;
1482 }
1483 if s == "@end" || s.starts_with("@end ") {
1484 return true;
1485 }
1486 if b == b'@' {
1487 return true;
1489 }
1490 false
1491}
1492
1493fn leading_ordered_marker(s: &str) -> Option<(u32, usize)> {
1494 let bytes = s.as_bytes();
1495 let mut i = 0;
1496 while i < bytes.len() && bytes[i].is_ascii_digit() {
1497 i += 1;
1498 }
1499 if i == 0 {
1500 return None;
1501 }
1502 if bytes.get(i) != Some(&b'.') {
1503 return None;
1504 }
1505 if bytes.get(i + 1) != Some(&b' ') {
1506 return None;
1507 }
1508 let n: u32 = s[..i].parse().ok()?;
1509 Some((n, i + 2))
1510}
1511
1512#[derive(Debug)]
1519struct RowSplit<'a> {
1520 cells: Vec<&'a str>,
1521 unclosed_backtick_at: Option<usize>,
1528}
1529
1530fn split_cells(line: &str) -> RowSplit<'_> {
1531 let bytes = line.as_bytes();
1532 let body_start = if bytes.first() == Some(&b'|') { 1 } else { 0 };
1534 let body = &line[body_start..];
1535 let body_bytes = body.as_bytes();
1536 let mut cells: Vec<&str> = Vec::new();
1537 let mut cell_start = 0usize;
1538 let mut i = 0usize;
1539 let mut unclosed: Option<usize> = None;
1543 while i < body_bytes.len() {
1544 let b = body_bytes[i];
1545 if b == b'\\' {
1546 i += 1;
1550 if i < body_bytes.len() {
1551 i += 1;
1552 }
1553 continue;
1554 }
1555 if b == b'`' {
1556 let ticks = if body_bytes.get(i + 1) == Some(&b'`') {
1559 2
1560 } else {
1561 1
1562 };
1563 let span_open = i;
1564 let needle: &[u8] = if ticks == 2 { b"``" } else { b"`" };
1565 let mut j = i + ticks;
1566 let mut closed = false;
1567 while j + ticks <= body_bytes.len() {
1568 if &body_bytes[j..j + ticks] == needle {
1569 j += ticks;
1570 closed = true;
1571 break;
1572 }
1573 j += 1;
1574 }
1575 if !closed {
1576 unclosed = Some(body_start + span_open);
1581 break;
1582 }
1583 i = j;
1584 continue;
1585 }
1586 if b == b'|' {
1587 cells.push(&body[cell_start..i]);
1588 cell_start = i + 1;
1589 i += 1;
1590 continue;
1591 }
1592 i += 1;
1593 }
1594 if unclosed.is_none() {
1595 let last = &body[cell_start..];
1599 if !(last.is_empty() && cell_start > 0 && body_bytes[cell_start - 1] == b'|') {
1600 cells.push(last);
1601 }
1602 }
1603 let trimmed: Vec<&str> = cells.into_iter().map(str::trim).collect();
1604 RowSplit {
1605 cells: trimmed,
1606 unclosed_backtick_at: unclosed,
1607 }
1608}
1609
1610#[cfg(test)]
1611mod tests {
1612 use super::*;
1613 use crate::lexer::lex;
1614
1615 fn p(s: &str) -> (Document, Vec<Diagnostic>) {
1616 let src = SourceMap::new("d.brf", s);
1617 let toks = lex(&src).unwrap();
1618 parse(toks, &src)
1619 }
1620
1621 #[test]
1622 fn heading_levels() {
1623 let (doc, d) = p("# A\n## B\n");
1624 assert!(d.is_empty(), "{:?}", d);
1625 assert_eq!(doc.blocks.len(), 2);
1626 if let Block::Heading { level, .. } = doc.blocks[0] {
1627 assert_eq!(level, 1);
1628 }
1629 if let Block::Heading { level, .. } = doc.blocks[1] {
1630 assert_eq!(level, 2);
1631 }
1632 }
1633
1634 #[test]
1635 fn heading_too_deep() {
1636 let (_, d) = p("####### x\n");
1637 assert!(d.iter().any(|x| x.code == Code::HeadingTooDeep));
1638 }
1639
1640 #[test]
1641 fn ordered_sequence() {
1642 let (_, d) = p("1. one\n3. three\n");
1643 assert!(d.iter().any(|x| x.code == Code::OrderedListSequence));
1644 }
1645
1646 #[test]
1647 fn ordered_ok() {
1648 let (doc, d) = p("1. one\n2. two\n");
1649 assert!(d.is_empty(), "{:?}", d);
1650 assert!(matches!(doc.blocks[0], Block::List { ordered: true, .. }));
1651 }
1652
1653 #[test]
1654 fn unordered_nested() {
1655 let (doc, d) = p("- a\n - a1\n- b\n");
1656 assert!(d.is_empty(), "{:?}", d);
1657 if let Block::List { items, .. } = &doc.blocks[0] {
1658 assert_eq!(items.len(), 2);
1659 assert_eq!(items[0].children.len(), 1);
1660 } else {
1661 panic!();
1662 }
1663 }
1664
1665 #[test]
1666 fn paragraph_join() {
1667 let (doc, d) = p("one\ntwo\n");
1668 assert!(d.is_empty());
1669 if let Block::Paragraph { content, .. } = &doc.blocks[0] {
1670 if let Inline::Text { value, .. } = &content[0] {
1671 assert_eq!(value, "one two");
1672 }
1673 }
1674 }
1675
1676 #[test]
1677 fn code_block() {
1678 let (doc, d) = p("```rust\nfn x() {}\n```\n");
1679 assert!(d.is_empty(), "{:?}", d);
1680 if let Block::CodeBlock {
1681 lang, body, attrs, ..
1682 } = &doc.blocks[0]
1683 {
1684 assert_eq!(lang.as_deref(), Some("rust"));
1685 assert_eq!(body, "fn x() {}");
1686 assert_eq!(*attrs, CodeAttrs::default());
1687 } else {
1688 panic!();
1689 }
1690 }
1691
1692 #[test]
1693 fn code_fence_nominify_attr() {
1694 let (doc, d) = p("```json @nominify\n{\"a\":1}\n```\n");
1695 assert!(d.is_empty(), "{:?}", d);
1696 if let Block::CodeBlock { lang, attrs, .. } = &doc.blocks[0] {
1697 assert_eq!(lang.as_deref(), Some("json"));
1698 assert!(attrs.nominify);
1699 assert!(!attrs.minify);
1700 } else {
1701 panic!();
1702 }
1703 }
1704
1705 #[test]
1706 fn code_fence_minify_attr() {
1707 let (doc, d) = p("```rust @minify\nfn x() {}\n```\n");
1708 assert!(d.is_empty(), "{:?}", d);
1709 if let Block::CodeBlock { lang, attrs, .. } = &doc.blocks[0] {
1710 assert_eq!(lang.as_deref(), Some("rust"));
1711 assert!(attrs.minify);
1712 } else {
1713 panic!();
1714 }
1715 }
1716
1717 #[test]
1718 fn code_fence_unknown_attr_errors() {
1719 let (_, d) = p("```json @bogus\n{}\n```\n");
1720 assert!(
1721 d.iter().any(|x| x.code == Code::UnknownCodeAttribute),
1722 "{:?}",
1723 d
1724 );
1725 }
1726
1727 #[test]
1728 fn code_fence_attr_without_at_sigil_errors() {
1729 let (_, d) = p("```json bogus\n{}\n```\n");
1730 assert!(
1731 d.iter().any(|x| x.code == Code::UnknownCodeAttribute),
1732 "{:?}",
1733 d
1734 );
1735 }
1736
1737 #[test]
1738 fn code_fence_conflicting_attrs() {
1739 let (_, d) = p("```json @nominify @minify\n{}\n```\n");
1740 assert!(
1741 d.iter().any(|x| x.code == Code::ConflictingCodeAttributes),
1742 "{:?}",
1743 d
1744 );
1745 }
1746
1747 #[test]
1748 fn code_fence_attr_only_no_lang() {
1749 let (doc, d) = p("``` @nominify\nbody\n```\n");
1752 assert!(d.is_empty(), "{:?}", d);
1753 if let Block::CodeBlock { lang, attrs, .. } = &doc.blocks[0] {
1754 assert!(lang.is_none());
1755 assert!(attrs.nominify);
1756 } else {
1757 panic!();
1758 }
1759 }
1760
1761 #[test]
1762 fn table_basic() {
1763 let (doc, d) = p("@t\n| A | B\n| 1 | 2\n");
1764 assert!(d.is_empty(), "{:?}", d);
1765 if let Block::Table { rows, .. } = &doc.blocks[0] {
1766 assert_eq!(rows.len(), 1);
1767 } else {
1768 panic!("{:?}", doc.blocks);
1769 }
1770 }
1771
1772 #[test]
1773 fn table_column_mismatch() {
1774 let (_, d) = p("@t\n| A | B | C\n| 1 | 2\n");
1775 assert!(d.iter().any(|x| x.code == Code::TableColumnMismatch));
1776 }
1777
1778 #[test]
1779 fn table_pipe_inside_inline_code_span_is_not_a_separator() {
1780 let (doc, d) = p("@t\n| Op | Meaning\n| `|>` | pipeline\n");
1783 assert!(d.is_empty(), "{:?}", d);
1784 if let crate::ast::Block::Table { rows, .. } = &doc.blocks[0] {
1785 assert_eq!(rows.len(), 1, "{:?}", rows);
1786 assert_eq!(rows[0].cells.len(), 2);
1787 } else {
1788 panic!("expected table");
1789 }
1790 }
1791
1792 #[test]
1793 fn table_pipe_inside_double_backtick_span_is_not_a_separator() {
1794 let (doc, d) = p("@t\n| A | B\n| ``a ` b | c`` | d\n");
1795 assert!(d.is_empty(), "{:?}", d);
1796 if let crate::ast::Block::Table { rows, .. } = &doc.blocks[0] {
1797 assert_eq!(rows.len(), 1);
1798 assert_eq!(rows[0].cells.len(), 2);
1799 } else {
1800 panic!();
1801 }
1802 }
1803
1804 #[test]
1805 fn table_unclosed_backtick_in_row_reports_unterminated_code_not_column_mismatch() {
1806 let (_doc, d) = p("@t\n| A | B\n| `oops | c\n");
1807 assert!(
1808 d.iter().any(|x| x.code == Code::UnterminatedCode),
1809 "{:?}",
1810 d
1811 );
1812 assert!(
1813 !d.iter().any(|x| x.code == Code::TableColumnMismatch),
1814 "{:?}",
1815 d
1816 );
1817 }
1818
1819 #[test]
1820 fn table_unclosed_backtick_with_indented_row_diagnostic_anchors_correctly() {
1821 let (_doc, d) = p(" @t\n | A | B\n | `oops | c\n");
1825 let unterm: Vec<_> = d
1826 .iter()
1827 .filter(|x| x.code == Code::UnterminatedCode)
1828 .collect();
1829 assert_eq!(unterm.len(), 1, "{:?}", d);
1830 }
1831
1832 #[test]
1833 fn block_shortcode() {
1834 let (doc, d) = p("@callout(kind: warning)\nbody\n@end\n");
1835 assert!(d.is_empty(), "{:?}", d);
1836 assert!(matches!(doc.blocks[0], Block::BlockShortcode { .. }));
1837 }
1838
1839 #[test]
1840 fn hr() {
1841 let (doc, _) = p("---\n");
1842 assert!(matches!(doc.blocks[0], Block::HorizontalRule { .. }));
1843 }
1844
1845 #[test]
1846 fn frontmatter_basic() {
1847 let input = "+++\ntitle = \"hi\"\nn = 3\n+++\n# Doc\n";
1848 let (doc, d) = p(input);
1849 assert!(d.is_empty(), "{:?}", d);
1850 let meta = doc.metadata.as_ref().expect("metadata present");
1851 assert_eq!(meta.get("title").and_then(|v| v.as_str()), Some("hi"));
1852 assert_eq!(meta.get("n").and_then(|v| v.as_integer()), Some(3));
1853 assert_eq!(doc.blocks.len(), 1);
1854 assert!(matches!(doc.blocks[0], Block::Heading { level: 1, .. }));
1855 }
1856
1857 #[test]
1858 fn frontmatter_empty_table() {
1859 let (doc, d) = p("+++\n+++\n");
1860 assert!(d.is_empty(), "{:?}", d);
1861 let meta = doc.metadata.as_ref().expect("metadata present");
1862 assert!(meta.is_empty());
1863 assert!(doc.blocks.is_empty());
1864 }
1865
1866 #[test]
1867 fn frontmatter_unterminated() {
1868 let (_, d) = p("+++\nfoo = 1\n");
1869 assert!(
1870 d.iter().any(|x| x.code == Code::UnterminatedFrontmatter),
1871 "{:?}",
1872 d
1873 );
1874 }
1875
1876 #[test]
1877 fn frontmatter_bad_toml() {
1878 let (_, d) = p("+++\nfoo === 1\n+++\n");
1879 assert!(d.iter().any(|x| x.code == Code::FrontmatterToml), "{:?}", d);
1880 }
1881
1882 #[test]
1883 fn frontmatter_only_first_line() {
1884 let (doc, _d) = p("\n+++\nfoo = 1\n+++\n");
1887 assert!(doc.metadata.is_none());
1888 }
1889
1890 #[test]
1891 fn frontmatter_indented_is_not_frontmatter() {
1892 let (doc, _d) = p(" +++\nfoo = 1\n+++\n");
1894 assert!(doc.metadata.is_none());
1895 }
1896
1897 #[test]
1898 fn frontmatter_no_open_means_none() {
1899 let (doc, _d) = p("# Heading\n");
1900 assert!(doc.metadata.is_none());
1901 }
1902
1903 #[test]
1904 fn frontmatter_crlf() {
1905 let input = "+++\r\ntitle = \"hi\"\r\n+++\r\n# Doc\r\n";
1906 let (doc, d) = p(input);
1907 assert!(d.is_empty(), "{:?}", d);
1908 let meta = doc.metadata.as_ref().expect("metadata present");
1909 assert_eq!(meta.get("title").and_then(|v| v.as_str()), Some("hi"));
1910 }
1911
1912 #[test]
1913 fn dl_basic_two_pairs() {
1914 let (doc, d) = p("@dl\nTerm 1\n: Definition 1.\nTerm 2\n: Definition 2.\n@end\n");
1915 assert!(d.is_empty(), "{:?}", d);
1916 let dl = match &doc.blocks[0] {
1917 Block::DefinitionList { items, .. } => items,
1918 other => panic!("expected DefinitionList, got {:?}", other),
1919 };
1920 assert_eq!(dl.len(), 2);
1921 let term0 = match &dl[0].term[0] {
1922 Inline::Text { value, .. } => value.as_str(),
1923 _ => panic!("expected Text in term"),
1924 };
1925 let def0 = match &dl[0].definition[0] {
1926 Inline::Text { value, .. } => value.as_str(),
1927 _ => panic!("expected Text in definition"),
1928 };
1929 assert_eq!(term0, "Term 1");
1930 assert_eq!(def0, "Definition 1.");
1931 let term1 = match &dl[1].term[0] {
1932 Inline::Text { value, .. } => value.as_str(),
1933 _ => panic!("expected Text in term"),
1934 };
1935 assert_eq!(term1, "Term 2");
1936 }
1937
1938 #[test]
1939 fn dl_continuation_joins_with_space() {
1940 let input = "@dl\nTerm\n: Definition that\n spans two lines.\n@end\n";
1941 let (doc, d) = p(input);
1942 assert!(d.is_empty(), "{:?}", d);
1943 let items = match &doc.blocks[0] {
1944 Block::DefinitionList { items, .. } => items,
1945 other => panic!("expected DefinitionList, got {:?}", other),
1946 };
1947 assert_eq!(items.len(), 1);
1948 let def_text = match &items[0].definition[0] {
1949 Inline::Text { value, .. } => value.as_str(),
1950 _ => panic!("expected Text"),
1951 };
1952 assert_eq!(def_text, "Definition that spans two lines.");
1953 }
1954
1955 #[test]
1956 fn dl_definition_without_term_is_b0505() {
1957 let (_, d) = p("@dl\n: Stray definition.\nTerm\n: Def.\n@end\n");
1958 assert!(
1959 d.iter().any(|x| x.code == Code::BadDefinitionList),
1960 "{:?}",
1961 d
1962 );
1963 }
1964
1965 #[test]
1966 fn dl_term_without_definition_is_b0505() {
1967 let (_, d) = p("@dl\nLonely term\n@end\n");
1968 assert!(
1969 d.iter().any(|x| x.code == Code::BadDefinitionList),
1970 "{:?}",
1971 d
1972 );
1973 }
1974
1975 #[test]
1976 fn dl_multiple_definitions_per_term_is_b0505() {
1977 let (_, d) = p("@dl\nTerm\n: First def.\n: Second def.\n@end\n");
1978 assert!(
1979 d.iter().any(|x| x.code == Code::BadDefinitionList),
1980 "{:?}",
1981 d
1982 );
1983 }
1984
1985 #[test]
1986 fn dl_empty_body_is_b0505() {
1987 let (_, d) = p("@dl\n@end\n");
1988 assert!(
1989 d.iter().any(|x| x.code == Code::BadDefinitionList),
1990 "{:?}",
1991 d
1992 );
1993 }
1994
1995 #[test]
1996 fn dl_unterminated_is_b0306() {
1997 let (_, d) = p("@dl\nTerm\n: Def.\n");
1998 assert!(
1999 d.iter().any(|x| x.code == Code::UnterminatedBlock),
2000 "{:?}",
2001 d
2002 );
2003 }
2004
2005 #[test]
2006 fn deep_list_nesting_is_b0318_not_crash() {
2007 let src: String = (0..6000)
2008 .map(|i| format!("{}- x\n", " ".repeat(i)))
2009 .collect();
2010 let (_, d) = p(&src);
2011 assert!(d.iter().any(|x| x.code == Code::NestingTooDeep), "{:?}", d);
2012 }
2013
2014 #[test]
2015 fn deep_shortcode_nesting_is_b0318_not_crash() {
2016 let src = format!("{}x\n{}", "@details\n".repeat(6000), "@end\n".repeat(6000));
2017 let (_, d) = p(&src);
2018 assert!(d.iter().any(|x| x.code == Code::NestingTooDeep), "{:?}", d);
2019 }
2020
2021 #[test]
2022 fn deep_blockquote_markers_is_b0318_not_crash() {
2023 let src = format!("{} hi\n", ">".repeat(50_000));
2024 let (_, d) = p(&src);
2025 assert!(d.iter().any(|x| x.code == Code::NestingTooDeep), "{:?}", d);
2026 }
2027
2028 #[test]
2029 fn code_shortcode_body_is_verbatim() {
2030 let (doc, d) = p("@code(lang: rust)\nlet x = *ptr;\n@end\n");
2031 assert!(d.is_empty(), "{:?}", d);
2032 match &doc.blocks[0] {
2033 Block::CodeBlock { lang, body, .. } => {
2034 assert_eq!(lang.as_deref(), Some("rust"));
2035 assert_eq!(body, "let x = *ptr;");
2036 }
2037 b => panic!("expected CodeBlock, got {:?}", b),
2038 }
2039 }
2040
2041 #[test]
2042 fn code_shortcode_positional_lang_and_nested_fence() {
2043 let (doc, d) = p("@code(brief)\n```rust\nfn main() {}\n```\n@end\n");
2044 assert!(d.is_empty(), "{:?}", d);
2045 match &doc.blocks[0] {
2046 Block::CodeBlock { lang, body, .. } => {
2047 assert_eq!(lang.as_deref(), Some("brief"));
2048 assert_eq!(body, "```rust\nfn main() {}\n```");
2049 }
2050 b => panic!("expected CodeBlock, got {:?}", b),
2051 }
2052 }
2053
2054 #[test]
2055 fn code_shortcode_unknown_arg_is_b0406() {
2056 let (_, d) = p("@code(bogus: 1)\nx\n@end\n");
2057 assert!(d.iter().any(|x| x.code == Code::BadArgSyntax), "{:?}", d);
2058 }
2059
2060 #[test]
2061 fn code_shortcode_unterminated_is_b0306() {
2062 let (_, d) = p("@code\nx\n");
2063 assert!(
2064 d.iter().any(|x| x.code == Code::UnterminatedBlock),
2065 "{:?}",
2066 d
2067 );
2068 }
2069
2070 #[test]
2071 fn table_align_value_outside_enum_is_b0404() {
2072 let (_, d) = p("@t(align: [\"left\\\"><script>x</script>\", right])\n| A | B\n| 1 | 2\n");
2073 assert!(d.iter().any(|x| x.code == Code::BadEnumValue), "{:?}", d);
2074 }
2075
2076 #[test]
2077 fn table_align_valid_values_ok() {
2078 let (_, d) = p("@t(align: [left, right, center])\n| A | B | C\n| 1 | 2 | 3\n");
2079 assert!(d.is_empty(), "{:?}", d);
2080 }
2081
2082 #[test]
2083 fn four_dash_rule_is_b0304() {
2084 let (_, d) = p("----\n");
2085 assert!(
2086 d.iter().any(|x| x.code == Code::BadHorizontalRule),
2087 "{:?}",
2088 d
2089 );
2090 }
2091
2092 #[test]
2093 fn two_dash_rule_is_b0304() {
2094 let (_, d) = p("--\n");
2095 assert!(
2096 d.iter().any(|x| x.code == Code::BadHorizontalRule),
2097 "{:?}",
2098 d
2099 );
2100 }
2101
2102 #[test]
2103 fn three_dash_rule_is_ok() {
2104 let (doc, d) = p("---\n");
2105 assert!(d.is_empty(), "{:?}", d);
2106 assert!(matches!(doc.blocks[0], Block::HorizontalRule { .. }));
2107 }
2108
2109 #[test]
2110 fn dl_extra_space_after_colon_is_b0505() {
2111 let (_, d) = p("@dl\nTerm\n: two spaces\n@end\n");
2112 assert!(
2113 d.iter().any(|x| x.code == Code::BadDefinitionList),
2114 "{:?}",
2115 d
2116 );
2117 }
2118
2119 #[test]
2120 fn dl_no_space_after_colon_is_b0505() {
2121 let (_, d) = p("@dl\nTerm\n:nospace\n@end\n");
2122 assert!(
2123 d.iter().any(|x| x.code == Code::BadDefinitionList),
2124 "{:?}",
2125 d
2126 );
2127 }
2128
2129 #[test]
2130 fn nesting_at_limit_is_ok() {
2131 let src: String = (0..MAX_NESTING_DEPTH)
2132 .map(|i| format!("{}- x\n", " ".repeat(i)))
2133 .collect();
2134 let (_, d) = p(&src);
2135 assert!(d.is_empty(), "{:?}", d);
2136 }
2137}