diff --git a/src/context/context.spec.ts b/src/context/context.spec.ts index e9262bc06..2e2220175 100644 --- a/src/context/context.spec.ts +++ b/src/context/context.spec.ts @@ -1,4 +1,5 @@ import { Context } from './context' +import { Drop } from '../drop/drop' import { Scope } from './scope' describe('Context', function () { @@ -250,6 +251,21 @@ describe('Context', function () { expect(ctx.getSync(['bar', 'foo'])).toEqual('foo') expect(ctx.getSync(['bar', 'bar'])).toEqual(undefined) }) + it('should wrap plain objects with null prototype', function () { + const scope = ctx.push({ foo: 'FOO' }) + expect(Object.getPrototypeOf(scope)).toBeNull() + }) + it('should return pushed scope for in-place mutation', function () { + const scope = ctx.push({}) + scope.item = 'ITEM' + expect(ctx.getSync(['item'])).toEqual('ITEM') + }) + it('should push Drop instances as-is', function () { + class TestDrop extends Drop {} + const drop = new TestDrop() + const pushed = ctx.push(drop) + expect(pushed).toBe(drop) + }) }) describe('.pop()', function () { it('should pop scope', async function () { diff --git a/src/context/context.ts b/src/context/context.ts index ea3fa93f8..957579794 100644 --- a/src/context/context.ts +++ b/src/context/context.ts @@ -94,8 +94,14 @@ export class Context { } return scope } - public push (ctx: object) { - return this.scopes.push(ctx) + public push (ctx: Scope): Scope { + const scope = ctx instanceof Drop + ? ctx + : Object.getPrototypeOf(ctx) === null + ? ctx + : createScope(ctx) + this.scopes.push(scope) + return scope } public pop () { return this.scopes.pop() diff --git a/src/context/scope.ts b/src/context/scope.ts index d0d77759e..8b2a4626f 100644 --- a/src/context/scope.ts +++ b/src/context/scope.ts @@ -24,15 +24,5 @@ export function shouldBlockScopeKeyWrite (key: PropertyKey, ownPropertyOnly: boo } export function createScope (from?: ScopeObject): ScopeObject { - return from ? sanitizeScope(from) : Object.create(null) -} - -export function sanitizeScope (obj: ScopeObject): ScopeObject { - const scope = Object.create(null) - for (const key of Object.keys(obj)) { - if (hasOwnProperty.call(obj, key)) { - scope[key] = obj[key] - } - } - return scope + return Object.assign(Object.create(null), from) } diff --git a/src/filters/array.ts b/src/filters/array.ts index c57b179a3..2a5cb8a0e 100644 --- a/src/filters/array.ts +++ b/src/filters/array.ts @@ -3,7 +3,6 @@ import { arrayIncludes, equals, evalToken, isTruthy } from '../render' import { Value, FilterImpl } from '../template' import { Tokenizer } from '../parser' import type { Scope } from '../context' -import { createScope } from '../context/scope' import { EmptyDrop } from '../drop' export const join = argumentsToValue(function (this: FilterImpl, v: any[], arg: string) { @@ -135,7 +134,7 @@ function * filter_exp (this: FilterImpl, include: boolean, arr const keyTemplate = new Value(stringify(exp), this.liquid) const array = toArray(arr) for (const item of array) { - this.context.push(createScope({ [itemName]: item })) + this.context.push({ [itemName]: item }) const value = yield keyTemplate.value(this.context) this.context.pop() if (value === include) filtered.push(item) @@ -176,7 +175,7 @@ export function * group_by_exp (this: FilterImpl, arr: T[], it const keyTemplate = new Value(stringify(exp), this.liquid) arr = toEnumerable(arr) for (const item of arr) { - this.context.push(createScope({ [itemName]: item })) + this.context.push({ [itemName]: item }) const key = yield keyTemplate.value(this.context) this.context.pop() if (!map.has(key)) map.set(key, []) diff --git a/src/tags/block.ts b/src/tags/block.ts index 56dca80af..947f0b3e5 100644 --- a/src/tags/block.ts +++ b/src/tags/block.ts @@ -1,4 +1,4 @@ -import { BlockMode, createScope } from '../context' +import { BlockMode } 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(createScope({ block: superBlock })) + ctx.push({ 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 ec964ac6a..c338a4018 100644 --- a/src/tags/for.ts +++ b/src/tags/for.ts @@ -1,6 +1,5 @@ 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' @@ -44,7 +43,7 @@ export default class extends Tag { * render (ctx: Context, emitter: Emitter): Generator { const r = this.liquid.renderer const continueKey = 'continue-' + this.variable + '-' + this.collection.getText() - ctx.push(createScope({ continue: ctx.getRegister(continueKey, {}) })) + ctx.push({ continue: ctx.getRegister(continueKey, {}) }) const hash = (yield this.hash.render(ctx)) as Record ctx.pop() @@ -68,8 +67,7 @@ export default class extends Tag { if (!this.templates.length) return - const scope = createScope({ forloop: new ForloopDrop(collection.length, this.collection.getText(), this.variable) }) - ctx.push(scope) + const scope = ctx.push({ forloop: new ForloopDrop(collection.length, this.collection.getText(), this.variable) }) for (const item of collection) { scope[this.variable] = item ctx.continueCalled = ctx.breakCalled = false diff --git a/src/tags/include.ts b/src/tags/include.ts index 41507757d..a0965a617 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, createScope, Scope } from '../context' +import { BlockMode, Scope } from '../context' import { Parser } from '../parser' import { Argument, Arguments, PartialScope } from '../template' import { isString, isValueToken } from '../util' @@ -37,10 +37,10 @@ export default class extends Tag { const saved = ctx.saveRegister('blocks', 'blockMode') ctx.setRegister('blocks', {}) ctx.setRegister('blockMode', BlockMode.OUTPUT) - const scope = createScope((yield hash.render(ctx)) as Scope) + const scope = (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 ? createScope({ include: scope }) : scope) + ctx.push(ctx.opts.jekyllInclude ? { 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 91e512ecb..ca8ddf017 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, createScope } from '../context' +import { BlockMode } from '../context' import { parseFilePath, renderFilePath, ParsedFileName } from './render' import { BlankDrop } from '../drop' import { Parser } from '../parser' @@ -41,7 +41,7 @@ export default class extends Tag { ctx.setRegister('blockMode', BlockMode.OUTPUT) // render the layout file use stored blocks - ctx.push(createScope((yield args.render(ctx)) as Scope)) + ctx.push((yield args.render(ctx)) as Scope) yield renderer.renderTemplates(templates, ctx, emitter) ctx.pop() ctx.depthLimit.release(1) diff --git a/src/tags/tablerow.ts b/src/tags/tablerow.ts index 563b44392..22352b698 100644 --- a/src/tags/tablerow.ts +++ b/src/tags/tablerow.ts @@ -1,5 +1,4 @@ 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' @@ -53,8 +52,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 = createScope({ tablerowloop }) - ctx.push(scope) + const scope = ctx.push({ tablerowloop }) for (let idx = 0; idx < collection.length; idx++, tablerowloop.next()) { scope[this.variable] = collection[idx]