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