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