feat: inline expressions in Spacebars#500
Open
dupontbertrand wants to merge 7 commits intometeor:release-3.1.0from
Open
feat: inline expressions in Spacebars#500dupontbertrand wants to merge 7 commits intometeor:release-3.1.0from
dupontbertrand wants to merge 7 commits intometeor:release-3.1.0from
Conversation
Add support for inline expressions in Spacebars templates, allowing
arithmetic, comparison, logical, and ternary operators directly in
template tags without requiring helper functions.
Supported: {{a + b}}, {{a > b}}, {{a && b}}, {{a ? "yes" : "no"}},
{{(a + b) * c}}, {{-a}}, {{#if score >= 90}}, etc.
100% backward compatible — existing helper call syntax ({{foo bar}})
is unchanged. The parser detects operators after the first path and
switches to expression mode only when an operator is found.
Implementation:
- Pratt parser in templatetag.js with JS-standard operator precedence
- codeGenInlineExpr() in codegen.js for recursive code generation
- Spacebars.expr() runtime helper for expression result post-processing
- Clear error messages for forbidden operators (|, &, ++, =, +=)
Known limitation: {{!foo}} is still parsed as a comment — use {{(!foo)}}
Add comprehensive documentation for the new inline expression feature: - New "Inline Expressions" section in api/spacebars.md covering all operators, precedence, attribute usage, block helpers, and limitations - HISTORY.md entry for vNEXT
Collaborator
|
@dupontbertrand I would not consider this part of 3.1.0 to keep the release manageable and constraint the surface of what needs to be field tested (and fixed if people report issues). If it's breaking and not 100% backwards compatible then we should target 4.0.0 otherwise it should target 3.2.0. I will create branches for both releases this weekend. |
Add comprehensive parser tests covering: - All arithmetic operators (+, -, *, /, %) - Operator precedence and left-associativity - Grouping with parentheses - All comparison operators (===, !==, >, >=, <, <=) - Logical operators (&&, ||) - Ternary expressions with nesting - Unary operators (-, !) - Literals (numbers, strings, booleans, null) - Dot paths in expressions - Whitespace variations - Backward compatibility (simple paths, helper calls, negative args, sub-expressions, keyword args) - Forbidden operator error messages (=, +=, |, &, incomplete ?)
…tion
- Don't eagerly enter expression mode for `(`, digits, or strings.
Instead, try inline expression speculatively and backtrack if it
doesn't consume the full tag. Fixes {{(dyn) arg}} and {{0 0}}.
- Check forbidden operators (|, &, ++, --) in shouldEnterExprMode
so {{a & b}} gives a clear error instead of a generic parse error.
When a sub-expression contains an inline expression like
{{helper (a + b)}}, use codeGenInlineExpr instead of codeGenMustache
to avoid generating invalid view.lookup("__expr__") code.
Add tests for:
- Triple-stache with expression ({{{a + b}}})
- BLOCKOPEN with expression ({{#if a > b}}, {{#unless a === b}})
- Sub-expression containing expression ({{helper (a + b)}})
- @index in expressions ({{@index + 1}})
- this.x in expressions
- ../parent paths in expressions
- Comparison + ternary combined ({{a === b ? "yes" : "no"}})
- Complex chained operators ({{a + b * c - d}})
- Path + string literal, two literals
- Decrement (--) forbidden operator
- Classic BLOCKOPEN and sub-expression backward compat
Move detection of forbidden multi-char operators (++, --, +=, -=, *=,
/=, %=) into peekOperator so they are caught before the single-char
operator (+, -, etc.) is consumed. Fixes error message for {{a += 1}}.
Author
|
@jankapunkt This is fully backward compatible 👍 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
{{price + tax}},{{#if score >= 90}},{{isActive ? "on" : "off"}}{{helper arg}}syntax is unchangedSupported operators
+,-,*,/,%===,!==,>,>=,<,<=&&,||,!? :( )-(negation),!(logical not)Examples
{{price * quantity + tax}} {{#if items.length > 0}}...{{/if}} <div class="{{isActive ? 'on' : 'off'}}"> {{score >= 90 ? "A" : score >= 80 ? "B" : "C"}} {{(a + b) * c}}How it works
The parser uses a Pratt parser (top-down operator precedence) that integrates with the existing scanner. After scanning the first path, it peeks for an operator — if found, it switches to expression mode. Since operators like
+,*,===,&&never appear in valid Handlebars, there is no ambiguity with existing syntax.templatetag.js— Pratt parser with JS-standard operator precedencecodegen.js— recursivecodeGenInlineExpr()for code generationspacebars-runtime.js— lightweightSpacebars.expr()post-processorKnown limitation
{{!foo}}is still parsed as a Spacebars comment. Use{{(!foo)}}as a workaround.Forbidden operators (with clear error messages)
|(reserved for future pipes),&,++,--,=,+=Test plan
+,-,*,/,%, chained, literals, negative multiplier&&,||,!prefix, negated groups-a,a + -b,a - -b,(-a) * (-b)items.length > 0,items.length * 10{{#if expr}},{{#unless expr}},{{#if a + b > 10}}{{a+b}},{{ a + b }},{{a +b}}Template.dynamic,#each item in items,Template.contentBlock{{this}},{{@index}},{{{raw}}},#let, global helpers