1use crate::error::Result;
10use crate::model::{
11 Align, Block, BlockImage, Cell, ColSpec, Color, Column, Columns, Document, ImageBorder,
12 ImageSource, List, ListItem, ListKind, Panel, PanelDecor, Shadow, Table, TableStyle,
13};
14
15mod attrs;
16mod inline;
17
18pub(crate) use attrs::{parse_attrs, Attr};
19
20pub fn parse(src: &str) -> Result<Document> {
22 let lines: Vec<String> = src.lines().map(|l| l.to_string()).collect();
23 Ok(Document { blocks: parse_blocks(&lines) })
24}
25
26fn indent_of(s: &str) -> usize {
28 s.len() - s.trim_start().len()
29}
30
31fn dedent(s: &str, n: usize) -> String {
33 let strip = s.bytes().take_while(|b| *b == b' ').count().min(n);
34 s[strip..].to_string()
35}
36
37fn parse_blocks(lines: &[String]) -> Vec<Block> {
39 parse_blocks_at(lines, 0)
40}
41
42const MAX_DEPTH: usize = 64;
45
46fn parse_blocks_at(lines: &[String], depth: usize) -> Vec<Block> {
47 if depth > MAX_DEPTH {
48 let text = lines.join("\n");
49 let t = text.trim();
50 if t.is_empty() {
51 return Vec::new();
52 }
53 return vec![Block::Paragraph { inlines: inline::parse_inlines(t), align: Align::Left }];
54 }
55 let mut blocks = Vec::new();
56 let mut i = 0;
57 while i < lines.len() {
58 let line = &lines[i];
59 if line.trim().is_empty() {
60 i += 1;
61 continue;
62 }
63 let ind = indent_of(line);
64 let content = line[ind..].to_string();
65
66 if content.starts_with("```") {
68 let ticks = content.bytes().take_while(|b| *b == b'`').count();
69 let lang = content[ticks..].trim().to_string();
70 let mut text = Vec::new();
71 i += 1;
72 while i < lines.len() && !is_code_fence_close(&lines[i], ticks) {
73 text.push(lines[i].clone());
74 i += 1;
75 }
76 i += 1; blocks.push(Block::Code {
78 lang: if lang.is_empty() { None } else { Some(lang) },
79 text: text.join("\n"),
80 });
81 continue;
82 }
83
84 if is_fence_open(&content) {
87 let (word, attrs) = split_fence_word(content[3..].trim());
88 let inner = gather_div(lines, &mut i); if word == "columns" {
90 let (cols, mut stray) = parse_columns(&inner, depth + 1);
91 blocks.push(Block::Columns(Columns { cols, gap: None }));
92 blocks.append(&mut stray); } else if word == "panel" {
95 blocks.push(Block::Panel(Panel {
96 blocks: parse_blocks_at(&inner, depth + 1),
97 decor: panel_decor(attrs),
98 }));
99 } else if let Some(align) = align_from_word(&word) {
100 let mut sub = parse_blocks_at(&inner, depth + 1);
101 apply_align(&mut sub, align);
102 blocks.append(&mut sub);
103 } else {
104 blocks.append(&mut parse_blocks_at(&inner, depth + 1)); }
106 continue;
107 }
108
109 if let Some((level, rest)) = heading(&content) {
111 let (text, align) = split_trailing_attrs(rest);
112 blocks.push(Block::Heading { level, inlines: inline::parse_inlines(&text), align });
113 i += 1;
114 continue;
115 }
116
117 if is_hr(&content) {
119 blocks.push(Block::Divider);
120 i += 1;
121 continue;
122 }
123
124 if content.starts_with('>') {
126 let mut inner = Vec::new();
127 while i < lines.len() {
128 let t = lines[i].trim_start();
129 let Some(r) = t.strip_prefix('>') else { break };
130 inner.push(r.strip_prefix(' ').unwrap_or(r).to_string());
131 i += 1;
132 }
133 blocks.push(Block::Quote(parse_blocks_at(&inner, depth + 1)));
134 continue;
135 }
136
137 if let Some(img) = block_image(&content) {
139 blocks.push(Block::Image(img));
140 i += 1;
141 continue;
142 }
143
144 if list_marker(&content).is_some() {
146 let (list, next) = parse_list(lines, i, ind);
147 blocks.push(Block::List(list));
148 i = next;
149 continue;
150 }
151
152 if content.contains('|')
154 && i + 1 < lines.len()
155 && is_table_delim(lines[i + 1].trim())
156 && split_row(lines[i + 1].trim()).len() == split_row(content.trim()).len()
157 {
158 let (table, next) = parse_table(lines, i);
159 blocks.push(Block::Table(table));
160 i = next;
161 continue;
162 }
163
164 let mut para = String::new();
166 while i < lines.len() {
167 let l = &lines[i];
168 if l.trim().is_empty() {
169 break;
170 }
171 let c = l[indent_of(l)..].to_string();
172 if is_block_start(&c) {
173 break;
174 }
175 let mut piece = c.trim();
176 let hard = piece.ends_with('\\');
177 if hard {
178 piece = piece[..piece.len() - 1].trim_end();
179 }
180 append_soft(&mut para, piece);
181 if hard {
182 para.push('\n');
183 }
184 i += 1;
185 }
186 let (text, align) = split_trailing_attrs(¶);
187 blocks.push(Block::Paragraph { inlines: inline::parse_inlines(&text), align });
188 }
189 blocks
190}
191
192fn is_block_start(c: &str) -> bool {
194 c.starts_with("```")
195 || is_fence_open(c)
196 || is_hr(c)
197 || c.starts_with('>')
198 || heading(c).is_some()
199 || list_marker(c).is_some()
200 || block_image(c).is_some()
201}
202
203fn is_hr(c: &str) -> bool {
205 let b = c.as_bytes();
206 b.len() >= 3 && matches!(b[0], b'-' | b'*' | b'_') && b.iter().all(|x| *x == b[0])
207}
208
209fn parse_list(lines: &[String], start: usize, base: usize) -> (List, usize) {
212 let (ordered, first_start, _) = list_marker(&lines[start][base..]).unwrap();
213 let kind = if ordered { ListKind::Ordered } else { ListKind::Unordered };
214 let mut items = Vec::new();
215 let mut i = start;
216 while i < lines.len() {
217 let line = &lines[i];
218 if line.trim().is_empty() {
219 if next_nonblank_indent(lines, i + 1).map(|n| n >= base).unwrap_or(false) {
221 i += 1;
222 continue;
223 }
224 break;
225 }
226 let ind = indent_of(line);
227 if ind < base {
228 break;
229 }
230 let Some((ord, _, off)) = list_marker(&line[ind..]) else {
231 break; };
233 if ind != base || ord != ordered {
234 break; }
236 let content_indent = base + off;
238 let (first_line, check) = split_task_mark(&line[ind..][off..]);
239 let mut item_lines = vec![first_line];
240 i += 1;
241 while i < lines.len() {
242 let l = &lines[i];
243 if l.trim().is_empty() {
244 if next_nonblank_indent(lines, i + 1).map(|n| n > base).unwrap_or(false) {
245 item_lines.push(String::new());
246 i += 1;
247 continue;
248 }
249 break;
250 }
251 if indent_of(l) > base {
252 item_lines.push(dedent(l, content_indent));
253 i += 1;
254 } else {
255 break;
256 }
257 }
258 items.push(ListItem { blocks: parse_blocks(&item_lines), check });
259 }
260 (List { kind, start: first_start.max(1), items }, i)
261}
262
263fn split_task_mark(s: &str) -> (String, Option<bool>) {
266 let done = match s.get(..3) {
267 Some("[ ]") => false,
268 Some("[x]") | Some("[X]") => true,
269 _ => return (s.to_string(), None),
270 };
271 match s[3..].chars().next() {
272 None => (String::new(), Some(done)),
273 Some(c) if c.is_whitespace() => (s[3 + c.len_utf8()..].to_string(), Some(done)),
274 _ => (s.to_string(), None),
275 }
276}
277
278fn next_nonblank_indent(lines: &[String], from: usize) -> Option<usize> {
280 lines[from..].iter().find(|l| !l.trim().is_empty()).map(|l| indent_of(l))
281}
282
283fn heading(c: &str) -> Option<(u8, &str)> {
285 let hashes = c.bytes().take_while(|b| *b == b'#').count();
286 if (1..=6).contains(&hashes) && c.as_bytes().get(hashes) == Some(&b' ') {
287 Some((hashes as u8, c[hashes + 1..].trim()))
288 } else {
289 None
290 }
291}
292
293fn list_marker(c: &str) -> Option<(bool, u32, usize)> {
295 let b = c.as_bytes();
296 if matches!(b.first(), Some(b'-' | b'*' | b'+')) && matches!(b.get(1), Some(b' ' | b'\t')) {
298 return Some((false, 0, 2));
299 }
300 let digits = c.bytes().take_while(|x| x.is_ascii_digit()).count();
302 if digits > 0
303 && matches!(b.get(digits), Some(b'.' | b')'))
304 && matches!(b.get(digits + 1), Some(b' ' | b'\t'))
305 {
306 let n = c[..digits].parse::<u32>().unwrap_or(1);
307 return Some((true, n, digits + 2));
308 }
309 None
310}
311
312fn block_image(c: &str) -> Option<BlockImage> {
314 let c = c.trim();
315 let rest = c.strip_prefix("?;
317 let after_src = &rest[close_alt + 2..];
318 let close_paren = after_src.find(')')?;
319 let src = &after_src[..close_paren];
320 if src.is_empty() {
321 return None;
322 }
323 let tail = after_src[close_paren + 1..].trim();
325 let attrs = if tail.is_empty() {
326 ""
327 } else if tail.starts_with('{') && tail.ends_with('}') {
328 &tail[1..tail.len() - 1]
329 } else {
330 return None;
331 };
332 let alt = &rest[..close_alt];
333 let mut img = BlockImage {
334 src: image_source(src),
335 width: None,
336 align: Align::Left,
337 caption: if alt.trim().is_empty() { None } else { Some(inline::parse_inlines(alt.trim())) },
338 decor: crate::model::ImageDecor::default(),
339 };
340 apply_image_attrs(&mut img, attrs);
341 Some(img)
342}
343
344fn apply_image_attrs(img: &mut BlockImage, attrs: &str) {
347 for a in parse_attrs(attrs) {
348 match a {
349 Attr::Kv(k, v) => match k.as_str() {
350 "width" => {
351 if let Some(pct) = v.strip_suffix('%') {
352 if let Ok(x) = pct.parse::<f32>() {
353 if x.is_finite() && x > 0.0 {
354 img.width = Some(crate::model::Length::Percent(x));
355 }
356 }
357 } else if let Ok(x) = v.parse::<f32>() {
358 if x.is_finite() && x > 0.0 {
359 img.width = Some(crate::model::Length::Px(x));
360 }
361 }
362 }
363 "align" => {
364 if let Some(al) = align_from_word(&v) {
365 img.align = al;
366 }
367 }
368 "rounded" => {
369 if let Ok(r) = v.parse::<f32>() {
370 if r.is_finite() && r > 0.0 {
371 img.decor.radius = r;
372 }
373 }
374 }
375 "border" => {
376 if let Some(color) = Color::hex(&v) {
377 img.decor.border = Some(ImageBorder { width: 2.0, color });
378 }
379 }
380 _ => {}
381 },
382 Attr::Flag(f) => {
383 if f == "shadow" {
384 img.decor.shadow = Some(Shadow::default());
385 }
386 }
387 }
388 }
389}
390
391pub(crate) fn image_source(src: &str) -> ImageSource {
393 match src.strip_prefix('@') {
394 Some(name) => ImageSource::Named(name.to_string()),
395 None => ImageSource::Path(src.into()),
396 }
397}
398
399fn align_from_word(w: &str) -> Option<Align> {
401 match w {
402 "center" | "centre" => Some(Align::Center),
403 "right" => Some(Align::Right),
404 "left" => Some(Align::Left),
405 "justify" => Some(Align::Justify),
406 _ => None,
407 }
408}
409
410fn is_fence_open(c: &str) -> bool {
412 c.starts_with(":::") && c.len() > 3 && !c[3..].trim().is_empty()
413}
414
415fn gather_div(lines: &[String], i: &mut usize) -> Vec<String> {
417 *i += 1;
418 let mut inner = Vec::new();
419 let mut depth = 1usize;
420 let mut code_ticks = 0usize; while *i < lines.len() {
422 let t = lines[*i].trim();
423 if code_ticks > 0 {
424 if is_code_fence_close(&lines[*i], code_ticks) {
425 code_ticks = 0;
426 }
427 } else if t.starts_with("```") {
428 code_ticks = t.bytes().take_while(|b| *b == b'`').count();
429 } else if t == ":::" {
430 depth -= 1;
431 if depth == 0 {
432 *i += 1;
433 break; }
435 } else if is_fence_open(t) {
436 depth += 1;
437 }
438 inner.push(lines[*i].clone());
439 *i += 1;
440 }
441 inner
442}
443
444fn is_code_fence_close(line: &str, ticks: usize) -> bool {
446 let t = line.trim();
447 let n = t.bytes().take_while(|b| *b == b'`').count();
448 n >= ticks && t[n..].trim().is_empty()
449}
450
451fn parse_columns(inner: &[String], depth: usize) -> (Vec<Column>, Vec<Block>) {
453 let mut cols = Vec::new();
454 let mut stray_lines: Vec<String> = Vec::new();
455 let mut i = 0;
456 while i < inner.len() {
457 let (head, attrs) =
458 split_fence_word(inner[i].trim().strip_prefix(":::").unwrap_or("").trim());
459 let mut parts = head.split_whitespace();
460 if parts.next() == Some("col") {
461 let weight = parts
462 .next()
463 .and_then(|s| s.parse::<f32>().ok())
464 .filter(|w| w.is_finite() && *w > 0.0)
465 .unwrap_or(1.0);
466 let col_lines = gather_div(inner, &mut i);
467 let mut blocks = parse_blocks_at(&col_lines, depth);
468 if !attrs.is_empty() {
470 blocks = vec![Block::Panel(Panel { blocks, decor: panel_decor(attrs) })];
471 }
472 cols.push(Column { blocks, weight });
473 } else {
474 stray_lines.push(inner[i].clone()); i += 1;
476 }
477 }
478 (cols, parse_blocks_at(&stray_lines, depth))
479}
480
481fn split_fence_word(s: &str) -> (String, &str) {
483 match (s.find('{'), s.rfind('}')) {
484 (Some(a), Some(b)) if b > a => (s[..a].trim().to_string(), &s[a + 1..b]),
485 _ => (s.trim().to_string(), ""),
486 }
487}
488
489fn panel_decor(attrs: &str) -> PanelDecor {
492 let mut d = PanelDecor::default();
493 let mut border_color: Option<Color> = None;
494 let mut border_width = 1.5f32;
495 for a in parse_attrs(attrs) {
496 match a {
497 Attr::Kv(k, v) => match k.as_str() {
498 "bg" => d.bg = Color::hex(&v).or(d.bg),
499 "border" => border_color = Color::hex(&v).or(border_color),
500 "border-width" => {
501 if let Ok(w) = v.parse::<f32>() {
502 if w.is_finite() && w > 0.0 {
503 border_width = w;
504 }
505 }
506 }
507 "rounded" => {
508 if let Ok(r) = v.parse::<f32>() {
509 if r.is_finite() && r >= 0.0 {
510 d.radius = Some(r);
511 }
512 }
513 }
514 "pad" => {
515 if let Ok(p) = v.parse::<f32>() {
516 if p.is_finite() && p >= 0.0 {
517 d.pad = Some(p);
518 }
519 }
520 }
521 _ => {}
522 },
523 Attr::Flag(f) => {
524 if f == "shadow" {
525 d.shadow = Some(Shadow::default());
526 }
527 }
528 }
529 }
530 d.border = border_color.map(|color| ImageBorder { width: border_width, color });
531 d
532}
533
534fn is_table_delim(t: &str) -> bool {
536 let cells = split_row(t);
537 !cells.is_empty()
538 && cells
539 .iter()
540 .all(|c| !c.is_empty() && c.contains('-') && c.bytes().all(|b| b == b'-' || b == b':'))
541}
542
543fn split_row(line: &str) -> Vec<String> {
546 let t = line.trim();
547 let t = t.strip_prefix('|').unwrap_or(t);
548 let t = t.strip_suffix('|').unwrap_or(t);
549 let mut cells = Vec::new();
550 let mut cur = String::new();
551 let mut in_code = false;
552 let mut chars = t.chars();
553 while let Some(ch) = chars.next() {
554 match ch {
555 '`' => {
556 in_code = !in_code;
557 cur.push('`');
558 }
559 '\\' if !in_code => {
561 cur.push('\\');
562 if let Some(n) = chars.next() {
563 cur.push(n);
564 }
565 }
566 '|' if !in_code => {
567 cells.push(cur.trim().to_string());
568 cur = String::new();
569 }
570 _ => cur.push(ch),
571 }
572 }
573 cells.push(cur.trim().to_string());
574 cells
575}
576
577fn parse_align_row(line: &str) -> Vec<Align> {
579 split_row(line)
580 .iter()
581 .map(|c| match (c.starts_with(':'), c.ends_with(':')) {
582 (true, true) => Align::Center,
583 (false, true) => Align::Right,
584 _ => Align::Left,
585 })
586 .collect()
587}
588
589fn parse_table(lines: &[String], start: usize) -> (Table, usize) {
591 let to_cells = |t: &str| -> Vec<Cell> {
592 split_row(t).iter().map(|s| Cell { inlines: inline::parse_inlines(s), bg: None }).collect()
593 };
594 let header = Some(to_cells(lines[start].trim()));
595 let cols: Vec<ColSpec> = parse_align_row(lines[start + 1].trim())
596 .into_iter()
597 .map(|a| ColSpec { align: a, width: None })
598 .collect();
599 let mut rows = Vec::new();
600 let mut i = start + 2;
601 while i < lines.len() {
602 let t = lines[i].trim();
603 if t.is_empty() || !t.contains('|') {
604 break;
605 }
606 rows.push(to_cells(t));
607 i += 1;
608 }
609 (Table { header, rows, cols, style: TableStyle::default() }, i)
610}
611
612fn apply_align(blocks: &mut [Block], align: Align) {
614 for b in blocks {
615 match b {
616 Block::Heading { align: a, .. } | Block::Paragraph { align: a, .. } => *a = align,
617 Block::Quote(inner) => apply_align(inner, align),
618 Block::Panel(p) => apply_align(&mut p.blocks, align),
619 Block::Image(bi) => bi.align = align,
620 Block::List(list) => {
621 for it in &mut list.items {
622 apply_align(&mut it.blocks, align);
623 }
624 }
625 _ => {}
626 }
627 }
628}
629
630fn split_trailing_attrs(s: &str) -> (String, Align) {
632 let t = s.trim_end();
633 if t.ends_with('}') {
634 if let Some(open) = t.rfind('{') {
635 let before = &t[..open];
636 if before.ends_with(' ') || before.is_empty() {
637 let inside = &t[open + 1..t.len() - 1];
638 if let Some(align) = parse_attrs(inside).iter().find_map(|a| match a {
640 Attr::Kv(k, v) if k == "align" => align_from_word(v),
641 Attr::Flag(f) => align_from_word(f),
642 _ => None,
643 }) {
644 return (before.trim_end().to_string(), align);
645 }
646 }
647 }
648 }
649 (t.to_string(), Align::Left)
650}
651
652fn append_soft(buf: &mut String, next: &str) {
654 if next.is_empty() {
655 return;
656 }
657 if let (Some(a), Some(b)) = (buf.chars().last(), next.chars().next()) {
658 if a != '\n' && needs_space(a, b) {
660 buf.push(' ');
661 }
662 }
663 buf.push_str(next);
664}
665
666fn needs_space(a: char, b: char) -> bool {
667 fn cjk(c: char) -> bool {
669 matches!(c, '\u{2E80}'..='\u{9FFF}' | '\u{FF00}'..='\u{FFEF}')
670 }
671 !cjk(a) && !cjk(b)
672}
673
674
675