1use serde::{Deserialize, Serialize};
32use serde_json::Value;
33use std::fmt::Write;
34use tiptap_rusty_parser::{HtmlOptions, Mark, Node};
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub enum UnknownNodePolicy {
40 #[default]
42 Transparent,
43 Html,
45 Skip,
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase", default)]
52pub struct RenderOptions {
53 pub unknown_node: UnknownNodePolicy,
55 pub bullet: char,
59 pub emphasis: char,
62}
63
64impl Default for RenderOptions {
65 fn default() -> Self {
66 Self {
67 unknown_node: UnknownNodePolicy::Transparent,
68 bullet: '-',
69 emphasis: '*',
70 }
71 }
72}
73
74pub fn to_markdown(node: &Node) -> String {
86 to_markdown_with(node, &RenderOptions::default())
87}
88
89pub fn to_markdown_with(node: &Node, opts: &RenderOptions) -> String {
94 let mut r = Renderer::new(opts, approx_size(node));
95 let mut st = St::default();
96 match node.node_type.as_deref() {
97 None | Some("doc") => r.blocks(node.children(), Sep::Blank, &mut st),
98 _ => r.blocks(std::slice::from_ref(node), Sep::Blank, &mut st),
99 }
100 let keep = r.out.trim_end_matches('\n').len();
101 r.out.truncate(keep);
102 if !r.out.is_empty() {
103 r.out.push('\n');
104 }
105 r.out
106}
107
108#[derive(Clone, Copy, PartialEq, Eq)]
112enum Sep {
113 Blank,
115 Tight,
117}
118
119#[derive(Clone, Copy, PartialEq, Eq, Default)]
121enum Prev {
122 #[default]
123 None,
124 Bullet,
125 Ordered,
126 Other,
127}
128
129#[derive(Default)]
130struct St {
131 prev: Prev,
132 alt: bool,
134 bullet: char,
136 task_line: bool,
139 item: bool,
141}
142
143#[derive(Clone, Copy, PartialEq, Eq)]
144enum Mode {
145 Normal,
147 Flat,
149 Cell,
151}
152
153struct Renderer<'a> {
154 opts: &'a RenderOptions,
155 bullet: char,
156 emphasis: char,
157 out: String,
158 prefix: String,
160 cs: usize,
162 bol: usize,
165 mode: Mode,
166 active: Vec<&'a Mark>,
168 code_buf: String,
170 ws: String,
172 hb: bool,
174 hb_marks: &'a [Mark],
176 ws_after: String,
178 hb_run: (usize, usize, usize),
181 amb_at: usize,
185 opened: (usize, usize, usize, Option<char>),
188 em_open: char,
191 bold_open: char,
194 closed_at: usize,
196}
197
198const CODE_RANK: u8 = 9;
199const SPACES: &str = " ";
200
201impl<'a> Renderer<'a> {
202 fn new(opts: &'a RenderOptions, cap: usize) -> Self {
203 Self {
204 opts,
205 bullet: if matches!(opts.bullet, '-' | '*' | '+') {
206 opts.bullet
207 } else {
208 '-'
209 },
210 emphasis: if opts.emphasis == '_' { '_' } else { '*' },
211 out: String::with_capacity(cap),
212 prefix: String::new(),
213 cs: 0,
214 bol: usize::MAX,
215 mode: Mode::Normal,
216 active: Vec::new(),
217 code_buf: String::new(),
218 ws: String::new(),
219 hb: false,
220 hb_marks: &[],
221 ws_after: String::new(),
222 hb_run: (0, usize::MAX, 0),
223 opened: (0, usize::MAX, 0, None),
224 amb_at: usize::MAX,
225 em_open: '*',
226 bold_open: '*',
227 closed_at: usize::MAX,
228 }
229 }
230
231 fn newline(&mut self) {
234 self.out.push('\n');
235 self.out.push_str(&self.prefix);
236 self.cs = self.out.len();
237 }
238
239 fn blank_line(&mut self) {
241 self.out.push('\n');
242 self.out.push_str(self.prefix.trim_end());
243 }
244
245 fn newline_for(&mut self, next_empty: bool) {
247 if next_empty {
248 self.blank_line();
249 self.cs = self.out.len();
250 } else {
251 self.newline();
252 }
253 }
254
255 fn at_line_start(&self) -> bool {
256 self.out
257 .as_bytes()
258 .get(self.cs..)
259 .is_some_and(|t| t.iter().all(|&b| b == b' '))
260 }
261
262 fn separate(&mut self, sep: Sep) {
263 if sep == Sep::Blank {
264 self.blank_line();
265 }
266 self.newline();
267 }
268
269 fn begin(&mut self, sep: Sep, st: &St) {
270 if st.prev != Prev::None {
271 self.separate(sep);
272 } else if st.task_line {
273 if self.out.ends_with(' ') {
274 self.out.pop();
275 }
276 self.newline();
277 }
278 }
279
280 fn raw(&mut self, s: &str) {
282 for (i, line) in s.split('\n').enumerate() {
283 if i > 0 {
284 self.newline_for(line.is_empty());
285 }
286 self.out.push_str(line);
287 }
288 }
289
290 fn html(&self, n: &Node) -> String {
291 n.to_html_with(&HtmlOptions {
292 unknown_node: tiptap_rusty_parser::UnknownNodePolicy::DataTypeDiv,
293 ..HtmlOptions::default()
294 })
295 }
296
297 fn blocks(&mut self, nodes: &'a [Node], sep: Sep, st: &mut St) {
300 let mut i = 0;
301 while i < nodes.len() {
302 if is_inline_kind(&nodes[i]) {
303 let mut j = i + 1;
305 if nodes[i].node_type.as_deref() != Some("image") {
306 while j < nodes.len() && is_run_kind(&nodes[j]) {
307 j += 1;
308 }
309 }
310 self.paragraph(&nodes[i..j], sep, st);
311 i = j;
312 } else {
313 self.block(&nodes[i], sep, st);
314 i += 1;
315 }
316 }
317 }
318
319 fn paragraph(&mut self, nodes: &'a [Node], sep: Sep, st: &mut St) {
321 if nodes.is_empty() {
322 return;
323 }
324 if nodes.len() > 1 && !(st.item && st.prev == Prev::None) && is_hoistable_images(nodes) {
328 for n in nodes {
329 if n.node_type.as_deref() == Some("image") {
330 self.paragraph(std::slice::from_ref(n), sep, st);
331 }
332 }
333 return;
334 }
335 let (len, cs) = (self.out.len(), self.cs);
336 if st.prev != Prev::None {
337 self.separate(sep);
338 }
339 let at = self.out.len();
340 self.inline(nodes);
341 if self.out.len() == at {
342 self.out.truncate(len);
343 self.cs = cs;
344 } else {
345 st.prev = Prev::Other;
346 }
347 }
348
349 fn block(&mut self, n: &'a Node, sep: Sep, st: &mut St) {
350 let ty = match n.node_type.as_deref() {
351 Some(t) => t,
352 None => return self.blocks(n.children(), sep, st),
353 };
354 match ty {
355 "paragraph" => {
356 self.paragraph(n.children(), sep, st);
357 }
358 "heading" => {
359 self.begin(sep, st);
360 let level = attr_u64(n, "level").unwrap_or(1).clamp(1, 6) as usize;
361 for _ in 0..level {
362 self.out.push('#');
363 }
364 self.out.push(' ');
365 let content_at = self.out.len();
366 self.bol = content_at;
367 let prev = std::mem::replace(&mut self.mode, Mode::Flat);
368 self.inline(n.children());
369 self.mode = prev;
370 self.bol = usize::MAX;
371 if self.out.len() == content_at {
372 self.out.pop();
373 } else if self.out.ends_with('#') && !self.out.ends_with("\\#") {
374 let l = self.out.len() - 1;
376 self.out.insert(l, '\\');
377 }
378 st.prev = Prev::Other;
379 }
380 "blockquote" => {
381 self.begin(sep, st);
382 self.out.push_str("> ");
383 self.cs = self.out.len();
384 let at = self.out.len();
385 let pl = self.prefix.len();
386 self.prefix.push_str("> ");
387 let mut inner = St::default();
388 self.blocks(n.children(), Sep::Blank, &mut inner);
389 self.prefix.truncate(pl);
390 if self.out.len() == at {
391 self.out.pop();
392 }
393 st.prev = Prev::Other;
394 }
395 "horizontalRule" => {
396 self.begin(sep, st);
397 let line = &self.out.as_bytes()[self.out.rfind('\n').map_or(0, |i| i + 1)..];
400 let rule = if !line.contains(&b'-') {
401 "---"
402 } else if !line.contains(&b'*') {
403 "***"
404 } else {
405 "___"
406 };
407 self.out.push_str(rule);
408 st.prev = Prev::Other;
409 }
410 "codeBlock" => {
411 self.begin(sep, st);
412 self.code_block(n);
413 st.prev = Prev::Other;
414 }
415 "bulletList" | "taskList" => {
416 if n.children().is_empty() {
417 return;
418 }
419 self.begin(list_sep(n, false, sep), st);
420 let avoid = if st.prev == Prev::Bullet {
422 st.bullet
423 } else {
424 '\0'
425 };
426 st.bullet = self.list(n, false, false, avoid);
427 st.prev = Prev::Bullet;
428 }
429 "orderedList" => {
430 if n.children().is_empty() {
431 return;
432 }
433 self.begin(list_sep(n, true, sep), st);
434 let alt = st.prev == Prev::Ordered && !st.alt;
435 self.list(n, true, alt, '\0');
436 (st.prev, st.alt) = (Prev::Ordered, alt);
437 }
438 "table" => {
439 if n.children().is_empty() {
440 return;
441 }
442 self.begin(sep, st);
443 if pipe_table_ok(n) {
444 self.table(n);
445 } else {
446 let h = table_html(n);
447 self.raw(&h);
448 }
449 st.prev = Prev::Other;
450 }
451 "doc" | "listItem" | "taskItem" | "tableRow" | "tableCell" | "tableHeader" => {
453 self.blocks(n.children(), sep, st)
454 }
455 _ => match self.opts.unknown_node {
456 UnknownNodePolicy::Skip => {}
457 UnknownNodePolicy::Transparent => self.blocks(n.children(), sep, st),
458 UnknownNodePolicy::Html => {
459 self.begin(sep, st);
460 let h = self.html(n);
461 self.raw(&h);
462 st.prev = Prev::Other;
463 }
464 },
465 }
466 }
467
468 fn code_block(&mut self, n: &Node) {
469 let (mut cur, mut max, mut any) = ([0usize; 2], [0usize; 2], false);
471 walk_text(n, &mut |s| {
472 any |= !s.is_empty();
473 for b in s.bytes() {
474 let k = match b {
475 b'`' => 0,
476 b'~' => 1,
477 _ => {
478 cur = [0; 2];
479 continue;
480 }
481 };
482 cur[k] += 1;
483 cur[1 - k] = 0;
484 max[k] = max[k].max(cur[k]);
485 }
486 });
487 let lang = match attr(n, "language") {
489 Some(Value::String(l)) => l.split_whitespace().next().unwrap_or(""),
490 _ => "",
491 };
492 let (fc, run) = if lang.contains('`') {
494 ('~', max[1])
495 } else {
496 ('`', max[0])
497 };
498 let fence = (run + 1).max(3);
499 for _ in 0..fence {
500 self.out.push(fc);
501 }
502 let lb = lang.as_bytes();
503 for (i, c) in lang.char_indices() {
504 match c {
505 '\n' | '\r' => {}
506 '\\' => self.out.push_str("\\\\"),
507 '&' if entity_like(&lb[i + 1..]) => self.out.push_str("\\&"),
508 _ => self.out.push(c),
509 }
510 }
511 self.newline();
512 walk_text(n, &mut |s| {
513 for (i, seg) in s.split('\n').enumerate() {
514 if i > 0 {
515 self.newline_for(seg.is_empty());
516 }
517 self.out.push_str(seg);
518 }
519 });
520 if any {
521 self.newline();
522 }
523 for _ in 0..fence {
524 self.out.push(fc);
525 }
526 }
527
528 fn list(&mut self, n: &'a Node, ordered: bool, alt: bool, avoid: char) -> char {
532 let items = n.children();
533 let sep = if is_tight(items) {
534 Sep::Tight
535 } else {
536 Sep::Blank
537 };
538 let mut num = attr_u64(n, "start").unwrap_or(1);
539 let other = if self.bullet == '-' { '*' } else { '-' };
540 let bullet = if ordered {
543 self.bullet
544 } else {
545 let line = &self.out.as_bytes()[self.out.rfind('\n').map_or(0, |i| i + 1)..];
546 let count = |c: char| line.iter().filter(|&&b| b == c as u8).count();
547 [self.bullet, other, '-', '*', '+']
548 .into_iter()
549 .find(|&c| c != avoid && count(c) < 2)
550 .unwrap_or(other)
551 };
552 let delim = if alt { ')' } else { '.' };
553 for (i, item) in items.iter().enumerate() {
554 if i > 0 {
555 self.separate(sep);
556 }
557 let kids: &[Node] = if is_item(item) {
558 item.children()
559 } else {
560 std::slice::from_ref(item)
561 };
562 let before = self.out.len();
563 if ordered {
564 let _ = write!(self.out, "{num}{delim} ");
565 num = num.wrapping_add(1);
566 } else {
567 self.out.push(bullet);
568 self.out.push(' ');
569 }
570 let width = self.out.len() - before;
571 let marker_end = self.out.len();
572 if item.node_type.as_deref() == Some("taskItem") {
573 let checked = attr(item, "checked").and_then(Value::as_bool) == Some(true);
574 self.out.push_str(if checked { "[x] " } else { "[ ] " });
575 }
576 self.cs = self.out.len();
577 let pl = self.prefix.len();
578 for _ in 0..width / SPACES.len() {
579 self.prefix.push_str(SPACES);
580 }
581 self.prefix.push_str(&SPACES[..width % SPACES.len()]);
582 let mut inner = St {
583 task_line: item.node_type.as_deref() == Some("taskItem"),
584 item: true,
585 ..St::default()
586 };
587 self.blocks(kids, sep, &mut inner);
588 self.prefix.truncate(pl);
589 if self.out.len() == marker_end {
590 self.out.pop(); }
592 }
593 bullet
594 }
595
596 fn table(&mut self, n: &'a Node) {
599 let rows = n.children();
600 let cols = rows.iter().map(|r| r.children().len()).max().unwrap_or(0);
601 for (ri, row) in rows.iter().enumerate() {
602 if ri > 0 {
603 self.newline();
604 }
605 self.out.push('|');
606 let cells = row.children();
607 for ci in 0..cols {
608 self.out.push(' ');
609 if let Some(cell) = cells.get(ci) {
610 self.bol = self.out.len();
611 let prev = std::mem::replace(&mut self.mode, Mode::Cell);
612 if let Some(p) = cell.children().first() {
613 self.inline(p.children());
614 }
615 self.mode = prev;
616 self.bol = usize::MAX;
617 }
618 self.out.push_str(" |");
619 }
620 if ri == 0 {
621 self.newline();
622 self.out.push('|');
623 for _ in 0..cols {
624 self.out.push_str(" --- |");
625 }
626 }
627 }
628 }
629
630 fn inline(&mut self, nodes: &'a [Node]) {
633 let start = self.out.len();
634 self.active.clear();
635 self.ws.clear();
636 self.ws_after.clear();
637 self.hb = false;
638 self.items(nodes);
639 self.close_not_in(&[], true);
640 let ascii_ws = [' ', '\t', '\n', '\r'];
642 let keep = self.ws_after.trim_end_matches(ascii_ws).len();
643 if self.hb && keep > 0 {
644 self.ws_after.truncate(keep);
645 self.flush_pending();
646 } else {
647 let keep = self.ws.trim_end_matches(ascii_ws).len();
648 if keep > 0 {
649 self.ws.truncate(keep);
650 self.flush_ws();
651 }
652 }
653 let mut dropped = self.take_hb_run().is_some();
656 let tail = self.prefix.len() + 2;
657 while self.out.len() >= start + tail
658 && self.out.ends_with(self.prefix.as_str())
659 && self.out.as_bytes()[..self.out.len() - self.prefix.len()].ends_with(b"\\\n")
660 && ends_unescaped(&self.out[..self.out.len() - tail + 1], b'\\')
661 {
662 self.out.truncate(self.out.len() - tail);
664 dropped = true;
665 }
666 if dropped {
667 let keep = self.out.trim_end_matches([' ', '\t']).len().max(start);
668 self.out.truncate(keep);
669 }
670 self.ws.clear();
671 self.ws_after.clear();
672 self.hb = false;
673 }
674
675 fn items(&mut self, nodes: &'a [Node]) {
676 let mut i = 0;
677 while i < nodes.len() {
678 let n = &nodes[i];
679 i += 1;
680 match n.node_type.as_deref() {
681 Some("text") => {
682 let marks = n.marks.as_deref().unwrap_or(&[]);
686 let flat = self.mode == Mode::Flat;
687 let same = |m: &Node| {
688 matches!(m.node_type.as_deref(), Some("text"))
689 || (flat && m.node_type.as_deref() == Some("hardBreak"))
690 } && m.marks.as_deref().unwrap_or(&[]) == marks;
691 let j = i + nodes[i..].iter().take_while(|m| same(m)).count();
692 if j == i {
693 self.text_item(n.text.as_deref().unwrap_or(""), marks);
694 } else {
695 let mut s = String::from(n.text.as_deref().unwrap_or(""));
696 for m in &nodes[i..j] {
697 s.push_str(m.text.as_deref().unwrap_or(" "));
698 }
699 self.text_item(&s, marks);
700 i = j;
701 }
702 }
703 Some("hardBreak") => self.hard_break(n),
704 Some("image") => self.image(n),
705 None => self.items(n.children()),
706 Some(ty) if is_known_container(ty) => self.items(n.children()),
707 Some(_) => match self.opts.unknown_node {
708 UnknownNodePolicy::Skip => {}
709 UnknownNodePolicy::Transparent => self.items(n.children()),
710 UnknownNodePolicy::Html => {
711 self.close_not_in(&[], false);
712 self.flush_pending();
713 let h = self.html(n);
714 let prev = self.mode;
715 if prev == Mode::Normal {
716 self.raw(&h);
717 } else {
718 self.out.push_str(&h.replace('\n', " "));
719 }
720 }
721 },
722 }
723 }
724 }
725
726 fn text_item(&mut self, text: &str, marks: &'a [Mark]) {
727 if text.is_empty() {
728 return;
729 }
730 if marks.iter().any(|m| m.mark_type == "code") {
731 self.close_not_in(marks, true);
732 if self.active.last().is_some_and(|a| a.mark_type == "code")
734 && marks.iter().any(|m| {
735 m.mark_type != "code"
736 && mark_rank(m).is_some()
737 && !self.active.iter().any(|a| a.mark_type == m.mark_type)
738 })
739 {
740 if let Some(c) = self.active.pop() {
741 self.close_mark(c);
742 }
743 }
744 self.open_after_pending(marks, true, "", '`');
745 self.code_buf.extend(
746 text.chars()
747 .map(|c| if matches!(c, '\n' | '\r') { ' ' } else { c }),
748 );
749 return;
750 }
751 let b = text.as_bytes();
752 let plain = |x: u8| x.is_ascii_graphic();
753 let (lead, body, core) = if plain(b[0]) && plain(b[b.len() - 1]) {
754 ("", text, text) } else {
756 let body = text.trim_start_matches(is_ws);
757 let lead = &text[..text.len() - body.len()];
758 (lead, body, body.trim_end_matches(is_ws))
759 };
760 self.close_not_in(marks, true);
761 if core.is_empty() {
762 let link = marks
764 .iter()
765 .find(|m| m.mark_type == "link" && mark_rank(m).is_some());
766 if let Some(link) = link {
767 if !self.active.iter().any(|a| a.mark_type == "link") {
768 self.open_after_pending(std::slice::from_ref(link), true, "", '[');
769 self.escape_text(text);
770 return;
771 }
772 }
773 if self.hb {
774 self.ws_after.push_str(text);
775 } else {
776 self.ws.push_str(text);
777 }
778 return;
779 }
780 self.open_after_pending(marks, true, lead, core.chars().next().unwrap_or(' '));
781 self.escape_text(core);
782 self.ws.push_str(&body[core.len()..]);
783 }
784
785 fn hard_break(&mut self, n: &'a Node) {
786 let marks = n.marks.as_deref().unwrap_or(&[]);
787 self.close_not_in(marks, false);
788 match self.mode {
789 Mode::Normal => {
790 if self.hb {
791 if self.hb_marks == marks {
794 self.open_after_pending(marks, false, "", '\\');
795 } else {
796 self.flush_pending();
797 }
798 }
799 let link = marks
801 .iter()
802 .find(|m| m.mark_type == "link" && mark_rank(m).is_some());
803 if let Some(link) = link {
804 if !self.active.iter().any(|a| a.mark_type == "link") {
805 self.open_after_pending(std::slice::from_ref(link), false, "", '[');
806 }
807 }
808 self.hb = true;
809 self.hb_marks = marks;
810 }
811 Mode::Flat if self.out.len() == self.bol && self.ws.is_empty() => {}
813 Mode::Flat => self.ws.push(' '),
814 Mode::Cell => {
815 self.flush_pending();
816 self.out.push_str("<br>");
817 }
818 }
819 }
820
821 fn image(&mut self, n: &'a Node) {
822 let marks = n.marks.as_deref().unwrap_or(&[]);
823 self.close_not_in(marks, false);
824 self.open_after_pending(marks, false, "", '!');
825 self.out.push_str(";
835 write_dest(
836 &mut self.out,
837 attr(n, "src").and_then(Value::as_str).unwrap_or(""),
838 );
839 if let Some(t) = attr(n, "title").and_then(Value::as_str) {
840 write_title(&mut self.out, t);
841 }
842 self.out.push(')');
843 }
844
845 fn open_after_pending(&mut self, marks: &'a [Mark], text: bool, lead: &str, next: char) {
849 if marks.is_empty() && !self.hb && self.ws.is_empty() && lead.is_empty() {
850 return;
851 }
852 self.flush_ws();
853 let from = self.collect_opens(marks, text);
854 let mut mid = from;
855 if self.hb {
856 let hbm = self.hb_marks;
859 let carried = |m: &Mark| m.mark_type != "code" && hbm.contains(m);
860 let new = &mut self.active[from..];
861 if new.iter().any(|m| carried(m)) {
862 new.sort_by_key(|m| !carried(m));
863 mid += new.iter().take_while(|m| carried(m)).count();
864 self.write_opens(from, mid, '\\');
865 }
866 }
867 self.flush_hb();
868 if !lead.is_empty() {
869 if self.active.get(mid).is_some_and(|m| m.mark_type == "link") {
872 self.write_opens(mid, mid + 1, ' ');
873 mid += 1;
874 }
875 self.escape_text(lead);
876 }
877 self.write_opens(mid, self.active.len(), next);
878 }
879
880 fn flush_pending(&mut self) {
881 let start = self.out.len();
882 self.flush_ws();
883 self.flush_hb();
884 if self.hb_run.1 == self.out.len() && self.hb_run.0 > start {
885 self.hb_run.0 = start;
887 }
888 }
889
890 fn flush_ws(&mut self) {
891 if !self.ws.is_empty() {
892 let w = std::mem::take(&mut self.ws);
893 self.escape_text(&w);
894 self.ws = w;
895 self.ws.clear();
896 }
897 }
898
899 fn flush_hb(&mut self) {
900 self.emit_hb(false);
901 }
902
903 fn emit_hb(&mut self, spaces: bool) {
905 if self.hb {
906 self.hb = false;
907 if self.out.len() != self.hb_run.1 {
908 self.hb_run.0 = self.out.len();
909 self.hb_run.2 = self.cs;
910 }
911 self.out.push_str(if spaces { " " } else { "\\" });
912 self.newline();
913 self.hb_run.1 = self.out.len();
914 if !self.ws_after.is_empty() {
915 let w = std::mem::take(&mut self.ws_after);
916 self.escape_text(&w);
917 if !w.contains(|c: char| !matches!(c, ' ' | '\t' | '\n' | '\r')) {
918 self.hb_run.1 = self.out.len();
920 }
921 self.ws_after = w;
922 self.ws_after.clear();
923 }
924 }
925 }
926
927 fn take_hb_run(&mut self) -> Option<String> {
931 if self.out.len() != self.hb_run.1 {
932 return None;
933 }
934 let tail = self.out.split_off(self.hb_run.0);
935 self.cs = self.hb_run.2;
936 self.hb_run.1 = usize::MAX;
937 Some(tail)
938 }
939
940 fn close_not_in(&mut self, marks: &[Mark], text: bool) {
942 let mut i = 0;
943 while i < self.active.len() {
944 let a = self.active[i];
945 if (text || a.mark_type != "code") && marks.iter().any(|m| m == a) {
946 i += 1;
947 } else {
948 break;
949 }
950 }
951 if i == self.active.len() {
952 return;
953 }
954 let delim = self
957 .active
958 .last()
959 .is_some_and(|m| matches!(m.mark_type.as_str(), "bold" | "italic" | "strike"));
960 let breaks = if self.active.len() > i && delim {
961 self.take_hb_run()
962 } else {
963 None
964 };
965 let (os, oe, of, restore) = self.opened;
967 if breaks.is_some() && self.out.len() == oe && of >= i && of < self.active.len() {
968 self.out.truncate(os);
969 if let Some(p) = restore {
970 self.out.push(p);
971 }
972 self.active.truncate(of);
973 }
974 while self.active.len() > i {
975 if let Some(m) = self.active.pop() {
976 if m.mark_type == "link" {
977 if self.ws.contains(['\n', '\r']) {
983 self.ws = self.ws.replace(['\n', '\r'], " ");
984 }
985 self.flush_ws();
986 if self.hb && self.hb_marks.contains(m) {
987 let spaces = self.out.len() == self.closed_at;
988 self.emit_hb(spaces);
989 }
990 }
991 self.close_mark(m);
992 }
993 }
994 if let Some(b) = breaks {
995 self.hb_run.0 = self.out.len();
996
997 self.out.push_str(&b);
998 self.cs = self.out.len();
999 self.hb_run.1 = self.out.len();
1000 }
1001 }
1002
1003 fn collect_opens(&mut self, marks: &'a [Mark], text: bool) -> usize {
1005 let from = self.active.len();
1006 for m in marks {
1007 let Some(rank) = mark_rank(m) else { continue };
1008 if rank == CODE_RANK && !text {
1009 continue;
1010 }
1011 if self.active.iter().any(|a| a.mark_type == m.mark_type) {
1012 continue;
1013 }
1014 self.active.push(m);
1015 }
1016 if self.active.len() - from > 1 {
1017 self.active[from..].sort_by_key(|m| mark_rank(m).unwrap_or(0));
1018 }
1019 from
1020 }
1021
1022 fn write_opens(&mut self, from: usize, to: usize, next: char) {
1024 if from >= to {
1025 return;
1026 }
1027 let start = self.out.len();
1028 for i in from..to {
1029 let m = self.active[i];
1030 self.open_mark(m);
1031 }
1032 self.opened = (start, self.out.len(), from, None);
1033 let b = &self.out.as_bytes()[start..];
1037 let Some(&c) = b.first().filter(|c| matches!(c, b'*' | b'_' | b'~')) else {
1038 return;
1039 };
1040 let run = b.iter().take_while(|&&x| x == c).count();
1041 let n = b.get(run).map_or(next, |&x| x as char);
1042 if let Some(p) = self.out[..start].chars().next_back() {
1043 if maybe_word(p) && (maybe_punct(n) || c == b'_') {
1044 let at = start - p.len_utf8();
1045 let undo = self.encode_before(at, p);
1046 self.closed_at = usize::MAX;
1047 let end = if undo.is_some() {
1049 self.out.len()
1050 } else {
1051 usize::MAX
1052 };
1053 self.opened = (at, end, from, undo);
1054 }
1055 }
1056 }
1057
1058 fn encode_before(&mut self, mut at: usize, mut p: char) -> Option<char> {
1063 let mut undo = Some(p);
1064 loop {
1065 let mut e = String::with_capacity(12);
1066 let _ = write!(e, "&#{};", p as u32);
1067 self.out.replace_range(at..at + p.len_utf8(), &e);
1068 let b = self.out.as_bytes();
1069 let Some(&d) = b[..at].last().filter(|d| matches!(d, b'*' | b'_' | b'~')) else {
1070 return undo;
1071 };
1072 if b[..at - 1]
1073 .iter()
1074 .rev()
1075 .take_while(|&&x| x == b'\\')
1076 .count()
1077 % 2
1078 == 1
1079 {
1080 return undo; }
1082 if d == b'_' && at >= 2 && b[at - 2].is_ascii_alphanumeric() {
1083 self.out.insert(at - 1, '\\');
1085 return None;
1086 }
1087 let run = b[..at].iter().rev().take_while(|&&x| x == d).count();
1088 match self.out[..at - run].chars().next_back() {
1089 Some(q) if maybe_word(q) => {
1090 at = at - run - q.len_utf8();
1091 p = q;
1092 undo = None;
1093 }
1094 _ => return undo,
1095 }
1096 }
1097 }
1098
1099 fn open_mark(&mut self, m: &Mark) {
1100 match m.mark_type.as_str() {
1101 "link" => {
1102 if ends_unescaped(&self.out, b'!') {
1104 let l = self.out.len() - 1;
1105 self.out.insert(l, '\\');
1106 }
1107 self.out.push('[');
1108 }
1109 "bold" => {
1110 let c = if self.out.len() == self.closed_at && self.out.ends_with('*') {
1112 '_'
1113 } else {
1114 '*'
1115 };
1116 self.bold_open = c;
1117 self.out.push(c);
1118 self.out.push(c);
1119 }
1120 "italic" => {
1121 let mut c = self.emphasis;
1124 if self.out.len() == self.closed_at && self.out.ends_with(c) {
1125 c = if c == '*' { '_' } else { '*' };
1126 }
1127 self.em_open = c;
1128 self.out.push(c);
1129 }
1130 "strike" => self.out.push_str("~~"),
1131 "underline" => self.out.push_str("<u>"),
1132 "subscript" => self.out.push_str("<sub>"),
1133 "superscript" => self.out.push_str("<sup>"),
1134 "code" => self.code_buf.clear(),
1135 _ => {}
1136 }
1137 }
1138
1139 fn close_mark(&mut self, m: &Mark) {
1140 match m.mark_type.as_str() {
1141 "link" => {
1142 self.out.push_str("](");
1143 write_dest(&mut self.out, mark_attr(m, "href").unwrap_or(""));
1144 if let Some(t) = mark_attr(m, "title") {
1145 write_title(&mut self.out, t);
1146 }
1147 self.out.push(')');
1148 }
1149 "bold" => {
1150 self.out.push(self.bold_open);
1151 self.out.push(self.bold_open);
1152 self.closed_at = self.out.len();
1153 }
1154 "italic" => {
1155 self.out.push(self.em_open);
1156 self.closed_at = self.out.len();
1157 }
1158 "strike" => {
1159 self.out.push_str("~~");
1160 self.closed_at = self.out.len();
1161 }
1162 "underline" => self.out.push_str("</u>"),
1163 "subscript" => self.out.push_str("</sub>"),
1164 "superscript" => self.out.push_str("</sup>"),
1165 "code" => self.flush_code(),
1166 _ => {}
1167 }
1168 }
1169
1170 fn flush_code(&mut self) {
1171 let s = std::mem::take(&mut self.code_buf);
1172 let (mut run, mut seen) = (0u32, 0u64);
1174 for b in s.bytes().chain(std::iter::once(b' ')) {
1175 if b == b'`' {
1176 run += 1;
1177 } else {
1178 if run > 0 && run < 64 {
1179 seen |= 1 << run;
1180 }
1181 run = 0;
1182 }
1183 }
1184 let n = (1..64).find(|k| seen & (1 << k) == 0).unwrap_or(1);
1185 let pad = s.starts_with('`')
1186 || s.ends_with('`')
1187 || (s.starts_with(' ') && s.ends_with(' ') && s.bytes().any(|b| b != b' '));
1188 for _ in 0..n {
1189 self.out.push('`');
1190 }
1191 if pad {
1192 self.out.push(' ');
1193 }
1194 if self.mode == Mode::Cell && s.contains('|') {
1195 self.out.push_str(&s.replace('|', "\\|"));
1196 } else {
1197 self.out.push_str(&s);
1198 }
1199 if pad {
1200 self.out.push(' ');
1201 }
1202 for _ in 0..n {
1203 self.out.push('`');
1204 }
1205 self.code_buf = s;
1206 self.code_buf.clear();
1207 }
1208
1209 fn escape_text(&mut self, s: &str) {
1212 let b = s.as_bytes();
1213 let mut ls = self.at_line_start();
1214 let mut lead = ls || self.out.len() == self.bol;
1216 let (mut last, mut i) = (0, 0);
1217 let mut enc_end = 0;
1219 if self.amb_at != usize::MAX && self.amb_at + 1 == self.out.len() {
1220 let bad = match self.out.as_bytes()[self.amb_at] {
1221 b'<' => b.first().is_some_and(|&x| starts_tag(x)),
1222 b'&' => entity_like(b),
1223 _ => false,
1224 };
1225 if bad {
1226 self.out.insert(self.amb_at, '\\');
1227 }
1228 }
1229 self.amb_at = usize::MAX;
1230 if self.out.len() == self.closed_at {
1231 if let Some(n) = s.chars().next().filter(|&n| maybe_word(n)) {
1235 let ob = self.out.as_bytes();
1236 let c = ob[ob.len() - 1];
1237 let run = ob.iter().rev().take_while(|&&x| x == c).count();
1238 let p = self.out[..ob.len() - run].chars().next_back();
1239 if c == b'_' || p.is_some_and(maybe_punct) {
1240 let _ = write!(self.out, "&#{};", n as u32);
1241 (last, i, lead) = (n.len_utf8(), n.len_utf8(), false);
1242 enc_end = i;
1243 }
1244 }
1245 }
1246 while i < b.len() {
1247 if !ls && !lead {
1248 match b[i..].iter().position(|&x| MID_LINE[x as usize]) {
1250 Some(k) => i += k,
1251 None => break,
1252 }
1253 }
1254 let c = b[i];
1255 if lead && self.mode != Mode::Normal && (c == b'\n' || c == b'\r') {
1256 self.out.push_str(&s[last..i]);
1258 last = i + 1;
1259 i += 1;
1260 continue;
1261 }
1262 if lead && (c == b' ' || c == b'\t') {
1263 self.out.push_str(&s[last..i]);
1264 self.out.push_str(if c == b' ' { " " } else { "	" });
1265 (last, ls, lead) = (i + 1, false, false);
1266 i += 1;
1267 continue;
1268 }
1269 lead = false;
1270 let esc = match c {
1271 b'\\' | b'*' | b'`' | b'[' | b']' | b'~' => true,
1272 b'_' => {
1273 let w = |x: Option<&u8>| x.is_some_and(|x| x.is_ascii_alphanumeric());
1274 !(i > 0 && i != enc_end && w(b.get(i - 1)) && w(b.get(i + 1)))
1275 }
1276 b'<' => b.get(i + 1).is_some_and(|&x| starts_tag(x)),
1278 b'&' => entity_like(&b[i + 1..]),
1279 b'|' => self.mode == Mode::Cell,
1280 b'#' if ls => {
1281 let mut j = i;
1282 while j < b.len() && b[j] == b'#' {
1283 j += 1;
1284 }
1285 j - i <= 6
1286 && b.get(j)
1287 .is_none_or(|x| matches!(x, b' ' | b'\t' | b'\n' | b'\r'))
1288 }
1289 b'>' if ls => true,
1290 b'-' | b'+' if ls => b.get(i + 1).is_none_or(|x| {
1291 matches!(x, b' ' | b'\t' | b'\n' | b'\r') || (c == b'-' && *x == b'-')
1292 }),
1293 b'=' if ls => b
1294 .get(i + 1)
1295 .is_none_or(|x| matches!(x, b' ' | b'\t' | b'=' | b'\n' | b'\r')),
1296 b'0'..=b'9' if ls => {
1297 let mut j = i;
1298 while j < b.len() && b[j].is_ascii_digit() {
1299 j += 1;
1300 }
1301 if j - i <= 9
1302 && matches!(b.get(j), Some(b'.' | b')'))
1303 && b.get(j + 1)
1304 .is_none_or(|x| matches!(x, b' ' | b'\t' | b'\n' | b'\r'))
1305 {
1306 self.out.push_str(&s[last..j]);
1307 self.out.push('\\');
1308 last = j;
1309 }
1310 ls = false;
1311 i = j;
1312 continue;
1313 }
1314 b'\r' if b.get(i + 1) == Some(&b'\n') => {
1315 self.out.push_str(&s[last..i]);
1317 last = i + 1;
1318 i += 1;
1319 continue;
1320 }
1321 b'\n' | b'\r' => {
1322 self.out.push_str(&s[last..i]);
1323 last = i + 1;
1324 if self.mode == Mode::Normal {
1325 if self.out.len() != self.hb_run.1 {
1327 self.hb_run.0 = self.out.len();
1328 self.hb_run.2 = self.cs;
1329 }
1330 self.out.push('\\');
1331 self.newline();
1332 self.hb_run.1 = self.out.len();
1333 (ls, lead) = (true, true);
1334 } else {
1335 self.out.push(' ');
1336 }
1337 i += 1;
1338 continue;
1339 }
1340 _ => false,
1341 };
1342 if esc {
1343 self.out.push_str(&s[last..i]);
1344 self.out.push('\\');
1345 last = i;
1346 }
1347 if c != b' ' && c != b'\t' {
1348 ls = false;
1349 }
1350 i += 1;
1351 }
1352 self.out.push_str(&s[last..]);
1353 if matches!(b.last(), Some(b'<' | b'&')) && last < b.len() {
1354 self.amb_at = self.out.len() - 1;
1355 }
1356 }
1357}
1358
1359fn ends_unescaped(out: &str, c: u8) -> bool {
1363 let b = out.as_bytes();
1364 let Some((&last, rest)) = b.split_last() else {
1365 return false;
1366 };
1367 last == c && rest.iter().rev().take_while(|&&x| x == b'\\').count() % 2 == 0
1368}
1369
1370fn maybe_word(c: char) -> bool {
1372 !c.is_whitespace() && !c.is_ascii_punctuation()
1373}
1374
1375fn maybe_punct(c: char) -> bool {
1377 !c.is_whitespace() && !c.is_alphanumeric()
1378}
1379
1380fn is_ws(c: char) -> bool {
1382 c.is_whitespace()
1383}
1384
1385fn is_hoistable_images(nodes: &[Node]) -> bool {
1388 let mut any = false;
1389 for c in nodes {
1390 match c.node_type.as_deref() {
1391 Some("image")
1394 if c.marks.as_ref().is_none_or(|m| {
1395 m.iter()
1396 .all(|m| m.mark_type == "code" || mark_rank(m).is_none())
1397 }) =>
1398 {
1399 any = true
1400 }
1401 Some("hardBreak") => {}
1402 Some("text") if c.text.as_deref().is_none_or(|t| t.trim().is_empty()) => {}
1403 _ => return false,
1404 }
1405 }
1406 any
1407}
1408
1409fn is_inline_kind(n: &Node) -> bool {
1410 matches!(n.node_type.as_deref(), Some("text" | "hardBreak" | "image"))
1411}
1412
1413fn is_run_kind(n: &Node) -> bool {
1415 matches!(n.node_type.as_deref(), Some("text" | "hardBreak"))
1416}
1417
1418fn table_html(n: &Node) -> String {
1420 let mut o = HtmlOptions {
1421 unknown_node: tiptap_rusty_parser::UnknownNodePolicy::DataTypeDiv,
1422 spread_attrs: true,
1423 ..HtmlOptions::default()
1424 };
1425 for (k, v) in [
1426 ("table", "table"),
1427 ("tableRow", "tr"),
1428 ("tableCell", "td"),
1429 ("tableHeader", "th"),
1430 ] {
1431 o.node_tags.insert(k.into(), v.into());
1432 }
1433 n.to_html_with(&o)
1434}
1435
1436fn is_item(n: &Node) -> bool {
1437 matches!(n.node_type.as_deref(), Some("listItem" | "taskItem"))
1438}
1439
1440fn is_list(n: &Node) -> bool {
1441 matches!(
1442 n.node_type.as_deref(),
1443 Some("bulletList" | "orderedList" | "taskList")
1444 )
1445}
1446
1447fn is_known_container(ty: &str) -> bool {
1449 matches!(
1450 ty,
1451 "doc"
1452 | "paragraph"
1453 | "heading"
1454 | "blockquote"
1455 | "bulletList"
1456 | "orderedList"
1457 | "taskList"
1458 | "listItem"
1459 | "taskItem"
1460 | "codeBlock"
1461 | "table"
1462 | "tableRow"
1463 | "tableCell"
1464 | "tableHeader"
1465 )
1466}
1467
1468fn is_tight(items: &[Node]) -> bool {
1470 items.iter().all(|item| {
1471 let kids: &[Node] = if is_item(item) {
1472 item.children()
1473 } else {
1474 return false;
1475 };
1476 kids.iter()
1477 .filter(|k| !renders_empty(k))
1478 .enumerate()
1479 .all(|(i, k)| is_list(k) || (i == 0 && k.node_type.as_deref() == Some("paragraph")))
1480 })
1481}
1482
1483fn renders_empty(p: &Node) -> bool {
1485 p.node_type.as_deref() == Some("paragraph")
1486 && p.children().iter().all(|c| {
1487 let text = c.node_type.as_deref() == Some("text");
1489 let kept = c.marks.as_deref().is_some_and(|m| {
1490 m.iter().any(|m| {
1491 (text && m.mark_type == "code")
1492 || (m.mark_type == "link" && mark_rank(m).is_some())
1493 })
1494 });
1495 !kept
1496 && match c.node_type.as_deref() {
1497 Some("hardBreak") => true,
1498 Some("text") => c
1499 .text
1500 .as_deref()
1501 .is_none_or(|t| t.trim_matches([' ', '\t', '\n', '\r']).is_empty()),
1502 _ => false,
1503 }
1504 })
1505}
1506
1507fn list_sep(list: &Node, ordered: bool, sep: Sep) -> Sep {
1510 if sep == Sep::Blank {
1511 return sep;
1512 }
1513 let empty_first = list.children().first().is_some_and(|it| {
1514 it.node_type.as_deref() == Some("listItem") && it.children().iter().all(renders_empty)
1515 });
1516 if empty_first || (ordered && attr_u64(list, "start").unwrap_or(1) != 1) {
1517 Sep::Blank
1518 } else {
1519 sep
1520 }
1521}
1522
1523fn pipe_table_ok(table: &Node) -> bool {
1524 let span_ok = |c: &Node| {
1525 ["colspan", "rowspan"]
1526 .iter()
1527 .all(|k| attr_u64(c, k).is_none_or(|v| v <= 1))
1528 };
1529 table.children().iter().all(|row| {
1530 row.node_type.as_deref() == Some("tableRow")
1531 && !row.children().is_empty()
1532 && row.children().iter().all(|cell| {
1533 matches!(cell.node_type.as_deref(), Some("tableCell" | "tableHeader"))
1534 && span_ok(cell)
1535 && match cell.children() {
1536 [] => true,
1537 [p] => {
1538 p.node_type.as_deref() == Some("paragraph")
1539 && p.children().iter().all(is_inline_kind)
1540 }
1541 _ => false,
1542 }
1543 })
1544 })
1545}
1546
1547fn attr_u64(n: &Node, key: &str) -> Option<u64> {
1548 match attr(n, key)? {
1549 Value::Number(x) => x.as_u64().or_else(|| x.as_f64().map(|f| f.max(0.0) as u64)),
1550 Value::String(s) => s.trim().parse().ok(),
1551 _ => None,
1552 }
1553}
1554
1555fn attr<'n>(n: &'n Node, key: &str) -> Option<&'n Value> {
1557 n.attrs
1558 .as_ref()?
1559 .iter()
1560 .find_map(|(k, v)| (k == key).then_some(v))
1561}
1562
1563fn mark_attr<'m>(m: &'m Mark, key: &str) -> Option<&'m str> {
1564 m.attrs
1565 .as_ref()?
1566 .iter()
1567 .find_map(|(k, v)| (k == key).then_some(v))?
1568 .as_str()
1569 .filter(|s| !s.is_empty())
1570}
1571
1572fn mark_rank(m: &Mark) -> Option<u8> {
1574 Some(match m.mark_type.as_str() {
1575 "link" => {
1576 m.attrs
1578 .as_ref()?
1579 .iter()
1580 .find_map(|(k, v)| (k == "href").then_some(v))?
1581 .as_str()?;
1582 0
1583 }
1584 "underline" => 1,
1585 "subscript" | "superscript" => 2,
1586 "strike" => 3,
1587 "bold" => 4,
1588 "italic" => 5,
1589 "code" => CODE_RANK,
1590 _ => return None,
1591 })
1592}
1593
1594fn walk_text(n: &Node, f: &mut impl FnMut(&str)) {
1596 if let Some(t) = &n.text {
1597 f(t);
1598 }
1599 for c in n.children() {
1600 walk_text(c, f);
1601 }
1602}
1603
1604fn approx_size(n: &Node) -> usize {
1605 fn go(n: &Node) -> usize {
1606 8 + n.text.as_ref().map_or(0, String::len) + n.children().iter().map(go).sum::<usize>()
1607 }
1608 let g = go(n);
1609 g + g / 8 + 16
1610}
1611
1612const MID_LINE: [bool; 256] = {
1614 let mut t = [false; 256];
1615 let s = b"\\*`[]~_<&|\n\r";
1616 let mut i = 0;
1617 while i < s.len() {
1618 t[s[i] as usize] = true;
1619 i += 1;
1620 }
1621 t
1622};
1623
1624fn starts_tag(x: u8) -> bool {
1626 x.is_ascii_alphabetic() || matches!(x, b'/' | b'!' | b'?')
1627}
1628
1629fn entity_like(rest: &[u8]) -> bool {
1631 for (n, &x) in rest.iter().take(33).enumerate() {
1632 if x == b';' {
1633 return n > 0;
1634 }
1635 if !(x.is_ascii_alphanumeric() || (n == 0 && x == b'#')) {
1636 return false;
1637 }
1638 }
1639 false
1640}
1641
1642fn write_dest(out: &mut String, href: &str) {
1643 let mut depth = 0i32;
1644 let mut balanced = true;
1645 for b in href.bytes() {
1646 match b {
1647 b'(' => depth += 1,
1648 b')' => {
1649 depth -= 1;
1650 balanced &= depth >= 0;
1651 }
1652 _ => {}
1653 }
1654 }
1655 let angle = href.is_empty()
1657 || href.starts_with('<')
1658 || depth != 0
1659 || !balanced
1660 || href.bytes().any(|b| b <= b' ' || b == 0x7f);
1661 if angle {
1662 out.push('<');
1663 for c in href.chars() {
1664 match c {
1665 '<' | '>' | '\\' => {
1666 out.push('\\');
1667 out.push(c);
1668 }
1669 '\n' => out.push_str("%0A"),
1670 '\r' => out.push_str("%0D"),
1671 _ => out.push(c),
1672 }
1673 }
1674 out.push('>');
1675 } else {
1676 for c in href.chars() {
1677 if c == '\\' {
1678 out.push('\\');
1679 }
1680 out.push(c);
1681 }
1682 }
1683}
1684
1685fn write_title(out: &mut String, t: &str) {
1686 if t.is_empty() {
1687 return;
1688 }
1689 out.push_str(" \"");
1690 for c in t.chars() {
1691 match c {
1692 '"' | '\\' => {
1693 out.push('\\');
1694 out.push(c);
1695 }
1696 '\n' | '\r' => out.push(' '),
1697 _ => out.push(c),
1698 }
1699 }
1700 out.push('"');
1701}