typst: support '+' binary operator in arg values
Build and Deploy Verso / deploy (push) Successful in 9m27s

stroke: 0.8pt + brand broke arg-list parsing because '+' was not a grammar
terminal. The parser exited CodeArgs via error recovery, so subsequent
named args (radius:, inset:, fill:) were never seen as CodeArgKey.

Add codeArgValue { codeValue | codeArgValue !add "+" codeValue } — a
left-recursive inline rule used only inside CodeArgs.  The !add cut point
gives the shift strict dominance over the reduce (prec add > 0 vs 0), so
a '+' after a value greedily extends the expression.  Because codeArgValue
only appears inside CodeArgs, the codeStatement* LALR-merging that caused
trouble for the earlier callSuffix* approach does not apply here.

Also add PLUS to codeIdentTokenizer's valid-predecessor list so identifiers
after '+' (the right-hand operand) are correctly tokenized as CodeIdent.
Add "+" to @tokens @precedence so it beats MarkupContent in merged states.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
claude
2026-06-10 06:07:32 +00:00
parent 0656ddfe52
commit 083b195462
3 changed files with 25 additions and 11 deletions
@@ -37,6 +37,7 @@ const COLON = 58 // :
const SEMICOLON = 59 // ;
const OPEN_ANGLE = 60 // <
const CLOSE_ANGLE = 62 // >
const PLUS = 43 // +
const KEYWORDS = new Set([
'let', 'set', 'show', 'import', 'include',
@@ -324,7 +325,7 @@ export const codeIdentTokenizer = new ExternalTokenizer(
while (input.peek(back) === SPACE || input.peek(back) === TAB || input.peek(back) === NEWLINE) back--
const prev = input.peek(back)
if (prev !== HASH && prev !== DOT && prev !== OPEN_PAREN && prev !== COMMA && prev !== EQUALS && prev !== COLON) {
if (prev !== HASH && prev !== DOT && prev !== OPEN_PAREN && prev !== COMMA && prev !== EQUALS && prev !== COLON && prev !== PLUS) {
if (!isIdentTail(prev)) {
// prev is a structural delimiter (e.g. ')' after a function call, '{' at
// block start, '}' after a nested block). These are valid statement-start
@@ -88,7 +88,11 @@ codeExprBody {
// kw — prefer CodeKeyword !kw callOrValueAndBody over CodeKeyword keywordBody?
// when an identifier follows the keyword. shift = kw << 2, reduce
// (second alternative) = 0; kw > 0, no @right needed.
@precedence { call @right, kw }
// add — resolves the shift/reduce conflict when a '+' follows a codeArgValue:
// SHIFT '+' (extend codeArgValue → codeArgValue !add "+" codeValue): prec add
// REDUCE codeArgItem → codeArgValue (complete arg): prec 0
// add > 0 → shift wins, so 0.8pt + brand stays as one arg value.
@precedence { call @right, kw, add }
// KeywordExpr: used in markup-level code (#show, #let, #set …) AND nested
// inside codeExprBody (e.g. the RHS after ':' in a show-rule).
@@ -140,10 +144,19 @@ callSuffix {
CodeArgs { "(" codeArgList? ")" }
codeArgList { codeArgItem ("," codeArgItem)* ","? }
codeArgItem {
CodeArgKey ":" codeValue |
codeValue
CodeArgKey ":" codeArgValue |
codeArgValue
}
// codeArgValue extends codeValue with '+' chaining for expressions like
// `stroke: 0.8pt + brand` or `fill: base + overlay`.
// Left-recursive rule: LALR state for codeArgValue · seeing '+':
// SHIFT '+' (extend, !add prec): prec add > 0
// REDUCE codeArgItem → codeArgValue (complete): prec 0
// add > 0 → shift wins cleanly. No @right needed (strict dominance).
// Only used inside CodeArgs, so codeStatement* LALR-merging does not apply.
codeArgValue { codeValue | codeArgValue !add "+" codeValue }
codeValue {
CodeString |
CodeNumber |
@@ -303,7 +316,7 @@ Escape { "\\" EscapeChar }
// CodeString > MarkupContent: '"' starts a string literal after a keyword.
// ":" > MarkupContent: keywordBody ':' wins over markup colon in code states.
// URL > MarkupContent: 'https://' / 'http://' wins over plain markup text.
@precedence { CodeBool EscapeChar CodeString URL "[" ":" "(" "." "]" ClosingSquare "_" spaces MarkupContent }
@precedence { CodeBool EscapeChar CodeString URL "[" ":" "(" "." "+" "]" ClosingSquare "_" spaces MarkupContent }
}
@skip { spaces }
File diff suppressed because one or more lines are too long