Skip to content

feat: inline expressions in Spacebars#500

Open
dupontbertrand wants to merge 7 commits intometeor:release-3.1.0from
dupontbertrand:feature/inline-expressions
Open

feat: inline expressions in Spacebars#500
dupontbertrand wants to merge 7 commits intometeor:release-3.1.0from
dupontbertrand:feature/inline-expressions

Conversation

@dupontbertrand
Copy link
Copy Markdown

Summary

  • Add inline expression support to Spacebars, allowing arithmetic, comparison, logical, and ternary operators directly in template tags
  • Eliminates the need for helper functions in common cases like {{price + tax}}, {{#if score >= 90}}, {{isActive ? "on" : "off"}}
  • 100% backward compatible — existing {{helper arg}} syntax is unchanged

Supported operators

Category Operators
Arithmetic +, -, *, /, %
Comparison ===, !==, >, >=, <, <=
Logical &&, ||, !
Ternary ? :
Grouping ( )
Unary - (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 precedence
  • codegen.js — recursive codeGenInlineExpr() for code generation
  • spacebars-runtime.js — lightweight Spacebars.expr() post-processor

Known 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

  • Tested with a 14-section Blaze app covering all expression types
  • Arithmetic: +, -, *, /, %, chained, literals, negative multiplier
  • Comparison: all 6 operators, string/boolean literals, mixed with logical
  • Logical: &&, ||, ! prefix, negated groups
  • Ternary: simple, nested, with arithmetic in branches
  • Grouping: precedence, single value, double parens, multiple groups
  • Unary: -a, a + -b, a - -b, (-a) * (-b)
  • Dot access: items.length > 0, items.length * 10
  • Block helpers: {{#if expr}}, {{#unless expr}}, {{#if a + b > 10}}
  • Attribute values: ternary in class/style, arithmetic in data-attr, input value
  • Whitespace: {{a+b}}, {{ a + b }}, {{a +b}}
  • Reactivity: expressions update when reactive sources change
  • Dynamic templates: Template.dynamic, #each item in items, Template.contentBlock
  • Backward compat: simple paths, dot paths, helper calls, negative args, sub-expressions, keyword args, {{this}}, {{@index}}, {{{raw}}}, #let, global helpers

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
@jankapunkt
Copy link
Copy Markdown
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}}.
@dupontbertrand
Copy link
Copy Markdown
Author

@jankapunkt This is fully backward compatible 👍
The parser only switches to expression mode when it detects an operator (+, ===, &&, etc.) after the first path, which never occurs in valid Handlebars syntax

@jankapunkt jankapunkt added this to the 3.2 milestone Apr 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants