1use std::borrow::Cow;
7
8use crate::ast::{
9 AttrValue, Block, Callout, CodeBlock, CowStr, Document, Figure, FootnoteDef, Footnotes,
10 Heading, HtmlBlock, List, ListItem, ListKind, MathBlock, Metadata, Module, Paragraph, Profile,
11 Quote, RawBlock, Table, TableCell, TableRow,
12};
13use crate::error::{ParseError, ParseErrors};
14use crate::lexer::Lexer;
15use crate::span::Span;
16
17#[derive(Debug)]
19pub struct ParseResult<'a> {
20 pub document: Document<'a>,
22 pub errors: ParseErrors,
24}
25
26impl<'a> ParseResult<'a> {
27 pub fn is_ok(&self) -> bool {
29 self.errors.is_empty()
30 }
31
32 pub fn has_fatal_errors(&self) -> bool {
34 self.errors.has_fatal()
35 }
36}
37
38pub struct Parser {
40 profile: Profile,
41 modules: Vec<Module>,
42 errors: ParseErrors,
44 recover_on_error: bool,
46}
47
48impl Parser {
49 #[inline]
51 pub fn new(profile: Profile) -> Self {
52 Self {
53 profile,
54 modules: Vec::new(),
55 errors: ParseErrors::new(),
56 recover_on_error: true,
57 }
58 }
59
60 pub fn with_recovery(mut self, recover: bool) -> Self {
66 self.recover_on_error = recover;
67 self
68 }
69
70 #[inline]
72 pub fn parse_with_recovery<'a>(&mut self, input: &'a str) -> ParseResult<'a> {
73 self.errors = ParseErrors::new();
74 let doc = self.parse_internal(input);
75 ParseResult {
76 document: doc,
77 errors: std::mem::take(&mut self.errors),
78 }
79 }
80
81 #[inline]
83 pub fn parse<'a>(&mut self, input: &'a str) -> Result<Document<'a>, ParseError> {
84 self.errors = ParseErrors::new();
85 let doc = self.parse_internal(input);
86 if self.errors.is_empty() {
87 Ok(doc)
88 } else {
89 Err(self.errors.iter().next().unwrap().clone())
91 }
92 }
93
94 #[inline]
95 fn parse_internal<'a>(&mut self, input: &'a str) -> Document<'a> {
96 let mut lexer = Lexer::new(input);
97
98 lexer.skip_blank_lines();
99
100 let profile = self.parse_profile_directive(&mut lexer);
101 let modules = self.parse_modules_directive(&mut lexer);
102 self.modules = modules.clone();
103
104 lexer.skip_blank_lines();
105
106 let metadata = self.parse_metadata(&mut lexer, input);
107
108 lexer.skip_blank_lines();
109
110 let blocks = self.parse_blocks(&mut lexer, input);
111
112 Document {
113 profile: profile.unwrap_or(self.profile),
114 modules,
115 metadata,
116 blocks,
117 span: Span::new(0, input.len() as u32),
118 }
119 }
120
121 #[inline]
123 fn record_error(&mut self, error: ParseError) {
124 self.errors.push(error);
125 }
126
127 #[inline]
129 pub fn has_module(&self, module: Module) -> bool {
130 self.modules.contains(&module)
131 }
132
133 #[inline]
134 fn parse_profile_directive(&self, lexer: &mut Lexer) -> Option<Profile> {
135 let line = lexer.peek_line()?;
136 let trimmed = line.trimmed();
137
138 if let Some(rest) = trimmed.strip_prefix("@profile") {
139 let profile = match rest.trim() {
140 "litedoc" => Some(Profile::Litedoc),
141 "md" => Some(Profile::Md),
142 "md-strict" => Some(Profile::MdStrict),
143 _ => None,
144 };
145 if profile.is_some() {
146 lexer.next_line();
147 lexer.skip_blank_lines();
148 }
149 profile
150 } else {
151 None
152 }
153 }
154
155 #[inline]
156 fn parse_modules_directive(&self, lexer: &mut Lexer) -> Vec<Module> {
157 let line = lexer.peek_line();
158 let trimmed = match line {
159 Some(l) => l.trimmed(),
160 None => return Vec::new(),
161 };
162
163 if let Some(rest) = trimmed.strip_prefix("@modules") {
164 let mut modules = Vec::with_capacity(4);
165 for part in rest.split(',') {
166 match part.trim() {
167 "tables" => modules.push(Module::Tables),
168 "footnotes" => modules.push(Module::Footnotes),
169 "math" => modules.push(Module::Math),
170 "tasks" => modules.push(Module::Tasks),
171 "strikethrough" => modules.push(Module::Strikethrough),
172 "autolink" => modules.push(Module::Autolink),
173 "html" => modules.push(Module::Html),
174 _ => {}
175 }
176 }
177 lexer.next_line();
178 lexer.skip_blank_lines();
179 modules
180 } else {
181 Vec::new()
182 }
183 }
184
185 #[inline]
186 fn parse_metadata<'a>(&self, lexer: &mut Lexer, input: &'a str) -> Option<Metadata<'a>> {
187 let start_span;
188 {
189 let line = lexer.peek_line()?;
190 let trimmed = line.trimmed();
191 if !trimmed.starts_with("---") || !trimmed.contains("meta") {
192 return None;
193 }
194 start_span = line.span;
195 }
196 lexer.next_line();
197
198 let mut entries: Vec<(CowStr<'a>, AttrValue<'a>)> = Vec::with_capacity(8);
199 let mut end_span = start_span;
200
201 loop {
202 let (trimmed, span, is_end) = {
203 match lexer.peek_line() {
204 Some(line) => {
205 let t = line.trimmed();
206 let s = line.span;
207 let end = t == "---";
208 (t.to_string(), s, end)
209 }
210 None => break,
211 }
212 };
213
214 if is_end {
215 end_span = span;
216 lexer.next_line();
217 break;
218 }
219
220 if let Some(_colon_pos) = trimmed.find(':') {
221 let line_start = span.start as usize;
223 let line_text = &input[line_start..span.end as usize];
224
225 if let Some(cp) = line_text.find(':') {
226 let key_slice = line_text[..cp].trim();
227 let val_slice = line_text[cp + 1..].trim();
228
229 let key: CowStr<'a> = Cow::Borrowed(key_slice);
230 let value = self.parse_attr_value(val_slice);
231 entries.push((key, value));
232 }
233 }
234
235 end_span = span;
236 lexer.next_line();
237 }
238
239 Some(Metadata {
240 entries,
241 span: Span::new(start_span.start, end_span.end),
242 })
243 }
244
245 #[inline]
246 fn parse_attr_value<'a>(&self, s: &'a str) -> AttrValue<'a> {
247 if s == "true" {
248 return AttrValue::Bool(true);
249 }
250 if s == "false" {
251 return AttrValue::Bool(false);
252 }
253
254 if s.starts_with('[') && s.ends_with(']') {
255 let inner = &s[1..s.len() - 1];
256 let items = self.parse_list_items(inner);
257 return AttrValue::List(items);
258 }
259
260 if let Ok(i) = s.parse::<i64>() {
261 return AttrValue::Int(i);
262 }
263
264 if s.contains('.') {
265 if let Ok(f) = s.parse::<f64>() {
266 return AttrValue::Float(f);
267 }
268 }
269
270 let unquoted = if (s.starts_with('"') && s.ends_with('"'))
271 || (s.starts_with('\'') && s.ends_with('\''))
272 {
273 &s[1..s.len() - 1]
274 } else {
275 s
276 };
277
278 AttrValue::Str(Cow::Borrowed(unquoted))
279 }
280
281 #[inline]
282 fn parse_list_items<'a>(&self, s: &'a str) -> Vec<AttrValue<'a>> {
283 let mut items = Vec::with_capacity(4);
284 let mut start = 0;
285 let mut in_quotes = false;
286 let bytes = s.as_bytes();
287
288 for i in 0..bytes.len() {
289 match bytes[i] {
290 b'"' | b'\'' => in_quotes = !in_quotes,
291 b',' if !in_quotes => {
292 let item = s[start..i].trim();
293 if !item.is_empty() {
294 items.push(self.parse_attr_value(item));
295 }
296 start = i + 1;
297 }
298 _ => {}
299 }
300 }
301
302 let item = s[start..].trim();
303 if !item.is_empty() {
304 items.push(self.parse_attr_value(item));
305 }
306
307 items
308 }
309
310 #[inline]
311 fn parse_blocks<'a>(&mut self, lexer: &mut Lexer, input: &'a str) -> Vec<Block<'a>> {
312 let mut blocks = Vec::with_capacity(16);
313
314 while !lexer.is_eof() {
315 lexer.skip_blank_lines();
316
317 if lexer.is_eof() {
318 break;
319 }
320
321 if let Some(block) = self.parse_block(lexer, input) {
322 blocks.push(block);
323 }
324 }
325
326 blocks
327 }
328
329 #[inline]
330 fn parse_block<'a>(&mut self, lexer: &mut Lexer, input: &'a str) -> Option<Block<'a>> {
331 let (first_byte, trimmed_starts_triple, is_hr, starts_colon, span) = {
332 let line = lexer.peek_line()?;
333 let trimmed = line.trimmed();
334 (
335 trimmed.as_bytes().first().copied(),
336 trimmed.starts_with("```"),
337 trimmed == "---",
338 trimmed.starts_with("::"),
339 line.span,
340 )
341 };
342
343 match first_byte {
344 Some(b'#') => self.parse_heading(lexer, input),
345 Some(b'`') if trimmed_starts_triple => self.parse_code_block(lexer, input),
346 Some(b'-') if is_hr => {
347 lexer.next_line();
348 Some(Block::ThematicBreak(span))
349 }
350 Some(b':') if starts_colon => self.parse_fenced_block(lexer, input),
351 _ => self.parse_paragraph(lexer, input),
352 }
353 }
354
355 #[inline]
356 fn parse_heading<'a>(&mut self, lexer: &mut Lexer, input: &'a str) -> Option<Block<'a>> {
357 let line = lexer.next_line()?;
358 let text = &input[line.span.start as usize..line.span.end as usize];
359 let bytes = text.as_bytes();
360
361 let level = bytes.iter().take_while(|&&b| b == b'#').count() as u8;
362
363 if level == 0 || level > 6 {
364 return Some(Block::Paragraph(Paragraph {
365 content: crate::inline::parse_inlines(text, line.span.start, input),
366 span: line.span,
367 }));
368 }
369
370 let rest = &text[level as usize..];
371 if !rest.starts_with(' ') && !rest.is_empty() {
372 return Some(Block::Paragraph(Paragraph {
373 content: crate::inline::parse_inlines(text, line.span.start, input),
374 span: line.span,
375 }));
376 }
377
378 let content_text = rest.trim_start();
379 let content_offset = line.span.start + (text.len() - content_text.len()) as u32;
380
381 Some(Block::Heading(Heading {
382 level,
383 content: crate::inline::parse_inlines(content_text, content_offset, input),
384 span: line.span,
385 }))
386 }
387
388 #[inline]
389 fn parse_code_block<'a>(&mut self, lexer: &mut Lexer, input: &'a str) -> Option<Block<'a>> {
390 let (start_span, lang_start, lang_end) = {
391 let open_line = lexer.next_line()?;
392 let text = &input[open_line.span.start as usize..open_line.span.end as usize];
393 let trimmed = text.trim();
394 let after_ticks = trimmed.strip_prefix("```").unwrap_or("");
395 let lang = after_ticks.trim();
396
397 let lang_offset = if lang.is_empty() {
399 open_line.span.end as usize
400 } else {
401 open_line.span.start as usize + text.find(lang).unwrap_or(0)
403 };
404
405 (open_line.span, lang_offset, lang_offset + lang.len())
406 };
407
408 let lang = &input[lang_start..lang_end];
409
410 let content_start = (start_span.end as usize + 1).min(input.len());
413 let mut content_end = content_start;
414 let mut end_span = start_span;
415
416 loop {
417 let (is_close, span) = {
418 match lexer.peek_line() {
419 Some(line) => (line.trimmed() == "```", line.span),
420 None => break,
421 }
422 };
423
424 if is_close {
425 end_span = span;
426 lexer.next_line();
427 break;
428 }
429
430 end_span = span;
431 content_end = span.end as usize;
432 lexer.next_line();
433 }
434
435 let content = if content_start < content_end && content_end <= input.len() {
436 &input[content_start..content_end]
437 } else {
438 ""
439 };
440
441 Some(Block::CodeBlock(CodeBlock {
442 lang: Cow::Borrowed(lang),
443 content: Cow::Borrowed(content),
444 span: Span::new(start_span.start, end_span.end),
445 }))
446 }
447
448 #[inline]
449 fn parse_fenced_block<'a>(&mut self, lexer: &mut Lexer, input: &'a str) -> Option<Block<'a>> {
450 let (block_type, span) = {
451 let line = lexer.peek_line()?;
452 let trimmed = line.trimmed();
453 let after_colons = trimmed.strip_prefix("::")?;
454 let bt = after_colons
455 .split_whitespace()
456 .next()
457 .unwrap_or("")
458 .to_string();
459 (bt, line.span)
460 };
461
462 match block_type.as_str() {
463 "list" => self.parse_list_block(lexer, input),
464 "callout" => self.parse_callout_block(lexer, input),
465 "quote" => self.parse_quote_block(lexer, input),
466 "figure" => self.parse_figure_block(lexer, input),
467 "table" => self.parse_table_block(lexer, input),
468 "footnotes" => self.parse_footnotes_block(lexer, input),
469 "math" => self.parse_math_block(lexer, input),
470 "html" => self.parse_html_block(lexer, input),
471 _ => {
472 if self.recover_on_error && !block_type.is_empty() {
474 self.record_error(ParseError::unknown_directive(&block_type, Some(span)));
475 }
476 self.parse_raw_fenced_block(lexer, input)
477 }
478 }
479 }
480
481 #[inline]
482 fn parse_html_block<'a>(&mut self, lexer: &mut Lexer, input: &'a str) -> Option<Block<'a>> {
483 if !self.has_module(Module::Html) {
485 return self.parse_raw_fenced_block(lexer, input);
486 }
487
488 let start_span = lexer.next_line()?.span;
489
490 let content_start = (start_span.end as usize + 1).min(input.len());
492 let mut content_end = content_start;
493 let mut end_span = start_span;
494
495 loop {
496 let (is_close, span) = {
497 match lexer.peek_line() {
498 Some(line) => (line.trimmed() == "::", line.span),
499 None => {
500 self.record_error(ParseError::unclosed_delimiter(
502 "HTML block",
503 Some(start_span),
504 ));
505 break;
506 }
507 }
508 };
509
510 if is_close {
511 end_span = span;
512 lexer.next_line();
513 break;
514 }
515
516 content_end = span.end as usize;
517 end_span = span;
518 lexer.next_line();
519 }
520
521 let content = if content_start < content_end && content_end <= input.len() {
522 &input[content_start..content_end]
523 } else {
524 ""
525 };
526
527 Some(Block::Html(HtmlBlock {
528 content: Cow::Borrowed(content),
529 span: Span::new(start_span.start, end_span.end),
530 }))
531 }
532
533 #[inline]
534 fn parse_list_block<'a>(&mut self, lexer: &mut Lexer, input: &'a str) -> Option<Block<'a>> {
535 let (start_span, kind, start_num) = {
536 let open_line = lexer.next_line()?;
537 let text = &input[open_line.span.start as usize..open_line.span.end as usize];
538 let trimmed = text.trim();
539 let after_list = trimmed.strip_prefix("::list").unwrap_or("").trim();
540
541 let mut k = ListKind::Unordered;
542 let mut sn: Option<u64> = None;
543
544 for part in after_list.split_whitespace() {
545 match part {
546 "ordered" => k = ListKind::Ordered,
547 "unordered" => k = ListKind::Unordered,
548 _ if part.starts_with("start=") => {
549 sn = part[6..].parse().ok();
550 }
551 _ => {}
552 }
553 }
554 (open_line.span, k, sn)
555 };
556
557 let mut items: Vec<ListItem<'a>> = Vec::with_capacity(8);
558 let mut item_start: Option<u32> = None;
559 let mut item_end: u32 = start_span.end;
560 let mut end_span = start_span;
561 let mut last_span = start_span;
562
563 let finalize_item = |items: &mut Vec<ListItem<'a>>, start: Option<u32>, end: u32| {
564 if let Some(start) = start {
565 let content_slice = &input[start as usize..end as usize];
566 let content = crate::inline::parse_inlines(content_slice, start, input);
567 items.push(ListItem {
568 blocks: vec![Block::Paragraph(Paragraph {
569 content,
570 span: Span::new(start, end),
571 })],
572 span: Span::new(start, end),
573 });
574 }
575 };
576
577 while let Some(&line) = lexer.peek_line() {
578 let text = &input[line.span.start as usize..line.span.end as usize];
579 let trimmed = text.trim();
580
581 if trimmed == "::" {
582 lexer.next_line();
583 finalize_item(&mut items, item_start.take(), item_end);
584 end_span = line.span;
585 break;
586 }
587
588 if trimmed.starts_with("- ") {
589 lexer.next_line();
590 finalize_item(&mut items, item_start.take(), item_end);
591 let dash_offset = text.find("- ").unwrap_or(0);
592 item_start = Some(line.span.start + dash_offset as u32 + 2);
593 item_end = line.span.end;
594 last_span = line.span;
595 end_span = line.span;
596 continue;
597 }
598
599 if trimmed.starts_with("| ") {
600 lexer.next_line();
601 item_end = line.span.end;
602 last_span = line.span;
603 end_span = line.span;
604 continue;
605 }
606
607 if line.is_blank() {
608 lexer.next_line();
609 last_span = line.span;
610 end_span = line.span;
611 continue;
612 }
613
614 if trimmed.starts_with("::")
615 || trimmed.starts_with("```")
616 || trimmed.starts_with('#')
617 || trimmed.starts_with("@profile")
618 || trimmed.starts_with("@modules")
619 || trimmed == "---"
620 || trimmed.starts_with("--- meta ---")
621 {
622 self.record_error(ParseError::unclosed_delimiter("::list", Some(start_span)));
623 finalize_item(&mut items, item_start.take(), item_end);
624 end_span = last_span;
625 break;
626 }
627
628 self.record_error(ParseError::invalid_syntax("list item", Some(line.span)));
629 finalize_item(&mut items, item_start.take(), item_end);
630 end_span = last_span;
631 break;
632 }
633
634 Some(Block::List(List {
635 kind,
636 start: start_num,
637 items,
638 span: Span::new(start_span.start, end_span.end),
639 }))
640 }
641
642 #[inline]
643 fn parse_callout_block<'a>(&mut self, lexer: &mut Lexer, input: &'a str) -> Option<Block<'a>> {
644 let (start_span, kind, title) = {
645 let open_line = lexer.next_line()?;
646 let text = &input[open_line.span.start as usize..open_line.span.end as usize];
647 let trimmed = text.trim();
648 let after_callout = trimmed.strip_prefix("::callout").unwrap_or("").trim();
649 let (k, t) = self.parse_callout_attrs(after_callout, input, open_line.span.start);
650 (open_line.span, k, t)
651 };
652
653 let (blocks, end_span) = self.parse_until_fence_close(lexer, input);
654
655 Some(Block::Callout(Callout {
656 kind,
657 title,
658 blocks,
659 span: Span::new(start_span.start, end_span.end),
660 }))
661 }
662
663 #[inline]
664 fn parse_callout_attrs<'a>(
665 &self,
666 s: &str,
667 _input: &'a str,
668 _base: u32,
669 ) -> (CowStr<'a>, Option<CowStr<'a>>) {
670 let mut kind: CowStr<'a> = Cow::Owned("note".to_string());
671 let mut title: Option<CowStr<'a>> = None;
672
673 let mut remaining = s;
674 while !remaining.is_empty() {
675 let eq_pos = match remaining.find('=') {
676 Some(p) => p,
677 None => break,
678 };
679
680 let key = remaining[..eq_pos].trim();
681 remaining = remaining[eq_pos + 1..].trim_start();
682
683 if remaining.starts_with('"') {
684 if let Some(end) = remaining[1..].find('"') {
685 let val = &remaining[1..end + 1];
686 remaining = remaining[end + 2..].trim_start();
687
688 match key {
689 "type" => kind = Cow::Owned(val.to_string()),
690 "title" => title = Some(Cow::Owned(val.to_string())),
691 _ => {}
692 }
693 } else {
694 break;
695 }
696 } else {
697 let end = remaining.find(' ').unwrap_or(remaining.len());
698 let val = &remaining[..end];
699 remaining = remaining[end..].trim_start();
700
701 match key {
702 "type" => kind = Cow::Owned(val.to_string()),
703 "title" => title = Some(Cow::Owned(val.to_string())),
704 _ => {}
705 }
706 }
707 }
708
709 (kind, title)
710 }
711
712 #[inline]
713 fn parse_quote_block<'a>(&mut self, lexer: &mut Lexer, input: &'a str) -> Option<Block<'a>> {
714 let start_span = lexer.next_line()?.span;
715 let (blocks, end_span) = self.parse_until_fence_close(lexer, input);
716
717 Some(Block::Quote(Quote {
718 blocks,
719 span: Span::new(start_span.start, end_span.end),
720 }))
721 }
722
723 #[inline]
724 fn parse_figure_block<'a>(&mut self, lexer: &mut Lexer, input: &'a str) -> Option<Block<'a>> {
725 let (start_span, src, alt, caption) = {
726 let open_line = lexer.next_line()?;
727 let text = &input[open_line.span.start as usize..open_line.span.end as usize];
728 let trimmed = text.trim();
729 let after_figure = trimmed.strip_prefix("::figure").unwrap_or("").trim();
730 let (s, a, c) = self.parse_figure_attrs(after_figure);
731 (open_line.span, s, a, c)
732 };
733
734 let mut end_span = start_span;
735 if let Some(line) = lexer.peek_line() {
736 if line.trimmed() == "::" {
737 end_span = line.span;
738 lexer.next_line();
739 }
740 }
741
742 Some(Block::Figure(Figure {
743 src,
744 alt,
745 caption,
746 span: Span::new(start_span.start, end_span.end),
747 }))
748 }
749
750 #[inline]
751 fn parse_figure_attrs<'a>(&self, s: &str) -> (CowStr<'a>, CowStr<'a>, Option<CowStr<'a>>) {
752 let mut src: CowStr<'a> = Cow::Owned(String::new());
753 let mut alt: CowStr<'a> = Cow::Owned(String::new());
754 let mut caption: Option<CowStr<'a>> = None;
755
756 let mut remaining = s;
757 while !remaining.is_empty() {
758 let eq_pos = match remaining.find('=') {
759 Some(p) => p,
760 None => break,
761 };
762
763 let key = remaining[..eq_pos].trim();
764 remaining = remaining[eq_pos + 1..].trim_start();
765
766 if remaining.starts_with('"') {
767 if let Some(end) = remaining[1..].find('"') {
768 let val = remaining[1..end + 1].to_string();
769 remaining = remaining[end + 2..].trim_start();
770
771 match key {
772 "src" => src = Cow::Owned(val),
773 "alt" => alt = Cow::Owned(val),
774 "caption" => caption = Some(Cow::Owned(val)),
775 _ => {}
776 }
777 } else {
778 break;
779 }
780 } else {
781 let end = remaining.find(' ').unwrap_or(remaining.len());
782 let val = remaining[..end].to_string();
783 remaining = remaining[end..].trim_start();
784
785 match key {
786 "src" => src = Cow::Owned(val),
787 "alt" => alt = Cow::Owned(val),
788 "caption" => caption = Some(Cow::Owned(val)),
789 _ => {}
790 }
791 }
792 }
793
794 (src, alt, caption)
795 }
796
797 #[inline]
798 fn parse_table_block<'a>(&mut self, lexer: &mut Lexer, input: &'a str) -> Option<Block<'a>> {
799 let start_span = lexer.next_line()?.span;
800
801 let mut rows: Vec<TableRow<'a>> = Vec::with_capacity(8);
802 let mut end_span = start_span;
803 let mut found_separator = false;
804
805 loop {
806 let (is_close, is_sep, is_row, span, line_text) = {
807 match lexer.peek_line() {
808 Some(line) => {
809 let text = &input[line.span.start as usize..line.span.end as usize];
810 let trimmed = text.trim();
811 (
812 trimmed == "::",
813 trimmed.starts_with('|') && trimmed.contains("---"),
814 trimmed.starts_with('|'),
815 line.span,
816 trimmed,
817 )
818 }
819 None => break,
820 }
821 };
822
823 if is_close {
824 end_span = span;
825 lexer.next_line();
826 break;
827 }
828
829 if is_sep {
830 found_separator = true;
831 end_span = span;
832 lexer.next_line();
833 continue;
834 }
835
836 if is_row {
837 let cells = self.parse_table_row(line_text, span.start, input);
838 let is_header = !found_separator && rows.is_empty();
839 rows.push(TableRow {
840 cells,
841 header: is_header,
842 span,
843 });
844 end_span = span;
845 lexer.next_line();
846 continue;
847 }
848
849 self.record_error(ParseError::unclosed_delimiter("::table", Some(start_span)));
850 break;
851 }
852
853 Some(Block::Table(Table {
854 rows,
855 span: Span::new(start_span.start, end_span.end),
856 }))
857 }
858
859 #[inline]
860 fn parse_table_row<'a>(
861 &self,
862 line: &'a str,
863 base_offset: u32,
864 input: &'a str,
865 ) -> Vec<TableCell<'a>> {
866 let mut cells = Vec::with_capacity(8);
867 let mut offset = base_offset;
868
869 for (i, part) in line.split('|').enumerate() {
870 if i == 0 {
871 offset += part.len() as u32 + 1;
872 continue;
873 }
874
875 let trimmed = part.trim();
876 if trimmed.is_empty() {
877 offset += part.len() as u32 + 1;
878 continue;
879 }
880
881 let content = crate::inline::parse_inlines(trimmed, offset, input);
882 cells.push(TableCell {
883 content,
884 span: Span::new(offset, offset + part.len() as u32),
885 });
886
887 offset += part.len() as u32 + 1;
888 }
889
890 cells
891 }
892
893 #[inline]
894 fn parse_footnotes_block<'a>(
895 &mut self,
896 lexer: &mut Lexer,
897 input: &'a str,
898 ) -> Option<Block<'a>> {
899 let start_span = lexer.next_line()?.span;
900
901 let mut defs: Vec<FootnoteDef<'a>> = Vec::with_capacity(4);
902 let mut end_span = start_span;
903
904 loop {
905 let (is_close, is_def, span, label, content_text) = {
906 match lexer.peek_line() {
907 Some(line) => {
908 let text = &input[line.span.start as usize..line.span.end as usize];
909 let trimmed = text.trim();
910 let is_close = trimmed == "::";
911 let mut is_def = false;
912 let mut label = "";
913 let mut content = "";
914
915 if trimmed.starts_with("[^") {
916 if let Some(bracket_end) = trimmed.find("]:") {
917 is_def = true;
918 label = &trimmed[2..bracket_end];
919 content = trimmed[bracket_end + 2..].trim();
920 }
921 }
922 (is_close, is_def, line.span, label, content)
923 }
924 None => break,
925 }
926 };
927
928 if is_close {
929 end_span = span;
930 lexer.next_line();
931 break;
932 }
933
934 if is_def {
935 let content_inlines = crate::inline::parse_inlines(content_text, span.start, input);
936 defs.push(FootnoteDef {
937 label: Cow::Owned(label.to_string()),
938 blocks: vec![Block::Paragraph(Paragraph {
939 content: content_inlines,
940 span,
941 })],
942 span,
943 });
944 }
945
946 end_span = span;
947 lexer.next_line();
948 }
949
950 Some(Block::Footnotes(Footnotes {
951 defs,
952 span: Span::new(start_span.start, end_span.end),
953 }))
954 }
955
956 #[inline]
957 fn parse_math_block<'a>(&mut self, lexer: &mut Lexer, input: &'a str) -> Option<Block<'a>> {
958 let (start_span, display) = {
959 let open_line = lexer.next_line()?;
960 let text = &input[open_line.span.start as usize..open_line.span.end as usize];
961 let trimmed = text.trim();
962 let d = trimmed.contains("block") || trimmed.contains("display");
963 (open_line.span, d)
964 };
965
966 let content_start = (start_span.end as usize + 1).min(input.len());
968 let mut content_end = content_start;
969 let mut end_span = start_span;
970
971 loop {
972 let (is_close, span) = {
973 match lexer.peek_line() {
974 Some(line) => (line.trimmed() == "::", line.span),
975 None => break,
976 }
977 };
978
979 if is_close {
980 end_span = span;
981 lexer.next_line();
982 break;
983 }
984
985 content_end = span.end as usize;
986 end_span = span;
987 lexer.next_line();
988 }
989
990 let content = if content_start < content_end && content_end <= input.len() {
991 &input[content_start..content_end]
992 } else {
993 ""
994 };
995
996 Some(Block::Math(MathBlock {
997 display,
998 content: Cow::Borrowed(content),
999 span: Span::new(start_span.start, end_span.end),
1000 }))
1001 }
1002
1003 #[inline]
1004 fn parse_raw_fenced_block<'a>(
1005 &mut self,
1006 lexer: &mut Lexer,
1007 input: &'a str,
1008 ) -> Option<Block<'a>> {
1009 let start_span = lexer.next_line()?.span;
1010
1011 let content_start = (start_span.end as usize + 1).min(input.len());
1013 let mut content_end = content_start;
1014 let mut end_span = start_span;
1015
1016 loop {
1017 let (is_close, span) = {
1018 match lexer.peek_line() {
1019 Some(line) => (line.trimmed() == "::", line.span),
1020 None => break,
1021 }
1022 };
1023
1024 if is_close {
1025 end_span = span;
1026 lexer.next_line();
1027 break;
1028 }
1029
1030 content_end = span.end as usize;
1031 end_span = span;
1032 lexer.next_line();
1033 }
1034
1035 let content = if content_start < content_end && content_end <= input.len() {
1036 &input[content_start..content_end]
1037 } else {
1038 ""
1039 };
1040
1041 Some(Block::Raw(RawBlock {
1042 content: Cow::Borrowed(content),
1043 span: Span::new(start_span.start, end_span.end),
1044 }))
1045 }
1046
1047 #[inline]
1048 fn parse_until_fence_close<'a>(
1049 &mut self,
1050 lexer: &mut Lexer,
1051 input: &'a str,
1052 ) -> (Vec<Block<'a>>, Span) {
1053 let mut blocks = Vec::with_capacity(4);
1054 let mut para_start: Option<u32> = None;
1055 let mut para_end: u32 = 0;
1056 let mut end_span = Span::new(0, 0);
1057
1058 loop {
1059 let (is_close, is_blank, span) = {
1060 match lexer.next_line() {
1061 Some(line) => (line.trimmed() == "::", line.is_blank(), line.span),
1062 None => break,
1063 }
1064 };
1065
1066 if is_close {
1067 if let Some(start) = para_start {
1068 let content_slice = &input[start as usize..para_end as usize];
1069 let content = crate::inline::parse_inlines(content_slice, start, input);
1070 blocks.push(Block::Paragraph(Paragraph {
1071 content,
1072 span: Span::new(start, para_end),
1073 }));
1074 }
1075 end_span = span;
1076 break;
1077 }
1078
1079 if is_blank {
1080 if let Some(start) = para_start.take() {
1081 let content_slice = &input[start as usize..para_end as usize];
1082 let content = crate::inline::parse_inlines(content_slice, start, input);
1083 blocks.push(Block::Paragraph(Paragraph {
1084 content,
1085 span: Span::new(start, para_end),
1086 }));
1087 }
1088 } else {
1089 if para_start.is_none() {
1090 para_start = Some(span.start);
1091 }
1092 para_end = span.end;
1093 }
1094
1095 end_span = span;
1096 }
1097
1098 (blocks, end_span)
1099 }
1100
1101 #[inline]
1102 fn parse_paragraph<'a>(&mut self, lexer: &mut Lexer, input: &'a str) -> Option<Block<'a>> {
1103 let mut start_span: Option<Span> = None;
1104 let mut end_span = Span::new(0, 0);
1105
1106 loop {
1107 let should_break = {
1108 match lexer.peek_line() {
1109 Some(line) => {
1110 if line.is_blank() {
1111 true
1112 } else {
1113 let trimmed = line.trimmed();
1114 let first = trimmed.as_bytes().first().copied();
1115 match first {
1116 Some(b'#') | Some(b':') => true,
1117 Some(b'`') if trimmed.starts_with("```") => true,
1118 Some(b'-') if trimmed == "---" => true,
1119 _ => false,
1120 }
1121 }
1122 }
1123 None => true,
1124 }
1125 };
1126
1127 if should_break {
1128 break;
1129 }
1130
1131 let line = lexer.next_line().unwrap();
1132 if start_span.is_none() {
1133 start_span = Some(line.span);
1134 }
1135 end_span = line.span;
1136 }
1137
1138 let start = start_span?;
1139 let content_slice = &input[start.start as usize..end_span.end as usize];
1140 let content = crate::inline::parse_inlines(content_slice, start.start, input);
1141
1142 Some(Block::Paragraph(Paragraph {
1143 content,
1144 span: Span::new(start.start, end_span.end),
1145 }))
1146 }
1147}