1use crate::{
15 doc::{Align, Block, BlockKind, Doc, Mark, Part, Text, quote_runs},
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 mut out = String::new();
38 let mut indents = Vec::with_capacity(doc.blocks.len());
39 for block in &doc.blocks {
40 let indent = indents
41 .last()
42 .map_or(0, |prev: &u8| block.indent.min(prev + 1));
43 indents.push(indent);
44 }
45 let runs = quote_runs(doc.blocks.iter().zip(&indents).map(|(b, i)| (b.quote, *i)));
46 let mut previous: Option<(&BlockKind, u8, bool)> = None;
47
48 for ((block, &indent), run) in doc.blocks.iter().zip(&indents).zip(runs) {
49 let (prefix, inner) = match run {
52 Some(run) => (
53 format!("{}> ", INDENT.repeat(run.level as usize)),
54 indent - run.level,
55 ),
56 None => (String::new(), indent),
57 };
58 let within = run.is_some_and(|run| !run.first);
59
60 if let Some((prev_kind, prev_indent, prev_quoted)) = previous {
61 out.push('\n');
62 let edge = run.is_some_and(|run| run.first) || (run.is_none() && prev_quoted);
66 if !tight_after(prev_kind, &block.kind, indent > prev_indent)
67 || (edge && !is_empty_marker(prev_kind))
68 {
69 if within {
70 out.push_str(prefix.trim_end());
71 }
72 out.push('\n');
73 }
74 }
75
76 let mut written = String::new();
77 write_block(&mut written, &block.kind, inner, marks);
78 if let Some(alert) = block.quote.and_then(|quote| quote.alert)
79 && run.is_some_and(|run| run.first)
80 {
81 let body = std::mem::take(&mut written);
83 written.push_str(alert.marker());
84 if !body.is_empty() {
85 written.push('\n');
86 written.push_str(&body);
87 }
88 }
89 for (ix, line) in written.split('\n').enumerate() {
90 if ix > 0 {
91 out.push('\n');
92 }
93 if line.is_empty() {
94 out.push_str(prefix.trim_end());
95 } else {
96 out.push_str(&prefix);
97 out.push_str(line);
98 }
99 }
100 previous = Some((&block.kind, indent, run.is_some()));
101 }
102
103 if doc
107 .blocks
108 .last()
109 .is_some_and(|block| matches!(&block.kind, BlockKind::Task { text, .. } if text.is_empty()))
110 {
111 out.push(' ');
112 }
113
114 out
115}
116
117fn marker_kind(kind: &BlockKind) -> Option<u8> {
120 match kind {
121 BlockKind::Bullet(_) => Some(0),
122 BlockKind::Ordered { .. } => Some(1),
123 BlockKind::Task { .. } => Some(2),
124 _ => None,
125 }
126}
127
128fn is_marker(kind: &BlockKind) -> bool {
129 marker_kind(kind).is_some()
130}
131
132fn tight_after(previous: &BlockKind, next: &BlockKind, nested: bool) -> bool {
158 if is_empty_marker(previous) {
159 return true;
160 }
161 if is_empty_marker(next) {
162 return false;
163 }
164 if nested {
165 return is_marker(previous)
166 && is_marker(next)
167 && !matches!(next, BlockKind::Ordered { number, .. } if *number != 1);
168 }
169 marker_kind(previous).is_some() && marker_kind(previous) == marker_kind(next)
170}
171
172fn is_empty_marker(kind: &BlockKind) -> bool {
173 is_marker(kind)
174 && Block::new(kind.clone())
175 .text_at(Part::Body)
176 .is_some_and(Text::is_empty)
177}
178
179fn write_block(out: &mut String, kind: &BlockKind, indent: u8, marks: &Marks) {
180 let pad = INDENT.repeat(indent as usize);
181
182 match kind {
183 BlockKind::Paragraph(text) => write_lines(out, &pad, &pad, &inline(text, marks)),
184 BlockKind::Heading { level, text } => {
185 let hashes = "#".repeat((*level).clamp(1, 6) as usize);
186 let body = inline_in(text, marks, Lead::Heading);
187 write_lines(out, &format!("{pad}{hashes} "), &pad, &body);
188 }
189 BlockKind::Bullet(text) => {
194 let marker = if text.is_empty() { "+ " } else { "- " };
195 write_marked(out, &pad, marker, text, marks)
196 }
197 BlockKind::Ordered { number, text } => {
198 write_marked(out, &pad, &format!("{number}. "), text, marks)
199 }
200 BlockKind::Task { checked, text } => {
201 let marker = if *checked { "- [x] " } else { "- [ ] " };
202 write_marked(out, &pad, marker, text, marks);
203 }
204 BlockKind::Code {
205 language,
206 code,
207 height,
208 } => {
209 let fence = "`".repeat(fence_width(&code.text));
210 out.push_str(&pad);
211 out.push_str(&fence);
212 out.push_str(language.as_deref().unwrap_or(""));
213 if let Some(height) = height {
214 out.push(' ');
215 out.push_str(&height.to_string());
216 }
217 for line in code.text.split('\n') {
218 out.push('\n');
219 out.push_str(&pad);
220 out.push_str(line);
221 }
222 out.push('\n');
223 out.push_str(&pad);
224 out.push_str(&fence);
225 }
226 BlockKind::Image { url, alt, width } => {
227 out.push_str(&pad);
228 out.push_str(";
238 write_destination(out, url);
239 out.push(')');
240 }
241 BlockKind::Bookmark { url, form } => {
245 out.push_str(&pad);
246 match form.title() {
247 None => {
248 out.push('<');
249 out.push_str(url);
250 out.push('>');
251 }
252 Some(title) => {
253 out.push('[');
254 out.push_str(url);
255 out.push_str("](");
256 write_destination(out, url);
257 out.push_str(&format!(" \"{title}\")"));
258 }
259 }
260 }
261 BlockKind::Table {
262 align,
263 header,
264 rows,
265 } => write_table(out, &pad, align, header, rows, marks),
266 BlockKind::Rule => {
267 out.push_str(&pad);
268 out.push_str("---");
269 }
270 }
271}
272
273fn write_marked(out: &mut String, pad: &str, marker: &str, text: &Text, marks: &Marks) {
275 let opener = if text.is_empty() {
278 marker.trim_end()
279 } else {
280 marker
281 };
282 let first = format!("{pad}{opener}");
283 let rest = format!("{pad}{}", " ".repeat(marker.chars().count()));
284 write_lines(out, &first, &rest, &inline(text, marks));
285}
286
287fn write_lines(out: &mut String, first: &str, rest: &str, body: &str) {
288 for (ix, line) in body.split('\n').enumerate() {
289 if ix > 0 {
290 out.push('\n');
291 }
292 out.push_str(if ix == 0 { first } else { rest });
293 out.push_str(line);
294 }
295}
296
297fn fence_width(code: &str) -> usize {
299 let mut longest = 0;
300 let mut run = 0;
301 for c in code.chars() {
302 run = if c == '`' { run + 1 } else { 0 };
303 longest = longest.max(run);
304 }
305 (longest + 1).max(3)
306}
307
308fn write_table(
309 out: &mut String,
310 pad: &str,
311 align: &[Align],
312 header: &[Text],
313 rows: &[Vec<Text>],
314 marks: &Marks,
315) {
316 let columns = align.len().max(header.len());
317 let row_of = |cells: &[Text]| {
318 let mut line = String::from("|");
319 for ix in 0..columns {
320 line.push(' ');
321 if let Some(cell) = cells.get(ix) {
322 line.push_str(&inline(cell, marks));
324 }
325 line.push_str(" |");
326 }
327 line
328 };
329
330 out.push_str(pad);
331 out.push_str(&row_of(header));
332 out.push('\n');
333 out.push_str(pad);
334 out.push('|');
335 for ix in 0..columns {
336 out.push_str(match align.get(ix).copied().unwrap_or_default() {
337 Align::Left => " --- |",
338 Align::Center => " :-: |",
339 Align::Right => " ---: |",
340 });
341 }
342 for row in rows {
343 out.push('\n');
344 out.push_str(pad);
345 out.push_str(&row_of(row));
346 }
347}
348
349fn inline(text: &Text, marks: &Marks) -> String {
353 inline_in(text, marks, Lead::Line)
354}
355
356#[derive(Clone, Copy)]
358enum Lead {
359 Line,
362 Heading,
364}
365
366fn inline_in(text: &Text, marks: &Marks, lead: Lead) -> String {
367 let mut out = String::new();
368 let mut open: Vec<usize> = Vec::new();
369 let mut started = vec![false; text.marks.len()];
370 let mut delimiters = vec!['_'; text.marks.len()];
372 let mut cursor = 0usize;
373
374 let mut boundaries: Vec<usize> = text
375 .marks
376 .iter()
377 .flat_map(|m| [m.range.start, m.range.end])
378 .chain([0, text.text.len()])
379 .collect();
380 boundaries.sort_unstable();
381 boundaries.dedup();
382
383 for point in boundaries {
384 if point < cursor {
385 continue;
386 }
387 escape_inline(&mut out, &text.text[cursor..point], marks, lead);
388 cursor = point;
389
390 while let Some(&top) = open.last() {
391 if text.marks[top].range.end <= point {
392 close_mark(&mut out, &text.marks[top].mark, delimiters[top], marks);
393 open.pop();
394 } else {
395 break;
396 }
397 }
398
399 for (ix, span) in text.marks.iter().enumerate() {
400 if started[ix] || span.range.start != point {
401 continue;
402 }
403 started[ix] = true;
404 if span.mark == Mark::Code {
407 let body = &text.text[span.range.clone()];
408 let ticks = "`".repeat(fence_width_inline(body));
409 out.push_str(&ticks);
410 out.push_str(body);
411 out.push_str(&ticks);
412 cursor = cursor.max(span.range.end);
413 continue;
414 }
415 if let Mark::Mention { url, .. } = &span.mark
421 && crate::parse::is_shorthand(text, ix)
422 {
423 out.push('<');
424 out.push_str(url);
425 out.push('>');
426 cursor = cursor.max(span.range.end);
427 continue;
428 }
429 if let Mark::Link(url) = &span.mark
436 && text.text.get(span.range.clone()) == Some(url.as_str())
437 && crate::parse::is_url(url)
438 && text.alone(ix)
439 {
440 out.push_str(url);
441 cursor = cursor.max(span.range.end);
442 continue;
443 }
444 let italic = italic_delimiter(&out, text, &span.range);
445 delimiters[ix] = italic;
446 open_mark(&mut out, &span.mark, italic, marks);
447 if span.range.is_empty() {
450 close_mark(&mut out, &span.mark, italic, marks);
451 } else {
452 open.push(ix);
453 }
454 }
455 }
456
457 escape_inline(
458 &mut out,
459 &text.text[cursor.min(text.text.len())..],
460 marks,
461 lead,
462 );
463 while let Some(ix) = open.pop() {
464 close_mark(&mut out, &text.marks[ix].mark, delimiters[ix], marks);
465 }
466 out
467}
468
469fn fence_width_inline(body: &str) -> usize {
470 let mut longest = 0;
471 let mut run = 0;
472 for c in body.chars() {
473 run = if c == '`' { run + 1 } else { 0 };
474 longest = longest.max(run);
475 }
476 longest + 1
477}
478
479fn italic_delimiter(written: &str, text: &Text, range: &std::ops::Range<usize>) -> char {
492 let intraword = written
493 .chars()
494 .next_back()
495 .is_some_and(char::is_alphanumeric)
496 || text.text[range.end..]
497 .chars()
498 .next()
499 .is_some_and(char::is_alphanumeric);
500 if intraword { '*' } else { '_' }
501}
502
503fn open_mark(out: &mut String, mark: &Mark, italic: char, marks: &Marks) {
504 match mark {
505 Mark::Bold => out.push_str("**"),
506 Mark::Italic => out.push(italic),
507 Mark::Strike => out.push_str("~~"),
508 Mark::Link(_) | Mark::Mention { .. } => out.push('['),
509 Mark::Image(_) => out.push_str(";
524 write_destination(out, url);
525 out.push(')');
526 }
527 Mark::Mention { url, form } => {
530 out.push_str("](");
531 write_destination(out, url);
532 out.push_str(" \"");
533 out.push_str(&form.title().unwrap_or_else(|| "chip".to_owned()));
534 out.push_str("\")");
535 }
536 Mark::Custom(name) => out.push_str(marks.delimiter(name).unwrap_or("")),
537 Mark::Code => {}
538 }
539}
540
541fn write_destination(out: &mut String, url: &str) {
547 if bare_destination(url) {
548 return out.push_str(url);
549 }
550 out.push('<');
551 for c in url.chars() {
552 match c {
553 '<' | '>' | '\\' => {
554 out.push('\\');
555 out.push(c);
556 }
557 c if c.is_ascii_control() => out.push_str(&format!("%{:02X}", c as u8)),
561 c => out.push(c),
562 }
563 }
564 out.push('>');
565}
566
567fn bare_destination(url: &str) -> bool {
571 if url.starts_with('<') {
572 return false;
573 }
574 let mut depth = 0i32;
575 for c in url.chars() {
576 match c {
577 '(' => depth += 1,
578 ')' if depth == 0 => return false,
579 ')' => depth -= 1,
580 '\\' => return false,
581 c if c.is_whitespace() || c.is_ascii_control() => return false,
582 _ => {}
583 }
584 }
585 depth == 0
586}
587
588fn escape_inline(out: &mut String, s: &str, marks: &Marks, lead: Lead) {
593 let mut line_start = out.is_empty() || out.ends_with('\n');
594 let mut lead = if out.is_empty() { lead } else { Lead::Line };
595 for (ix, line) in s.split('\n').enumerate() {
596 if ix > 0 {
597 out.push('\n');
598 line_start = true;
599 lead = Lead::Line;
600 }
601 let body = if line_start {
602 escape_block_marker(out, line, lead)
603 } else {
604 line
605 };
606 escape_span(out, body, marks);
607 line_start = false;
608 }
609}
610
611fn escape_block_marker<'a>(out: &mut String, line: &'a str, lead: Lead) -> &'a str {
615 let after_space = |rest: &str| rest.starts_with([' ', '\t']) || rest.is_empty();
616
617 let hashes = line.len() - line.trim_start_matches('#').len();
618 if hashes > 0 && after_space(&line[hashes..]) {
619 out.push('\\');
620 out.push_str(&line[..hashes]);
621 return &line[hashes..];
622 }
623 if let Lead::Heading = lead {
624 return line;
625 }
626
627 if let Some(rest) = line.strip_prefix('>') {
628 out.push_str("\\>");
629 return rest;
630 }
631
632 if (line.starts_with('-') || line.starts_with('+')) && after_space(&line[1..]) {
635 out.push('\\');
636 out.push_str(&line[..1]);
637 return &line[1..];
638 }
639
640 let digits = line.len() - line.trim_start_matches(|c: char| c.is_ascii_digit()).len();
641 if digits > 0 {
642 let after = &line[digits..];
643 if (after.starts_with('.') || after.starts_with(')')) && after_space(&after[1..]) {
644 out.push_str(&line[..digits]);
645 out.push('\\');
646 out.push_str(&after[..1]);
647 return &after[1..];
648 }
649 }
650
651 let trimmed = line.trim_end();
653 if !trimmed.is_empty() && trimmed.chars().all(|c| c == '=' || c == '-') {
654 out.push('\\');
655 out.push_str(&line[..1]);
656 return &line[1..];
657 }
658
659 line
660}
661
662fn escape_span(out: &mut String, s: &str, marks: &Marks) {
664 let mut skip = 0usize;
665 for (ix, c) in s.char_indices() {
666 if ix < skip {
667 continue;
668 }
669 let rest = &s[ix + c.len_utf8()..];
670 if let Some(entry) = marks
675 .sorted()
676 .into_iter()
677 .find(|entry| s[ix..].starts_with(entry.delimiter.as_ref()))
678 {
679 for c in entry.delimiter.chars() {
680 out.push('\\');
681 out.push(c);
682 }
683 skip = ix + entry.delimiter.len();
684 continue;
685 }
686 match c {
687 '\\' | '*' | '`' | '[' | ']' | '~' | '|' => {
693 out.push('\\');
694 out.push(c);
695 }
696 '_' => {
699 let before = s[..ix].chars().next_back();
700 let inside_word = before.is_some_and(char::is_alphanumeric)
701 && rest.chars().next().is_some_and(char::is_alphanumeric);
702 if !inside_word {
703 out.push('\\');
704 }
705 out.push('_');
706 }
707 '<' if rest
709 .chars()
710 .next()
711 .is_some_and(|c| c.is_alphanumeric() || matches!(c, '/' | '!' | '?')) =>
712 {
713 out.push_str("\\<")
714 }
715 '&' if rest
716 .chars()
717 .next()
718 .is_some_and(|c| c.is_alphanumeric() || c == '#') =>
719 {
720 out.push_str("\\&")
721 }
722 _ => out.push(c),
723 }
724 }
725}
726
727const SENTINELS: std::ops::RangeInclusive<u32> = 0xF0000..=0xFFFFD;
730
731pub(crate) fn sentinel(ix: usize) -> Option<char> {
733 let code = SENTINELS.start().checked_add(u32::try_from(ix).ok()?)?;
734 SENTINELS
735 .contains(&code)
736 .then(|| char::from_u32(code))
737 .flatten()
738}
739
740pub(crate) fn sentinel_index(c: char) -> Option<usize> {
742 SENTINELS
743 .contains(&(c as u32))
744 .then(|| (c as u32 - SENTINELS.start()) as usize)
745}
746
747fn carries_sentinel(doc: &Doc) -> bool {
749 doc.blocks.iter().any(|block| {
750 block.parts().into_iter().any(|part| {
751 block
752 .text_at(part)
753 .is_some_and(|text| text.text.chars().any(|c| sentinel_index(c).is_some()))
754 })
755 })
756}
757
758pub fn serialize_at(doc: &Doc, at: Cursor, marks: &Marks) -> (String, usize) {
767 let (source, offsets) = serialize_at_many(doc, &[at], marks);
768 let end = source.len();
769 (source, offsets[0].unwrap_or(end))
770}
771
772pub fn serialize_at_many(doc: &Doc, at: &[Cursor], marks: &Marks) -> (String, Vec<Option<usize>>) {
775 let mut doc = doc.clone();
776 let mut placed = vec![false; at.len()];
777 if !carries_sentinel(&doc) {
778 let mut order: Vec<usize> = (0..at.len()).collect();
781 order.sort_by_key(|&ix| std::cmp::Reverse(at[ix]));
782 for ix in order {
783 let Some(mark) = sentinel(ix) else { continue };
784 let Some(text) = doc
785 .blocks
786 .get_mut(at[ix].block)
787 .and_then(|block| block.text_at_mut(at[ix].part))
788 else {
789 continue;
790 };
791 let offset = at[ix].offset.min(text.text.len());
792 if text.text.is_char_boundary(offset) {
793 text.insert(offset, mark.encode_utf8(&mut [0; 4]));
794 placed[ix] = true;
795 }
796 }
797 }
798 doc.normalize_with(marks);
802 let written = serialize_with(&doc, marks);
803 if !placed.contains(&true) {
804 return (written, vec![None; at.len()]);
805 }
806 let mut source = String::with_capacity(written.len());
807 let mut offsets = vec![None; at.len()];
808 for c in written.chars() {
809 match sentinel_index(c) {
810 Some(ix) => {
811 if placed.get(ix) == Some(&true) {
812 offsets[ix] = Some(source.len());
813 }
814 }
815 None => source.push(c),
816 }
817 }
818 (source, offsets)
819}