Skip to main content

markdown/
serialize.rs

1//! [`Doc`] → markdown.
2//!
3//! The guarantee is a **fixed point**: `parse(serialize(parse(s)))` equals
4//! `parse(s)` for any input. An edit/save cycle can therefore never drift,
5//! which is the property an editor actually needs — stronger than pretty
6//! output and weaker (honestly so) than byte-identical round tripping, which a
7//! flat model cannot promise for arbitrarily nested CommonMark.
8//!
9//! Escaping is deliberately narrow. Over-escaping is its own bug: escaping `#`
10//! everywhere turns a `#123` reference into `\#123`, which no reader matches.
11//! So `#`, `>`, `-` and friends are escaped only at the start of a line, where
12//! they would actually mean something.
13
14use crate::{
15    doc::{Align, Block, BlockKind, Doc, Mark, Part, Text},
16    marks::Marks,
17    select::Cursor,
18};
19
20/// Four spaces per level: enough to sit inside any list marker's content
21/// column (`- ` is 2, `10. ` is 4), and never enough to become an indented
22/// code block, because a block at depth N+1 always follows its marker at N.
23const INDENT: &str = "    ";
24
25pub fn serialize(doc: &Doc) -> String {
26    serialize_with(doc, &Marks::default())
27}
28
29/// [`serialize`] with the app's own marks — see [`crate::Marks`].
30pub fn serialize_with(doc: &Doc, marks: &Marks) -> String {
31    let mut out = String::new();
32    let mut previous: Option<(&BlockKind, u8)> = None;
33
34    for block in &doc.blocks {
35        let indent = match previous {
36            Some((_, prev)) => block.indent.min(prev + 1),
37            None => 0,
38        };
39
40        if let Some((prev_kind, prev_indent)) = previous {
41            out.push('\n');
42            if !tight_after(prev_kind, &block.kind, indent > prev_indent) {
43                out.push('\n');
44            }
45        }
46
47        write_block(&mut out, &block.kind, indent, marks);
48        previous = Some((&block.kind, indent));
49    }
50
51    // GFM reads `[ ]` as a task marker only when whitespace follows it, and an
52    // empty task has no text to supply it. Every block but the last is followed
53    // by the newline of the block after; the last is followed by nothing.
54    if doc
55        .blocks
56        .last()
57        .is_some_and(|block| matches!(&block.kind, BlockKind::Task { text, .. } if text.is_empty()))
58    {
59        out.push(' ');
60    }
61
62    out
63}
64
65/// Which list a marker block belongs to. Two markers of different kinds are two
66/// different lists even when they are adjacent.
67fn marker_kind(kind: &BlockKind) -> Option<u8> {
68    match kind {
69        BlockKind::Bullet(_) => Some(0),
70        BlockKind::Ordered { .. } => Some(1),
71        BlockKind::Task { .. } => Some(2),
72        _ => None,
73    }
74}
75
76fn is_marker(kind: &BlockKind) -> bool {
77    marker_kind(kind).is_some()
78}
79
80/// Whether a blank line between these two blocks would be wrong.
81///
82/// Items of one list stay tight: a blank line between them makes the list loose,
83/// and `- a\n- b` should come back out as it went in. A blank line does go
84/// between two *different* lists, which is legal — they were already separate —
85/// and is the difference between a readable document and a wall.
86///
87/// An *empty* marker is a special case on both sides, and in opposite
88/// directions. Nothing may separate it from what follows: CommonMark lets a
89/// list item begin with at most one blank line, so a blank line there ends the
90/// list and the item's child becomes a top-level indented code block. But a
91/// blank line must come *before* it, because an empty list item cannot
92/// interrupt a paragraph — written tight against the item above, it is read as
93/// a lazy continuation of that item's text instead of as a list of its own.
94///
95/// Everywhere else the blank line is required too: without it a child block is
96/// read as a lazy continuation of its item.
97///
98/// `nested` says the next block opens a level deeper, which makes it a *new*
99/// list whatever its marker — a checklist under a bullet is as much its own list
100/// as a bullet under a bullet, so all that is left to ask is whether it can
101/// interrupt the paragraph above it, which it has to do to be seen at all. A
102/// bullet may; an ordered list may only when it starts at 1. So a nested `2.`
103/// written tight is read as more text in the item above, and needs the blank
104/// line that ends that paragraph.
105fn tight_after(previous: &BlockKind, next: &BlockKind, nested: bool) -> bool {
106    if is_empty_marker(previous) {
107        return true;
108    }
109    if is_empty_marker(next) {
110        return false;
111    }
112    if nested {
113        return is_marker(previous)
114            && is_marker(next)
115            && !matches!(next, BlockKind::Ordered { number, .. } if *number != 1);
116    }
117    marker_kind(previous).is_some() && marker_kind(previous) == marker_kind(next)
118}
119
120fn is_empty_marker(kind: &BlockKind) -> bool {
121    is_marker(kind)
122        && Block::new(kind.clone())
123            .text_at(Part::Body)
124            .is_some_and(Text::is_empty)
125}
126
127fn write_block(out: &mut String, kind: &BlockKind, indent: u8, marks: &Marks) {
128    let pad = INDENT.repeat(indent as usize);
129
130    match kind {
131        BlockKind::Paragraph(text) => write_lines(out, &pad, &pad, &inline(text, marks)),
132        BlockKind::Heading { level, text } => {
133            let hashes = "#".repeat((*level).clamp(1, 6) as usize);
134            write_lines(out, &format!("{pad}{hashes} "), &pad, &inline(text, marks));
135        }
136        // A bullet with no text would be written as a line holding nothing but
137        // a dash — and a line of dashes directly under a paragraph is a setext
138        // heading underline, not a list item. `+` is the bullet marker that
139        // cannot be read as one.
140        BlockKind::Bullet(text) => {
141            let marker = if text.is_empty() { "+ " } else { "- " };
142            write_marked(out, &pad, marker, text, marks)
143        }
144        BlockKind::Ordered { number, text } => {
145            write_marked(out, &pad, &format!("{number}. "), text, marks)
146        }
147        BlockKind::Task { checked, text } => {
148            let marker = if *checked { "- [x] " } else { "- [ ] " };
149            write_marked(out, &pad, marker, text, marks);
150        }
151        BlockKind::Quote { kind, text } => {
152            let prefix = format!("{pad}> ");
153            // Rendered before the marker is written: an empty [`Text`] can
154            // still carry a mark, and a marker line stands alone only when
155            // there is nothing at all under it.
156            let body = inline(text, marks);
157            if let Some(kind) = kind {
158                out.push_str(&prefix);
159                out.push_str(kind.marker());
160                if body.is_empty() {
161                    return;
162                }
163                out.push('\n');
164            }
165            write_lines(out, &prefix, &prefix, &body);
166        }
167        BlockKind::Code { language, code } => {
168            let fence = "`".repeat(fence_width(&code.text));
169            out.push_str(&pad);
170            out.push_str(&fence);
171            out.push_str(language.as_deref().unwrap_or(""));
172            for line in code.text.split('\n') {
173                out.push('\n');
174                out.push_str(&pad);
175                out.push_str(line);
176            }
177            out.push('\n');
178            out.push_str(&pad);
179            out.push_str(&fence);
180        }
181        BlockKind::Image { url, alt, width } => {
182            out.push_str(&pad);
183            out.push_str("![");
184            escape_inline(out, &alt.text, marks);
185            // After the escaping, and bare: every `|` a caption holds is
186            // written `\|` to keep two body lines from reconstituting into a
187            // table, so an unescaped one is the delimiter and nothing else.
188            if let Some(width) = width {
189                out.push('|');
190                out.push_str(&width.to_string());
191            }
192            out.push_str("](");
193            write_destination(out, url);
194            out.push(')');
195        }
196        // The angles are what makes a line with a link on it into a card, and
197        // they are core CommonMark — every other reader still shows a link
198        // here. The other two forms have no shorthand and say their name.
199        BlockKind::Bookmark { url, form } => {
200            out.push_str(&pad);
201            match form.title() {
202                None => {
203                    out.push('<');
204                    out.push_str(url);
205                    out.push('>');
206                }
207                Some(title) => {
208                    out.push('[');
209                    out.push_str(url);
210                    out.push_str("](");
211                    write_destination(out, url);
212                    out.push_str(&format!(" \"{title}\")"));
213                }
214            }
215        }
216        BlockKind::Table {
217            align,
218            header,
219            rows,
220        } => write_table(out, &pad, align, header, rows, marks),
221        BlockKind::Rule => {
222            out.push_str(&pad);
223            out.push_str("---");
224        }
225    }
226}
227
228/// A list item: the marker on the first line, its content column on the rest.
229fn write_marked(out: &mut String, pad: &str, marker: &str, text: &Text, marks: &Marks) {
230    // An empty item has nothing for the marker's space to hold apart from it,
231    // so the space is trailing whitespace no one typed.
232    let opener = if text.is_empty() {
233        marker.trim_end()
234    } else {
235        marker
236    };
237    let first = format!("{pad}{opener}");
238    let rest = format!("{pad}{}", " ".repeat(marker.chars().count()));
239    write_lines(out, &first, &rest, &inline(text, marks));
240}
241
242fn write_lines(out: &mut String, first: &str, rest: &str, body: &str) {
243    for (ix, line) in body.split('\n').enumerate() {
244        if ix > 0 {
245            out.push('\n');
246        }
247        out.push_str(if ix == 0 { first } else { rest });
248        out.push_str(line);
249    }
250}
251
252/// Long enough to survive any run of backticks the code itself contains.
253fn fence_width(code: &str) -> usize {
254    let mut longest = 0;
255    let mut run = 0;
256    for c in code.chars() {
257        run = if c == '`' { run + 1 } else { 0 };
258        longest = longest.max(run);
259    }
260    (longest + 1).max(3)
261}
262
263fn write_table(
264    out: &mut String,
265    pad: &str,
266    align: &[Align],
267    header: &[Text],
268    rows: &[Vec<Text>],
269    marks: &Marks,
270) {
271    let columns = align.len().max(header.len());
272    let row_of = |cells: &[Text]| {
273        let mut line = String::from("|");
274        for ix in 0..columns {
275            line.push(' ');
276            if let Some(cell) = cells.get(ix) {
277                // `escape_span` already escapes the pipes.
278                line.push_str(&inline(cell, marks));
279            }
280            line.push_str(" |");
281        }
282        line
283    };
284
285    out.push_str(pad);
286    out.push_str(&row_of(header));
287    out.push('\n');
288    out.push_str(pad);
289    out.push('|');
290    for ix in 0..columns {
291        out.push_str(match align.get(ix).copied().unwrap_or_default() {
292            Align::Left => " --- |",
293            Align::Center => " :-: |",
294            Align::Right => " ---: |",
295        });
296    }
297    for row in rows {
298        out.push('\n');
299        out.push_str(pad);
300        out.push_str(&row_of(row));
301    }
302}
303
304/// Render inline content with its marks. Marks are stored outermost first, so
305/// opening them in order and closing them in reverse reproduces the nesting —
306/// which is what keeps `**_x_**` and `_**x**_` distinct.
307fn inline(text: &Text, marks: &Marks) -> String {
308    let mut out = String::new();
309    let mut open: Vec<usize> = Vec::new();
310    let mut started = vec![false; text.marks.len()];
311    // The delimiter a span opened with, so it closes with the same one.
312    let mut delimiters = vec!['_'; text.marks.len()];
313    let mut cursor = 0usize;
314
315    let mut boundaries: Vec<usize> = text
316        .marks
317        .iter()
318        .flat_map(|m| [m.range.start, m.range.end])
319        .chain([0, text.text.len()])
320        .collect();
321    boundaries.sort_unstable();
322    boundaries.dedup();
323
324    for point in boundaries {
325        if point < cursor {
326            continue;
327        }
328        escape_inline(&mut out, &text.text[cursor..point], marks);
329        cursor = point;
330
331        while let Some(&top) = open.last() {
332            if text.marks[top].range.end <= point {
333                close_mark(&mut out, &text.marks[top].mark, delimiters[top], marks);
334                open.pop();
335            } else {
336                break;
337            }
338        }
339
340        for (ix, span) in text.marks.iter().enumerate() {
341            if started[ix] || span.range.start != point {
342                continue;
343            }
344            started[ix] = true;
345            // Code spans are literal to their closing backtick: nothing inside
346            // is markup, so they are emitted whole rather than opened.
347            if span.mark == Mark::Code {
348                let body = &text.text[span.range.clone()];
349                let ticks = "`".repeat(fence_width_inline(body));
350                out.push_str(&ticks);
351                out.push_str(body);
352                out.push_str(&ticks);
353                cursor = cursor.max(span.range.end);
354                continue;
355            }
356            // The shorthand is its angles, emitted whole for the same reason a
357            // code span is: the text between them *is* the URL, so there is
358            // nothing inside for another mark to open against. Anything the
359            // angles cannot hold falls through to the explicit spelling, which
360            // is why a mention never has to stop being one.
361            if let Mark::Mention { url, .. } = &span.mark
362                && crate::parse::is_shorthand(text, ix)
363            {
364                out.push('<');
365                out.push_str(url);
366                out.push('>');
367                cursor = cursor.max(span.range.end);
368                continue;
369            }
370            // A link whose text is the URL it points at is written bare, which
371            // is what the linkifier reads back — so a URL in a sentence
372            // survives byte for byte instead of growing brackets it never had.
373            // Only when no other mark touches it: like a code span this is
374            // emitted whole, and a boundary inside it would have nowhere to
375            // land.
376            if let Mark::Link(url) = &span.mark
377                && text.text.get(span.range.clone()) == Some(url.as_str())
378                && crate::parse::is_url(url)
379                && text.alone(ix)
380            {
381                out.push_str(url);
382                cursor = cursor.max(span.range.end);
383                continue;
384            }
385            let italic = italic_delimiter(&out, text, &span.range);
386            delimiters[ix] = italic;
387            open_mark(&mut out, &span.mark, italic, marks);
388            // A mark over nothing — an image with no alt text — closes here.
389            // Leaving it on the stack would stretch it to the next boundary.
390            if span.range.is_empty() {
391                close_mark(&mut out, &span.mark, italic, marks);
392            } else {
393                open.push(ix);
394            }
395        }
396    }
397
398    escape_inline(&mut out, &text.text[cursor.min(text.text.len())..], marks);
399    while let Some(ix) = open.pop() {
400        close_mark(&mut out, &text.marks[ix].mark, delimiters[ix], marks);
401    }
402    out
403}
404
405fn fence_width_inline(body: &str) -> usize {
406    let mut longest = 0;
407    let mut run = 0;
408    for c in body.chars() {
409        run = if c == '`' { run + 1 } else { 0 };
410        longest = longest.max(run);
411    }
412    longest + 1
413}
414
415/// Which delimiter spells italic for this span.
416///
417/// `_` is preferred because it nests unambiguously inside `**` — `***x***` is
418/// read as emphasis wrapping strong, so writing bold-outside-italic with stars
419/// would come back inside out. But `_` cannot open or close against a letter,
420/// so an emphasis that starts or ends mid-word has to use `*` instead.
421///
422/// What counts as "against a letter" is the *output*, not the source text: a
423/// mark opening right after a code span is preceded by a backtick, which is
424/// punctuation, even though the character before it in the text is a letter.
425/// Deciding from `written` is what keeps `` `a`**_x_** `` from being spelled
426/// `***`, which reads back inside out.
427fn italic_delimiter(written: &str, text: &Text, range: &std::ops::Range<usize>) -> char {
428    let intraword = written
429        .chars()
430        .next_back()
431        .is_some_and(char::is_alphanumeric)
432        || text.text[range.end..]
433            .chars()
434            .next()
435            .is_some_and(char::is_alphanumeric);
436    if intraword { '*' } else { '_' }
437}
438
439fn open_mark(out: &mut String, mark: &Mark, italic: char, marks: &Marks) {
440    match mark {
441        Mark::Bold => out.push_str("**"),
442        Mark::Italic => out.push(italic),
443        Mark::Strike => out.push_str("~~"),
444        Mark::Link(_) | Mark::Mention { .. } => out.push('['),
445        Mark::Image(_) => out.push_str("!["),
446        // A name no registry spells writes nothing and reads back as the text
447        // it wrapped, which is the only degradation that cannot corrupt a file.
448        Mark::Custom(name) => out.push_str(marks.delimiter(name).unwrap_or("")),
449        Mark::Code => {}
450    }
451}
452
453fn close_mark(out: &mut String, mark: &Mark, italic: char, marks: &Marks) {
454    match mark {
455        Mark::Bold => out.push_str("**"),
456        Mark::Italic => out.push(italic),
457        Mark::Strike => out.push_str("~~"),
458        Mark::Link(url) | Mark::Image(url) => {
459            out.push_str("](");
460            write_destination(out, url);
461            out.push(')');
462        }
463        // The title names the form. It is the only slot CommonMark leaves for
464        // it, and the shorthand having been ruled out is what got us here.
465        Mark::Mention { url, form } => {
466            out.push_str("](");
467            write_destination(out, url);
468            out.push_str(" \"");
469            out.push_str(form.title().unwrap_or("chip"));
470            out.push_str("\")");
471        }
472        Mark::Custom(name) => out.push_str(marks.delimiter(name).unwrap_or("")),
473        Mark::Code => {}
474    }
475}
476
477/// A link destination, in whichever of CommonMark's two spellings reads back
478/// as the URL it was handed. Bare wherever that works, because every reader
479/// shows it and it is what a URL was written as; in angles for a destination
480/// bare would swallow or cut short — the space in `/My Notes/a.png` ends a
481/// bare destination, and the rest of it becomes text.
482fn write_destination(out: &mut String, url: &str) {
483    if bare_destination(url) {
484        return out.push_str(url);
485    }
486    out.push('<');
487    for c in url.chars() {
488        match c {
489            '<' | '>' | '\\' => {
490                out.push('\\');
491                out.push(c);
492            }
493            // The one thing neither spelling can hold. Percent-encoding is
494            // what a URL says instead, and leaving it raw would end the
495            // destination the same way the space did.
496            c if c.is_ascii_control() => out.push_str(&format!("%{:02X}", c as u8)),
497            c => out.push(c),
498        }
499    }
500    out.push('>');
501}
502
503/// Whether `url` survives being written without its angles: no whitespace, no
504/// backslash to be read as an escape, and parentheses balanced — an unmatched
505/// `)` is where the destination ends.
506fn bare_destination(url: &str) -> bool {
507    if url.starts_with('<') {
508        return false;
509    }
510    let mut depth = 0i32;
511    for c in url.chars() {
512        match c {
513            '(' => depth += 1,
514            ')' if depth == 0 => return false,
515            ')' => depth -= 1,
516            '\\' => return false,
517            c if c.is_whitespace() || c.is_ascii_control() => return false,
518            _ => {}
519        }
520    }
521    depth == 0
522}
523
524/// Escape only what would otherwise re-parse as syntax.
525///
526/// Called with slices between mark boundaries, so "line start" means the start
527/// of a line in the *output*, not in the slice.
528fn escape_inline(out: &mut String, s: &str, marks: &Marks) {
529    let mut line_start = out.is_empty() || out.ends_with('\n');
530    for (ix, line) in s.split('\n').enumerate() {
531        if ix > 0 {
532            out.push('\n');
533            line_start = true;
534        }
535        let body = if line_start {
536            escape_block_marker(out, line)
537        } else {
538            line
539        };
540        escape_span(out, body, marks);
541        line_start = false;
542    }
543}
544
545/// Escape a leading run that would open a block, returning what is left of the
546/// line. Only ever fires at a line start — mid-line these characters are
547/// ordinary text, and escaping them there is what turns `#123` into `\#123`.
548fn escape_block_marker<'a>(out: &mut String, line: &'a str) -> &'a str {
549    let after_space = |rest: &str| rest.starts_with([' ', '\t']) || rest.is_empty();
550
551    let hashes = line.len() - line.trim_start_matches('#').len();
552    if hashes > 0 && after_space(&line[hashes..]) {
553        out.push('\\');
554        out.push_str(&line[..hashes]);
555        return &line[hashes..];
556    }
557
558    if let Some(rest) = line.strip_prefix('>') {
559        out.push_str("\\>");
560        return rest;
561    }
562
563    // `*` is escaped by `escape_span` wherever it appears, so only `-` and `+`
564    // need catching here.
565    if (line.starts_with('-') || line.starts_with('+')) && after_space(&line[1..]) {
566        out.push('\\');
567        out.push_str(&line[..1]);
568        return &line[1..];
569    }
570
571    let digits = line.len() - line.trim_start_matches(|c: char| c.is_ascii_digit()).len();
572    if digits > 0 {
573        let after = &line[digits..];
574        if (after.starts_with('.') || after.starts_with(')')) && after_space(&after[1..]) {
575            out.push_str(&line[..digits]);
576            out.push('\\');
577            out.push_str(&after[..1]);
578            return &after[1..];
579        }
580    }
581
582    // A run of `-` or `=` alone is a thematic break or a setext underline.
583    let trimmed = line.trim_end();
584    if !trimmed.is_empty() && trimmed.chars().all(|c| c == '=' || c == '-') {
585        out.push('\\');
586        out.push_str(&line[..1]);
587        return &line[1..];
588    }
589
590    line
591}
592
593/// Per-character escaping within one line.
594fn escape_span(out: &mut String, s: &str, marks: &Marks) {
595    let mut skip = 0usize;
596    for (ix, c) in s.char_indices() {
597        if ix < skip {
598            continue;
599        }
600        let rest = &s[ix + c.len_utf8()..];
601        // A registered delimiter standing in the text is text, and has to come
602        // back as text: every character of it takes a backslash, or the next
603        // read finds a mark nobody wrote. Longest first, so `===` is not
604        // escaped as `==` and a stray `=`.
605        if let Some(entry) = marks
606            .sorted()
607            .into_iter()
608            .find(|entry| s[ix..].starts_with(entry.delimiter.as_ref()))
609        {
610            for c in entry.delimiter.chars() {
611                out.push('\\');
612                out.push(c);
613            }
614            skip = ix + entry.delimiter.len();
615            continue;
616        }
617        match c {
618            // Every tilde, not just a doubled one: GFM strikes on `~x~` as
619            // well, so escaping only the first of a pair leaves the survivors
620            // to find each other. Pipes are here because two consecutive body
621            // lines that happen to look like a header and a delimiter row will
622            // otherwise reconstitute themselves into a table.
623            '\\' | '*' | '`' | '[' | ']' | '~' | '|' => {
624                out.push('\\');
625                out.push(c);
626            }
627            // Intraword underscores are not emphasis in CommonMark, and
628            // escaping them would mangle every snake_case identifier.
629            '_' => {
630                let before = s[..ix].chars().next_back();
631                let inside_word = before.is_some_and(char::is_alphanumeric)
632                    && rest.chars().next().is_some_and(char::is_alphanumeric);
633                if !inside_word {
634                    out.push('\\');
635                }
636                out.push('_');
637            }
638            // `<` matters for autolinks and raw tags, not for `1 < 2`.
639            '<' if rest
640                .chars()
641                .next()
642                .is_some_and(|c| c.is_alphanumeric() || matches!(c, '/' | '!' | '?')) =>
643            {
644                out.push_str("\\<")
645            }
646            '&' if rest
647                .chars()
648                .next()
649                .is_some_and(|c| c.is_alphanumeric() || c == '#') =>
650            {
651                out.push_str("\\&")
652            }
653            _ => out.push(c),
654        }
655    }
656}
657
658/// A mark nothing escapes, nothing renders and no document carries: the
659/// private-use codepoint both directions of the caret mapping ride on.
660pub(crate) const SENTINEL: char = '\u{E000}';
661
662/// The document as markdown, and where `at` landed in it.
663///
664/// Exact through markers, escapes and marks because it *is* the serializer: a
665/// sentinel goes in at the caret, the document is written, and where the
666/// sentinel came out is the answer. The string comes back without it.
667///
668/// The offset is the end of the output for a caret this cannot place — a
669/// document already carrying the sentinel, or a part that no longer exists.
670pub fn serialize_at(doc: &Doc, at: Cursor, marks: &Marks) -> (String, usize) {
671    let mut doc = doc.clone();
672    let placed = doc
673        .blocks
674        .get_mut(at.block)
675        .and_then(|block| block.text_at_mut(at.part))
676        .filter(|text| !text.text.contains(SENTINEL))
677        .map(|text| {
678            text.insert(
679                at.offset.min(text.text.len()),
680                SENTINEL.encode_utf8(&mut [0; 4]),
681            )
682        })
683        .is_some();
684    // Normalized *after* the sentinel goes in, so the string this returns is
685    // the one the offset indexes into — a trailing space is only trailing
686    // while nothing sits after it.
687    doc.normalize_with(marks);
688    let mut source = serialize_with(&doc, marks);
689    let Some(offset) = placed.then(|| source.find(SENTINEL)).flatten() else {
690        source = source.replace(SENTINEL, "");
691        let end = source.len();
692        return (source, end);
693    };
694    source.remove(offset);
695    (source, offset)
696}