Skip to main content

oxc_parser/
markup.rs

1//! Host-embedded markup: markup written inside a JS/TS program.
2//!
3//! This is the reverse of embedding script in markup. The host grammar stays
4//! the owner of every expression position. When it reaches a position where a
5//! registered [`MarkupGrammar`] opens a region, it hands that position to the
6//! grammar, which parses the region and returns its end. The host keeps an
7//! empty placeholder whose span is exactly the region. While parsing, the
8//! grammar asks this same parser, through a [`MarkupCursor`], to parse every
9//! embedded expression, statement, binding and head, so those nodes are
10//! ordinary AST nodes in the same arena with original spans.
11//!
12//! Positions that can open a region:
13//! - a `<` where JSX is enabled ([`MarkupSite::Expression`]);
14//! - an `@` at the start of an expression or statement, when the grammar opens
15//!   it ([`MarkupSite::Expression`]);
16//! - an `@` where a function body would start ([`MarkupSite::FunctionBody`]).
17//!
18//! While a grammar is registered, a `<` that starts a line and is followed by a
19//! tag-like character ends the preceding expression (so automatic semicolon
20//! insertion separates `const a = b` from a following `<tag>`), matching the
21//! line-start rule of markup-in-script languages.
22
23use oxc_allocator::{Allocator, ArenaBox, ArenaVec, GetAllocator};
24use oxc_ast::ast::*;
25use oxc_diagnostics::OxcDiagnostic;
26use oxc_span::Span;
27
28use crate::{
29    Context, Parser, ParserConfig as Config, ParserImpl, ParserReturn, StatementContext,
30    config::NoTokensParserConfig,
31    diagnostics::{self, ParserDiagnostic},
32    js::{VariableDeclarationParent, grammar::CoverGrammar},
33    lexer::{Kind, Token},
34};
35
36/// Where a markup region opens in the host grammar.
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38pub enum MarkupSite {
39    /// An expression (or expression statement) position.
40    Expression,
41    /// A function, method or arrow body position.
42    FunctionBody,
43}
44
45/// A markup grammar embedded in host JS/TS.
46///
47/// The grammar is shared (`&self`) so a region may re-enter the host parser,
48/// which may open nested regions with the same grammar.
49pub trait MarkupGrammar<'a> {
50    /// Whether a region opens at `offset` (the start of an `@` token) for `site`.
51    /// A `<` in a JSX-enabled profile always opens an expression region.
52    fn opens(&self, source: &'a str, offset: u32, site: MarkupSite) -> bool;
53
54    /// Parse one region that starts at `cursor.token()`, and return the offset where
55    /// it ends. Errors are reported with [`MarkupCursor::fatal`].
56    fn region(&self, cursor: &mut MarkupCursor<'_, 'a>, site: MarkupSite) -> u32;
57}
58
59/// The host operations a markup grammar may use inside one region.
60pub struct MarkupCursor<'p, 'a> {
61    host: &'p mut dyn MarkupHost<'a>,
62}
63
64trait MarkupHost<'a> {
65    fn source(&self) -> &'a str;
66    fn allocator(&self) -> &'a Allocator;
67    fn token(&self) -> Token;
68    fn seat(&mut self, offset: u32) -> bool;
69    fn bump(&mut self);
70    fn expression(&mut self) -> Expression<'a>;
71    fn sequence(&mut self) -> Expression<'a>;
72    fn statement(&mut self) -> Statement<'a>;
73    fn for_of_head(&mut self) -> Option<(ForStatementLeft<'a>, Expression<'a>)>;
74    fn binding_identifier(&mut self) -> BindingIdentifier<'a>;
75    fn formal_parameters(&mut self) -> ArenaBox<'a, FormalParameters<'a>>;
76    fn fatal(&mut self, diagnostic: OxcDiagnostic);
77    fn failed(&self) -> bool;
78}
79
80impl<'a> MarkupCursor<'_, 'a> {
81    /// The whole host source.
82    pub fn source(&self) -> &'a str {
83        self.host.source()
84    }
85
86    /// The arena of the host AST.
87    pub fn allocator(&self) -> &'a Allocator {
88        self.host.allocator()
89    }
90
91    /// The current host token.
92    pub fn token(&self) -> Token {
93        self.host.token()
94    }
95
96    /// Move the host to `offset` and read the token there (after trivia).
97    /// Returns `false` (and reports a fatal error) when `offset` is not a
98    /// character boundary of the source.
99    pub fn seat(&mut self, offset: u32) -> bool {
100        self.host.seat(offset)
101    }
102
103    /// Advance past the current host token.
104    pub fn bump(&mut self) {
105        self.host.bump();
106    }
107
108    /// Parse an `AssignmentExpression` at the current token, with `in` allowed.
109    pub fn expression(&mut self) -> Expression<'a> {
110        self.host.expression()
111    }
112
113    /// Parse an `Expression` (a comma sequence) at the current token.
114    pub fn sequence(&mut self) -> Expression<'a> {
115        self.host.sequence()
116    }
117
118    /// Parse one `StatementListItem` at the current token.
119    pub fn statement(&mut self) -> Statement<'a> {
120        self.host.statement()
121    }
122
123    /// Parse `( ForDeclaration of AssignmentExpression` at the current `(`, leaving
124    /// the token after the iterable (a `;` clause or the closing `)`).
125    /// Returns `None`, leaving the token after the left side, when the head is not a for-of head.
126    pub fn for_of_head(&mut self) -> Option<(ForStatementLeft<'a>, Expression<'a>)> {
127        self.host.for_of_head()
128    }
129
130    /// Parse a `BindingIdentifier` at the current token.
131    pub fn binding_identifier(&mut self) -> BindingIdentifier<'a> {
132        self.host.binding_identifier()
133    }
134
135    /// Parse parenthesized `FormalParameters` at the current `(`.
136    pub fn formal_parameters(&mut self) -> ArenaBox<'a, FormalParameters<'a>> {
137        self.host.formal_parameters()
138    }
139
140    /// Report an error that ends the parse.
141    pub fn fatal(&mut self, diagnostic: OxcDiagnostic) {
142        self.host.fatal(diagnostic);
143    }
144
145    /// Whether the parse has already failed fatally.
146    pub fn failed(&self) -> bool {
147        self.host.failed()
148    }
149}
150
151impl<'a, C: Config> MarkupHost<'a> for ParserImpl<'a, C> {
152    fn source(&self) -> &'a str {
153        self.source_text
154    }
155
156    fn allocator(&self) -> &'a Allocator {
157        GetAllocator::allocator(self)
158    }
159
160    fn token(&self) -> Token {
161        self.token
162    }
163
164    fn seat(&mut self, offset: u32) -> bool {
165        self.seat_at(offset)
166    }
167
168    fn bump(&mut self) {
169        self.bump_any();
170    }
171
172    fn expression(&mut self) -> Expression<'a> {
173        self.context_add(Context::In, ParserImpl::parse_assignment_expression_or_higher)
174    }
175
176    fn sequence(&mut self) -> Expression<'a> {
177        self.context_add(Context::In, ParserImpl::parse_expr)
178    }
179
180    fn statement(&mut self) -> Statement<'a> {
181        self.parse_statement_list_item(StatementContext::StatementList)
182    }
183
184    fn for_of_head(&mut self) -> Option<(ForStatementLeft<'a>, Expression<'a>)> {
185        self.parse_markup_for_of_head()
186    }
187
188    fn binding_identifier(&mut self) -> BindingIdentifier<'a> {
189        self.parse_binding_identifier()
190    }
191
192    fn formal_parameters(&mut self) -> ArenaBox<'a, FormalParameters<'a>> {
193        let (_, parameters) = self.parse_formal_parameters(
194            crate::js::FunctionKind::Expression,
195            FormalParameterKind::FormalParameter,
196        );
197        parameters
198    }
199
200    fn fatal(&mut self, diagnostic: OxcDiagnostic) {
201        self.set_fatal_error(ParserDiagnostic::Eager(Box::new(diagnostic)));
202    }
203
204    fn failed(&self) -> bool {
205        self.fatal_error.is_some()
206    }
207}
208
209impl<'a, C: Config> ParserImpl<'a, C> {
210    /// Move to `offset` and make the token there current.
211    pub(crate) fn seat_at(&mut self, offset: u32) -> bool {
212        if let Some(token) = self.lexer.seat(offset) {
213            self.prev_token_end = offset;
214            self.token = token;
215            true
216        } else {
217            self.set_fatal_error(diagnostics::unexpected_token(Span::empty(offset)));
218            false
219        }
220    }
221
222    /// Whether the current `@` opens a markup region for `site`.
223    #[inline]
224    pub(crate) fn at_markup_directive(&self, site: MarkupSite) -> bool {
225        C::MARKUP && self.at(Kind::At) && self.markup_opens(site)
226    }
227
228    #[cold]
229    #[inline(never)]
230    fn markup_opens(&self, site: MarkupSite) -> bool {
231        self.markup
232            .is_some_and(|grammar| grammar.opens(self.source_text, self.cur_token().start(), site))
233    }
234
235    /// Whether the current `<` starts a line and a tag while a grammar is registered.
236    #[inline]
237    pub(crate) fn at_markup_line_start_tag(&self) -> bool {
238        C::MARKUP && self.markup_line_start_tag()
239    }
240
241    #[cold]
242    #[inline(never)]
243    fn markup_line_start_tag(&self) -> bool {
244        let token = self.cur_token();
245        token.kind() == Kind::LAngle
246            && token.is_on_new_line()
247            && self.source_text.as_bytes().get(token.end() as usize).is_some_and(|byte| {
248                byte.is_ascii_alphabetic() || matches!(byte, b'_' | b'$' | b'>' | b'{')
249            })
250    }
251
252    /// Run the registered grammar on the region at the current token; return its end.
253    fn run_markup(&mut self, site: MarkupSite) -> u32 {
254        let start = self.cur_token().start();
255        let Some(grammar) = self.markup else {
256            self.set_unexpected();
257            return start;
258        };
259        let end = grammar.region(&mut MarkupCursor { host: self }, site);
260        if self.fatal_error.is_none() && (end <= start || !self.seat_at(end)) {
261            self.set_fatal_error(diagnostics::unexpected_token(Span::empty(start)));
262        }
263        end.max(start)
264    }
265
266    /// An expression-position region, kept as an empty fragment with the region's span.
267    #[cold]
268    #[inline(never)]
269    pub(crate) fn parse_markup_expression(&mut self) -> Expression<'a> {
270        let start = self.cur_token().start();
271        let end = self.run_markup(MarkupSite::Expression);
272        let opening = JSXOpeningFragment::new(Span::empty(start), self);
273        let closing = JSXClosingFragment::new(Span::empty(end), self);
274        Expression::JSXFragment(JSXFragment::boxed(
275            Span::new(start, end),
276            opening,
277            ArenaVec::new_in(self),
278            closing,
279            self,
280        ))
281    }
282
283    /// A function-body region, kept as an empty body with the region's span.
284    #[cold]
285    #[inline(never)]
286    pub(crate) fn parse_markup_function_body(&mut self) -> ArenaBox<'a, FunctionBody<'a>> {
287        let start = self.cur_token().start();
288        let end = self.context(Context::Return, Context::TopLevel, |p| {
289            p.run_markup(MarkupSite::FunctionBody)
290        });
291        FunctionBody::boxed(
292            Span::new(start, end),
293            ArenaVec::new_in(self),
294            ArenaVec::new_in(self),
295            self,
296        )
297    }
298
299    fn parse_markup_for_of_head(&mut self) -> Option<(ForStatementLeft<'a>, Expression<'a>)> {
300        self.expect(Kind::LParen);
301        let declaration_kind = match self.cur_kind() {
302            Kind::Const => Some(VariableDeclarationKind::Const),
303            Kind::Let => Some(VariableDeclarationKind::Let),
304            Kind::Var => Some(VariableDeclarationKind::Var),
305            _ => None,
306        };
307        let left = if let Some(kind) = declaration_kind {
308            let start = self.start_span();
309            self.bump_any();
310            let declaration = self.context_remove(Context::In, |p| {
311                p.parse_variable_declaration(start, kind, VariableDeclarationParent::For, false)
312            });
313            ForStatementLeft::VariableDeclaration(declaration)
314        } else {
315            let target =
316                self.context_remove(Context::In, ParserImpl::parse_lhs_expression_or_higher);
317            ForStatementLeft::from(AssignmentTarget::cover(target, self))
318        };
319        if !self.at(Kind::Of) {
320            return None;
321        }
322        self.bump_any();
323        let right =
324            self.context_add(Context::In, ParserImpl::parse_assignment_expression_or_higher);
325        Some((left, right))
326    }
327}
328
329impl<'a> Parser<'a, NoTokensParserConfig> {
330    /// Parse a program whose markup regions belong to `grammar`.
331    ///
332    /// The result is an ordinary [`ParserReturn`]. It never mints
333    /// [`crate::ProgramObservation`] admission: its program contains
334    /// placeholders that only the grammar's own artifact can interpret.
335    pub fn parse_with_markup(self, grammar: &'a dyn MarkupGrammar<'a>) -> ParserReturn<'a> {
336        crate::parse_with_markup(
337            self.allocator,
338            self.source_text,
339            self.source_type,
340            self.options,
341            grammar,
342        )
343    }
344}
345
346#[cfg(test)]
347mod tests {
348    use std::cell::RefCell;
349
350    use oxc_allocator::Allocator;
351    use oxc_ast::ast::{Expression, Statement};
352    use oxc_span::{GetSpan, SourceType};
353
354    use super::{MarkupCursor, MarkupGrammar, MarkupSite};
355    use crate::{Kind, Parser};
356
357    type Recorded = Vec<(MarkupSite, u32, u32, String)>;
358
359    /// `<{ expression }>` regions and `@{ statements }` bodies.
360    #[derive(Default)]
361    struct Toy {
362        regions: RefCell<Recorded>,
363    }
364
365    impl<'a> MarkupGrammar<'a> for Toy {
366        fn opens(&self, source: &'a str, offset: u32, _: MarkupSite) -> bool {
367            source.as_bytes().get(offset as usize + 1) == Some(&b'{')
368        }
369
370        fn region(&self, cursor: &mut MarkupCursor<'_, 'a>, site: MarkupSite) -> u32 {
371            let start = cursor.token().start();
372            cursor.seat(start + 2);
373            let (inner, end) = if cursor.source().as_bytes()[start as usize] == b'<' {
374                let expression = cursor.expression();
375                let close = cursor.token();
376                assert_eq!(close.kind(), Kind::RCurly);
377                (expression.span().source_text(cursor.source()).to_string(), close.end() + 1)
378            } else {
379                let mut statements = Vec::new();
380                while cursor.token().kind() != Kind::RCurly {
381                    let statement = cursor.statement();
382                    statements.push(statement.span().source_text(cursor.source()).to_string());
383                }
384                (statements.join("|"), cursor.token().end())
385            };
386            self.regions.borrow_mut().push((site, start, end, inner));
387            end
388        }
389    }
390
391    fn parse(source: &str) -> (Recorded, usize, Option<(u32, u32)>) {
392        let toy = Toy::default();
393        let allocator = Allocator::default();
394        let parsed = Parser::new(&allocator, source, SourceType::tsx()).parse_with_markup(&toy);
395        assert!(parsed.diagnostics.is_empty(), "{source}: {:?}", parsed.diagnostics);
396        let placeholder = match parsed.program.body.first() {
397            Some(Statement::VariableDeclaration(declaration)) => {
398                match declaration.declarations[0].init.as_ref() {
399                    Some(Expression::JSXFragment(fragment)) => {
400                        assert!(fragment.children.is_empty());
401                        Some((fragment.span.start, fragment.span.end))
402                    }
403                    _ => None,
404                }
405            }
406            Some(Statement::FunctionDeclaration(function)) => {
407                let body = function.body.as_ref().unwrap();
408                assert!(body.statements.is_empty());
409                Some((body.span.start, body.span.end))
410            }
411            _ => None,
412        };
413        let regions = toy.regions.borrow().clone();
414        (regions, parsed.program.body.len(), placeholder)
415    }
416
417    #[test]
418    fn regions_keep_exact_placeholders_and_reenter_the_host() {
419        let (regions, statements, placeholder) = parse("const a = <{b + [<{c}>][0]}>;");
420        assert_eq!(statements, 1);
421        assert_eq!(placeholder, Some((10, 28)));
422        assert_eq!(
423            regions,
424            [
425                (MarkupSite::Expression, 17, 22, "c".to_string()),
426                (MarkupSite::Expression, 10, 28, "b + [<{c}>][0]".to_string()),
427            ]
428        );
429    }
430
431    #[test]
432    fn function_bodies_and_statement_starts_open_regions() {
433        let (regions, _, placeholder) = parse("function f() @{ const x = 1; g(x) }");
434        assert_eq!(placeholder, Some((13, 35)));
435        assert_eq!(regions, [(MarkupSite::FunctionBody, 13, 35, "const x = 1;|g(x)".to_string())]);
436        let (regions, statements, _) = parse("let v;\n@{ v = 1 }\n@dec class K {}");
437        assert_eq!(statements, 3);
438        assert_eq!(regions, [(MarkupSite::Expression, 7, 17, "v = 1".to_string())]);
439    }
440
441    #[test]
442    fn a_line_start_tag_ends_the_previous_expression() {
443        let (regions, statements, _) = parse("const a = b\n<{c}>\nconst d = e < f;");
444        assert_eq!(statements, 3);
445        assert_eq!(regions, [(MarkupSite::Expression, 12, 17, "c".to_string())]);
446    }
447}