1use super::*;
4
5pub 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 if doc != plain { start() } else { (doc, at) }
43}
44
45pub(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
55pub(super) const OPEN: char = '\u{E010}';
62pub(super) const CLOSE: char = '\u{E011}';
63
64pub(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
76pub(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 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 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
137pub(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
171pub(super) fn line_end(source: &str, at: usize) -> usize {
173 source[at..].find('\n').map_or(source.len(), |ix| at + ix)
174}
175
176pub(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 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 Event::Start(Tag::Image { .. }) => out.push(range),
197 _ => {}
198 }
199 }
200 out
201}
202
203pub(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 let mut cut: Vec<(usize, usize)> = Vec::new();
213 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 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}