markdown/parse/mod.rs
1//! Markdown → [`Doc`].
2//!
3//! CommonMark nests; a [`Doc`] does not. Indent counts **list nesting only**:
4//!
5//! - a list item's first paragraph becomes its marker block (bullet, ordered,
6//! task) at one level shallower than the open list count, and anything else
7//! in that item becomes a child at the list count itself;
8//! - everything else keeps its kind at the open list count.
9//!
10//! A block inside a blockquote carries a [`Block::quote`] with the GFM alert
11//! the blockquote opened with. A blockquote never adds to the indent.
12//! Blockquotes nested in one another flatten to one, and the fixed-point test
13//! in [`crate::serialize`] keeps whatever the first parse decides.
14//!
15//! The parse also normalizes what markdown itself would not preserve: leading
16//! and trailing whitespace per line, blank lines at a block's edges, headings
17//! and table cells flattened to one line, and ordered runs renumbered
18//! consecutively. Each of those is a place where writing the document back out
19//! and reading it again would otherwise land somewhere new.
20//!
21//! Raw HTML is read as the text it spells. Footnotes and reference-style link
22//! definitions are not carried. A setext heading is read as a heading and
23//! serialized with `#`.
24
25use pulldown_cmark::{
26 Alignment, BlockQuoteKind, CodeBlockKind, Event, LinkType, Options, Parser, Tag, TagEnd,
27};
28use std::ops::Range;
29
30use crate::{
31 doc::{Align, Block, BlockKind, Doc, Form, Mark, MarkSpan, QuoteKind, Quoted, Text},
32 marks::Marks,
33 select::Cursor,
34};
35
36mod inline;
37mod lift;
38mod state;
39
40pub use inline::*;
41pub use lift::*;
42use state::*;
43
44/// A word and a size after it, as a fence's info string and a link's title
45/// spell them: `mermaid 360`, `embed 360`. A last word that is not a positive
46/// whole number is part of the first.
47pub(crate) fn split_size(spelled: &str) -> (&str, Option<u32>) {
48 let Some((name, tail)) = spelled.rsplit_once(char::is_whitespace) else {
49 return (spelled, None);
50 };
51 match tail.parse().ok().filter(|size| *size > 0) {
52 Some(size) => (name.trim_end(), Some(size)),
53 None => (spelled, None),
54 }
55}
56
57/// The extensions this crate reads. [`crate::source`] colours with the same set.
58pub(crate) const OPTIONS: Options = Options::ENABLE_TABLES
59 .union(Options::ENABLE_STRIKETHROUGH)
60 .union(Options::ENABLE_TASKLISTS)
61 .union(Options::ENABLE_GFM);
62
63impl From<BlockQuoteKind> for QuoteKind {
64 fn from(kind: BlockQuoteKind) -> Self {
65 match kind {
66 BlockQuoteKind::Note => Self::Note,
67 BlockQuoteKind::Tip => Self::Tip,
68 BlockQuoteKind::Important => Self::Important,
69 BlockQuoteKind::Warning => Self::Warning,
70 BlockQuoteKind::Caution => Self::Caution,
71 }
72 }
73}
74
75/// Parse a markdown document.
76pub fn parse(source: &str) -> Doc {
77 parse_plain(source)
78}
79
80/// A parse, and where in the source each block came from.
81pub struct ParsedDoc {
82 pub doc: Doc,
83 /// One range per `doc.blocks` entry, in document order.
84 ///
85 /// The ranges partition the source: the first starts at 0, each one ends
86 /// where the next begins, and the last ends at `source.len()`. Splicing
87 /// them back in order reproduces the source byte for byte.
88 ///
89 /// A block the source spells inside another's bytes — the empty bullet of
90 /// `- ` — takes an empty range. The one per entry holds
91 /// either way.
92 pub block_ranges: Vec<Range<usize>>,
93}
94
95/// [`parse`], keeping the source range each block was parsed from.
96pub fn parse_ranges(source: &str) -> ParsedDoc {
97 parse_ranges_with(source, &Marks::default())
98}
99
100/// [`parse_ranges`] with the app's own marks — see [`parse_with`].
101pub fn parse_ranges_with(source: &str, marks: &Marks) -> ParsedDoc {
102 if marks.is_empty() {
103 let (doc, starts) = parse_spanned(source);
104 return ParsedDoc {
105 block_ranges: ranges(&starts, source.len()),
106 doc,
107 };
108 }
109 let (lifted, aligned) = lift(source, marks);
110 let (mut doc, starts) = parse_spanned(&lifted);
111 settle_all(&mut doc, marks);
112 let starts: Vec<usize> = starts.iter().map(|&at| unlift(&aligned, at)).collect();
113 ParsedDoc {
114 block_ranges: ranges(&starts, source.len()),
115 doc,
116 }
117}
118
119impl From<&str> for Doc {
120 fn from(source: &str) -> Self {
121 parse(source)
122 }
123}
124
125impl From<&str> for ParsedDoc {
126 fn from(source: &str) -> Self {
127 parse_ranges(source)
128 }
129}
130
131impl From<(&str, &Marks)> for Doc {
132 fn from((source, marks): (&str, &Marks)) -> Self {
133 parse_with(source, marks)
134 }
135}
136
137/// Block starts, in document order, to one range each.
138///
139/// Each block runs to where the next one starts, so the partition is the
140/// shape of the loop rather than something the parser has to get right: the
141/// first range opens at 0, the last closes at `len`, and a start that arrives
142/// behind the one before it takes an empty range instead of a backwards one.
143fn ranges(starts: &[usize], len: usize) -> Vec<Range<usize>> {
144 let mut out = Vec::with_capacity(starts.len());
145 let mut at = 0;
146 for &start in starts.iter().skip(1) {
147 let start = start.clamp(at, len);
148 out.push(at..start);
149 at = start;
150 }
151 if !starts.is_empty() {
152 out.push(at..len);
153 }
154 out
155}
156
157/// [`parse`] with the app's own marks — see [`crate::Marks`].
158///
159/// Registered delimiters are lifted out of the source *before* CommonMark sees
160/// it, which is the only place the difference between `==` and `\=\=` still
161/// exists: a backslash escape is gone by the time there is a [`Text`] to scan,
162/// and a pass over one would read an escaped delimiter back as a mark and move
163/// the document on every save.
164pub fn parse_with(source: &str, marks: &Marks) -> Doc {
165 if marks.is_empty() {
166 return parse_plain(source);
167 }
168 let mut doc = parse_plain(&lift(source, marks).0);
169 settle_all(&mut doc, marks);
170 doc
171}
172
173/// Turn every lifted mark in `doc` back into the mark it stands for.
174fn settle_all(doc: &mut Doc, marks: &Marks) {
175 for block in &mut doc.blocks {
176 for part in block.parts() {
177 if let Some(text) = block.text_at_mut(part) {
178 settle(text, marks);
179 }
180 }
181 }
182}
183
184fn parse_plain(source: &str) -> Doc {
185 parse_spanned(source).0
186}
187
188/// The parse every entry point runs, with the offset each block started at.
189///
190/// `renumber` rewrites numbers and adds no block, so the starts stay one per
191/// block — the invariant [`ParsedDoc::block_ranges`] rests on.
192fn parse_spanned(source: &str) -> (Doc, Vec<usize>) {
193 let mut state = ParseState::default();
194 for (event, range) in Parser::new_ext(source, OPTIONS).into_offset_iter() {
195 state.event(event, range);
196 }
197 state.doc.renumber();
198 (state.doc, state.starts)
199}
200
201/// Accumulates one run of inline content and the marks over it.
202#[derive(Default)]
203struct TextBuilder {
204 text: String,
205 marks: Vec<MarkSpan>,
206 /// Indices into `marks` for the marks still open, innermost last.
207 open: Vec<usize>,
208}
209
210impl TextBuilder {
211 /// Open a mark at the cursor. Marks land in the list in the order they
212 /// open, which is outermost first — the ordering [`crate::serialize`] reads
213 /// back to reproduce the nesting.
214 fn open(&mut self, mark: Mark) {
215 let ix = self.marks.len();
216 let at = self.text.len();
217 self.marks.push(MarkSpan {
218 range: at..at,
219 mark,
220 });
221 self.open.push(ix);
222 }
223
224 /// Whether anything at all has accumulated — an image with no alt text is
225 /// a mark and no text, and still has to close as a block.
226 fn is_empty(&self) -> bool {
227 self.text.is_empty() && self.marks.is_empty()
228 }
229
230 fn close(&mut self) {
231 if let Some(ix) = self.open.pop() {
232 self.marks[ix].range.end = self.text.len();
233 }
234 }
235
236 /// A mark that opens and closes around `s` in one event (inline code).
237 fn wrap(&mut self, mark: Mark, s: &str) {
238 let start = self.text.len();
239 self.text.push_str(s);
240 self.marks.push(MarkSpan {
241 range: start..self.text.len(),
242 mark,
243 });
244 }
245
246 fn take(&mut self) -> Text {
247 self.open.clear();
248 let mut text = normalize(
249 &std::mem::take(&mut self.text),
250 &std::mem::take(&mut self.marks),
251 );
252 settle_mentions(&mut text);
253 linkify(&mut text);
254 text
255 }
256}