Skip to main content

mos_parse/
syntax.rs

1use std::path::PathBuf;
2
3use mos_core::{Diagnostic, Severity, SourceSpan};
4
5/// Concrete syntax tree for a single `.mos` source file.
6#[derive(Debug, Clone)]
7pub struct SyntaxTree {
8    pub file: PathBuf,
9    pub items: Vec<Item>,
10}
11
12/// Top-level construct in a `.mos` file.
13#[derive(Debug, Clone)]
14pub enum Item {
15    /// `= Title`, `== Subtitle`, `=== Subsubtitle`. A trailing
16    /// `<label>` token after the title attaches to this heading.
17    Heading {
18        level: u8,
19        inlines: Vec<Inline>,
20        label: Option<String>,
21        label_span: Option<SourceSpan>,
22        span: SourceSpan,
23    },
24    /// One or more consecutive non-blank lines that are not a heading
25    /// and not a `#set` block. A leading `<label>` token (possibly
26    /// preceded by ASCII whitespace) attaches to this paragraph.
27    Paragraph {
28        inlines: Vec<Inline>,
29        label: Option<String>,
30        label_span: Option<SourceSpan>,
31        span: SourceSpan,
32    },
33    /// `#set name(...)`, `#image(...)`, `#figure(...)`. The body is
34    /// lexed into typed `(key, value)` args; semantic validation
35    /// (known target/key, type coercion, sanity floors) happens in
36    /// the lowerer. `kind` distinguishes the `#set`-style configuration
37    /// directive from standalone calls like `#image` and `#figure`,
38    /// which the lowerer dispatches to dedicated paths.
39    Set {
40        kind: DirectiveKind,
41        name: String,
42        args: Vec<SetArg>,
43        span: SourceSpan,
44    },
45    /// Raw preformatted text or code block. Both forms preserve their
46    /// long-bracket body as text; the kind leaves room for later styling
47    /// or language-aware code rendering.
48    RawBlock {
49        kind: RawBlockKind,
50        args: Vec<SetArg>,
51        text: String,
52        label: Option<String>,
53        label_span: Option<SourceSpan>,
54        span: SourceSpan,
55    },
56    /// A bullet (`- `) or numbered (`\d+\. `) list. Sibling items at
57    /// the same indent are grouped under one list; deeper indents
58    /// become nested lists hanging off the most recent item. Numbered
59    /// lists always renumber from 1 in MVP: explicit `start: N` is
60    /// deferred.
61    List {
62        ordered: bool,
63        items: Vec<ListItem>,
64        span: SourceSpan,
65    },
66    /// A `/** … */` documentation comment. Unlike `//` line and `/*` block
67    /// comments (both dropped), a doc comment is preserved so the lowerer can
68    /// attach its `text` as a `doc` attribute on the semantic node it
69    /// precedes (a heading, or a labelled block), which the LSP surfaces on
70    /// hover. `text` is the cleaned inner body, fences removed.
71    DocComment { text: String, span: SourceSpan },
72}
73
74/// One entry inside an [`Item::List`].
75///
76/// `blocks` preserves source order between item paragraphs and nested lists;
77/// nested lists live only there. `inlines` mirrors the first paragraph for
78/// older consumers.
79#[derive(Debug, Clone)]
80pub struct ListItem {
81    pub inlines: Vec<Inline>,
82    pub blocks: Vec<ListItemBlock>,
83    pub span: SourceSpan,
84}
85
86/// Ordered content inside a [`ListItem`].
87#[derive(Debug, Clone)]
88pub enum ListItemBlock {
89    Paragraph {
90        inlines: Vec<Inline>,
91        span: SourceSpan,
92    },
93    List {
94        ordered: bool,
95        items: Vec<ListItem>,
96        span: SourceSpan,
97    },
98}
99
100/// Tag for the directive shapes [`Item::Set`] can represent.
101///
102/// Distinguishes `#set <target>(...)` from standalone `#image(...)`,
103/// `#figure(...)`, and `#bibliography(...)` calls so the lowerer does not
104/// infer semantics from [`Item::Set::name`].
105#[derive(Debug, Clone, Copy, Eq, PartialEq)]
106pub enum DirectiveKind {
107    /// `#set <name>(...)`: sets defaults on a style target.
108    Set,
109    /// `#image("path", ...)`: raster image directive.
110    Image,
111    /// `#figure(image: ..., caption: ...)`: captioned image container.
112    Figure,
113    /// `#bibliography("refs.bib")`: declares a bibliography source
114    /// database. The lowerer records the (source-relative) path so a
115    /// later BibTeX-parsing slice can read it; citation resolution and
116    /// rendering are not part of this directive.
117    Bibliography,
118}
119
120#[derive(Debug, Clone, Copy, Eq, PartialEq)]
121pub enum RawBlockKind {
122    Pre,
123    Code,
124}
125
126/// Borrowed view of an [`Item::RawBlock`] payload.
127#[derive(Debug, Clone, Copy)]
128pub struct RawBlockView<'a> {
129    pub kind: RawBlockKind,
130    pub args: &'a [SetArg],
131    pub text: &'a str,
132    pub label: Option<&'a str>,
133    pub label_span: Option<&'a SourceSpan>,
134    pub span: &'a SourceSpan,
135}
136
137/// One argument inside a directive body: either a `key: value`
138/// pair (the only form `#set` accepts) or a positional value (a
139/// leading string literal allowed on `#image(...)` / `#figure(...)`).
140///
141/// This used to be a struct with an empty-string `key` standing in
142/// for "positional," but that sentinel was a brittle public contract:
143/// any consumer that forgot the special-case would silently treat a
144/// positional path as a named arg called `""`. The enum form makes
145/// the two shapes explicit so the compiler can enforce exhaustive
146/// matches.
147#[derive(Debug, Clone)]
148pub enum SetArg {
149    /// A `key: value` argument. `key_span` covers the identifier
150    /// before the colon; `value_span` covers the literal.
151    Named {
152        key: String,
153        value: SetValue,
154        key_span: SourceSpan,
155        value_span: SourceSpan,
156    },
157    /// A leading positional value. The parser currently only accepts
158    /// string literals here (used for `#image("path.png")`); other
159    /// literal kinds in a positional slot would surface as a parse
160    /// error rather than land in this variant.
161    Positional {
162        value: SetValue,
163        value_span: SourceSpan,
164    },
165}
166
167impl SetArg {
168    /// Borrow the value carried by this argument, regardless of shape.
169    #[must_use]
170    pub const fn value(&self) -> &SetValue {
171        match self {
172            Self::Named { value, .. } | Self::Positional { value, .. } => value,
173        }
174    }
175
176    /// The span covering the argument's value literal.
177    #[must_use]
178    pub const fn value_span(&self) -> &SourceSpan {
179        match self {
180            Self::Named { value_span, .. } | Self::Positional { value_span, .. } => value_span,
181        }
182    }
183
184    /// The key identifier for [`Self::Named`]; `None` for
185    /// [`Self::Positional`].
186    #[must_use]
187    pub const fn key(&self) -> Option<&str> {
188        match self {
189            Self::Named { key, .. } => Some(key.as_str()),
190            Self::Positional { .. } => None,
191        }
192    }
193
194    /// The span covering the key identifier, for [`Self::Named`].
195    /// `None` for [`Self::Positional`].
196    #[must_use]
197    pub const fn key_span(&self) -> Option<&SourceSpan> {
198        match self {
199            Self::Named { key_span, .. } => Some(key_span),
200            Self::Positional { .. } => None,
201        }
202    }
203}
204
205/// Literal values recognised inside a `#set` body. Full expression
206/// evaluation (`#let`, function calls, `if`) is deferred to MVP 5; this
207/// covers what the manifest examples actually use.
208#[derive(Debug, Clone, PartialEq)]
209pub enum SetValue {
210    Str(String),
211    Int(i64),
212    Float(f64),
213    Length(f64, LengthUnit),
214    Ident(String),
215}
216
217#[derive(Debug, Clone, Copy, Eq, PartialEq)]
218pub enum LengthUnit {
219    Mm,
220    Pt,
221    Em,
222}
223
224/// Inline run produced by the markup tokenizer.
225#[derive(Debug, Clone)]
226pub struct Inline {
227    pub kind: InlineKind,
228    pub text: String,
229    pub span: SourceSpan,
230    /// For [`InlineKind::Reference`] / [`InlineKind::PageReference`], the
231    /// source span of the label *identifier* alone; the `intro` in `@intro`
232    /// or `@page(intro)`, excluding the `@` sigil and the `@page(`…`)`
233    /// wrapper. The lowerer stamps it as the node's `label_span` so editor
234    /// features (rename) read the identifier range directly instead of
235    /// re-deriving it from [`Self::span`] geometry. `None` for every other
236    /// inline kind.
237    pub label_span: Option<SourceSpan>,
238}
239
240#[derive(Debug, Clone, Copy, Eq, PartialEq)]
241pub enum InlineKind {
242    Text,
243    Emphasis,
244    Strong,
245    BoldItalic,
246    Code,
247    /// `@label`: a cross-reference to a labelled block. The
248    /// [`Inline::text`] payload is the bare label name (no leading
249    /// `@`); the resolver rewrites it to the target's resolved text.
250    Reference,
251    /// `@page(label)`: a reference to the printed *page number* of a
252    /// labelled target. The [`Inline::text`] payload is the bare label name
253    /// (the `page(` wrapper and `)` stripped). Distinct from
254    /// [`Reference`](Self::Reference), which resolves to the target's section
255    /// or figure number; a page reference resolves to where the target lands,
256    /// which is only known after layout. Resolution runs through the
257    /// resolve↔layout fixpoint (issue #72); this slice parses and models the
258    /// reference but leaves it unresolved (placeholder text).
259    PageReference,
260    /// `[@key]`: a citation to a bibliography entry. The
261    /// [`Inline::text`] payload is the bare citation key (no leading
262    /// `[@` or trailing `]`); bibliography loading and rendering are
263    /// future work tracked under MVP 4. The key alphabet matches the
264    /// label alphabet (`[A-Za-z0-9_:.-]`); a single key per
265    /// `[@…]` group is the only form recognised in this slice: list
266    /// forms like `[@a; @b]` and prefix/suffix bodies are deferred.
267    Citation,
268    /// `\\`: a forced line break inside a paragraph. The line
269    /// breaks here without the extra leading a blank-line paragraph
270    /// break would give. Carries no text payload. The shorthand for
271    /// a soft hyphen `\-` lowers to a literal U+00AD inside a
272    /// surrounding [`InlineKind::Text`] run, not to a separate variant.
273    HardBreak,
274}
275
276impl Item {
277    /// Borrow the heading payload if `self` is [`Item::Heading`].
278    #[must_use]
279    pub fn as_heading(&self) -> Option<(u8, &[Inline], &SourceSpan)> {
280        if let Self::Heading {
281            level,
282            inlines,
283            span,
284            ..
285        } = self
286        {
287            Some((*level, inlines, span))
288        } else {
289            None
290        }
291    }
292
293    /// Borrow the paragraph payload if `self` is [`Item::Paragraph`].
294    #[must_use]
295    pub fn as_paragraph(&self) -> Option<(&[Inline], &SourceSpan)> {
296        if let Self::Paragraph { inlines, span, .. } = self {
297            Some((inlines, span))
298        } else {
299            None
300        }
301    }
302
303    /// Borrow the directive payload if `self` is [`Item::Set`].
304    ///
305    /// The returned tuple is `(name, args, span)`; the caller can also
306    /// reach [`DirectiveKind`] via [`Self::directive_kind`]. The
307    /// accessor name is retained for back-compat; every existing
308    /// caller pre-dates the `#image`/`#figure` directives and only
309    /// looks at name/args/span.
310    #[must_use]
311    pub const fn as_set(&self) -> Option<(&str, &[SetArg], &SourceSpan)> {
312        if let Self::Set {
313            name, args, span, ..
314        } = self
315        {
316            Some((name.as_str(), args.as_slice(), span))
317        } else {
318            None
319        }
320    }
321
322    /// Borrow the raw block payload if `self` is [`Item::RawBlock`].
323    #[must_use]
324    pub fn as_raw_block(&self) -> Option<RawBlockView<'_>> {
325        if let Self::RawBlock {
326            kind,
327            args,
328            text,
329            label,
330            label_span,
331            span,
332        } = self
333        {
334            Some(RawBlockView {
335                kind: *kind,
336                args: args.as_slice(),
337                text: text.as_str(),
338                label: label.as_deref(),
339                label_span: label_span.as_ref(),
340                span,
341            })
342        } else {
343            None
344        }
345    }
346
347    /// Borrow the [`DirectiveKind`] tag if `self` is [`Item::Set`].
348    #[must_use]
349    pub const fn directive_kind(&self) -> Option<DirectiveKind> {
350        if let Self::Set { kind, .. } = self {
351            Some(*kind)
352        } else {
353            None
354        }
355    }
356
357    /// Borrow the list payload if `self` is [`Item::List`]. The
358    /// returned tuple is `(ordered, items, span)`.
359    #[must_use]
360    pub const fn as_list(&self) -> Option<(bool, &[ListItem], &SourceSpan)> {
361        if let Self::List {
362            ordered,
363            items,
364            span,
365        } = self
366        {
367            Some((*ordered, items.as_slice(), span))
368        } else {
369            None
370        }
371    }
372
373    /// Borrow the `/** … */` doc-comment payload if `self` is
374    /// [`Item::DocComment`]. The returned tuple is `(cleaned text, span)`.
375    #[must_use]
376    pub fn as_doc_comment(&self) -> Option<(&str, &SourceSpan)> {
377        if let Self::DocComment { text, span } = self {
378            Some((text.as_str(), span))
379        } else {
380            None
381        }
382    }
383
384    /// Borrow the explicit `<label>` attached to this block, if any.
385    /// Returns `None` for [`Item::Set`] and [`Item::List`] (label
386    /// syntax is not yet defined on those blocks).
387    #[must_use]
388    pub fn label(&self) -> Option<&str> {
389        match self {
390            Self::Heading { label, .. }
391            | Self::Paragraph { label, .. }
392            | Self::RawBlock { label, .. } => label.as_deref(),
393            Self::Set { .. } | Self::List { .. } | Self::DocComment { .. } => None,
394        }
395    }
396
397    /// Borrow the source span covering only the label token text, if any.
398    /// The delimiters (`<`, `>`, or directive string quotes) are excluded so a
399    /// structured suggestion can replace just the label bytes.
400    #[must_use]
401    pub const fn label_span(&self) -> Option<&SourceSpan> {
402        match self {
403            Self::Heading { label_span, .. }
404            | Self::Paragraph { label_span, .. }
405            | Self::RawBlock { label_span, .. } => label_span.as_ref(),
406            Self::Set { .. } | Self::List { .. } | Self::DocComment { .. } => None,
407        }
408    }
409}
410
411/// Output of [`crate::parse`]. Diagnostics may include warnings even
412/// when the tree is structurally usable; callers decide what to do per
413/// [`ParseResult::has_errors`].
414#[derive(Debug)]
415pub struct ParseResult {
416    pub tree: SyntaxTree,
417    pub diagnostics: Vec<Diagnostic>,
418}
419
420impl ParseResult {
421    #[must_use]
422    pub fn has_errors(&self) -> bool {
423        self.diagnostics
424            .iter()
425            .any(|d| d.severity() == Severity::Error)
426    }
427}