1use crate::{
15 doc::{Align, Block, BlockKind, Doc, Mark, Part, Run, Text},
16 marks::Marks,
17 select::Cursor,
18};
19
20const INDENT: &str = " ";
24
25pub fn serialize(doc: &Doc) -> String {
26 serialize_with(doc, &Marks::default())
27}
28
29impl From<&Doc> for String {
30 fn from(doc: &Doc) -> Self {
31 serialize(doc)
32 }
33}
34
35pub fn serialize_with(doc: &Doc, marks: &Marks) -> String {
37 let layout = Layout::of(&doc.blocks);
38 write_range(&doc.blocks, &layout, 0..doc.blocks.len(), marks)
39}
40
41#[derive(Clone, Debug, PartialEq, Eq)]
49pub(crate) struct Layout {
50 pub(crate) at: Vec<Option<Place>>,
52}
53
54#[derive(Clone, Copy, Debug, PartialEq, Eq)]
55pub(crate) struct Place {
56 pub(crate) indent: u8,
57 pub(crate) run: Option<Run>,
58}
59
60impl Layout {
61 pub(crate) fn of(blocks: &[Block]) -> Self {
62 let mut at = Vec::with_capacity(blocks.len());
63 let mut previous: Option<u8> = None;
64 let mut runs = crate::doc::Runs::default();
65 for block in blocks {
66 if block.blank() {
67 at.push(None);
68 continue;
69 }
70 let indent = previous.map_or(0, |previous| block.indent.min(previous + 1));
71 previous = Some(indent);
72 at.push(Some(Place {
73 indent,
74 run: runs.next(block.quote, indent),
75 }));
76 }
77 Self { at }
78 }
79
80 pub(crate) fn respliced(
85 &self,
86 blocks: &[Block],
87 removed: std::ops::Range<usize>,
88 added: usize,
89 ) -> Self {
90 let start = (0..removed.start.min(self.at.len()))
91 .rev()
92 .find(|&ix| self.top(ix))
93 .unwrap_or(0);
94 let delta = added as isize - removed.len() as isize;
95 let changed_end = removed.start + added;
96 let mut at = self.at[..start].to_vec();
97 let mut previous: Option<u8> = None;
98 let mut runs = crate::doc::Runs::default();
99 for (ix, block) in blocks.iter().enumerate().skip(start) {
100 let place = (!block.blank()).then(|| {
101 let indent = previous.map_or(0, |previous| block.indent.min(previous + 1));
102 previous = Some(indent);
103 Place {
104 indent,
105 run: runs.next(block.quote, indent),
106 }
107 });
108 let was = (ix as isize - delta) as usize;
109 let top = place
110 .is_some_and(|place| place.indent == 0 && place.run.is_none_or(|run| run.first));
111 if ix >= changed_end && top && self.at.get(was) == Some(&place) {
112 at.extend_from_slice(&self.at[was..]);
113 return Self { at };
114 }
115 at.push(place);
116 }
117 Self { at }
118 }
119
120 pub(crate) fn top(&self, ix: usize) -> bool {
124 self.at[ix].is_some_and(|place| place.indent == 0 && place.run.is_none_or(|run| run.first))
125 }
126}
127
128pub(crate) fn write_range(
135 blocks: &[Block],
136 layout: &Layout,
137 range: std::ops::Range<usize>,
138 marks: &Marks,
139) -> String {
140 let mut out = String::new();
141 for ix in range {
142 write_one(&mut out, blocks, layout, ix, marks);
143 }
144 out
145}
146
147pub(crate) fn write_one(
149 out: &mut String,
150 blocks: &[Block],
151 layout: &Layout,
152 ix: usize,
153 marks: &Marks,
154) {
155 write_one_as(out, &blocks[ix], blocks, layout, ix, marks);
156}
157
158pub(crate) fn write_one_as(
161 out: &mut String,
162 block: &Block,
163 blocks: &[Block],
164 layout: &Layout,
165 ix: usize,
166 marks: &Marks,
167) {
168 let Some(place) = layout.at[ix] else { return };
169 write_text(out, block, place, marks);
170 write_gap_after(out, blocks, layout, ix);
171}
172
173pub(crate) fn write_gap_after(out: &mut String, blocks: &[Block], layout: &Layout, ix: usize) {
175 let Some(place) = layout.at[ix] else { return };
176 let next = (ix + 1..blocks.len()).find_map(|next| layout.at[next].map(|at| (next, at)));
177 match next {
178 Some((next, at)) => write_gap(out, &blocks[ix].kind, place, &blocks[next].kind, at),
179 None => {
180 if matches!(&blocks[ix].kind, BlockKind::Task { text, .. } if text.is_empty()) {
185 out.push(' ');
186 }
187 }
188 }
189}
190
191fn quote_prefix(place: Place) -> (String, u8) {
194 match place.run {
195 Some(run) => (
196 format!("{}> ", INDENT.repeat(run.level as usize)),
197 place.indent - run.level,
198 ),
199 None => (String::new(), place.indent),
200 }
201}
202
203fn write_text(out: &mut String, block: &Block, place: Place, marks: &Marks) {
204 let (prefix, inner) = quote_prefix(place);
205 let mut written = String::new();
206 write_block(&mut written, &block.kind, inner, marks);
207 if let Some(alert) = block.quote.and_then(|quote| quote.alert)
208 && place.run.is_some_and(|run| run.first)
209 {
210 let body = std::mem::take(&mut written);
212 written.push_str(alert.marker());
213 if !body.is_empty() {
214 written.push('\n');
215 written.push_str(&body);
216 }
217 }
218 for (ix, line) in written.split('\n').enumerate() {
219 if ix > 0 {
220 out.push('\n');
221 }
222 if line.is_empty() {
223 out.push_str(prefix.trim_end());
224 } else {
225 out.push_str(&prefix);
226 out.push_str(line);
227 }
228 }
229}
230
231fn write_gap(
233 out: &mut String,
234 previous: &BlockKind,
235 before: Place,
236 next: &BlockKind,
237 after: Place,
238) {
239 out.push('\n');
240 let edge =
244 after.run.is_some_and(|run| run.first) || (after.run.is_none() && before.run.is_some());
245 if !tight_after(previous, next, after.indent > before.indent)
246 || (edge && !is_empty_marker(previous))
247 {
248 if after.run.is_some_and(|run| !run.first) {
249 out.push_str(quote_prefix(after).0.trim_end());
250 }
251 out.push('\n');
252 }
253}
254
255fn marker_kind(kind: &BlockKind) -> Option<u8> {
258 match kind {
259 BlockKind::Bullet(_) => Some(0),
260 BlockKind::Ordered { .. } => Some(1),
261 BlockKind::Task { .. } => Some(2),
262 _ => None,
263 }
264}
265
266fn is_marker(kind: &BlockKind) -> bool {
267 marker_kind(kind).is_some()
268}
269
270fn tight_after(previous: &BlockKind, next: &BlockKind, nested: bool) -> bool {
296 if is_empty_marker(previous) {
297 return true;
298 }
299 if is_empty_marker(next) {
300 return false;
301 }
302 if nested {
303 return is_marker(previous)
304 && is_marker(next)
305 && !matches!(next, BlockKind::Ordered { number, .. } if *number != 1);
306 }
307 marker_kind(previous).is_some() && marker_kind(previous) == marker_kind(next)
308}
309
310fn is_empty_marker(kind: &BlockKind) -> bool {
311 is_marker(kind)
312 && Block::new(kind.clone())
313 .text_at(Part::Body)
314 .is_some_and(Text::is_empty)
315}
316
317fn write_block(out: &mut String, kind: &BlockKind, indent: u8, marks: &Marks) {
318 let pad = INDENT.repeat(indent as usize);
319
320 match kind {
321 BlockKind::Paragraph(text) => write_lines(out, &pad, &pad, &inline(text, marks)),
322 BlockKind::Heading { level, text } => {
323 let hashes = "#".repeat((*level).clamp(1, 6) as usize);
324 let body = inline_in(text, marks, Lead::Heading);
325 write_lines(out, &format!("{pad}{hashes} "), &pad, &body);
326 }
327 BlockKind::Bullet(text) => {
332 let marker = if text.is_empty() { "+ " } else { "- " };
333 write_marked(out, &pad, marker, text, marks)
334 }
335 BlockKind::Ordered { number, text } => {
336 write_marked(out, &pad, &format!("{number}. "), text, marks)
337 }
338 BlockKind::Task { checked, text } => {
339 let marker = if *checked { "- [x] " } else { "- [ ] " };
340 write_marked(out, &pad, marker, text, marks);
341 }
342 BlockKind::Code {
343 language,
344 code,
345 height,
346 } => {
347 let fence = "`".repeat(fence_width(&code.text));
348 out.push_str(&pad);
349 out.push_str(&fence);
350 out.push_str(language.as_deref().unwrap_or(""));
351 if let Some(height) = height {
352 out.push(' ');
353 out.push_str(&height.to_string());
354 }
355 for line in code.text.split('\n') {
356 out.push('\n');
357 out.push_str(&pad);
358 out.push_str(line);
359 }
360 out.push('\n');
361 out.push_str(&pad);
362 out.push_str(&fence);
363 }
364 BlockKind::Image { url, alt, width } => {
365 out.push_str(&pad);
366 out.push_str(";
376 write_destination(out, url);
377 out.push(')');
378 }
379 BlockKind::Bookmark { url, form } => {
383 out.push_str(&pad);
384 match form.title() {
385 None => {
386 out.push('<');
387 out.push_str(url);
388 out.push('>');
389 }
390 Some(title) => {
391 out.push('[');
392 out.push_str(url);
393 out.push_str("](");
394 write_destination(out, url);
395 out.push_str(&format!(" \"{title}\")"));
396 }
397 }
398 }
399 BlockKind::Table {
400 align,
401 header,
402 rows,
403 } => write_table(out, &pad, align, header, rows, marks),
404 BlockKind::Rule => {
405 out.push_str(&pad);
406 out.push_str("---");
407 }
408 }
409}
410
411fn write_marked(out: &mut String, pad: &str, marker: &str, text: &Text, marks: &Marks) {
413 let opener = if text.is_empty() {
416 marker.trim_end()
417 } else {
418 marker
419 };
420 let first = format!("{pad}{opener}");
421 let rest = format!("{pad}{}", " ".repeat(marker.chars().count()));
422 write_lines(out, &first, &rest, &inline(text, marks));
423}
424
425fn write_lines(out: &mut String, first: &str, rest: &str, body: &str) {
426 for (ix, line) in body.split('\n').enumerate() {
427 if ix > 0 {
428 out.push('\n');
429 }
430 out.push_str(if ix == 0 { first } else { rest });
431 out.push_str(line);
432 }
433}
434
435fn fence_width(code: &str) -> usize {
437 let mut longest = 0;
438 let mut run = 0;
439 for c in code.chars() {
440 run = if c == '`' { run + 1 } else { 0 };
441 longest = longest.max(run);
442 }
443 (longest + 1).max(3)
444}
445
446fn write_table(
447 out: &mut String,
448 pad: &str,
449 align: &[Align],
450 header: &[Text],
451 rows: &[Vec<Text>],
452 marks: &Marks,
453) {
454 let columns = align.len().max(header.len());
455 let row_of = |cells: &[Text]| {
456 let mut line = String::from("|");
457 for ix in 0..columns {
458 line.push(' ');
459 if let Some(cell) = cells.get(ix) {
460 line.push_str(&inline(cell, marks));
462 }
463 line.push_str(" |");
464 }
465 line
466 };
467
468 out.push_str(pad);
469 out.push_str(&row_of(header));
470 out.push('\n');
471 out.push_str(pad);
472 out.push('|');
473 for ix in 0..columns {
474 out.push_str(match align.get(ix).copied().unwrap_or_default() {
475 Align::Left => " --- |",
476 Align::Center => " :-: |",
477 Align::Right => " ---: |",
478 });
479 }
480 for row in rows {
481 out.push('\n');
482 out.push_str(pad);
483 out.push_str(&row_of(row));
484 }
485}
486
487fn inline(text: &Text, marks: &Marks) -> String {
491 inline_in(text, marks, Lead::Line)
492}
493
494#[derive(Clone, Copy)]
496enum Lead {
497 Line,
500 Heading,
502}
503
504fn inline_in(text: &Text, marks: &Marks, lead: Lead) -> String {
505 let mut out = String::new();
506 let mut open: Vec<usize> = Vec::new();
507 let mut started = vec![false; text.marks.len()];
508 let mut delimiters = vec!['_'; text.marks.len()];
510 let mut cursor = 0usize;
511
512 let mut boundaries: Vec<usize> = text
513 .marks
514 .iter()
515 .flat_map(|m| [m.range.start, m.range.end])
516 .chain([0, text.text.len()])
517 .collect();
518 boundaries.sort_unstable();
519 boundaries.dedup();
520
521 for point in boundaries {
522 if point < cursor {
523 continue;
524 }
525 escape_inline(&mut out, &text.text[cursor..point], marks, lead);
526 cursor = point;
527
528 while let Some(&top) = open.last() {
529 if text.marks[top].range.end <= point {
530 close_mark(&mut out, &text.marks[top].mark, delimiters[top], marks);
531 open.pop();
532 } else {
533 break;
534 }
535 }
536
537 for (ix, span) in text.marks.iter().enumerate() {
538 if started[ix] || span.range.start != point {
539 continue;
540 }
541 started[ix] = true;
542 if span.mark == Mark::Code {
545 let body = &text.text[span.range.clone()];
546 let ticks = "`".repeat(fence_width_inline(body));
547 out.push_str(&ticks);
548 out.push_str(body);
549 out.push_str(&ticks);
550 cursor = cursor.max(span.range.end);
551 continue;
552 }
553 if let Mark::Mention { url, .. } = &span.mark
559 && crate::parse::is_shorthand(text, ix)
560 {
561 out.push('<');
562 out.push_str(url);
563 out.push('>');
564 cursor = cursor.max(span.range.end);
565 continue;
566 }
567 if let Mark::Link(url) = &span.mark
574 && text.text.get(span.range.clone()) == Some(url.as_str())
575 && crate::parse::is_url(url)
576 && text.alone(ix)
577 {
578 out.push_str(url);
579 cursor = cursor.max(span.range.end);
580 continue;
581 }
582 let italic = italic_delimiter(&out, text, &span.range);
583 delimiters[ix] = italic;
584 open_mark(&mut out, &span.mark, italic, marks);
585 if span.range.is_empty() {
588 close_mark(&mut out, &span.mark, italic, marks);
589 } else {
590 open.push(ix);
591 }
592 }
593 }
594
595 escape_inline(
596 &mut out,
597 &text.text[cursor.min(text.text.len())..],
598 marks,
599 lead,
600 );
601 while let Some(ix) = open.pop() {
602 close_mark(&mut out, &text.marks[ix].mark, delimiters[ix], marks);
603 }
604 out
605}
606
607fn fence_width_inline(body: &str) -> usize {
608 let mut longest = 0;
609 let mut run = 0;
610 for c in body.chars() {
611 run = if c == '`' { run + 1 } else { 0 };
612 longest = longest.max(run);
613 }
614 longest + 1
615}
616
617fn italic_delimiter(written: &str, text: &Text, range: &std::ops::Range<usize>) -> char {
630 let intraword = written
631 .chars()
632 .next_back()
633 .is_some_and(char::is_alphanumeric)
634 || text.text[range.end..]
635 .chars()
636 .next()
637 .is_some_and(char::is_alphanumeric);
638 if intraword { '*' } else { '_' }
639}
640
641fn open_mark(out: &mut String, mark: &Mark, italic: char, marks: &Marks) {
642 match mark {
643 Mark::Bold => out.push_str("**"),
644 Mark::Italic => out.push(italic),
645 Mark::Strike => out.push_str("~~"),
646 Mark::Link(_) | Mark::Mention { .. } => out.push('['),
647 Mark::Image(_) => out.push_str(";
662 write_destination(out, url);
663 out.push(')');
664 }
665 Mark::Mention { url, form } => {
668 out.push_str("](");
669 write_destination(out, url);
670 out.push_str(" \"");
671 out.push_str(&form.title().unwrap_or_else(|| "chip".to_owned()));
672 out.push_str("\")");
673 }
674 Mark::Custom(name) => out.push_str(marks.delimiter(name).unwrap_or("")),
675 Mark::Code => {}
676 }
677}
678
679fn write_destination(out: &mut String, url: &str) {
685 if bare_destination(url) {
686 return out.push_str(url);
687 }
688 out.push('<');
689 for c in url.chars() {
690 match c {
691 '<' | '>' | '\\' => {
692 out.push('\\');
693 out.push(c);
694 }
695 c if c.is_ascii_control() => out.push_str(&format!("%{:02X}", c as u8)),
699 c => out.push(c),
700 }
701 }
702 out.push('>');
703}
704
705fn bare_destination(url: &str) -> bool {
709 if url.starts_with('<') {
710 return false;
711 }
712 let mut depth = 0i32;
713 for c in url.chars() {
714 match c {
715 '(' => depth += 1,
716 ')' if depth == 0 => return false,
717 ')' => depth -= 1,
718 '\\' => return false,
719 c if c.is_whitespace() || c.is_ascii_control() => return false,
720 _ => {}
721 }
722 }
723 depth == 0
724}
725
726fn escape_inline(out: &mut String, s: &str, marks: &Marks, lead: Lead) {
731 let mut line_start = out.is_empty() || out.ends_with('\n');
732 let mut lead = if out.is_empty() { lead } else { Lead::Line };
733 for (ix, line) in s.split('\n').enumerate() {
734 if ix > 0 {
735 out.push('\n');
736 line_start = true;
737 lead = Lead::Line;
738 }
739 let body = if line_start {
740 escape_block_marker(out, line, lead)
741 } else {
742 line
743 };
744 escape_span(out, body, marks);
745 line_start = false;
746 }
747}
748
749fn escape_block_marker<'a>(out: &mut String, line: &'a str, lead: Lead) -> &'a str {
753 let placed = line.len()
756 - line
757 .trim_start_matches(|c| sentinel_index(c).is_some())
758 .len();
759 if placed > 0 {
760 out.push_str(&line[..placed]);
761 return escape_block_marker(out, &line[placed..], lead);
762 }
763 let after_space = |rest: &str| rest.starts_with([' ', '\t']) || rest.is_empty();
764
765 let hashes = line.len() - line.trim_start_matches('#').len();
766 if hashes > 0 && after_space(&line[hashes..]) {
767 out.push('\\');
768 out.push_str(&line[..hashes]);
769 return &line[hashes..];
770 }
771 if let Lead::Heading = lead {
772 return line;
773 }
774
775 if let Some(rest) = line.strip_prefix('>') {
776 out.push_str("\\>");
777 return rest;
778 }
779
780 if (line.starts_with('-') || line.starts_with('+')) && after_space(&line[1..]) {
783 out.push('\\');
784 out.push_str(&line[..1]);
785 return &line[1..];
786 }
787
788 let digits = line.len() - line.trim_start_matches(|c: char| c.is_ascii_digit()).len();
789 if digits > 0 {
790 let after = &line[digits..];
791 if (after.starts_with('.') || after.starts_with(')')) && after_space(&after[1..]) {
792 out.push_str(&line[..digits]);
793 out.push('\\');
794 out.push_str(&after[..1]);
795 return &after[1..];
796 }
797 }
798
799 let trimmed = line.trim_end();
801 let mut bare = trimmed
802 .chars()
803 .filter(|c| sentinel_index(*c).is_none())
804 .peekable();
805 if bare.peek().is_some() && bare.all(|c| c == '=' || c == '-') {
806 out.push('\\');
807 out.push_str(&line[..1]);
808 return &line[1..];
809 }
810
811 line
812}
813
814fn escape_span(out: &mut String, s: &str, marks: &Marks) {
816 let mut skip = 0usize;
817 for (ix, c) in s.char_indices() {
818 if ix < skip {
819 continue;
820 }
821 let rest = &s[ix + c.len_utf8()..];
822 if let Some(entry) = marks
827 .sorted()
828 .into_iter()
829 .find(|entry| s[ix..].starts_with(entry.delimiter.as_ref()))
830 {
831 for c in entry.delimiter.chars() {
832 out.push('\\');
833 out.push(c);
834 }
835 skip = ix + entry.delimiter.len();
836 continue;
837 }
838 match c {
839 '\\' | '*' | '`' | '[' | ']' | '~' | '|' => {
845 out.push('\\');
846 out.push(c);
847 }
848 '_' => {
851 let before = s[..ix].chars().next_back();
852 let inside_word = before.is_some_and(char::is_alphanumeric)
853 && rest.chars().next().is_some_and(char::is_alphanumeric);
854 if !inside_word {
855 out.push('\\');
856 }
857 out.push('_');
858 }
859 '<' if rest
861 .chars()
862 .next()
863 .is_some_and(|c| c.is_alphanumeric() || matches!(c, '/' | '!' | '?')) =>
864 {
865 out.push_str("\\<")
866 }
867 '&' if rest
868 .chars()
869 .next()
870 .is_some_and(|c| c.is_alphanumeric() || c == '#') =>
871 {
872 out.push_str("\\&")
873 }
874 _ => out.push(c),
875 }
876 }
877}
878
879const SENTINELS: std::ops::RangeInclusive<u32> = 0xF0000..=0xFFFFD;
882
883pub(crate) fn sentinel(ix: usize) -> Option<char> {
885 let code = SENTINELS.start().checked_add(u32::try_from(ix).ok()?)?;
886 SENTINELS
887 .contains(&code)
888 .then(|| char::from_u32(code))
889 .flatten()
890}
891
892pub(crate) fn sentinel_index(c: char) -> Option<usize> {
894 SENTINELS
895 .contains(&(c as u32))
896 .then(|| (c as u32 - SENTINELS.start()) as usize)
897}
898
899fn carries_sentinel(doc: &Doc) -> bool {
901 doc.blocks.iter().any(|block| {
902 block.parts().into_iter().any(|part| {
903 block
904 .text_at(part)
905 .is_some_and(|text| text.text.chars().any(|c| sentinel_index(c).is_some()))
906 })
907 })
908}
909
910pub fn serialize_at(doc: &Doc, at: Cursor, marks: &Marks) -> (String, usize) {
919 let (source, offsets) = serialize_at_many(doc, &[at], marks);
920 let end = source.len();
921 (source, offsets[0].unwrap_or(end))
922}
923
924pub fn serialize_at_many(doc: &Doc, at: &[Cursor], marks: &Marks) -> (String, Vec<Option<usize>>) {
927 let mut doc = doc.clone();
928 let mut placed = vec![false; at.len()];
929 if !carries_sentinel(&doc) {
930 let mut order: Vec<usize> = (0..at.len()).collect();
933 order.sort_by_key(|&ix| std::cmp::Reverse(at[ix]));
934 for ix in order {
935 let Some(mark) = sentinel(ix) else { continue };
936 let Some(text) = doc
937 .blocks
938 .get_mut(at[ix].block)
939 .and_then(|block| block.text_at_mut(at[ix].part))
940 else {
941 continue;
942 };
943 let offset = at[ix].offset.min(text.text.len());
944 if text.text.is_char_boundary(offset) {
945 text.insert(offset, mark.encode_utf8(&mut [0; 4]));
946 placed[ix] = true;
947 }
948 }
949 }
950 doc.normalize_with(marks);
954 let written = serialize_with(&doc, marks);
955 if !placed.contains(&true) {
956 return (written, vec![None; at.len()]);
957 }
958 let mut source = String::with_capacity(written.len());
959 let mut offsets = vec![None; at.len()];
960 for c in written.chars() {
961 match sentinel_index(c) {
962 Some(ix) => {
963 if placed.get(ix) == Some(&true) {
964 offsets[ix] = Some(source.len());
965 }
966 }
967 None => source.push(c),
968 }
969 }
970 (source, offsets)
971}