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.
14pub fn parse_at(source: &str, offset: usize, marks: &Marks) -> (Doc, Cursor) {
15    let (doc, at) = parse_at_many(source, &[offset], marks);
16    let at = at[0].unwrap_or_else(|| Cursor::default().clamp(&doc));
17    (doc, at)
18}
19
20/// [`parse_at`] for many offsets: where each landed, in the same order, `None`
21/// for one whose sentinel would change what the source means or that is not
22/// on a character boundary.
23pub fn parse_at_many(source: &str, offsets: &[usize], marks: &Marks) -> (Doc, Vec<Option<Cursor>>) {
24    let plain = parse_with(source, marks);
25    let mut at = vec![None; offsets.len()];
26    if !source
27        .chars()
28        .any(|c| crate::serialize::sentinel_index(c).is_some())
29    {
30        let offset = |ix: usize| offsets[ix].min(source.len());
31        let mut which: Vec<usize> = (0..offsets.len())
32            .filter(|&ix| {
33                source.is_char_boundary(offset(ix)) && crate::serialize::sentinel(ix).is_some()
34            })
35            .collect();
36        which.sort_by_key(|&ix| offset(ix));
37        place(source, &offset, &which, &plain, marks, &mut at);
38    }
39    (plain, at)
40}
41
42/// Place each of `which` with one parse, halving the set wherever its
43/// sentinels change the document, so one offset in the markup costs only
44/// itself.
45fn place(
46    source: &str,
47    offset: &impl Fn(usize) -> usize,
48    which: &[usize],
49    plain: &Doc,
50    marks: &Marks,
51    out: &mut [Option<Cursor>],
52) {
53    if which.is_empty() {
54        return;
55    }
56    if let Some(found) = lifted(source, offset, which, plain, marks) {
57        for (ix, at) in found {
58            out[ix] = Some(at);
59        }
60    } else if which.len() > 1 {
61        let (left, right) = which.split_at(which.len() / 2);
62        place(source, offset, left, plain, marks, out);
63        place(source, offset, right, plain, marks, out);
64    }
65}
66
67/// Parse with a sentinel at each of `which`, sorted by offset: where each
68/// landed, or `None` when the sentinels made a different document.
69fn lifted(
70    source: &str,
71    offset: &impl Fn(usize) -> usize,
72    which: &[usize],
73    plain: &Doc,
74    marks: &Marks,
75) -> Option<Vec<(usize, Cursor)>> {
76    let mut marked = String::with_capacity(source.len() + 4 * which.len());
77    let mut from = 0;
78    for &ix in which {
79        marked.push_str(&source[from..offset(ix)]);
80        marked.push(crate::serialize::sentinel(ix)?);
81        from = offset(ix);
82    }
83    marked.push_str(&source[from..]);
84
85    let mut doc = parse_with(&marked, marks);
86    let mut found = Vec::with_capacity(which.len());
87    for (block_ix, block) in doc.blocks.iter_mut().enumerate() {
88        for part in block.parts() {
89            let Some(text) = block.text_at_mut(part) else {
90                continue;
91            };
92            let hits: Vec<(usize, char)> = text
93                .text
94                .char_indices()
95                .filter(|(_, c)| crate::serialize::sentinel_index(*c).is_some())
96                .collect();
97            let mut removed = 0;
98            for &(at, c) in &hits {
99                let ix = crate::serialize::sentinel_index(c)?;
100                found.push((ix, Cursor::new(block_ix, part, at - removed)));
101                removed += c.len_utf8();
102            }
103            for &(at, c) in hits.iter().rev() {
104                text.remove(at..at + c.len_utf8());
105            }
106        }
107    }
108    // A sentinel is a character like any other to the parser, so a document
109    // one changed the shape of is not the one the caller handed in.
110    (found.len() == which.len() && doc == *plain).then_some(found)
111}
112
113/// A registered mark, lifted out of the source and into two private-use
114/// characters CommonMark carries through as ordinary text.
115///
116/// The pair rather than the delimiter itself, because the delimiter is what the
117/// escape question is about: by the time pulldown has finished, `\=\=` and `==`
118/// are the same two bytes, and only the source still knows which was written.
119pub(super) const OPEN: char = '\u{E010}';
120pub(super) const CLOSE: char = '\u{E011}';
121
122/// Which registered mark an [`OPEN`] belongs to, as a character of its own so
123/// the pair needs no length prefix.
124pub(super) fn tag(ix: usize) -> Option<char> {
125    char::from_u32(0xE020 + u32::try_from(ix).ok()?).filter(|_| ix < 0x100)
126}
127
128pub(super) fn tag_index(c: char) -> Option<usize> {
129    (0xE020..0xE120)
130        .contains(&(c as u32))
131        .then(|| c as usize - 0xE020)
132}
133
134/// The source with every registered delimiter pair replaced by its sentinels,
135/// and where the two line up: `(lifted, source)` offsets in order, one after
136/// each replacement. Between two of them, and after the last, the texts run
137/// byte for byte.
138pub(super) fn lift(source: &str, marks: &Marks) -> (String, Vec<(usize, usize)>) {
139    let skipped = literal(source);
140    let mut aligned = Vec::new();
141    let entries = marks.sorted();
142    let mut out = String::with_capacity(source.len());
143    let mut open: Vec<(usize, &str)> = Vec::new();
144    let mut at = 0usize;
145
146    while at < source.len() {
147        // Inside a fence, a code span or a link's destination the delimiter is
148        // not markup and never was.
149        if let Some(range) = covering(&skipped, at) {
150            out.push_str(&source[at..range.end]);
151            at = range.end;
152            continue;
153        }
154        let rest = &source[at..];
155        // A backslash takes the next character with it, delimiter or not.
156        if let Some(escaped) = rest.strip_prefix('\\') {
157            let width = escaped.chars().next().map_or(1, |c| 1 + c.len_utf8());
158            out.push_str(&rest[..width.min(rest.len())]);
159            at += width.min(rest.len());
160            continue;
161        }
162        let found = entries
163            .iter()
164            .find(|entry| rest.starts_with(entry.delimiter.as_ref()));
165        if let Some(entry) = found {
166            let delimiter: &str = entry.delimiter.as_ref();
167            let closes = open.last().is_some_and(|(_, open)| *open == delimiter);
168            if closes && !source[..at].ends_with(char::is_whitespace) {
169                out.push(CLOSE);
170                open.pop();
171                at += delimiter.len();
172                aligned.push((out.len(), at));
173                continue;
174            }
175            if !closes
176                && let Some(ix) = marks.position(entry)
177                && let Some(tag) = tag(ix)
178                && closing(
179                    source,
180                    at + delimiter.len(),
181                    delimiter,
182                    &skipped,
183                    line_end(source, at),
184                )
185            {
186                out.push(OPEN);
187                out.push(tag);
188                open.push((ix, delimiter));
189                at += delimiter.len();
190                aligned.push((out.len(), at));
191                continue;
192            }
193        }
194        let c = rest.chars().next().unwrap_or_default();
195        out.push(c);
196        at += c.len_utf8();
197    }
198    (out, aligned)
199}
200
201/// The source offset a lifted offset stands for, by [`lift`]'s alignment.
202pub(super) fn unlift(aligned: &[(usize, usize)], lifted: usize) -> usize {
203    let ix = aligned.partition_point(|(at, _)| *at <= lifted);
204    match ix.checked_sub(1) {
205        Some(ix) => aligned[ix].1 + (lifted - aligned[ix].0),
206        None => lifted,
207    }
208}
209
210/// Whether a delimiter opened at `from` has a partner to close against: an
211/// unescaped one, on the same line, outside everything literal, with something
212/// between them that neither opens nor closes on a space — the rule emphasis
213/// already follows.
214///
215/// The same line, and only ever the same line. Emphasis may reach across a soft
216/// break; a mark this crate does not know the meaning of may not, because the
217/// next line may belong to another block — a lazy continuation out of a quote,
218/// a list item's second paragraph — and no mark can span two of those. An open
219/// with no close on its own line stays the text it was written as.
220pub(super) fn closing(
221    source: &str,
222    from: usize,
223    delimiter: &str,
224    skipped: &[Range<usize>],
225    line_end: usize,
226) -> bool {
227    if source[from..].starts_with(char::is_whitespace) {
228        return false;
229    }
230    let mut at = from;
231    while let Some(found) = source[at..line_end.max(at)].find(delimiter) {
232        let found = at + found;
233        let escaped = source[..found].ends_with('\\');
234        let literal = covering(skipped, found).is_some();
235        let spaced = source[..found].ends_with(char::is_whitespace);
236        if !escaped && !literal && !spaced && found > from {
237            return true;
238        }
239        at = found + delimiter.len();
240    }
241    false
242}
243
244/// Where the line `at` sits on ends.
245pub(super) fn line_end(source: &str, at: usize) -> usize {
246    source[at..].find('\n').map_or(source.len(), |ix| at + ix)
247}
248
249/// The range of `ranges` holding `at`. `ranges` is sorted and disjoint, as
250/// [`literal`] answers them.
251fn covering(ranges: &[Range<usize>], at: usize) -> Option<&Range<usize>> {
252    let ix = ranges.partition_point(|range| range.end <= at);
253    ranges.get(ix).filter(|range| range.start <= at)
254}
255
256/// The source ranges a delimiter means nothing in: a fence, a code span, raw
257/// HTML, and a link's destination. Sorted and disjoint: nested ranges are
258/// merged into the one around them.
259pub(super) fn literal(source: &str) -> Vec<Range<usize>> {
260    let mut out: Vec<Range<usize>> = Vec::new();
261    for (event, range) in Parser::new_ext(source, OPTIONS).into_offset_iter() {
262        match event {
263            Event::Code(_) | Event::Html(_) | Event::InlineHtml(_) => out.push(range),
264            Event::Start(Tag::CodeBlock(_)) => out.push(range),
265            // A link's destination only: its label is prose, and a mark is
266            // welcome in it. An autolink has no `](` and is a destination all
267            // through.
268            Event::Start(Tag::Link { .. }) => {
269                let at = source[range.clone()]
270                    .rfind("](")
271                    .map_or(range.start, |ix| range.start + ix);
272                out.push(at..range.end);
273            }
274            // A picture whole: its label is alt text, which markdown writes as
275            // a plain string — a mark placed there would have nowhere to go on
276            // the way out.
277            Event::Start(Tag::Image { .. }) => out.push(range),
278            _ => {}
279        }
280    }
281    out.retain(|range| !range.is_empty());
282    out.sort_by_key(|range| range.start);
283    let mut merged: Vec<Range<usize>> = Vec::with_capacity(out.len());
284    for range in out {
285        match merged.last_mut() {
286            Some(last) if range.start < last.end => last.end = last.end.max(range.end),
287            _ => merged.push(range),
288        }
289    }
290    merged
291}
292
293/// Take the sentinels back out of a parsed text, leaving the marks they stood
294/// for — and move every mark the ordinary parse produced, whose offsets were
295/// measured with the sentinels still in.
296pub(super) fn settle(text: &mut Text, marks: &Marks) {
297    if !text.text.contains(OPEN) {
298        return;
299    }
300    let mut settled = String::with_capacity(text.text.len());
301    // Where a sentinel was, and how many bytes it took with it.
302    let mut cut: Vec<(usize, usize)> = Vec::new();
303    // Each open takes a number, because a mark is closed inner first and the
304    // list is read outermost first — `++==x==++` is underline over highlight,
305    // and writing it the other way round is a different document.
306    let mut open: Vec<(usize, usize, usize)> = Vec::new();
307    let mut found: Vec<(usize, MarkSpan)> = Vec::new();
308    let mut opened = 0usize;
309    let mut chars = text.text.char_indices();
310
311    while let Some((at, c)) = chars.next() {
312        match c {
313            OPEN => {
314                let width = match chars.next() {
315                    Some((_, tag)) => {
316                        if let Some(ix) = tag_index(tag) {
317                            open.push((ix, settled.len(), opened));
318                            opened += 1;
319                        }
320                        OPEN.len_utf8() + tag.len_utf8()
321                    }
322                    None => OPEN.len_utf8(),
323                };
324                cut.push((at, width));
325            }
326            CLOSE => {
327                if let Some((ix, from, seq)) = open.pop()
328                    && let Some(entry) = marks.index(ix)
329                {
330                    found.push((
331                        seq,
332                        MarkSpan {
333                            range: from..settled.len(),
334                            mark: Mark::Custom(entry.name.to_string()),
335                        },
336                    ));
337                }
338                cut.push((at, CLOSE.len_utf8()));
339            }
340            _ => settled.push(c),
341        }
342    }
343
344    let moved = |offset: usize| {
345        offset
346            - cut
347                .iter()
348                .filter(|(at, _)| *at < offset)
349                .map(|(_, width)| width)
350                .sum::<usize>()
351    };
352    for span in &mut text.marks {
353        span.range = moved(span.range.start)..moved(span.range.end);
354    }
355    text.text = settled;
356    found.sort_by_key(|(seq, _)| *seq);
357    text.marks.extend(found.into_iter().map(|(_, span)| span));
358    // Outermost first is what the serializer writes the nesting from. A stable
359    // sort leaves the ordinary marks in the order the parse put them.
360    text.marks
361        .sort_by_key(|span| (span.range.start, std::cmp::Reverse(span.range.end)));
362    text.marks.retain(|span| !span.range.is_empty());
363}