From cf8133fe4662f1a70b86952134c34a5c9bc27ea6 Mon Sep 17 00:00:00 2001 From: Yang Jun Date: Sat, 16 May 2026 01:58:10 +0800 Subject: [PATCH] feat(context): null-prototype scope frames via createScope - Add createScope() building Object.create(null) with optional own props - Initialize context stack bottom with createScope() for assign/capture - Push null-proto scopes from for, tablerow, block, layout, include (incl. Jekyll) Co-authored-by: Cursor --- src/context/context.ts | 4 ++-- src/context/scope.ts | 8 +++++++- src/tags/block.ts | 4 ++-- src/tags/for.ts | 5 +++-- src/tags/include.ts | 6 +++--- src/tags/layout.ts | 4 ++-- src/tags/tablerow.ts | 3 ++- 7 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/context/context.ts b/src/context/context.ts index 205a576229..083a17cfed 100644 --- a/src/context/context.ts +++ b/src/context/context.ts @@ -2,7 +2,7 @@ import { getPerformance } from '../util/performance' import { Drop } from '../drop/drop' import { __assign } from 'tslib' import { NormalizedFullOptions, defaultOptions, RenderOptions } from '../liquid-options' -import { Scope } from './scope' +import { createScope, Scope } from './scope' import { hasOwnProperty, isArray, isNil, isUndefined, isString, isFunction, toLiquid, InternalUndefinedVariableError, toValueSync, isObject, Limiter, toValue } from '../util' type PropertyKey = string | number; @@ -12,7 +12,7 @@ export class Context { * insert a Context-level empty scope, * for tags like `{% capture %}` `{% assign %}` to operate */ - private scopes: Scope[] = [{}] + private scopes: Scope[] = [createScope()] private registers = {} /** * user passed in scope diff --git a/src/context/scope.ts b/src/context/scope.ts index 9fcc06dae3..2cd2615882 100644 --- a/src/context/scope.ts +++ b/src/context/scope.ts @@ -1,7 +1,13 @@ import { Drop } from '../drop/drop' -interface ScopeObject extends Record { +export interface ScopeObject extends Record { toLiquid?: () => any; } export type Scope = ScopeObject | Drop + +export function createScope (from?: ScopeObject): ScopeObject { + const scope = Object.create(null) + if (from) Object.assign(scope, from) + return scope +} diff --git a/src/tags/block.ts b/src/tags/block.ts index 947f0b3e59..56dca80af8 100644 --- a/src/tags/block.ts +++ b/src/tags/block.ts @@ -1,4 +1,4 @@ -import { BlockMode } from '../context' +import { BlockMode, createScope } from '../context' import { isTagToken } from '../util' import { BlockDrop } from '../drop' import { Liquid, TagToken, TopLevelToken, Template, Context, Emitter, Tag } from '..' @@ -38,7 +38,7 @@ export default class extends Tag { if (stack.includes(self)) throw new Error('block tag cannot be nested') stack.push(self) - ctx.push({ block: superBlock }) + ctx.push(createScope({ block: superBlock })) yield liquid.renderer.renderTemplates(templates, ctx, emitter) ctx.pop() stack.pop() diff --git a/src/tags/for.ts b/src/tags/for.ts index 0d29dee78f..fec0b06030 100644 --- a/src/tags/for.ts +++ b/src/tags/for.ts @@ -1,5 +1,6 @@ import { Hash, ValueToken, Liquid, Tag, evalToken, Emitter, TagToken, TopLevelToken, Context, Template, ParseStream } from '..' import { assertEmpty, isValueToken, toEnumerable } from '../util' +import { createScope } from '../context/scope' import { ForloopDrop } from '../drop/forloop-drop' import { Parser } from '../parser' import { Arguments } from '../template' @@ -50,7 +51,7 @@ export default class extends Tag { } const continueKey = 'continue-' + this.variable + '-' + this.collection.getText() - ctx.push({ continue: ctx.getRegister(continueKey, {}) }) + ctx.push(createScope({ continue: ctx.getRegister(continueKey, {}) })) const hash = yield this.hash.render(ctx) ctx.pop() @@ -65,7 +66,7 @@ export default class extends Tag { }, collection) ctx.setRegister(continueKey, (hash['offset'] || 0) + collection.length) - const scope = { forloop: new ForloopDrop(collection.length, this.collection.getText(), this.variable) } + const scope = createScope({ forloop: new ForloopDrop(collection.length, this.collection.getText(), this.variable) }) ctx.push(scope) for (const item of collection) { scope[this.variable] = item diff --git a/src/tags/include.ts b/src/tags/include.ts index 3ad785b907..0db1ee7496 100644 --- a/src/tags/include.ts +++ b/src/tags/include.ts @@ -1,5 +1,5 @@ import { Template, ValueToken, TopLevelToken, Liquid, Tag, assert, evalToken, Hash, Emitter, TagToken, Context } from '..' -import { BlockMode, Scope } from '../context' +import { BlockMode, createScope, Scope } from '../context' import { Parser } from '../parser' import { Argument, Arguments, PartialScope } from '../template' import { isString, isValueToken } from '../util' @@ -34,10 +34,10 @@ export default class extends Tag { const saved = ctx.saveRegister('blocks', 'blockMode') ctx.setRegister('blocks', {}) ctx.setRegister('blockMode', BlockMode.OUTPUT) - const scope = (yield hash.render(ctx)) as Scope + const scope = createScope((yield hash.render(ctx)) as Scope) if (withVar) scope[filepath] = yield evalToken(withVar, ctx) const templates = (yield liquid._parsePartialFile(filepath, ctx.sync, this['currentFile'])) as Template[] - ctx.push(ctx.opts.jekyllInclude ? { include: scope } : scope) + ctx.push(ctx.opts.jekyllInclude ? createScope({ include: scope }) : scope) yield renderer.renderTemplates(templates, ctx, emitter) ctx.pop() ctx.restoreRegister(saved) diff --git a/src/tags/layout.ts b/src/tags/layout.ts index cf1e7270f7..ab2b3b539e 100644 --- a/src/tags/layout.ts +++ b/src/tags/layout.ts @@ -1,5 +1,5 @@ import { Scope, Template, Liquid, Tag, assert, Emitter, Hash, TagToken, TopLevelToken, Context } from '..' -import { BlockMode } from '../context' +import { BlockMode, createScope } from '../context' import { parseFilePath, renderFilePath, ParsedFileName } from './render' import { BlankDrop } from '../drop' import { Parser } from '../parser' @@ -39,7 +39,7 @@ export default class extends Tag { ctx.setRegister('blockMode', BlockMode.OUTPUT) // render the layout file use stored blocks - ctx.push((yield args.render(ctx)) as Scope) + ctx.push(createScope((yield args.render(ctx)) as Scope)) yield renderer.renderTemplates(templates, ctx, emitter) ctx.pop() } diff --git a/src/tags/tablerow.ts b/src/tags/tablerow.ts index 91f8404451..1676fe46df 100644 --- a/src/tags/tablerow.ts +++ b/src/tags/tablerow.ts @@ -1,4 +1,5 @@ import { isValueToken, toEnumerable } from '../util' +import { createScope } from '../context/scope' import { ValueToken, Liquid, Tag, evalToken, Emitter, Hash, TagToken, TopLevelToken, Context, Template, ParseStream } from '..' import { TablerowloopDrop } from '../drop/tablerowloop-drop' import { Parser } from '../parser' @@ -48,7 +49,7 @@ export default class extends Tag { const r = this.liquid.renderer const tablerowloop = new TablerowloopDrop(collection.length, cols, this.collection.getText(), this.variable) - const scope = { tablerowloop } + const scope = createScope({ tablerowloop }) ctx.push(scope) for (let idx = 0; idx < collection.length; idx++, tablerowloop.next()) {