Skip to main content

markdown/parse/
lift.rs

1//! Parsing with the caret carried through, and custom marks lifted to sentinels.
2
3use super::*;
4
5/// Parse markdown, and say where `offset` in it landed in the document.
6///
7/// The inverse of [`crate::serialize_at`] and the same trick: a sentinel goes
8/// into the source at the offset, the source is parsed, and the text holding
9/// the sentinel is the caret's. The document comes back without it.
10///
11/// The caret is the start of the document where the sentinel would have
12/// changed what the source *means* — between a `#` and its space, inside a
13/// fence's delimiter — because a caret in the right place is worth less than a
14/// document that is still the one you were editing.
15pub fn parse_at(source: &str, offset: usize, marks: &Marks) -> (Doc, Cursor) {
16    let plain = parse_with(source, marks);
17    let start = || (plain.clone(), Cursor::default().clamp(&plain));
18    if source.contains(crate::serialize::SENTINEL) {
19        return start();
20    }
21    let mut marked = String::with_capacity(source.len() + 3);
22    let offset = offset.min(source.len());
23    if !source.is_char_boundary(offset) {
24        return start();
25    }
26    marked.push_str(&source[..offset]);
27    marked.push(crate::serialize::SENTINEL);
28    marked.push_str(&source[offset..]);
29
30    let mut doc = parse_with(&marked, marks);
31    let Some(at) = find(&doc) else { return start() };
32    let Some(text) = doc
33        .blocks
34        .get_mut(at.block)
35        .and_then(|block| block.text_at_mut(at.part))
36    else {
37        return start();
38    };
39    text.remove(at.offset..at.offset + crate::serialize::SENTINEL.len_utf8());
40    // The sentinel is a character like any other to the parser, so a document
41    // it changed the shape of is not the one the caller handed in.
42    if doc != plain { start() } else { (doc, at) }
43}
44
45/// Where the sentinel sits, in document order.
46pub(super) fn find(doc: &Doc) -> Option<Cursor> {
47    doc.blocks.iter().enumerate().find_map(|(ix, block)| {
48        block.parts().into_iter().find_map(|part| {
49            let at = block.text_at(part)?.text.find(crate::serialize::SENTINEL)?;
50            Some(Cursor::new(ix, part, at))
51        })
52    })
53}
54
55/// A registered mark, lifted out of the source and into two private-use
56/// characters CommonMark carries through as ordinary text.
57///
58/// The pair rather than the delimiter itself, because the delimiter is what the
59/// escape question is about: by the time pulldown has finished, `\=\=` and `==`
60/// are the same two bytes, and only the source still knows which was written.
61pub(super) const OPEN: char = '\u{E010}';
62pub(super) const CLOSE: char = '\u{E011}';
63
64/// Which registered mark an [`OPEN`] belongs to, as a character of its own so
65/// the pair needs no length prefix.
66pub(super) fn tag(ix: usize) -> Option<char> {
67    char::from_u32(0xE020 + u32::try_from(ix).ok()?).filter(|_| ix < 0x100)
68}
69
70pub(super) fn tag_index(c: char) -> Option<usize> {
71    (0xE020..0xE120)
72        .contains(&(c as u32))
73        .then(|| c as usize - 0xE020)
74}
75
76/// The source with every registered delimiter pair replaced by its sentinels.
77pub(super) fn lift(source: &str, marks: &Marks) -> String {
78    let skipped = literal(source);
79    let entries = marks.sorted();
80    let mut out = String::with_capacity(source.len());
81    let mut open: Vec<(usize, &str)> = Vec::new();
82    let mut at = 0usize;
83
84    while at < source.len() {
85        // Inside a fence, a code span or a link's destination the delimiter is
86        // not markup and never was.
87        if let Some(range) = skipped.iter().find(|range| range.contains(&at)) {
88            out.push_str(&source[at..range.end]);
89            at = range.end;
90            continue;
91        }
92        let rest = &source[at..];
93        // A backslash takes the next character with it, delimiter or not.
94        if let Some(escaped) = rest.strip_prefix('\\') {
95            let width = escaped.chars().next().map_or(1, |c| 1 + c.len_utf8());
96            out.push_str(&rest[..width.min(rest.len())]);
97            at += width.min(rest.len());
98            continue;
99        }
100        let found = entries
101            .iter()
102            .find(|entry| rest.starts_with(entry.delimiter.as_ref()));
103        if let Some(entry) = found {
104            let delimiter: &str = entry.delimiter.as_ref();
105            let closes = open.last().is_some_and(|(_, open)| *open == delimiter);
106            if closes && !source[..at].ends_with(char::is_whitespace) {
107                out.push(CLOSE);
108                open.pop();
109                at += delimiter.len();
110                continue;
111            }
112            if !closes
113                && let Some(ix) = marks.position(entry)
114                && let Some(tag) = tag(ix)
115                && closing(
116                    source,
117                    at + delimiter.len(),
118                    delimiter,
119                    &skipped,
120                    line_end(source, at),
121                )
122            {
123                out.push(OPEN);
124                out.push(tag);
125                open.push((ix, delimiter));
126                at += delimiter.len();
127                continue;
128            }
129        }
130        let c = rest.chars().next().unwrap_or_default();
131        out.push(c);
132        at += c.len_utf8();
133    }
134    out
135}
136
137/// Whether a delimiter opened at `from` has a partner to close against: an
138/// unescaped one, on the same line, outside everything literal, with something
139/// between them that neither opens nor closes on a space — the rule emphasis
140/// already follows.
141///
142/// The same line, and only ever the same line. Emphasis may reach across a soft
143/// break; a mark this crate does not know the meaning of may not, because the
144/// next line may belong to another block — a lazy continuation out of a quote,
145/// a list item's second paragraph — and no mark can span two of those. An open
146/// with no close on its own line stays the text it was written as.
147pub(super) fn closing(
148    source: &str,
149    from: usize,
150    delimiter: &str,
151    skipped: &[Range<usize>],
152    line_end: usize,
153) -> bool {
154    if source[from..].starts_with(char::is_whitespace) {
155        return false;
156    }
157    let mut at = from;
158    while let Some(found) = source[at..line_end.max(at)].find(delimiter) {
159        let found = at + found;
160        let escaped = source[..found].ends_with('\\');
161        let literal = skipped.iter().any(|range| range.contains(&found));
162        let spaced = source[..found].ends_with(char::is_whitespace);
163        if !escaped && !literal && !spaced && found > from {
164            return true;
165        }
166        at = found + delimiter.len();
167    }
168    false
169}
170
171/// Where the line `at` sits on ends.
172pub(super) fn line_end(source: &str, at: usize) -> usize {
173    source[at..].find('\n').map_or(source.len(), |ix| at + ix)
174}
175
176/// The source ranges a delimiter means nothing in: a fence, a code span, raw
177/// HTML, and a link's destination.
178pub(super) fn literal(source: &str) -> Vec<Range<usize>> {
179    let mut out = Vec::new();
180    for (event, range) in Parser::new_ext(source, OPTIONS).into_offset_iter() {
181        match event {
182            Event::Code(_) | Event::Html(_) | Event::InlineHtml(_) => out.push(range),
183            Event::Start(Tag::CodeBlock(_)) => out.push(range),
184            // A link's destination only: its label is prose, and a mark is
185            // welcome in it. An autolink has no `](` and is a destination all
186            // through.
187            Event::Start(Tag::Link { .. }) => {
188                let at = source[range.clone()]
189                    .rfind("](")
190                    .map_or(range.start, |ix| range.start + ix);
191                out.push(at..range.end);
192            }
193            // A picture whole: its label is alt text, which markdown writes as
194            // a plain string — a mark placed there would have nowhere to go on
195            // the way out.
196            Event::Start(Tag::Image { .. }) => out.push(range),
197            _ => {}
198        }
199    }
200    out
201}
202
203/// Take the sentinels back out of a parsed text, leaving the marks they stood
204/// for — and move every mark the ordinary parse produced, whose offsets were
205/// measured with the sentinels still in.
206pub(super) fn settle(text: &mut Text, marks: &Marks) {
207    if !text.text.contains(OPEN) {
208        return;
209    }
210    let mut settled = String::with_capacity(text.text.len());
211    // Where a sentinel was, and how many bytes it took with it.
212    let mut cut: Vec<(usize, usize)> = Vec::new();
213    // Each open takes a number, because a mark is closed inner first and the
214    // list is read outermost first — `++==x==++` is underline over highlight,
215    // and writing it the other way round is a different document.
216    let mut open: Vec<(usize, usize, usize)> = Vec::new();
217    let mut found: Vec<(usize, MarkSpan)> = Vec::new();
218    let mut opened = 0usize;
219    let mut chars = text.text.char_indices();
220
221    while let Some((at, c)) = chars.next() {
222        match c {
223            OPEN => {
224                let width = match chars.next() {
225                    Some((_, tag)) => {
226                        if let Some(ix) = tag_index(tag) {
227                            open.push((ix, settled.len(), opened));
228                            opened += 1;
229                        }
230                        OPEN.len_utf8() + tag.len_utf8()
231                    }
232                    None => OPEN.len_utf8(),
233                };
234                cut.push((at, width));
235            }
236            CLOSE => {
237                if let Some((ix, from, seq)) = open.pop()
238                    && let Some(entry) = marks.index(ix)
239                {
240                    found.push((
241                        seq,
242                        MarkSpan {
243                            range: from..settled.len(),
244                            mark: Mark::Custom(entry.name.to_string()),
245                        },
246                    ));
247                }
248                cut.push((at, CLOSE.len_utf8()));
249            }
250            _ => settled.push(c),
251        }
252    }
253
254    let moved = |offset: usize| {
255        offset
256            - cut
257                .iter()
258                .filter(|(at, _)| *at < offset)
259                .map(|(_, width)| width)
260                .sum::<usize>()
261    };
262    for span in &mut text.marks {
263        span.range = moved(span.range.start)..moved(span.range.end);
264    }
265    text.text = settled;
266    found.sort_by_key(|(seq, _)| *seq);
267    text.marks.extend(found.into_iter().map(|(_, span)| span));
268    // Outermost first is what the serializer writes the nesting from. A stable
269    // sort leaves the ordinary marks in the order the parse put them.
270    text.marks
271        .sort_by_key(|span| (span.range.start, std::cmp::Reverse(span.range.end)));
272    text.marks.retain(|span| !span.range.is_empty());
273}