Skip to main content

mf2_syntax/
lib.rs

1//! `mf2-syntax` — MessageFormat 2 syntax for Rust MF2: a parser to a lossless
2//! concrete syntax tree with error recovery, lowering to the data model of
3//! [`mf2_model`], the six Data Model Errors, a serializer back to MF2 source,
4//! and the variable analysis the build's manifest needs.
5//!
6//! | Entry point | Gives |
7//! |---|---|
8//! | [`parse_cst`] / [`Parser::parse_cst`] | a lossless [`Cst`] (tooling: formatter, diagnostics, editors) |
9//! | [`parse_model`] / [`Parser::parse_model`] / [`Frontend`] | the data model plus syntax *or* data-model diagnostics (what the build uses) |
10//! | [`validate`] | the Data Model Errors of a model built in code |
11//! | [`serialize`] | canonical MF2 source for a model |
12//! | [`analyze`] | external and local variables, markup and functions of a model |
13//!
14//! Both parse entry points run one byte-oriented, single-pass, non-recursive
15//! parser writing a flat arena ([`cst`]); `parse_model` keeps no trivia and
16//! lowers the arena to the model. A [`Parser`] keeps the arena between calls,
17//! so a pass over many messages allocates it once. A message without `{`,
18//! `}`, `\` or a leading `.` (79 % of real messages) takes a fast path: one
19//! scan, and a borrowed single-text model with no allocation.
20//!
21//! Diagnostics carry a stable detail code: see [`code`].
22//!
23//! `#![no_std]` + `alloc`; never linked into the client wasm.
24//!
25//! # The user guide
26//!
27//! The [Rust MF2 book](https://evancarroll.github.io/rust-mf2/) is the user
28//! guide: how the crates fit together, web and native applications, the
29//! command line, and what 2.x promises.
30//! An application formatting messages starts at
31//! [`mf2`](https://docs.rs/mf2); this crate is the stand-alone parser, for
32//! tools.
33
34#![warn(missing_docs)]
35// docs.rs (`cargo xtask docs-rs`): each feature-gated item says which features it needs.
36#![cfg_attr(docsrs, feature(doc_cfg))]
37#![no_std]
38#![forbid(unsafe_code)]
39
40extern crate alloc;
41
42mod analyze;
43mod chars;
44pub mod code;
45pub mod cst;
46mod error;
47mod lower;
48mod norm;
49mod parser;
50mod serialize;
51mod validate;
52
53use alloc::borrow::Cow;
54use alloc::vec::Vec;
55
56use mf2_model::{Diagnostic, Diagnostics, Frontend, Message, Parsed, Pattern, PatternMessage};
57
58pub use analyze::{Analysis, Name, analyze};
59pub use cst::{Cst, CstRef, Node, SyntaxKind, SyntaxNode};
60pub use error::{Error, NameRole};
61pub use serialize::serialize;
62pub use validate::validate;
63
64/// Parses `source` into a lossless CST (with its syntax diagnostics).
65pub fn parse_cst(source: &str) -> Cst<'_> {
66    let mut nodes = Vec::new();
67    let mut diagnostics = Vec::new();
68    parser::parse_into(source, &mut nodes, &mut diagnostics, true);
69    Cst {
70        src: source,
71        nodes,
72        diagnostics,
73    }
74}
75
76/// Parses `source` into the data model and validates it.
77///
78/// With a syntax error: no model, and every syntax error the parser could
79/// recover to. Otherwise: the model, and every data-model error. Spans are
80/// byte offsets into `source`.
81pub fn parse_model(source: &str) -> Parsed<'_> {
82    if let Some(parsed) = simple_text(source) {
83        return parsed;
84    }
85    Parser::new().parse_model(source)
86}
87
88/// A parser that keeps its arena between calls (the "reused" state of the
89/// parser gate): a pass over many messages grows it once.
90#[derive(Clone, Debug, Default)]
91pub struct Parser {
92    nodes: Vec<Node>,
93    diagnostics: Vec<Diagnostic>,
94}
95
96impl Parser {
97    /// A parser with an empty arena (does not allocate).
98    pub const fn new() -> Self {
99        Parser {
100            nodes: Vec::new(),
101            diagnostics: Vec::new(),
102        }
103    }
104
105    /// A parser whose arena has room for `nodes` entries.
106    pub fn with_capacity(nodes: usize) -> Self {
107        Parser {
108            nodes: Vec::with_capacity(nodes),
109            diagnostics: Vec::new(),
110        }
111    }
112
113    /// Parses `source` into a lossless CST held in this parser's arena.
114    pub fn parse_cst<'p, 'src>(&'p mut self, source: &'src str) -> CstRef<'p, 'src> {
115        parser::parse_into(source, &mut self.nodes, &mut self.diagnostics, true);
116        CstRef {
117            src: source,
118            nodes: &self.nodes,
119            diagnostics: &self.diagnostics,
120        }
121    }
122
123    /// See [`parse_model`]; reuses this parser's arena.
124    pub fn parse_model<'src>(&mut self, source: &'src str) -> Parsed<'src> {
125        if let Some(parsed) = simple_text(source) {
126            return parsed;
127        }
128        parser::parse_into(source, &mut self.nodes, &mut self.diagnostics, false);
129        if !self.diagnostics.is_empty() || self.nodes.is_empty() {
130            return Parsed {
131                message: None,
132                diagnostics: Diagnostics::from(core::mem::take(&mut self.diagnostics)),
133            };
134        }
135        model_from_arena(source, &self.nodes)
136    }
137}
138
139impl Frontend for Parser {
140    fn parse<'src>(&mut self, source: &'src str) -> Parsed<'src> {
141        self.parse_model(source)
142    }
143}
144
145/// The fast path: a message with no `{`, `}`, `\` or U+0000 whose first
146/// non-whitespace character is not `.` is a simple message of one text run
147/// (possibly empty), valid by construction.
148fn simple_text(source: &str) -> Option<Parsed<'_>> {
149    let b = source.as_bytes();
150    if chars::find_text_end(b, 0) != b.len() {
151        return None;
152    }
153    let mut i = 0;
154    while let Some((n, _)) = chars::trivia_at(b, i) {
155        i += n;
156    }
157    if b.get(i) == Some(&b'.') {
158        return None;
159    }
160    Some(Parsed {
161        message: Some(Message::Pattern(PatternMessage {
162            declarations: Vec::new(),
163            pattern: Pattern::from_text(Cow::Borrowed(source)),
164        })),
165        diagnostics: Diagnostics::new(),
166    })
167}
168
169/// Lowers an arena without syntax errors and validates the model, mapping
170/// each data-model error to its span.
171pub(crate) fn model_from_arena<'src>(source: &'src str, nodes: &[Node]) -> Parsed<'src> {
172    let arena = lower::Arena { src: source, nodes };
173    let message = arena.message();
174    let mut found = Vec::new();
175    validate::check(&message, &mut |kind, code, loc| {
176        found.push((kind, code, loc));
177    });
178    if found.is_empty() {
179        return Parsed {
180            message: Some(message),
181            diagnostics: Diagnostics::new(),
182        };
183    }
184    let mut locator = lower::Locator::new(&arena);
185    let mut diagnostics: Vec<Diagnostic> = found
186        .iter()
187        .map(|(kind, code, loc)| Diagnostic::new(*kind, *code, locator.resolve(loc)))
188        .collect();
189    // Stable: equal positions keep the checks' order.
190    diagnostics.sort_by_key(|d| d.span.map_or(0, |s| s.start));
191    Parsed {
192        message: Some(message),
193        diagnostics: Diagnostics::from(diagnostics),
194    }
195}