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)]
61pub struct Block {
62    pub kind: BlockKind,
63    pub indent: u8,
64}
65
66impl Block {
67    pub fn new(kind: BlockKind) -> Self {
68        Self { kind, indent: 0 }
69    }
70
71    pub fn at(kind: BlockKind, indent: u8) -> Self {
72        Self { kind, indent }
73    }
74
75    /// One of the block's editable texts. `None` when the block has no such
76    /// part — every block is atomic to some [`Part`], and bookmarks and rules
77    /// are atomic to all of them.
78    pub fn text_at(&self, part: Part) -> Option<&Text> {
79        match (&self.kind, part) {
80            (
81                BlockKind::Paragraph(text)
82                | BlockKind::Heading { text, .. }
83                | BlockKind::Bullet(text)
84                | BlockKind::Ordered { text, .. }
85                | BlockKind::Task { text, .. }
86                | BlockKind::Quote { text, .. },
87                Part::Body,
88            ) => Some(text),
89            (BlockKind::Code { code, .. }, Part::Code) => Some(code),
90            (BlockKind::Image { alt, .. }, Part::Caption) => Some(alt),
91            (BlockKind::Table { header, .. }, Part::Cell { row: 0, column }) => header.get(column),
92            (BlockKind::Table { rows, .. }, Part::Cell { row, column }) => {
93                rows.get(row - 1)?.get(column)
94            }
95            _ => None,
96        }
97    }
98
99    pub fn text_at_mut(&mut self, part: Part) -> Option<&mut Text> {
100        match (&mut self.kind, part) {
101            (
102                BlockKind::Paragraph(text)
103                | BlockKind::Heading { text, .. }
104                | BlockKind::Bullet(text)
105                | BlockKind::Ordered { text, .. }
106                | BlockKind::Task { text, .. }
107                | BlockKind::Quote { text, .. },
108                Part::Body,
109            ) => Some(text),
110            (BlockKind::Code { code, .. }, Part::Code) => Some(code),
111            (BlockKind::Image { alt, .. }, Part::Caption) => Some(alt),
112            (BlockKind::Table { header, .. }, Part::Cell { row: 0, column }) => {
113                header.get_mut(column)
114            }
115            (BlockKind::Table { rows, .. }, Part::Cell { row, column }) => {
116                rows.get_mut(row - 1)?.get_mut(column)
117            }
118            _ => None,
119        }
120    }
121
122    /// Every part a caret can sit in, in document order.
123    pub fn parts(&self) -> Vec<Part> {
124        match &self.kind {
125            BlockKind::Paragraph(_)
126            | BlockKind::Heading { .. }
127            | BlockKind::Bullet(_)
128            | BlockKind::Ordered { .. }
129            | BlockKind::Task { .. }
130            | BlockKind::Quote { .. } => vec![Part::Body],
131            BlockKind::Code { .. } => vec![Part::Code],
132            BlockKind::Image { .. } => vec![Part::Caption],
133            BlockKind::Table { header, rows, .. } => {
134                let mut parts = Vec::new();
135                if !header.is_empty() {
136                    parts.extend((0..header.len()).map(|column| Part::Cell { row: 0, column }));
137                }
138                for (ix, row) in rows.iter().enumerate() {
139                    parts.extend((0..row.len()).map(|column| Part::Cell {
140                        row: ix + 1,
141                        column,
142                    }));
143                }
144                parts
145            }
146            BlockKind::Bookmark { .. } | BlockKind::Rule => Vec::new(),
147        }
148    }
149
150    /// Whether what the block paints past its parts holds no caret — a picture,
151    /// a card, a line. What a selection has to wash for itself, since there is
152    /// no text under it to carry the highlight.
153    pub fn opaque(&self) -> bool {
154        matches!(
155            self.kind,
156            BlockKind::Image { .. } | BlockKind::Bookmark { .. } | BlockKind::Rule
157        )
158    }
159}
160
161/// Which of a block's texts a caret sits in.
162///
163/// A block has one kind of part and never a mix — prose blocks have a body, a
164/// code block has its code, a table has cells — so this is a coordinate rather
165/// than a path, and the model stays flat. The ordering is document order, which
166/// is what makes a [`crate::Cursor`] comparable and therefore what makes a
167/// selection a range.
168#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
169pub enum Part {
170    #[default]
171    Body,
172    Code,
173    /// An image's caption, which is also its alt text.
174    Caption,
175    /// Row 0 is the header row; row `n` is `rows[n - 1]`.
176    Cell {
177        row: usize,
178        column: usize,
179    },
180}
181
182/// The block vocabulary. Closed by design — a consumer that needs a block of
183/// its own is a reason to widen this enum rather than to grow an extension
184/// system.
185#[derive(Debug, Clone, PartialEq, Eq)]
186pub enum BlockKind {
187    Paragraph(Text),
188    Heading {
189        /// 1–6.
190        level: u8,
191        text: Text,
192    },
193    Bullet(Text),
194    Ordered {
195        /// The rendered number. Stored rather than derived so a list starting
196        /// at 3 survives the round trip.
197        number: u64,
198        text: Text,
199    },
200    Task {
201        checked: bool,
202        text: Text,
203    },
204    /// A blockquote. `kind` is the GFM alert it opens with — see [`QuoteKind`].
205    Quote {
206        kind: Option<QuoteKind>,
207        text: Text,
208    },
209    /// The code carries a [`Text`] like every other editable region, so one
210    /// accessor and one edit path cover the whole document. Its marks are
211    /// unreachable rather than forbidden: nothing that writes here creates one.
212    Code {
213        language: Option<String>,
214        code: Text,
215    },
216    /// The caption is the alt text — markdown has one slot, and a reader that
217    /// cannot see the picture reads the same words. Like [`BlockKind::Code`]'s,
218    /// its marks are unreachable rather than forbidden.
219    Image {
220        url: String,
221        alt: Text,
222        /// A drag off the handle in `bezel-editor`, in whole pixels — `None` is
223        /// the natural width. `u32` rather than a float: this derives `Eq`, and
224        /// a `f32` cannot because of NaN. Spelled `![alt|480](url)`, which
225        /// leaves the title slot to say what a title says.
226        width: Option<u32>,
227    },
228    /// A link with a block to itself, painted richly. Atomic on purpose:
229    /// everything it shows past the URL comes from [`crate::preview`], so there
230    /// is nothing here for a caret to edit.
231    ///
232    /// [`Form`] picks which of the three — a chip, a card, or a card with its
233    /// picture across the width. Off a line of its own the same link is a
234    /// [`Mark::Mention`], which is the same three minus what shaped text cannot
235    /// hold.
236    Bookmark {
237        url: String,
238        form: Form,
239    },
240    Table {
241        align: Vec<Align>,
242        header: Vec<Text>,
243        rows: Vec<Vec<Text>>,
244    },
245    Rule,
246}
247
248/// A GFM alert's kind — the `[!NOTE]` marker a blockquote opens with.
249///
250/// Only recognised when the marker is alone on the quote's first line and
251/// names one of these five; anything else stays the text it was written as.
252/// A blockquote holding two paragraphs becomes two [`BlockKind::Quote`] blocks
253/// and each carries the kind, so writing the document back gives two alerts.
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub enum QuoteKind {
256    Note,
257    Tip,
258    Important,
259    Warning,
260    Caution,
261}
262
263impl QuoteKind {
264    /// The marker line, bracket to bracket.
265    pub fn marker(self) -> &'static str {
266        match self {
267            Self::Note => "[!NOTE]",
268            Self::Tip => "[!TIP]",
269            Self::Important => "[!IMPORTANT]",
270            Self::Warning => "[!WARNING]",
271            Self::Caution => "[!CAUTION]",
272        }
273    }
274
275    /// What the alert calls itself where it is painted.
276    pub fn label(self) -> &'static str {
277        match self {
278            Self::Note => "Note",
279            Self::Tip => "Tip",
280            Self::Important => "Important",
281            Self::Warning => "Warning",
282            Self::Caution => "Caution",
283        }
284    }
285}
286
287/// GFM column alignment.
288#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
289pub enum Align {
290    #[default]
291    Left,
292    Center,
293    Right,
294}
295
296/// Inline content: a string, plus marks over byte ranges of it.
297///
298/// Marks are a separate list rather than flags on a run because an editor has
299/// to *map* them through insertions and deletions, and because run flags lose
300/// nesting order — under flags `**_x_**` and `_**x**_` are the same value.
301/// Here they differ by the order of the two spans, and both survive a round
302/// trip.
303///
304/// A newline in `text` is a line break within the block (markdown's soft or
305/// hard break, which this model does not distinguish — neither does Notion).
306/// Whether it paints as a break or a space is a rendering decision.
307#[derive(Debug, Clone, Default, PartialEq, Eq)]
308pub struct Text {
309    pub text: String,
310    /// Outermost first. Ranges may overlap and may be identical.
311    pub marks: Vec<MarkSpan>,
312}
313
314impl Text {
315    /// Unmarked text.
316    pub fn plain(text: impl Into<String>) -> Self {
317        Self {
318            text: text.into(),
319            marks: Vec::new(),
320        }
321    }
322
323    /// A URL that links to itself — what a pasted link is, and what a bookmark
324    /// hands back when it turns into prose.
325    pub fn link(url: &str) -> Self {
326        Self {
327            text: url.to_string(),
328            marks: vec![MarkSpan {
329                range: 0..url.len(),
330                mark: Mark::Link(url.to_string()),
331            }],
332        }
333    }
334
335    pub fn is_empty(&self) -> bool {
336        self.text.is_empty()
337    }
338
339    /// Whether no other mark overlaps the one at `ix`.
340    ///
341    /// A mark written whole — a code span, a mention — leaves no room inside
342    /// itself for another mark's boundary, so this is what decides whether it
343    /// can be spelled its own way at all.
344    pub(crate) fn alone(&self, ix: usize) -> bool {
345        let span = &self.marks[ix].range;
346        self.marks.iter().enumerate().all(|(other, mark)| {
347            other == ix || mark.range.end <= span.start || mark.range.start >= span.end
348        })
349    }
350}
351
352#[derive(Debug, Clone, PartialEq, Eq)]
353pub struct MarkSpan {
354    pub range: Range<usize>,
355    pub mark: Mark,
356}
357
358/// How a [`Mark::Mention`] was written down, which is also how it paints.
359///
360/// `Auto` is the shorthand `<https://x>`: a chip in a sentence, a card on a
361/// line of its own. It is a variant rather than a resolved form because the
362/// spelling is what has to survive the round trip — resolve it at parse and
363/// every `<url>` grows brackets the first time the file is saved.
364///
365/// The other two are CommonMark's title slot, `[url](url "chip")`, which is
366/// core, ignored by every other renderer, and the only place left to say what
367/// the shorthand cannot: a chip alone on a line, and the bigger card.
368#[derive(Debug, Clone, Copy, PartialEq, Eq)]
369pub enum Form {
370    Auto,
371    Chip,
372    Embed,
373}
374
375impl Form {
376    /// The title that spells this form, and `None` for the shorthand.
377    pub(crate) fn title(self) -> Option<&'static str> {
378        match self {
379            Self::Auto => None,
380            Self::Chip => Some("chip"),
381            Self::Embed => Some("embed"),
382        }
383    }
384
385    pub(crate) fn from_title(title: &str) -> Option<Self> {
386        match title {
387            "chip" => Some(Self::Chip),
388            "embed" => Some(Self::Embed),
389            _ => None,
390        }
391    }
392}
393
394#[derive(Debug, Clone, PartialEq, Eq)]
395pub enum Mark {
396    Bold,
397    Italic,
398    Strike,
399    Code,
400    Link(String),
401    /// A link painted richly rather than as underlined text — a chip inline, a
402    /// [`BlockKind::Bookmark`] with a block to itself.
403    ///
404    /// The chip shows the URL, because a [`Text`] is one string and every caret
405    /// offset is a byte into it: an inline atom painted wider or narrower than
406    /// the text under it has nowhere to put the offsets in between.
407    Mention {
408        url: String,
409        form: Form,
410    },
411    /// A mark the app spells itself — underline, a highlight, a colour. The
412    /// name is [`crate::Marks`]'s, and the delimiter that writes it comes from
413    /// the same registry.
414    Custom(String),
415    /// An image among text. [`BlockKind::Image`] is the shape an editor offers;
416    /// this is what keeps `see ![x](u) here` from silently becoming a link when
417    /// the document is saved. No width: one among text has no box of its own to
418    /// resize, and the `|480` that would say so stays the ordinary text it is.
419    Image(String),
420}