diff --git a/services/web/frontend/js/features/source-editor/lezer-typst/typst.grammar b/services/web/frontend/js/features/source-editor/lezer-typst/typst.grammar index 4b0b52d027..ad485b1704 100644 --- a/services/web/frontend/js/features/source-editor/lezer-typst/typst.grammar +++ b/services/web/frontend/js/features/source-editor/lezer-typst/typst.grammar @@ -31,12 +31,15 @@ item { // HeadingMark is produced by an external tokenizer that enforces the // start-of-line constraint and captures the "=+" prefix + trailing space. Heading { HeadingMark HeadingTitle } -// headingTitleItem+ (not *) so HeadingTitle is never empty: the generator -// would otherwise produce a shift/reduce conflict on "*" after HeadingMark, -// unable to tell if "*" starts a Strong inside the heading or at doc level. +// Strong and Emphasis are intentionally excluded from headingTitleItem. +// Including them causes an LALR(1) conflict: since Strong/Emphasis also appear +// in document-level `item`, the LR automaton merges heading-title states with +// document-item states, making "*" ambiguous (Strong opener vs. end of heading). +// Instead, HeadingText is widened to consume "*" and "_" as plain text inside +// headings — they are not interpreted as markup delimiters there. HeadingTitle { headingTitleItem+ } headingTitleItem { - Strong | Emphasis | CodeExpr | InlineMath | RawInline | Label | Ref | HeadingText + CodeExpr | InlineMath | RawInline | Label | Ref | HeadingText } // ── Comments ────────────────────────────────────────────────────────────── @@ -186,7 +189,7 @@ Escape { "\\" EscapeChar } MathContent { ![$\n]+ } // Text tokens for different markup contexts; each excludes its own delimiters. - HeadingText { ![\n*_$#`<@\\]+ } + HeadingText { ![\n$#`<@\\]+ } StrongText { ![\n*$#`@\\]+ } EmphText { ![\n_$#`@\\]+ }