1use alloc::string::String;
24use alloc::vec::Vec;
25use core::fmt::Write as _;
26use core::mem;
27
28use super::{is_space, push_verbatim, skip_newline, walk};
29use crate::node::NodeKind;
30use crate::tags::{self, Display};
31use crate::tree::{Dom, NodeRef};
32
33const MAX_NEST: usize = 32;
35
36const MAX_PAD: usize = 64;
39
40const MAX_NUMBER: i64 = 999_999_999;
43
44impl Dom {
45 pub fn to_markdown(&self) -> String {
67 let mut md = Markdown::default();
68 for root in self.roots() {
69 walk(root, |node, enter| md.visit(node, enter));
70 }
71 md.finish()
72 }
73}
74
75impl NodeRef<'_> {
76 pub fn to_markdown(&self) -> String {
86 let mut md = Markdown::default();
87 walk(*self, |node, enter| md.visit(node, enter));
88 md.finish()
89 }
90}
91
92#[derive(Clone, Copy)]
94enum Mark<'a> {
95 Strong,
96 Emphasis,
97 Strike,
98 Link(NodeRef<'a>),
99}
100
101#[derive(Clone, Copy)]
104struct Wrap<'a> {
105 mark: Mark<'a>,
106 open: bool,
107 used: bool,
108 spaced: bool,
110}
111
112impl Wrap<'_> {
113 fn pending(&self) -> bool {
116 match self.mark {
117 Mark::Link(_) => !self.used,
118 _ => !self.open,
119 }
120 }
121}
122
123enum Prefix {
125 Quote(bool),
127 Item { list: Option<usize>, width: usize },
130}
131
132struct List {
133 ordered: bool,
134 reversed: bool,
136 alt: bool,
139 next: u32,
140 written: bool,
142}
143
144struct Code<'a> {
146 buf: String,
147 block: bool,
148 fresh: bool,
150 lang: &'a str,
151}
152
153#[derive(Default)]
155struct Table {
156 rows: Vec<Vec<String>>,
157 cells: Vec<String>,
158}
159
160#[derive(Clone, Copy, Default)]
162struct Frame {
163 block: bool,
164 space: bool,
165 mark: bool,
166 prefix: bool,
167 list: bool,
168 inline: bool,
169 heading: bool,
170 code: bool,
171 role: Role,
172}
173
174#[derive(Clone, Copy, Default)]
175enum Role {
176 #[default]
177 None,
178 Table,
179 Row,
180 Cell,
181}
182
183#[derive(Default)]
184struct Markdown<'a> {
185 out: String,
186 sep: u8,
188 prefixes: Vec<Prefix>,
189 lists: Vec<List>,
190 last_list: Option<(usize, usize, bool, bool)>,
194 frames: Vec<Frame>,
195 line: String,
197 space: bool,
198 breaks: u8,
199 wraps: Vec<Wrap<'a>>,
200 closing: Vec<Mark<'a>>,
203 code_run: String,
205 heading: u8,
206 inline_only: u32,
208 code: Option<Code<'a>>,
209 table: Option<Table>,
210}
211
212impl<'a> Markdown<'a> {
213 fn visit(&mut self, node: NodeRef<'a>, enter: bool) -> bool {
214 match node.kind() {
215 NodeKind::Text(t) => self.text(t),
216 NodeKind::Element { tag } if enter => return self.open(node, tag),
217 NodeKind::Element { .. } => self.close(),
218 NodeKind::Comment(_) | NodeKind::Doctype(_) => {}
219 }
220 false
221 }
222
223 fn open(&mut self, node: NodeRef<'a>, tag: &'a str) -> bool {
224 let display = tags::display(tag);
225 if display == Display::Hidden {
226 return false;
227 }
228 let mut f = Frame::default();
229 if let Some(code) = &mut self.code {
230 if tag == "br" {
232 code.br();
233 } else if display != Display::Inline {
234 code.newline();
235 f.block = true;
236 }
237 self.frames.push(f);
238 return true;
239 }
240 let top = self.inline_only == 0;
241 match tag {
242 "br" if !top => self.space = true,
243 "br" if !self.line.is_empty() || !self.code_run.is_empty() => {
244 self.breaks = (self.breaks + 1).min(2);
245 }
246 "hr" if top => {
247 self.block(&mut f);
248 self.begin_block();
249 self.out.push_str("---");
250 }
251 "img" => self.image(node),
252 "code" | "kbd" | "samp" | "tt" => self.start_code(&mut f, false, ""),
253 "pre" if top => {
254 self.block(&mut f);
255 self.start_code(&mut f, true, code_lang(node));
256 }
257 "pre" => {
258 self.block(&mut f); self.start_code(&mut f, false, "");
260 }
261 "strong" | "b" => f.mark = self.push_mark(Mark::Strong),
262 "em" | "i" => f.mark = self.push_mark(Mark::Emphasis),
263 "del" | "s" | "strike" => f.mark = self.push_mark(Mark::Strike),
264 "a" if node.attr("href").is_some_and(|h| !h.trim().is_empty()) => {
265 f.mark = self.push_mark(Mark::Link(node));
266 }
267 "dt" => {
268 self.block(&mut f);
269 f.mark = self.push_mark(Mark::Strong);
270 }
271 "h1" | "h2" | "h3" | "h4" | "h5" | "h6" if top => {
272 self.block(&mut f);
273 self.heading = tag.as_bytes().get(1).map_or(1, |b| b.saturating_sub(b'0'));
274 self.inline_only += 1;
275 f.heading = true;
276 f.inline = true;
277 }
278 "ul" | "ol" | "menu" if top => {
279 self.flush();
280 let nested = matches!(self.prefixes.last(), Some(Prefix::Item { .. }));
282 self.sep(if nested { 1 } else { 2 });
283 f.block = true;
284 if self.prefixes.len() < MAX_NEST {
285 let ordered = tag == "ol";
286 let reversed = ordered && node.has_attr("reversed");
287 let start = number(node.attr("start")).unwrap_or_else(|| {
288 if reversed {
289 let items =
290 node.child_elements().filter(|c| c.tag_name() == Some("li"));
291 items.count() as i64
292 } else {
293 1
294 }
295 });
296 let depth = self.prefixes.len();
298 let alt = matches!(self.last_list, Some((d, at, o, false))
299 if d == depth && at == self.out.len() && o == ordered);
300 self.lists.push(List {
301 ordered,
302 reversed,
303 alt,
304 next: start.clamp(0, MAX_NUMBER) as u32,
305 written: false,
306 });
307 f.list = true;
308 }
309 }
310 "li" if top => {
311 self.flush();
312 let list = self.frames.last().filter(|p| p.list);
313 let list = list.and_then(|_| self.lists.len().checked_sub(1));
314 if let (Some(l), Some(v)) = (
316 list.and_then(|i| self.lists.get_mut(i)),
317 number(node.attr("value")),
318 ) {
319 l.next = v.clamp(0, MAX_NUMBER) as u32;
320 }
321 if list
323 .and_then(|i| self.lists.get(i))
324 .is_some_and(|l| l.written)
325 {
326 self.force_sep(1);
327 } else {
328 self.sep(1);
329 }
330 f.block = true;
331 if self.prefixes.len() < MAX_NEST {
332 self.prefixes.push(Prefix::Item { list, width: 0 });
333 self.last_list = None; f.prefix = true;
335 } else {
336 self.sep(2);
337 }
338 }
339 "blockquote" if top => {
340 self.block(&mut f);
341 if self.prefixes.len() < MAX_NEST {
342 self.prefixes.push(Prefix::Quote(false));
343 f.prefix = true;
344 }
345 }
346 "table" if top && self.table.is_none() => {
347 self.block(&mut f);
348 self.table = Some(Table::default());
349 f.role = Role::Table;
350 }
351 "tr" | "td" | "th" if top && self.table.is_some() => {
352 self.flush(); if tag == "tr" {
354 if let Some(t) = &mut self.table {
355 t.end_row();
356 }
357 f.role = Role::Row;
358 } else {
359 self.inline_only += 1;
360 f.role = Role::Cell;
361 f.inline = true;
362 }
363 }
364 "td" | "th" => {
365 self.space = true;
366 f.space = true;
367 }
368 _ if display != Display::Inline => self.block(&mut f),
369 _ => {}
370 }
371 self.frames.push(f);
372 true
373 }
374
375 fn close(&mut self) {
376 let Some(f) = self.frames.pop() else {
377 return;
378 };
379 if f.code {
380 self.end_code();
381 } else if let Some(code) = &mut self.code {
382 if f.block {
383 code.newline();
384 }
385 return;
386 }
387 if f.mark {
388 self.pop_mark();
389 }
390 match f.role {
391 Role::Cell => {
392 let cell = self.take_cell();
393 if let Some(t) = &mut self.table {
394 t.cells.push(cell);
395 }
396 }
397 Role::Row => {
398 if let Some(t) = &mut self.table {
399 t.end_row();
400 }
401 }
402 Role::Table => {
403 self.flush();
404 if let Some(t) = self.table.take() {
405 self.emit_table(t);
406 }
407 }
408 Role::None => {}
409 }
410 if f.block {
411 self.flush();
412 }
413 if f.heading {
414 self.heading = 0;
415 }
416 if f.inline {
417 self.inline_only = self.inline_only.saturating_sub(1);
418 }
419 if f.prefix {
420 self.prefixes.pop();
421 }
422 if f.list {
423 if let Some(l) = self.lists.pop() {
424 self.last_list = Some((self.prefixes.len(), self.out.len(), l.ordered, l.alt));
425 }
426 }
427 if f.block {
428 self.sep(2);
429 }
430 if f.space {
431 self.space = true;
432 }
433 }
434
435 fn block(&mut self, f: &mut Frame) {
438 if self.inline_only > 0 {
439 self.space = true;
440 f.space = true;
441 } else {
442 self.flush();
443 self.sep(2);
444 f.block = true;
445 }
446 }
447
448 fn text(&mut self, text: &str) {
451 if let Some(code) = &mut self.code {
452 code.push(text);
453 return;
454 }
455 for (i, word) in text.split(is_space).enumerate() {
456 if i > 0 {
457 self.space = true;
458 }
459 if !word.is_empty() {
460 self.inline();
461 let line_start =
462 self.inline_only == 0 && (self.line.is_empty() || self.line.ends_with('\n'));
463 escape(&mut self.line, word, line_start);
464 }
465 }
466 }
467
468 fn inline(&mut self) {
472 self.settle();
473 if !self.line.is_empty() {
474 if self.breaks > 0 {
475 for _ in 0..self.breaks {
476 self.line.push_str("\\\n");
477 }
478 } else if self.space {
479 self.line.push(' ');
480 }
481 }
482 self.breaks = 0;
483 self.space = false;
484 for w in self.wraps.iter_mut().filter(|w| w.pending()) {
485 w.open = true;
486 w.used = true;
487 self.line.push_str(match w.mark {
488 Mark::Strong => "**",
489 Mark::Emphasis => "*",
490 Mark::Strike => "~~",
491 Mark::Link(_) => "[",
492 });
493 }
494 }
495
496 fn settle(&mut self) {
498 if !self.code_run.is_empty() {
499 push_code_span(&mut self.line, &self.code_run);
500 self.code_run.clear();
501 }
502 for mark in self.closing.drain(..) {
503 push_close(&mut self.line, mark);
504 }
505 }
506
507 fn push_mark(&mut self, mark: Mark<'a>) -> bool {
508 let kind = mem::discriminant(&mark);
510 if self
511 .wraps
512 .iter()
513 .any(|w| mem::discriminant(&w.mark) == kind)
514 {
515 return false;
516 }
517 let resume = !matches!(mark, Mark::Link(_))
519 && !self.space
520 && self.breaks == 0
521 && self
522 .closing
523 .last()
524 .is_some_and(|m| mem::discriminant(m) == kind)
525 && !self.wraps.iter().any(Wrap::pending);
526 if resume {
527 self.closing.pop();
528 }
529 self.wraps.push(Wrap {
530 mark,
531 open: resume,
532 used: resume,
533 spaced: self.space,
534 });
535 true
536 }
537
538 fn pop_mark(&mut self) {
539 if let Some(Wrap {
540 mark: Mark::Link(a),
541 used: false,
542 spaced,
543 ..
544 }) = self.wraps.last().copied()
545 {
546 let after = !spaced && mem::take(&mut self.space);
549 self.text(a.attr("href").unwrap_or("").trim());
550 self.space |= after;
551 }
552 if let Some(w) = self.wraps.pop() {
553 if w.open {
554 self.closing.push(w.mark);
555 }
556 }
557 }
558
559 fn image(&mut self, img: NodeRef<'_>) {
560 let alt = img.attr("alt").unwrap_or("");
561 let src = img.attr("src").unwrap_or("").trim();
562 if src.is_empty() {
563 self.text(alt);
564 return;
565 }
566 self.inline();
567 self.line.push_str(";
575 push_destination(&mut self.line, src);
576 push_title(&mut self.line, img.attr("title"));
577 self.line.push(')');
578 }
579
580 fn start_code(&mut self, f: &mut Frame, block: bool, lang: &'a str) {
581 self.code = Some(Code {
582 buf: String::new(),
583 block,
584 fresh: block,
585 lang,
586 });
587 f.code = true;
588 }
589
590 fn end_code(&mut self) {
591 let Some(code) = self.code.take() else {
592 return;
593 };
594 if code.block {
595 self.code_block(&code.buf, code.lang);
596 return;
597 }
598 let mut text = String::new();
599 for word in code.buf.split(is_space).filter(|w| !w.is_empty()) {
600 if !text.is_empty() {
601 text.push(' ');
602 }
603 text.push_str(word);
604 }
605 if text.is_empty() {
606 return;
607 }
608 let adjacent = !self.code_run.is_empty()
611 && self.closing.is_empty()
612 && !self.space
613 && self.breaks == 0
614 && !self.wraps.iter().any(Wrap::pending);
615 if adjacent {
616 self.code_run.push_str(&text);
617 } else {
618 self.inline();
619 self.code_run = text;
620 }
621 }
622
623 fn flush(&mut self) {
627 self.finish_line();
628 if self.line.is_empty() {
629 return;
630 }
631 let line = mem::take(&mut self.line);
632 self.begin_block();
633 if self.heading > 0 {
634 push_n(&mut self.out, '#', usize::from(self.heading));
635 self.out.push(' ');
636 }
637 for (i, part) in line.split('\n').enumerate() {
638 if i > 0 {
639 self.next_line(false);
640 }
641 self.out.push_str(part);
642 }
643 self.line = line;
644 self.line.clear();
645 }
646
647 fn finish_line(&mut self) {
650 self.settle();
651 for w in self.wraps.iter_mut().rev().filter(|w| w.open) {
652 w.open = false;
653 push_close(&mut self.line, w.mark);
654 }
655 self.space = false;
656 self.breaks = 0;
657 }
658
659 fn begin_block(&mut self) {
661 if !self.out.is_empty() {
662 if self.sep >= 2 {
663 self.next_line(true);
664 }
665 self.out.push('\n');
666 }
667 self.sep = 0;
668 self.write_prefix(false);
669 }
670
671 fn next_line(&mut self, blank: bool) {
672 self.out.push('\n');
673 self.write_prefix(blank);
674 }
675
676 fn write_prefix(&mut self, blank: bool) {
680 let Markdown {
681 out,
682 prefixes,
683 lists,
684 ..
685 } = self;
686 for prefix in prefixes.iter_mut() {
687 match prefix {
688 Prefix::Quote(started) => {
689 if !*started {
690 if blank {
691 break;
692 }
693 *started = true;
694 }
695 out.push_str("> ");
696 }
697 Prefix::Item { width, .. } if *width > 0 => push_n(out, ' ', *width),
698 Prefix::Item { .. } if blank => break,
699 Prefix::Item { list, width } => {
700 let at = out.len();
701 match list.and_then(|i| lists.get_mut(i)) {
702 Some(l) if l.ordered => {
703 let _ = write!(out, "{}{} ", l.next, if l.alt { ')' } else { '.' });
704 l.next = if l.reversed {
705 l.next.saturating_sub(1)
706 } else {
707 l.next.saturating_add(1).min(MAX_NUMBER as u32)
708 };
709 l.written = true;
710 }
711 Some(l) => {
712 out.push_str(if l.alt { "* " } else { "- " });
713 l.written = true;
714 }
715 None => out.push_str("- "),
716 }
717 *width = out.len() - at;
718 }
719 }
720 }
721 if blank {
722 let end = out.trim_end_matches(' ').len();
723 out.truncate(end);
724 }
725 }
726
727 fn started(&self) -> bool {
731 match self.prefixes.last() {
732 Some(Prefix::Quote(started)) => *started,
733 Some(Prefix::Item { width, .. }) => *width > 0,
734 None => true,
735 }
736 }
737
738 fn sep(&mut self, n: u8) {
739 if self.started() {
740 self.sep = self.sep.max(n);
741 }
742 }
743
744 fn force_sep(&mut self, n: u8) {
745 if self.started() {
746 self.sep = n;
747 }
748 }
749
750 fn code_block(&mut self, raw: &str, lang: &str) {
751 let body = raw.trim_end();
752 if body.trim_start().is_empty() {
753 return;
754 }
755 let fence = longest_run(body, '`').max(2) + 1;
756 self.begin_block();
757 push_n(&mut self.out, '`', fence);
758 self.out.push_str(lang);
759 for line in body.split('\n') {
760 self.next_line(line.is_empty());
761 self.out.push_str(line);
762 }
763 self.next_line(false);
764 push_n(&mut self.out, '`', fence);
765 }
766
767 fn take_cell(&mut self) -> String {
768 self.finish_line();
769 let cell = self.line.trim().replace('|', "\\|");
770 self.line.clear();
771 cell
772 }
773
774 fn emit_table(&mut self, mut table: Table) {
775 table.end_row();
776 let cols = table.rows.iter().map(Vec::len).max().unwrap_or(0);
777 let Some((head, body)) = table.rows.split_first().filter(|_| cols > 0) else {
778 return;
779 };
780 let pad = if cols <= MAX_PAD { cols } else { 0 };
783 self.sep(2);
784 self.begin_block();
785 push_row(&mut self.out, head, cols);
786 self.next_line(false);
787 self.out.push('|');
788 for _ in 0..cols {
789 self.out.push_str(" --- |");
790 }
791 for cells in body {
792 self.next_line(false);
793 push_row(&mut self.out, cells, pad);
794 }
795 }
796
797 fn finish(mut self) -> String {
798 self.flush();
799 let end = self.out.trim_end().len();
800 self.out.truncate(end);
801 self.out
802 }
803}
804
805impl Code<'_> {
806 fn push(&mut self, text: &str) {
807 let text = if mem::take(&mut self.fresh) {
808 skip_newline(text)
809 } else {
810 text
811 };
812 push_verbatim(&mut self.buf, text);
813 }
814
815 fn br(&mut self) {
816 self.fresh = false;
817 self.buf.push(if self.block { '\n' } else { ' ' });
818 }
819
820 fn newline(&mut self) {
822 if !self.block {
823 self.buf.push(' ');
824 } else if !self.buf.is_empty() && !self.buf.ends_with('\n') {
825 self.buf.push('\n');
826 }
827 }
828}
829
830impl Table {
831 fn end_row(&mut self) {
832 if !self.cells.is_empty() {
833 self.rows.push(mem::take(&mut self.cells));
834 }
835 }
836}
837
838fn number(value: Option<&str>) -> Option<i64> {
840 value.and_then(|v| v.trim().parse().ok())
841}
842
843fn push_code_span(out: &mut String, text: &str) {
845 let fence = longest_run(text, '`') + 1;
846 let pad = text.starts_with('`') || text.ends_with('`');
847 push_n(out, '`', fence);
848 if pad {
849 out.push(' ');
850 }
851 out.push_str(text);
852 if pad {
853 out.push(' ');
854 }
855 push_n(out, '`', fence);
856}
857
858fn code_lang<'a>(pre: NodeRef<'a>) -> &'a str {
861 let lang = |n: NodeRef<'a>| {
862 n.classes().find_map(|c| {
863 c.strip_prefix("language-")
864 .or_else(|| c.strip_prefix("lang-"))
865 })
866 };
867 lang(pre)
868 .or_else(|| {
869 pre.child_elements()
870 .find(|c| c.tag_name() == Some("code"))
871 .and_then(lang)
872 })
873 .filter(|l| !l.contains('`'))
874 .unwrap_or("")
875}
876
877fn escape(out: &mut String, word: &str, line_start: bool) {
881 let bytes = word.as_bytes();
882 let first = bytes.first().copied().unwrap_or(0);
883 let all = |b: u8| bytes.iter().all(|&x| x == b);
884 let at = if all(b'#') {
886 Some(0) } else if !line_start {
888 None
889 } else if first == b'>' || (matches!(first, b'-' | b'+' | b'=') && all(first)) {
890 Some(0) } else {
892 let digits = bytes.iter().take_while(|b| b.is_ascii_digit()).count();
894 let marker = (1..=9).contains(&digits)
895 && bytes.len() == digits + 1
896 && matches!(bytes.get(digits), Some(b'.' | b')'));
897 marker.then_some(digits)
898 };
899 let mut prev = None;
900 let mut chars = word.char_indices().peekable();
901 while let Some((i, c)) = chars.next() {
902 let next = chars.peek().map(|&(_, n)| n);
903 let escaped = match c {
904 '\\' | '*' | '`' | '[' | ']' | '~' => true,
905 '_' => {
907 !(prev.is_some_and(char::is_alphanumeric)
908 && next.is_some_and(char::is_alphanumeric))
909 }
910 '<' => next.is_some_and(|n| n.is_ascii_alphabetic() || matches!(n, '/' | '!' | '?')),
912 '&' => next.is_some_and(|n| n.is_ascii_alphanumeric() || n == '#'),
914 _ => Some(i) == at,
915 };
916 if escaped {
917 out.push('\\');
918 }
919 out.push(c);
920 prev = Some(c);
921 }
922}
923
924fn push_close(out: &mut String, mark: Mark<'_>) {
925 match mark {
926 Mark::Strong => out.push_str("**"),
927 Mark::Emphasis => out.push('*'),
928 Mark::Strike => out.push_str("~~"),
929 Mark::Link(a) => {
930 out.push_str("](");
931 push_destination(out, a.attr("href").unwrap_or("").trim());
932 push_title(out, a.attr("title"));
933 out.push(')');
934 }
935 }
936}
937
938fn push_destination(out: &mut String, url: &str) {
942 let pointy = url.chars().any(|c| {
943 matches!(c, ' ' | '(' | ')' | '<' | '>')
944 || (c.is_ascii_control() && !matches!(c, '\n' | '\r' | '\t'))
945 });
946 if pointy {
947 out.push('<');
948 }
949 for c in url.chars() {
950 match c {
951 '\n' | '\r' | '\t' => {}
952 '\\' => out.push_str("\\\\"),
953 '<' | '>' => {
954 out.push('\\');
955 out.push(c);
956 }
957 c => out.push(c),
958 }
959 }
960 if pointy {
961 out.push('>');
962 }
963}
964
965fn push_title(out: &mut String, title: Option<&str>) {
966 let Some(title) = title.map(str::trim).filter(|t| !t.is_empty()) else {
967 return;
968 };
969 out.push_str(" \"");
970 for c in title.chars() {
971 match c {
972 '"' | '\\' => {
973 out.push('\\');
974 out.push(c);
975 }
976 c if is_space(c) => out.push(' '),
977 c => out.push(c),
978 }
979 }
980 out.push('"');
981}
982
983fn push_row(out: &mut String, cells: &[String], width: usize) {
985 out.push('|');
986 for i in 0..width.max(cells.len()) {
987 match cells.get(i).filter(|c| !c.is_empty()) {
988 Some(cell) => {
989 out.push(' ');
990 out.push_str(cell);
991 out.push_str(" |");
992 }
993 None => out.push_str(" |"),
994 }
995 }
996}
997
998fn longest_run(s: &str, ch: char) -> usize {
999 let (mut best, mut run) = (0, 0);
1000 for c in s.chars() {
1001 run = if c == ch { run + 1 } else { 0 };
1002 best = best.max(run);
1003 }
1004 best
1005}
1006
1007fn push_n(out: &mut String, ch: char, n: usize) {
1008 for _ in 0..n {
1009 out.push(ch);
1010 }
1011}