diff --git a/src/__tests__/templates/bindingSourcesAndTokens.test.ts b/src/__tests__/templates/bindingSourcesAndTokens.test.ts index 17712ba6f..6f625f81e 100644 --- a/src/__tests__/templates/bindingSourcesAndTokens.test.ts +++ b/src/__tests__/templates/bindingSourcesAndTokens.test.ts @@ -164,10 +164,10 @@ describe('containsTokens', () => { expect(containsTokens('Hello {site.name}')).toBe(true) }) - it('returns true even for escaped { (the parser will resolve it correctly)', () => { - // Cheap check is conservative — false positives are fine because the - // parser short-circuits on no real tokens. - expect(containsTokens('Literal \\{not-a-token}')).toBe(false) + it('returns true even for escaped { (the parser resolves it to a literal)', () => { + // Cheap check is conservative — an escaped `\{` still needs a parse so + // the backslash is stripped; a false positive only costs one parse. + expect(containsTokens('Literal \\{not-a-token}')).toBe(true) }) }) @@ -303,6 +303,14 @@ describe('interpolateTokens', () => { const c = ctx({ entryStack: [] }) expect(interpolateTokens('Welcome, {currentEntry.title}!', c)).toBe('Welcome, !') }) + + it('strips the backslash of an escaped `\\{` and emits a literal `{`', () => { + expect(interpolateTokens('Literal \\{not-a-token}', ctx())).toBe('Literal {not-a-token}') + }) + + it('strips the escape even when the string carries no real token', () => { + expect(interpolateTokens('\\{just-braces}', ctx())).toBe('{just-braces}') + }) }) // --------------------------------------------------------------------------- diff --git a/src/core/templates/tokenInterpolation.ts b/src/core/templates/tokenInterpolation.ts index 6946abc99..f0a1a456b 100644 --- a/src/core/templates/tokenInterpolation.ts +++ b/src/core/templates/tokenInterpolation.ts @@ -65,15 +65,12 @@ export function bindingToToken(source: DynamicPropBinding['source'], fieldPath: * string-typed props in a published page contain no token syntax at all * (button labels, html tags, etc.). Detecting that case in O(n) without * allocations short-circuits a full parse + concat per render. + * + * Any `{` warrants a parse — including an escaped `\{`, which the parser + * still processes to strip the backslash and emit a literal `{`. */ export function containsTokens(value: string): boolean { - // Need at least `{x.y}` — 5 chars minimum. Looking for an unescaped `{`. - if (value.length < 5) return false - for (let i = 0; i < value.length; i++) { - const c = value[i] - if (c === '{' && (i === 0 || value[i - 1] !== '\\')) return true - } - return false + return value.includes('{') } // ---------------------------------------------------------------------------