Skip to main content

markdown/
doc.rs

1//! The document model.
2//!
3//! A [`Doc`] is a **flat** list of [`Block`]s with an indent level, not a
4//! nested tree. That is Notion's model rather than CommonMark's, and it is the
5//! decision the rest of this crate hangs off: editing a flat list means Enter
6//! splits, Backspace merges, and Tab indents — all list operations. On a
7//! nested tree "the previous block" is a traversal and every edit is a
8//! restructure.
9//!
10//! The trade is that arbitrarily nested CommonMark does not survive a round
11//! trip: a list inside a quote inside a list flattens. Notion has the same
12//! limitation. What is guaranteed is [`crate::serialize`]'s fixed point —
13//! parse, serialize, parse again, and the document is unchanged — so an
14//! edit/save cycle never drifts.
15
16use std::ops::Range;
17
18/// A markdown document: blocks in document order.
19#[derive(Debug, Clone, Default, PartialEq, Eq)]
20pub struct Doc {
21    pub blocks: Vec<Block>,
22}
23
24impl Doc {
25    /// Make each run of ordered items consecutive.
26    ///
27    /// Markdown honours only the *first* number in a list — `1.` followed by `9.`
28    /// renders as 1, 2. Two source lists that used different delimiters (`1.` then
29    /// `9)`) are separate lists, but a flat document has no list identity to
30    /// preserve, so they serialize as one and the second item's number would move
31    /// on the next read. Deciding it here means the document already holds what the
32    /// next parse would produce.
33    pub(crate) fn renumber(&mut self) {
34        // The number owed to the next ordered item at each indent level. A run
35        // survives blocks nested under it and ends at anything else.
36        let mut expected: Vec<Option<u64>> = Vec::new();
37        for block in &mut self.blocks {
38            let indent = block.indent as usize;
39            expected.truncate(indent + 1);
40            expected.resize(indent + 1, None);
41
42            if let BlockKind::Ordered { number, .. } = &mut block.kind {
43                if let Some(next) = expected[indent] {
44                    *number = next;
45                }
46                expected[indent] = Some(number.saturating_add(1));
47            } else {
48                expected[indent] = None;
49            }
50        }
51    }
52}
53
54/// One block, and how deeply it is nested.
55///
56/// `indent` obeys one invariant, established by the parser and relied on by
57/// the serializer: the first block is at 0, and no block is more than one
58/// level deeper than the block before it. A document that satisfies it always
59/// serializes to markdown that parses back to the same indents.
60#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61pub struct Block {
62    pub kind: BlockKind,
63    pub indent: u8,
64}
65
66impl From<BlockKind> for Block {
67    fn from(kind: BlockKind) -> Self {
68        Self::new(kind)
69    }
70}
71
72impl Block {
73    pub fn new(kind: BlockKind) -> Self {
74        Self { kind, indent: 0 }
75    }
76
77    pub fn at(kind: BlockKind, indent: u8) -> Self {
78        Self { kind, indent }
79    }
80
81    /// One of the block's editable texts. `None` when the block has no such
82    /// part — every block is atomic to some [`Part`], and bookmarks and rules
83    /// are atomic to all of them.
84    pub fn text_at(&self, part: Part) -> Option<&Text> {
85        match (&self.kind, part) {
86            (
87                BlockKind::Paragraph(text)
88                | BlockKind::Heading { text, .. }
89                | BlockKind::Bullet(text)
90                | BlockKind::Ordered { text, .. }
91                | BlockKind::Task { text, .. }
92                | BlockKind::Quote { text, .. },
93                Part::Body,
94            ) => Some(text),
95            (BlockKind::Code { code, .. }, Part::Code) => Some(code),
96            (BlockKind::Image { alt, .. }, Part::Caption) => Some(alt),
97            (BlockKind::Table { header, .. }, Part::Cell { row: 0, column }) => header.get(column),
98            (BlockKind::Table { rows, .. }, Part::Cell { row, column }) => {
99                rows.get(row - 1)?.get(column)
100            }
101            _ => None,
102        }
103    }
104
105    pub fn text_at_mut(&mut self, part: Part) -> Option<&mut Text> {
106        match (&mut self.kind, part) {
107            (
108                BlockKind::Paragraph(text)
109                | BlockKind::Heading { text, .. }
110                | BlockKind::Bullet(text)
111                | BlockKind::Ordered { text, .. }
112                | BlockKind::Task { text, .. }
113                | BlockKind::Quote { text, .. },
114                Part::Body,
115            ) => Some(text),
116            (BlockKind::Code { code, .. }, Part::Code) => Some(code),
117            (BlockKind::Image { alt, .. }, Part::Caption) => Some(alt),
118            (BlockKind::Table { header, .. }, Part::Cell { row: 0, column }) => {
119                header.get_mut(column)
120            }
121            (BlockKind::Table { rows, .. }, Part::Cell { row, column }) => {
122                rows.get_mut(row - 1)?.get_mut(column)
123            }
124            _ => None,
125        }
126    }
127
128    /// Every part a caret can sit in, in document order.
129    pub fn parts(&self) -> Vec<Part> {
130        match &self.kind {
131            BlockKind::Paragraph(_)
132            | BlockKind::Heading { .. }
133            | BlockKind::Bullet(_)
134            | BlockKind::Ordered { .. }
135            | BlockKind::Task { .. }
136            | BlockKind::Quote { .. } => vec![Part::Body],
137            BlockKind::Code { .. } => vec![Part::Code],
138            BlockKind::Image { .. } => vec![Part::Caption],
139            BlockKind::Table { header, rows, .. } => {
140                let mut parts = Vec::new();
141                if !header.is_empty() {
142                    parts.extend((0..header.len()).map(|column| Part::Cell { row: 0, column }));
143                }
144                for (ix, row) in rows.iter().enumerate() {
145                    parts.extend((0..row.len()).map(|column| Part::Cell {
146                        row: ix + 1,
147                        column,
148                    }));
149                }
150                parts
151            }
152            BlockKind::Bookmark { .. } | BlockKind::Rule => Vec::new(),
153        }
154    }
155
156    /// Whether what the block paints past its parts holds no caret — a picture,
157    /// a card, a line. What a selection has to wash for itself, since there is
158    /// no text under it to carry the highlight.
159    pub fn opaque(&self) -> bool {
160        matches!(
161            self.kind,
162            BlockKind::Image { .. } | BlockKind::Bookmark { .. } | BlockKind::Rule
163        )
164    }
165}
166
167/// Which of a block's texts a caret sits in.
168///
169/// A block has one kind of part and never a mix — prose blocks have a body, a
170/// code block has its code, a table has cells — so this is a coordinate rather
171/// than a path, and the model stays flat. The ordering is document order, which
172/// is what makes a [`crate::Cursor`] comparable and therefore what makes a
173/// selection a range.
174#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
175pub enum Part {
176    #[default]
177    Body,
178    Code,
179    /// An image's caption, which is also its alt text.
180    Caption,
181    /// Row 0 is the header row; row `n` is `rows[n - 1]`.
182    Cell {
183        row: usize,
184        column: usize,
185    },
186}
187
188/// The block vocabulary. Closed by design — a consumer that needs a block of
189/// its own is a reason to widen this enum rather than to grow an extension
190/// system.
191#[derive(Debug, Clone, PartialEq, Eq, Hash)]
192pub enum BlockKind {
193    Paragraph(Text),
194    Heading {
195        /// 1–6.
196        level: u8,
197        text: Text,
198    },
199    Bullet(Text),
200    Ordered {
201        /// The rendered number. Stored rather than derived so a list starting
202        /// at 3 survives the round trip.
203        number: u64,
204        text: Text,
205    },
206    Task {
207        checked: bool,
208        text: Text,
209    },
210    /// A blockquote. `kind` is the GFM alert it opens with — see [`QuoteKind`].
211    Quote {
212        kind: Option<QuoteKind>,
213        text: Text,
214    },
215    /// The code carries a [`Text`] like every other editable region, so one
216    /// accessor and one edit path cover the whole document. Its marks are
217    /// unreachable rather than forbidden: nothing that writes here creates one.
218    Code {
219        language: Option<String>,
220        code: Text,
221        /// The height a painted fence stands at, in whole pixels, from a drag
222        /// off its handle in `bezel-editor`. `None` is the block's own height.
223        /// Spelled as the info string's second word: ` ```mermaid 360 `.
224        height: Option<u32>,
225    },
226    /// The caption is the alt text — markdown has one slot, and a reader that
227    /// cannot see the picture reads the same words. Like [`BlockKind::Code`]'s,
228    /// its marks are unreachable rather than forbidden.
229    Image {
230        url: String,
231        alt: Text,
232        /// A drag off the handle in `bezel-editor`, in whole pixels — `None` is
233        /// the natural width. `u32` rather than a float: this derives `Eq`, and
234        /// a `f32` cannot because of NaN. Spelled `![alt|480](url)`, which
235        /// leaves the title slot to say what a title says.
236        width: Option<u32>,
237    },
238    /// A link with a block to itself, painted richly. Atomic on purpose:
239    /// everything it shows past the URL comes from [`crate::preview`], so there
240    /// is nothing here for a caret to edit.
241    ///
242    /// [`Form`] picks a card or a card with its picture across the width;
243    /// [`Form::Chip`] paints as the card. A chip is a [`Mark::Mention`], on a
244    /// line of its own or not.
245    Bookmark {
246        url: String,
247        form: Form,
248    },
249    Table {
250        align: Vec<Align>,
251        header: Vec<Text>,
252        rows: Vec<Vec<Text>>,
253    },
254    Rule,
255}
256
257/// A GFM alert's kind — the `[!NOTE]` marker a blockquote opens with.
258///
259/// Only recognised when the marker is alone on the quote's first line and
260/// names one of these five; anything else stays the text it was written as.
261/// A blockquote holding two paragraphs becomes two [`BlockKind::Quote`] blocks
262/// and each carries the kind, so writing the document back gives two alerts.
263#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
264pub enum QuoteKind {
265    Note,
266    Tip,
267    Important,
268    Warning,
269    Caution,
270}
271
272impl QuoteKind {
273    /// The marker line, bracket to bracket.
274    pub fn marker(self) -> &'static str {
275        match self {
276            Self::Note => "[!NOTE]",
277            Self::Tip => "[!TIP]",
278            Self::Important => "[!IMPORTANT]",
279            Self::Warning => "[!WARNING]",
280            Self::Caution => "[!CAUTION]",
281        }
282    }
283
284    /// What the alert calls itself where it is painted.
285    pub fn label(self) -> &'static str {
286        match self {
287            Self::Note => "Note",
288            Self::Tip => "Tip",
289            Self::Important => "Important",
290            Self::Warning => "Warning",
291            Self::Caution => "Caution",
292        }
293    }
294}
295
296/// GFM column alignment.
297#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
298pub enum Align {
299    #[default]
300    Left,
301    Center,
302    Right,
303}
304
305/// Inline content: a string, plus marks over byte ranges of it.
306///
307/// Marks are a separate list rather than flags on a run because an editor has
308/// to *map* them through insertions and deletions, and because run flags lose
309/// nesting order — under flags `**_x_**` and `_**x**_` are the same value.
310/// Here they differ by the order of the two spans, and both survive a round
311/// trip.
312///
313/// A newline in `text` is a line break within the block (markdown's soft or
314/// hard break, which this model does not distinguish — neither does Notion).
315/// Whether it paints as a break or a space is a rendering decision.
316#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
317pub struct Text {
318    pub text: String,
319    /// Outermost first. Ranges may overlap and may be identical.
320    pub marks: Vec<MarkSpan>,
321}
322
323impl From<&str> for Text {
324    fn from(text: &str) -> Self {
325        Self::plain(text)
326    }
327}
328
329impl From<String> for Text {
330    fn from(text: String) -> Self {
331        Self::plain(text)
332    }
333}
334
335impl Text {
336    /// Unmarked text.
337    pub fn plain(text: impl Into<String>) -> Self {
338        Self {
339            text: text.into(),
340            marks: Vec::new(),
341        }
342    }
343
344    /// A URL that links to itself — what a pasted link is, and what a bookmark
345    /// hands back when it turns into prose.
346    pub fn link(url: &str) -> Self {
347        Self {
348            text: url.to_string(),
349            marks: vec![MarkSpan {
350                range: 0..url.len(),
351                mark: Mark::Link(url.to_string()),
352            }],
353        }
354    }
355
356    pub fn is_empty(&self) -> bool {
357        self.text.is_empty()
358    }
359
360    /// The mention `offset` falls strictly inside. A caret has no place there:
361    /// the mention shows its preview, not its text.
362    pub(crate) fn mention_around(&self, offset: usize) -> Option<Range<usize>> {
363        self.marks
364            .iter()
365            .find(|span| {
366                matches!(span.mark, Mark::Mention { .. })
367                    && span.range.start < offset
368                    && offset < span.range.end
369            })
370            .map(|span| span.range.clone())
371    }
372
373    /// Whether no other mark overlaps the one at `ix`.
374    ///
375    /// A mark written whole — a code span, a mention — leaves no room inside
376    /// itself for another mark's boundary, so this is what decides whether it
377    /// can be spelled its own way at all.
378    pub(crate) fn alone(&self, ix: usize) -> bool {
379        let span = &self.marks[ix].range;
380        self.marks.iter().enumerate().all(|(other, mark)| {
381            other == ix || mark.range.end <= span.start || mark.range.start >= span.end
382        })
383    }
384}
385
386#[derive(Debug, Clone, PartialEq, Eq, Hash)]
387pub struct MarkSpan {
388    pub range: Range<usize>,
389    pub mark: Mark,
390}
391
392/// How a [`Mark::Mention`] was written down, which is also how it paints.
393///
394/// `Auto` is the shorthand `<https://x>`: a chip in a sentence, a card on a
395/// line of its own. It is a variant rather than a resolved form because the
396/// spelling is what has to survive the round trip — resolve it at parse and
397/// every `<url>` grows brackets the first time the file is saved.
398///
399/// The other two are CommonMark's title slot, `[url](url "chip")`, which is
400/// core, ignored by every other renderer, and the only place left to say what
401/// the shorthand cannot: a chip alone on a line, and the bigger card.
402/// [`Form::Chip`] stays inline wherever it is.
403#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
404pub enum Form {
405    Auto,
406    Chip,
407    /// The bigger card, at a height in whole pixels from a drag off its
408    /// handle in `bezel-editor`, or its own height for `None`. Spelled
409    /// `"embed 360"`.
410    Embed(Option<u32>),
411}
412
413impl Form {
414    /// The title that spells this form, and `None` for the shorthand.
415    pub(crate) fn title(self) -> Option<String> {
416        match self {
417            Self::Auto => None,
418            Self::Chip => Some("chip".to_owned()),
419            Self::Embed(None) => Some("embed".to_owned()),
420            Self::Embed(Some(height)) => Some(format!("embed {height}")),
421        }
422    }
423
424    pub(crate) fn from_title(title: &str) -> Option<Self> {
425        let (name, height) = crate::parse::split_size(title);
426        match (name, height) {
427            ("chip", None) => Some(Self::Chip),
428            ("embed", height) => Some(Self::Embed(height)),
429            _ => None,
430        }
431    }
432}
433
434#[derive(Debug, Clone, PartialEq, Eq, Hash)]
435pub enum Mark {
436    Bold,
437    Italic,
438    Strike,
439    Code,
440    Link(String),
441    /// A link painted richly rather than as underlined text — a chip inline, a
442    /// [`BlockKind::Bookmark`] with a block to itself.
443    ///
444    /// The chip shows a favicon and the [`crate::preview`] title in place of
445    /// its text, and opens the card on hover. Its text is atomic: a caret sits
446    /// at either end, never inside, and typing at its end falls outside it.
447    Mention {
448        url: String,
449        form: Form,
450    },
451    /// A mark the app spells itself — underline, a highlight, a colour. The
452    /// name is [`crate::Marks`]'s, and the delimiter that writes it comes from
453    /// the same registry.
454    Custom(String),
455    /// An image among text. [`BlockKind::Image`] is the shape an editor offers;
456    /// this is what keeps `see ![x](u) here` from silently becoming a link when
457    /// the document is saved. No width: one among text has no box of its own to
458    /// resize, and the `|480` that would say so stays the ordinary text it is.
459    Image(String),
460}