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.
135pub(super) fn lift(source: &str, marks: &Marks) -> String {
136    let skipped = literal(source);
137    let entries = marks.sorted();
138    let mut out = String::with_capacity(source.len());
139    let mut open: Vec<(usize, &str)> = Vec::new();
140    let mut at = 0usize;
141
142    while at < source.len() {
143        // Inside a fence, a code span or a link's destination the delimiter is
144        // not markup and never was.
145        if let Some(range) = skipped.iter().find(|range| range.contains(&at)) {
146            out.push_str(&source[at..range.end]);
147            at = range.end;
148            continue;
149        }
150        let rest = &source[at..];
151        // A backslash takes the next character with it, delimiter or not.
152        if let Some(escaped) = rest.strip_prefix('\\') {
153            let width = escaped.chars().next().map_or(1, |c| 1 + c.len_utf8());
154            out.push_str(&rest[..width.min(rest.len())]);
155            at += width.min(rest.len());
156            continue;
157        }
158        let found = entries
159            .iter()
160            .find(|entry| rest.starts_with(entry.delimiter.as_ref()));
161        if let Some(entry) = found {
162            let delimiter: &str = entry.delimiter.as_ref();
163            let closes = open.last().is_some_and(|(_, open)| *open == delimiter);
164            if closes && !source[..at].ends_with(char::is_whitespace) {
165                out.push(CLOSE);
166                open.pop();
167                at += delimiter.len();
168                continue;
169            }
170            if !closes
171                && let Some(ix) = marks.position(entry)
172                && let Some(tag) = tag(ix)
173                && closing(
174                    source,
175                    at + delimiter.len(),
176                    delimiter,
177                    &skipped,
178                    line_end(source, at),
179                )
180            {
181                out.push(OPEN);
182                out.push(tag);
183                open.push((ix, delimiter));
184                at += delimiter.len();
185                continue;
186            }
187        }
188        let c = rest.chars().next().unwrap_or_default();
189        out.push(c);
190        at += c.len_utf8();
191    }
192    out
193}
194
195/// Whether a delimiter opened at `from` has a partner to close against: an
196/// unescaped one, on the same line, outside everything literal, with something
197/// between them that neither opens nor closes on a space — the rule emphasis
198/// already follows.
199///
200/// The same line, and only ever the same line. Emphasis may reach across a soft
201/// break; a mark this crate does not know the meaning of may not, because the
202/// next line may belong to another block — a lazy continuation out of a quote,
203/// a list item's second paragraph — and no mark can span two of those. An open
204/// with no close on its own line stays the text it was written as.
205pub(super) fn closing(
206    source: &str,
207    from: usize,
208    delimiter: &str,
209    skipped: &[Range<usize>],
210    line_end: usize,
211) -> bool {
212    if source[from..].starts_with(char::is_whitespace) {
213        return false;
214    }
215    let mut at = from;
216    while let Some(found) = source[at..line_end.max(at)].find(delimiter) {
217        let found = at + found;
218        let escaped = source[..found].ends_with('\\');
219        let literal = skipped.iter().any(|range| range.contains(&found));
220        let spaced = source[..found].ends_with(char::is_whitespace);
221        if !escaped && !literal && !spaced && found > from {
222            return true;
223        }
224        at = found + delimiter.len();
225    }
226    false
227}
228
229/// Where the line `at` sits on ends.
230pub(super) fn line_end(source: &str, at: usize) -> usize {
231    source[at..].find('\n').map_or(source.len(), |ix| at + ix)
232}
233
234/// The source ranges a delimiter means nothing in: a fence, a code span, raw
235/// HTML, and a link's destination.
236pub(super) fn literal(source: &str) -> Vec<Range<usize>> {
237    let mut out = Vec::new();
238    for (event, range) in Parser::new_ext(source, OPTIONS).into_offset_iter() {
239        match event {
240            Event::Code(_) | Event::Html(_) | Event::InlineHtml(_) => out.push(range),
241            Event::Start(Tag::CodeBlock(_)) => out.push(range),
242            // A link's destination only: its label is prose, and a mark is
243            // welcome in it. An autolink has no `](` and is a destination all
244            // through.
245            Event::Start(Tag::Link { .. }) => {
246                let at = source[range.clone()]
247                    .rfind("](")
248                    .map_or(range.start, |ix| range.start + ix);
249                out.push(at..range.end);
250            }
251            // A picture whole: its label is alt text, which markdown writes as
252            // a plain string — a mark placed there would have nowhere to go on
253            // the way out.
254            Event::Start(Tag::Image { .. }) => out.push(range),
255            _ => {}
256        }
257    }
258    out
259}
260
261/// Take the sentinels back out of a parsed text, leaving the marks they stood
262/// for — and move every mark the ordinary parse produced, whose offsets were
263/// measured with the sentinels still in.
264pub(super) fn settle(text: &mut Text, marks: &Marks) {
265    if !text.text.contains(OPEN) {
266        return;
267    }
268    let mut settled = String::with_capacity(text.text.len());
269    // Where a sentinel was, and how many bytes it took with it.
270    let mut cut: Vec<(usize, usize)> = Vec::new();
271    // Each open takes a number, because a mark is closed inner first and the
272    // list is read outermost first — `++==x==++` is underline over highlight,
273    // and writing it the other way round is a different document.
274    let mut open: Vec<(usize, usize, usize)> = Vec::new();
275    let mut found: Vec<(usize, MarkSpan)> = Vec::new();
276    let mut opened = 0usize;
277    let mut chars = text.text.char_indices();
278
279    while let Some((at, c)) = chars.next() {
280        match c {
281            OPEN => {
282                let width = match chars.next() {
283                    Some((_, tag)) => {
284                        if let Some(ix) = tag_index(tag) {
285                            open.push((ix, settled.len(), opened));
286                            opened += 1;
287                        }
288                        OPEN.len_utf8() + tag.len_utf8()
289                    }
290                    None => OPEN.len_utf8(),
291                };
292                cut.push((at, width));
293            }
294            CLOSE => {
295                if let Some((ix, from, seq)) = open.pop()
296                    && let Some(entry) = marks.index(ix)
297                {
298                    found.push((
299                        seq,
300                        MarkSpan {
301                            range: from..settled.len(),
302                            mark: Mark::Custom(entry.name.to_string()),
303                        },
304                    ));
305                }
306                cut.push((at, CLOSE.len_utf8()));
307            }
308            _ => settled.push(c),
309        }
310    }
311
312    let moved = |offset: usize| {
313        offset
314            - cut
315                .iter()
316                .filter(|(at, _)| *at < offset)
317                .map(|(_, width)| width)
318                .sum::<usize>()
319    };
320    for span in &mut text.marks {
321        span.range = moved(span.range.start)..moved(span.range.end);
322    }
323    text.text = settled;
324    found.sort_by_key(|(seq, _)| *seq);
325    text.marks.extend(found.into_iter().map(|(_, span)| span));
326    // Outermost first is what the serializer writes the nesting from. A stable
327    // sort leaves the ordinary marks in the order the parse put them.
328    text.marks
329        .sort_by_key(|span| (span.range.start, std::cmp::Reverse(span.range.end)));
330    text.marks.retain(|span| !span.range.is_empty());
331}