1use crate::{
15 doc::{Align, Block, BlockKind, Doc, Mark, Part, 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 mut out = String::new();
38 let mut previous: Option<(&BlockKind, u8)> = None;
39
40 for block in &doc.blocks {
41 let indent = match previous {
42 Some((_, prev)) => block.indent.min(prev + 1),
43 None => 0,
44 };
45
46 if let Some((prev_kind, prev_indent)) = previous {
47 out.push('\n');
48 if !tight_after(prev_kind, &block.kind, indent > prev_indent) {
49 out.push('\n');
50 }
51 }
52
53 write_block(&mut out, &block.kind, indent, marks);
54 previous = Some((&block.kind, indent));
55 }
56
57 if doc
61 .blocks
62 .last()
63 .is_some_and(|block| matches!(&block.kind, BlockKind::Task { text, .. } if text.is_empty()))
64 {
65 out.push(' ');
66 }
67
68 out
69}
70
71fn marker_kind(kind: &BlockKind) -> Option<u8> {
74 match kind {
75 BlockKind::Bullet(_) => Some(0),
76 BlockKind::Ordered { .. } => Some(1),
77 BlockKind::Task { .. } => Some(2),
78 _ => None,
79 }
80}
81
82fn is_marker(kind: &BlockKind) -> bool {
83 marker_kind(kind).is_some()
84}
85
86fn tight_after(previous: &BlockKind, next: &BlockKind, nested: bool) -> bool {
112 if is_empty_marker(previous) {
113 return true;
114 }
115 if is_empty_marker(next) {
116 return false;
117 }
118 if nested {
119 return is_marker(previous)
120 && is_marker(next)
121 && !matches!(next, BlockKind::Ordered { number, .. } if *number != 1);
122 }
123 marker_kind(previous).is_some() && marker_kind(previous) == marker_kind(next)
124}
125
126fn is_empty_marker(kind: &BlockKind) -> bool {
127 is_marker(kind)
128 && Block::new(kind.clone())
129 .text_at(Part::Body)
130 .is_some_and(Text::is_empty)
131}
132
133fn write_block(out: &mut String, kind: &BlockKind, indent: u8, marks: &Marks) {
134 let pad = INDENT.repeat(indent as usize);
135
136 match kind {
137 BlockKind::Paragraph(text) => write_lines(out, &pad, &pad, &inline(text, marks)),
138 BlockKind::Heading { level, text } => {
139 let hashes = "#".repeat((*level).clamp(1, 6) as usize);
140 let body = inline_in(text, marks, Lead::Heading);
141 write_lines(out, &format!("{pad}{hashes} "), &pad, &body);
142 }
143 BlockKind::Bullet(text) => {
148 let marker = if text.is_empty() { "+ " } else { "- " };
149 write_marked(out, &pad, marker, text, marks)
150 }
151 BlockKind::Ordered { number, text } => {
152 write_marked(out, &pad, &format!("{number}. "), text, marks)
153 }
154 BlockKind::Task { checked, text } => {
155 let marker = if *checked { "- [x] " } else { "- [ ] " };
156 write_marked(out, &pad, marker, text, marks);
157 }
158 BlockKind::Quote { kind, text } => {
159 let prefix = format!("{pad}> ");
160 let body = inline(text, marks);
164 if let Some(kind) = kind {
165 out.push_str(&prefix);
166 out.push_str(kind.marker());
167 if body.is_empty() {
168 return;
169 }
170 out.push('\n');
171 }
172 write_lines(out, &prefix, &prefix, &body);
173 }
174 BlockKind::Code {
175 language,
176 code,
177 height,
178 } => {
179 let fence = "`".repeat(fence_width(&code.text));
180 out.push_str(&pad);
181 out.push_str(&fence);
182 out.push_str(language.as_deref().unwrap_or(""));
183 if let Some(height) = height {
184 out.push(' ');
185 out.push_str(&height.to_string());
186 }
187 for line in code.text.split('\n') {
188 out.push('\n');
189 out.push_str(&pad);
190 out.push_str(line);
191 }
192 out.push('\n');
193 out.push_str(&pad);
194 out.push_str(&fence);
195 }
196 BlockKind::Image { url, alt, width } => {
197 out.push_str(&pad);
198 out.push_str(";
208 write_destination(out, url);
209 out.push(')');
210 }
211 BlockKind::Bookmark { url, form } => {
215 out.push_str(&pad);
216 match form.title() {
217 None => {
218 out.push('<');
219 out.push_str(url);
220 out.push('>');
221 }
222 Some(title) => {
223 out.push('[');
224 out.push_str(url);
225 out.push_str("](");
226 write_destination(out, url);
227 out.push_str(&format!(" \"{title}\")"));
228 }
229 }
230 }
231 BlockKind::Table {
232 align,
233 header,
234 rows,
235 } => write_table(out, &pad, align, header, rows, marks),
236 BlockKind::Rule => {
237 out.push_str(&pad);
238 out.push_str("---");
239 }
240 }
241}
242
243fn write_marked(out: &mut String, pad: &str, marker: &str, text: &Text, marks: &Marks) {
245 let opener = if text.is_empty() {
248 marker.trim_end()
249 } else {
250 marker
251 };
252 let first = format!("{pad}{opener}");
253 let rest = format!("{pad}{}", " ".repeat(marker.chars().count()));
254 write_lines(out, &first, &rest, &inline(text, marks));
255}
256
257fn write_lines(out: &mut String, first: &str, rest: &str, body: &str) {
258 for (ix, line) in body.split('\n').enumerate() {
259 if ix > 0 {
260 out.push('\n');
261 }
262 out.push_str(if ix == 0 { first } else { rest });
263 out.push_str(line);
264 }
265}
266
267fn fence_width(code: &str) -> usize {
269 let mut longest = 0;
270 let mut run = 0;
271 for c in code.chars() {
272 run = if c == '`' { run + 1 } else { 0 };
273 longest = longest.max(run);
274 }
275 (longest + 1).max(3)
276}
277
278fn write_table(
279 out: &mut String,
280 pad: &str,
281 align: &[Align],
282 header: &[Text],
283 rows: &[Vec<Text>],
284 marks: &Marks,
285) {
286 let columns = align.len().max(header.len());
287 let row_of = |cells: &[Text]| {
288 let mut line = String::from("|");
289 for ix in 0..columns {
290 line.push(' ');
291 if let Some(cell) = cells.get(ix) {
292 line.push_str(&inline(cell, marks));
294 }
295 line.push_str(" |");
296 }
297 line
298 };
299
300 out.push_str(pad);
301 out.push_str(&row_of(header));
302 out.push('\n');
303 out.push_str(pad);
304 out.push('|');
305 for ix in 0..columns {
306 out.push_str(match align.get(ix).copied().unwrap_or_default() {
307 Align::Left => " --- |",
308 Align::Center => " :-: |",
309 Align::Right => " ---: |",
310 });
311 }
312 for row in rows {
313 out.push('\n');
314 out.push_str(pad);
315 out.push_str(&row_of(row));
316 }
317}
318
319fn inline(text: &Text, marks: &Marks) -> String {
323 inline_in(text, marks, Lead::Line)
324}
325
326#[derive(Clone, Copy)]
328enum Lead {
329 Line,
332 Heading,
334}
335
336fn inline_in(text: &Text, marks: &Marks, lead: Lead) -> String {
337 let mut out = String::new();
338 let mut open: Vec<usize> = Vec::new();
339 let mut started = vec![false; text.marks.len()];
340 let mut delimiters = vec!['_'; text.marks.len()];
342 let mut cursor = 0usize;
343
344 let mut boundaries: Vec<usize> = text
345 .marks
346 .iter()
347 .flat_map(|m| [m.range.start, m.range.end])
348 .chain([0, text.text.len()])
349 .collect();
350 boundaries.sort_unstable();
351 boundaries.dedup();
352
353 for point in boundaries {
354 if point < cursor {
355 continue;
356 }
357 escape_inline(&mut out, &text.text[cursor..point], marks, lead);
358 cursor = point;
359
360 while let Some(&top) = open.last() {
361 if text.marks[top].range.end <= point {
362 close_mark(&mut out, &text.marks[top].mark, delimiters[top], marks);
363 open.pop();
364 } else {
365 break;
366 }
367 }
368
369 for (ix, span) in text.marks.iter().enumerate() {
370 if started[ix] || span.range.start != point {
371 continue;
372 }
373 started[ix] = true;
374 if span.mark == Mark::Code {
377 let body = &text.text[span.range.clone()];
378 let ticks = "`".repeat(fence_width_inline(body));
379 out.push_str(&ticks);
380 out.push_str(body);
381 out.push_str(&ticks);
382 cursor = cursor.max(span.range.end);
383 continue;
384 }
385 if let Mark::Mention { url, .. } = &span.mark
391 && crate::parse::is_shorthand(text, ix)
392 {
393 out.push('<');
394 out.push_str(url);
395 out.push('>');
396 cursor = cursor.max(span.range.end);
397 continue;
398 }
399 if let Mark::Link(url) = &span.mark
406 && text.text.get(span.range.clone()) == Some(url.as_str())
407 && crate::parse::is_url(url)
408 && text.alone(ix)
409 {
410 out.push_str(url);
411 cursor = cursor.max(span.range.end);
412 continue;
413 }
414 let italic = italic_delimiter(&out, text, &span.range);
415 delimiters[ix] = italic;
416 open_mark(&mut out, &span.mark, italic, marks);
417 if span.range.is_empty() {
420 close_mark(&mut out, &span.mark, italic, marks);
421 } else {
422 open.push(ix);
423 }
424 }
425 }
426
427 escape_inline(
428 &mut out,
429 &text.text[cursor.min(text.text.len())..],
430 marks,
431 lead,
432 );
433 while let Some(ix) = open.pop() {
434 close_mark(&mut out, &text.marks[ix].mark, delimiters[ix], marks);
435 }
436 out
437}
438
439fn fence_width_inline(body: &str) -> usize {
440 let mut longest = 0;
441 let mut run = 0;
442 for c in body.chars() {
443 run = if c == '`' { run + 1 } else { 0 };
444 longest = longest.max(run);
445 }
446 longest + 1
447}
448
449fn italic_delimiter(written: &str, text: &Text, range: &std::ops::Range<usize>) -> char {
462 let intraword = written
463 .chars()
464 .next_back()
465 .is_some_and(char::is_alphanumeric)
466 || text.text[range.end..]
467 .chars()
468 .next()
469 .is_some_and(char::is_alphanumeric);
470 if intraword { '*' } else { '_' }
471}
472
473fn open_mark(out: &mut String, mark: &Mark, italic: char, marks: &Marks) {
474 match mark {
475 Mark::Bold => out.push_str("**"),
476 Mark::Italic => out.push(italic),
477 Mark::Strike => out.push_str("~~"),
478 Mark::Link(_) | Mark::Mention { .. } => out.push('['),
479 Mark::Image(_) => out.push_str(";
494 write_destination(out, url);
495 out.push(')');
496 }
497 Mark::Mention { url, form } => {
500 out.push_str("](");
501 write_destination(out, url);
502 out.push_str(" \"");
503 out.push_str(&form.title().unwrap_or_else(|| "chip".to_owned()));
504 out.push_str("\")");
505 }
506 Mark::Custom(name) => out.push_str(marks.delimiter(name).unwrap_or("")),
507 Mark::Code => {}
508 }
509}
510
511fn write_destination(out: &mut String, url: &str) {
517 if bare_destination(url) {
518 return out.push_str(url);
519 }
520 out.push('<');
521 for c in url.chars() {
522 match c {
523 '<' | '>' | '\\' => {
524 out.push('\\');
525 out.push(c);
526 }
527 c if c.is_ascii_control() => out.push_str(&format!("%{:02X}", c as u8)),
531 c => out.push(c),
532 }
533 }
534 out.push('>');
535}
536
537fn bare_destination(url: &str) -> bool {
541 if url.starts_with('<') {
542 return false;
543 }
544 let mut depth = 0i32;
545 for c in url.chars() {
546 match c {
547 '(' => depth += 1,
548 ')' if depth == 0 => return false,
549 ')' => depth -= 1,
550 '\\' => return false,
551 c if c.is_whitespace() || c.is_ascii_control() => return false,
552 _ => {}
553 }
554 }
555 depth == 0
556}
557
558fn escape_inline(out: &mut String, s: &str, marks: &Marks, lead: Lead) {
563 let mut line_start = out.is_empty() || out.ends_with('\n');
564 let mut lead = if out.is_empty() { lead } else { Lead::Line };
565 for (ix, line) in s.split('\n').enumerate() {
566 if ix > 0 {
567 out.push('\n');
568 line_start = true;
569 lead = Lead::Line;
570 }
571 let body = if line_start {
572 escape_block_marker(out, line, lead)
573 } else {
574 line
575 };
576 escape_span(out, body, marks);
577 line_start = false;
578 }
579}
580
581fn escape_block_marker<'a>(out: &mut String, line: &'a str, lead: Lead) -> &'a str {
585 let after_space = |rest: &str| rest.starts_with([' ', '\t']) || rest.is_empty();
586
587 let hashes = line.len() - line.trim_start_matches('#').len();
588 if hashes > 0 && after_space(&line[hashes..]) {
589 out.push('\\');
590 out.push_str(&line[..hashes]);
591 return &line[hashes..];
592 }
593 if let Lead::Heading = lead {
594 return line;
595 }
596
597 if let Some(rest) = line.strip_prefix('>') {
598 out.push_str("\\>");
599 return rest;
600 }
601
602 if (line.starts_with('-') || line.starts_with('+')) && after_space(&line[1..]) {
605 out.push('\\');
606 out.push_str(&line[..1]);
607 return &line[1..];
608 }
609
610 let digits = line.len() - line.trim_start_matches(|c: char| c.is_ascii_digit()).len();
611 if digits > 0 {
612 let after = &line[digits..];
613 if (after.starts_with('.') || after.starts_with(')')) && after_space(&after[1..]) {
614 out.push_str(&line[..digits]);
615 out.push('\\');
616 out.push_str(&after[..1]);
617 return &after[1..];
618 }
619 }
620
621 let trimmed = line.trim_end();
623 if !trimmed.is_empty() && trimmed.chars().all(|c| c == '=' || c == '-') {
624 out.push('\\');
625 out.push_str(&line[..1]);
626 return &line[1..];
627 }
628
629 line
630}
631
632fn escape_span(out: &mut String, s: &str, marks: &Marks) {
634 let mut skip = 0usize;
635 for (ix, c) in s.char_indices() {
636 if ix < skip {
637 continue;
638 }
639 let rest = &s[ix + c.len_utf8()..];
640 if let Some(entry) = marks
645 .sorted()
646 .into_iter()
647 .find(|entry| s[ix..].starts_with(entry.delimiter.as_ref()))
648 {
649 for c in entry.delimiter.chars() {
650 out.push('\\');
651 out.push(c);
652 }
653 skip = ix + entry.delimiter.len();
654 continue;
655 }
656 match c {
657 '\\' | '*' | '`' | '[' | ']' | '~' | '|' => {
663 out.push('\\');
664 out.push(c);
665 }
666 '_' => {
669 let before = s[..ix].chars().next_back();
670 let inside_word = before.is_some_and(char::is_alphanumeric)
671 && rest.chars().next().is_some_and(char::is_alphanumeric);
672 if !inside_word {
673 out.push('\\');
674 }
675 out.push('_');
676 }
677 '<' if rest
679 .chars()
680 .next()
681 .is_some_and(|c| c.is_alphanumeric() || matches!(c, '/' | '!' | '?')) =>
682 {
683 out.push_str("\\<")
684 }
685 '&' if rest
686 .chars()
687 .next()
688 .is_some_and(|c| c.is_alphanumeric() || c == '#') =>
689 {
690 out.push_str("\\&")
691 }
692 _ => out.push(c),
693 }
694 }
695}
696
697const SENTINELS: std::ops::RangeInclusive<u32> = 0xF0000..=0xFFFFD;
700
701pub(crate) fn sentinel(ix: usize) -> Option<char> {
703 let code = SENTINELS.start().checked_add(u32::try_from(ix).ok()?)?;
704 SENTINELS
705 .contains(&code)
706 .then(|| char::from_u32(code))
707 .flatten()
708}
709
710pub(crate) fn sentinel_index(c: char) -> Option<usize> {
712 SENTINELS
713 .contains(&(c as u32))
714 .then(|| (c as u32 - SENTINELS.start()) as usize)
715}
716
717fn carries_sentinel(doc: &Doc) -> bool {
719 doc.blocks.iter().any(|block| {
720 block.parts().into_iter().any(|part| {
721 block
722 .text_at(part)
723 .is_some_and(|text| text.text.chars().any(|c| sentinel_index(c).is_some()))
724 })
725 })
726}
727
728pub fn serialize_at(doc: &Doc, at: Cursor, marks: &Marks) -> (String, usize) {
737 let (source, offsets) = serialize_at_many(doc, &[at], marks);
738 let end = source.len();
739 (source, offsets[0].unwrap_or(end))
740}
741
742pub fn serialize_at_many(doc: &Doc, at: &[Cursor], marks: &Marks) -> (String, Vec<Option<usize>>) {
745 let mut doc = doc.clone();
746 let mut placed = vec![false; at.len()];
747 if !carries_sentinel(&doc) {
748 let mut order: Vec<usize> = (0..at.len()).collect();
751 order.sort_by_key(|&ix| std::cmp::Reverse(at[ix]));
752 for ix in order {
753 let Some(mark) = sentinel(ix) else { continue };
754 let Some(text) = doc
755 .blocks
756 .get_mut(at[ix].block)
757 .and_then(|block| block.text_at_mut(at[ix].part))
758 else {
759 continue;
760 };
761 let offset = at[ix].offset.min(text.text.len());
762 if text.text.is_char_boundary(offset) {
763 text.insert(offset, mark.encode_utf8(&mut [0; 4]));
764 placed[ix] = true;
765 }
766 }
767 }
768 doc.normalize_with(marks);
772 let written = serialize_with(&doc, marks);
773 if !placed.contains(&true) {
774 return (written, vec![None; at.len()]);
775 }
776 let mut source = String::with_capacity(written.len());
777 let mut offsets = vec![None; at.len()];
778 for c in written.chars() {
779 match sentinel_index(c) {
780 Some(ix) => {
781 if placed.get(ix) == Some(&true) {
782 offsets[ix] = Some(source.len());
783 }
784 }
785 None => source.push(c),
786 }
787 }
788 (source, offsets)
789}