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