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 write_lines(out, &format!("{pad}{hashes} "), &pad, &inline(text, marks));
141 }
142 BlockKind::Bullet(text) => {
147 let marker = if text.is_empty() { "+ " } else { "- " };
148 write_marked(out, &pad, marker, text, marks)
149 }
150 BlockKind::Ordered { number, text } => {
151 write_marked(out, &pad, &format!("{number}. "), text, marks)
152 }
153 BlockKind::Task { checked, text } => {
154 let marker = if *checked { "- [x] " } else { "- [ ] " };
155 write_marked(out, &pad, marker, text, marks);
156 }
157 BlockKind::Quote { kind, text } => {
158 let prefix = format!("{pad}> ");
159 let body = inline(text, marks);
163 if let Some(kind) = kind {
164 out.push_str(&prefix);
165 out.push_str(kind.marker());
166 if body.is_empty() {
167 return;
168 }
169 out.push('\n');
170 }
171 write_lines(out, &prefix, &prefix, &body);
172 }
173 BlockKind::Code { language, code } => {
174 let fence = "`".repeat(fence_width(&code.text));
175 out.push_str(&pad);
176 out.push_str(&fence);
177 out.push_str(language.as_deref().unwrap_or(""));
178 for line in code.text.split('\n') {
179 out.push('\n');
180 out.push_str(&pad);
181 out.push_str(line);
182 }
183 out.push('\n');
184 out.push_str(&pad);
185 out.push_str(&fence);
186 }
187 BlockKind::Image { url, alt, width } => {
188 out.push_str(&pad);
189 out.push_str(";
199 write_destination(out, url);
200 out.push(')');
201 }
202 BlockKind::Bookmark { url, form } => {
206 out.push_str(&pad);
207 match form.title() {
208 None => {
209 out.push('<');
210 out.push_str(url);
211 out.push('>');
212 }
213 Some(title) => {
214 out.push('[');
215 out.push_str(url);
216 out.push_str("](");
217 write_destination(out, url);
218 out.push_str(&format!(" \"{title}\")"));
219 }
220 }
221 }
222 BlockKind::Table {
223 align,
224 header,
225 rows,
226 } => write_table(out, &pad, align, header, rows, marks),
227 BlockKind::Rule => {
228 out.push_str(&pad);
229 out.push_str("---");
230 }
231 }
232}
233
234fn write_marked(out: &mut String, pad: &str, marker: &str, text: &Text, marks: &Marks) {
236 let opener = if text.is_empty() {
239 marker.trim_end()
240 } else {
241 marker
242 };
243 let first = format!("{pad}{opener}");
244 let rest = format!("{pad}{}", " ".repeat(marker.chars().count()));
245 write_lines(out, &first, &rest, &inline(text, marks));
246}
247
248fn write_lines(out: &mut String, first: &str, rest: &str, body: &str) {
249 for (ix, line) in body.split('\n').enumerate() {
250 if ix > 0 {
251 out.push('\n');
252 }
253 out.push_str(if ix == 0 { first } else { rest });
254 out.push_str(line);
255 }
256}
257
258fn fence_width(code: &str) -> usize {
260 let mut longest = 0;
261 let mut run = 0;
262 for c in code.chars() {
263 run = if c == '`' { run + 1 } else { 0 };
264 longest = longest.max(run);
265 }
266 (longest + 1).max(3)
267}
268
269fn write_table(
270 out: &mut String,
271 pad: &str,
272 align: &[Align],
273 header: &[Text],
274 rows: &[Vec<Text>],
275 marks: &Marks,
276) {
277 let columns = align.len().max(header.len());
278 let row_of = |cells: &[Text]| {
279 let mut line = String::from("|");
280 for ix in 0..columns {
281 line.push(' ');
282 if let Some(cell) = cells.get(ix) {
283 line.push_str(&inline(cell, marks));
285 }
286 line.push_str(" |");
287 }
288 line
289 };
290
291 out.push_str(pad);
292 out.push_str(&row_of(header));
293 out.push('\n');
294 out.push_str(pad);
295 out.push('|');
296 for ix in 0..columns {
297 out.push_str(match align.get(ix).copied().unwrap_or_default() {
298 Align::Left => " --- |",
299 Align::Center => " :-: |",
300 Align::Right => " ---: |",
301 });
302 }
303 for row in rows {
304 out.push('\n');
305 out.push_str(pad);
306 out.push_str(&row_of(row));
307 }
308}
309
310fn inline(text: &Text, marks: &Marks) -> String {
314 let mut out = String::new();
315 let mut open: Vec<usize> = Vec::new();
316 let mut started = vec![false; text.marks.len()];
317 let mut delimiters = vec!['_'; text.marks.len()];
319 let mut cursor = 0usize;
320
321 let mut boundaries: Vec<usize> = text
322 .marks
323 .iter()
324 .flat_map(|m| [m.range.start, m.range.end])
325 .chain([0, text.text.len()])
326 .collect();
327 boundaries.sort_unstable();
328 boundaries.dedup();
329
330 for point in boundaries {
331 if point < cursor {
332 continue;
333 }
334 escape_inline(&mut out, &text.text[cursor..point], marks);
335 cursor = point;
336
337 while let Some(&top) = open.last() {
338 if text.marks[top].range.end <= point {
339 close_mark(&mut out, &text.marks[top].mark, delimiters[top], marks);
340 open.pop();
341 } else {
342 break;
343 }
344 }
345
346 for (ix, span) in text.marks.iter().enumerate() {
347 if started[ix] || span.range.start != point {
348 continue;
349 }
350 started[ix] = true;
351 if span.mark == Mark::Code {
354 let body = &text.text[span.range.clone()];
355 let ticks = "`".repeat(fence_width_inline(body));
356 out.push_str(&ticks);
357 out.push_str(body);
358 out.push_str(&ticks);
359 cursor = cursor.max(span.range.end);
360 continue;
361 }
362 if let Mark::Mention { url, .. } = &span.mark
368 && crate::parse::is_shorthand(text, ix)
369 {
370 out.push('<');
371 out.push_str(url);
372 out.push('>');
373 cursor = cursor.max(span.range.end);
374 continue;
375 }
376 if let Mark::Link(url) = &span.mark
383 && text.text.get(span.range.clone()) == Some(url.as_str())
384 && crate::parse::is_url(url)
385 && text.alone(ix)
386 {
387 out.push_str(url);
388 cursor = cursor.max(span.range.end);
389 continue;
390 }
391 let italic = italic_delimiter(&out, text, &span.range);
392 delimiters[ix] = italic;
393 open_mark(&mut out, &span.mark, italic, marks);
394 if span.range.is_empty() {
397 close_mark(&mut out, &span.mark, italic, marks);
398 } else {
399 open.push(ix);
400 }
401 }
402 }
403
404 escape_inline(&mut out, &text.text[cursor.min(text.text.len())..], marks);
405 while let Some(ix) = open.pop() {
406 close_mark(&mut out, &text.marks[ix].mark, delimiters[ix], marks);
407 }
408 out
409}
410
411fn fence_width_inline(body: &str) -> usize {
412 let mut longest = 0;
413 let mut run = 0;
414 for c in body.chars() {
415 run = if c == '`' { run + 1 } else { 0 };
416 longest = longest.max(run);
417 }
418 longest + 1
419}
420
421fn italic_delimiter(written: &str, text: &Text, range: &std::ops::Range<usize>) -> char {
434 let intraword = written
435 .chars()
436 .next_back()
437 .is_some_and(char::is_alphanumeric)
438 || text.text[range.end..]
439 .chars()
440 .next()
441 .is_some_and(char::is_alphanumeric);
442 if intraword { '*' } else { '_' }
443}
444
445fn open_mark(out: &mut String, mark: &Mark, italic: char, marks: &Marks) {
446 match mark {
447 Mark::Bold => out.push_str("**"),
448 Mark::Italic => out.push(italic),
449 Mark::Strike => out.push_str("~~"),
450 Mark::Link(_) | Mark::Mention { .. } => out.push('['),
451 Mark::Image(_) => out.push_str(";
466 write_destination(out, url);
467 out.push(')');
468 }
469 Mark::Mention { url, form } => {
472 out.push_str("](");
473 write_destination(out, url);
474 out.push_str(" \"");
475 out.push_str(form.title().unwrap_or("chip"));
476 out.push_str("\")");
477 }
478 Mark::Custom(name) => out.push_str(marks.delimiter(name).unwrap_or("")),
479 Mark::Code => {}
480 }
481}
482
483fn write_destination(out: &mut String, url: &str) {
489 if bare_destination(url) {
490 return out.push_str(url);
491 }
492 out.push('<');
493 for c in url.chars() {
494 match c {
495 '<' | '>' | '\\' => {
496 out.push('\\');
497 out.push(c);
498 }
499 c if c.is_ascii_control() => out.push_str(&format!("%{:02X}", c as u8)),
503 c => out.push(c),
504 }
505 }
506 out.push('>');
507}
508
509fn bare_destination(url: &str) -> bool {
513 if url.starts_with('<') {
514 return false;
515 }
516 let mut depth = 0i32;
517 for c in url.chars() {
518 match c {
519 '(' => depth += 1,
520 ')' if depth == 0 => return false,
521 ')' => depth -= 1,
522 '\\' => return false,
523 c if c.is_whitespace() || c.is_ascii_control() => return false,
524 _ => {}
525 }
526 }
527 depth == 0
528}
529
530fn escape_inline(out: &mut String, s: &str, marks: &Marks) {
535 let mut line_start = out.is_empty() || out.ends_with('\n');
536 for (ix, line) in s.split('\n').enumerate() {
537 if ix > 0 {
538 out.push('\n');
539 line_start = true;
540 }
541 let body = if line_start {
542 escape_block_marker(out, line)
543 } else {
544 line
545 };
546 escape_span(out, body, marks);
547 line_start = false;
548 }
549}
550
551fn escape_block_marker<'a>(out: &mut String, line: &'a str) -> &'a str {
555 let after_space = |rest: &str| rest.starts_with([' ', '\t']) || rest.is_empty();
556
557 let hashes = line.len() - line.trim_start_matches('#').len();
558 if hashes > 0 && after_space(&line[hashes..]) {
559 out.push('\\');
560 out.push_str(&line[..hashes]);
561 return &line[hashes..];
562 }
563
564 if let Some(rest) = line.strip_prefix('>') {
565 out.push_str("\\>");
566 return rest;
567 }
568
569 if (line.starts_with('-') || line.starts_with('+')) && after_space(&line[1..]) {
572 out.push('\\');
573 out.push_str(&line[..1]);
574 return &line[1..];
575 }
576
577 let digits = line.len() - line.trim_start_matches(|c: char| c.is_ascii_digit()).len();
578 if digits > 0 {
579 let after = &line[digits..];
580 if (after.starts_with('.') || after.starts_with(')')) && after_space(&after[1..]) {
581 out.push_str(&line[..digits]);
582 out.push('\\');
583 out.push_str(&after[..1]);
584 return &after[1..];
585 }
586 }
587
588 let trimmed = line.trim_end();
590 if !trimmed.is_empty() && trimmed.chars().all(|c| c == '=' || c == '-') {
591 out.push('\\');
592 out.push_str(&line[..1]);
593 return &line[1..];
594 }
595
596 line
597}
598
599fn escape_span(out: &mut String, s: &str, marks: &Marks) {
601 let mut skip = 0usize;
602 for (ix, c) in s.char_indices() {
603 if ix < skip {
604 continue;
605 }
606 let rest = &s[ix + c.len_utf8()..];
607 if let Some(entry) = marks
612 .sorted()
613 .into_iter()
614 .find(|entry| s[ix..].starts_with(entry.delimiter.as_ref()))
615 {
616 for c in entry.delimiter.chars() {
617 out.push('\\');
618 out.push(c);
619 }
620 skip = ix + entry.delimiter.len();
621 continue;
622 }
623 match c {
624 '\\' | '*' | '`' | '[' | ']' | '~' | '|' => {
630 out.push('\\');
631 out.push(c);
632 }
633 '_' => {
636 let before = s[..ix].chars().next_back();
637 let inside_word = before.is_some_and(char::is_alphanumeric)
638 && rest.chars().next().is_some_and(char::is_alphanumeric);
639 if !inside_word {
640 out.push('\\');
641 }
642 out.push('_');
643 }
644 '<' if rest
646 .chars()
647 .next()
648 .is_some_and(|c| c.is_alphanumeric() || matches!(c, '/' | '!' | '?')) =>
649 {
650 out.push_str("\\<")
651 }
652 '&' if rest
653 .chars()
654 .next()
655 .is_some_and(|c| c.is_alphanumeric() || c == '#') =>
656 {
657 out.push_str("\\&")
658 }
659 _ => out.push(c),
660 }
661 }
662}
663
664pub(crate) const SENTINEL: char = '\u{E000}';
667
668pub fn serialize_at(doc: &Doc, at: Cursor, marks: &Marks) -> (String, usize) {
677 let mut doc = doc.clone();
678 let placed = doc
679 .blocks
680 .get_mut(at.block)
681 .and_then(|block| block.text_at_mut(at.part))
682 .filter(|text| !text.text.contains(SENTINEL))
683 .map(|text| {
684 text.insert(
685 at.offset.min(text.text.len()),
686 SENTINEL.encode_utf8(&mut [0; 4]),
687 )
688 })
689 .is_some();
690 doc.normalize_with(marks);
694 let mut source = serialize_with(&doc, marks);
695 let Some(offset) = placed.then(|| source.find(SENTINEL)).flatten() else {
696 source = source.replace(SENTINEL, "");
697 let end = source.len();
698 return (source, end);
699 };
700 source.remove(offset);
701 (source, offset)
702}