Skip to main content

documents/markdown/
tree.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! What a preview carries: a Markdown window read as blocks and inline
7//! marks (`crates/documents/Spec.lean` D21).
8//!
9//! Data, never HTML. A page draws each variant as an element of its own,
10//! so no character a model or a resident wrote is handed to `innerHTML`.
11//! What the grammar reads but a page does not draw is [`Block::Unsupported`]
12//! or [`Inline::Unsupported`] with its source text, which is a different
13//! shape from an empty block (D22).
14
15use serde::{Deserialize, Serialize};
16
17use crate::encoding::Encoding;
18use crate::span::Span;
19
20/// One window of a Markdown version, laid out, or the reason it was not.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
24pub enum Preview {
25    Laid(Laid),
26    /// The version is text in an encoding whose bytes do not line up with
27    /// the text read from them, so no block could name its bytes (D25).
28    Unsupported {
29        encoding: Encoding,
30    },
31}
32
33/// A stretch of Markdown that was read, and its blocks: what a preview
34/// and a reply both answer (D32). An empty stretch has none.
35#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
37pub struct Laid {
38    pub span: Span,
39    pub blocks: Vec<Block>,
40}
41
42/// Whether a model's reply is still arriving (D30).
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
46pub enum ReplyState {
47    /// More text may follow, so only the blocks before the closure point
48    /// are read.
49    Streaming,
50    /// The call settled, so the whole text is read, as a version holding
51    /// it would be previewed.
52    Settled,
53}
54
55/// One block of a window, with the bytes of the version it was read from
56/// (D26).
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
60pub enum Block {
61    /// `level` is 1 to 6.
62    Heading {
63        span: Span,
64        level: u8,
65        inline: Vec<Inline>,
66    },
67    Paragraph {
68        span: Span,
69        inline: Vec<Inline>,
70    },
71    List {
72        span: Span,
73        order: Order,
74        spacing: Spacing,
75        items: Vec<ListItem>,
76    },
77    Quote {
78        span: Span,
79        blocks: Vec<Block>,
80    },
81    /// A fenced or indented code block; `info` is the fence's info string,
82    /// empty for an indented block, and `text` may be empty.
83    Code {
84        span: Span,
85        info: String,
86        text: String,
87    },
88    Table {
89        span: Span,
90        align: Vec<Align>,
91        head: Row,
92        body: Vec<Row>,
93    },
94    /// A thematic break.
95    Rule {
96        span: Span,
97    },
98    /// A footnote's definition, where the source put it.
99    Footnote {
100        span: Span,
101        name: String,
102        blocks: Vec<Block>,
103    },
104    /// Read but not drawn: the page shows `source`, the block's own text.
105    Unsupported {
106        span: Span,
107        construct: Construct,
108        source: String,
109    },
110}
111
112/// One item of a list, with the blocks inside it.
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
115pub struct ListItem {
116    pub span: Span,
117    pub check: Check,
118    pub blocks: Vec<Block>,
119}
120
121/// One row of a table: one run of inline marks per cell.
122#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
123#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
124pub struct Row {
125    pub cells: Vec<Vec<Inline>>,
126}
127
128/// One inline mark. Inline marks carry no span: the readers of today
129/// line a preview up with its source block by block (D26).
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
131#[serde(rename_all = "snake_case")]
132#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
133pub enum Inline {
134    Text(String),
135    Code(String),
136    Emphasis(Vec<Inline>),
137    Strong(Vec<Inline>),
138    Strikethrough(Vec<Inline>),
139    /// A target the grammar admits: relative, `http`, `https` or `mailto`
140    /// (D23). `title` is empty when the source gave none.
141    Link {
142        target: String,
143        title: String,
144        content: Vec<Inline>,
145    },
146    /// An image whose target the grammar admits, with its alternative text.
147    Image {
148        target: String,
149        title: String,
150        alt: String,
151    },
152    FootnoteReference {
153        name: String,
154    },
155    SoftBreak,
156    LineBreak,
157    /// Read but not drawn: the page shows `source`.
158    Unsupported {
159        construct: Construct,
160        source: String,
161    },
162}
163
164/// Whether a list counts its items, and from where.
165#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
166#[serde(rename_all = "snake_case")]
167#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
168pub enum Order {
169    Bullet,
170    Ordered { start: u64 },
171}
172
173/// Whether a list's items are paragraphs apart or one line apart.
174#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
175#[serde(rename_all = "snake_case")]
176#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
177pub enum Spacing {
178    Tight,
179    Loose,
180}
181
182/// Whether an item is a task, and whether it is done.
183#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
184#[serde(rename_all = "snake_case")]
185#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
186pub enum Check {
187    NotATask,
188    Open,
189    Done,
190}
191
192/// How a table column is aligned.
193#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
194#[serde(rename_all = "snake_case")]
195#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
196pub enum Align {
197    None,
198    Left,
199    Center,
200    Right,
201}
202
203/// What a page is shown the source of instead of a drawing (D22, D24).
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
205#[serde(rename_all = "snake_case")]
206#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
207pub enum Construct {
208    /// Raw HTML, a block or a tag.
209    Html,
210    /// A formula between dollar signs.
211    Math,
212    /// The metadata between two `---` lines at the top of a document.
213    FrontMatter,
214    /// A container deeper than `NESTING_MAX` levels, with all it holds.
215    Nesting,
216    /// A node of a comrak extension this grammar does not turn on; it
217    /// appears only if the grammar's options change.
218    Extension,
219}