From d705888c8d46d9dd95fcb41c0fb7bfbaf647eddd Mon Sep 17 00:00:00 2001 From: Yang Jun Date: Wed, 16 Oct 2024 22:02:44 +0800 Subject: [PATCH] feat: expose FilterToken to filter `this`, #762 --- src/parser/tokenizer.ts | 10 +++++----- src/template/filter-impl-options.ts | 2 ++ src/template/filter.spec.ts | 18 +++++++++--------- src/template/filter.ts | 11 +++++++---- src/template/output.ts | 4 +++- src/template/value.ts | 2 +- test/e2e/issues.spec.ts | 9 +++++++++ 7 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/parser/tokenizer.ts b/src/parser/tokenizer.ts index 365ae3f9c6..89c4c3e233 100644 --- a/src/parser/tokenizer.ts +++ b/src/parser/tokenizer.ts @@ -80,9 +80,7 @@ export class Tokenizer { readFilter (): FilterToken | null { this.skipBlank() if (this.end()) return null - this.assert(this.peek() === '|', `expected "|" before filter`) - this.p++ - const begin = this.p + this.assert(this.read() === '|', `expected "|" before filter`) const name = this.readIdentifier() if (!name.size()) { this.assert(this.end(), `expected filter name`) @@ -103,7 +101,7 @@ export class Tokenizer { } else { throw this.error('expected ":" after filter name') } - return new FilterToken(name.getText(), args, this.input, begin, this.p, this.file) + return new FilterToken(name.getText(), args, this.input, name.begin, this.p, this.file) } readFilterArg (): FilterArg | undefined { @@ -298,7 +296,9 @@ export class Tokenizer { end () { return this.p >= this.N } - + read () { + return this.input[this.p++] + } readTo (end: string): number { while (this.p < this.N) { ++this.p diff --git a/src/template/filter-impl-options.ts b/src/template/filter-impl-options.ts index d94d7b591a..3c3f9bc3c6 100644 --- a/src/template/filter-impl-options.ts +++ b/src/template/filter-impl-options.ts @@ -1,8 +1,10 @@ import type { Context } from '../context' import type { Liquid } from '../liquid' +import type { FilterToken } from '../tokens' export interface FilterImpl { context: Context; + token: FilterToken; liquid: Liquid; } diff --git a/src/template/filter.spec.ts b/src/template/filter.spec.ts index baf26fe507..e184bc5a0f 100644 --- a/src/template/filter.spec.ts +++ b/src/template/filter.spec.ts @@ -7,14 +7,14 @@ describe('filter', function () { const ctx = new Context({ thirty: 30 }) const liquid = { testVersion: '1.0' } as any it('should not change input if filter not registered', async function () { - const filter = new Filter('foo', undefined as any, [], liquid) + const filter = new Filter({ name: 'foo', args: [] } as any, undefined as any, liquid) expect(await toPromise(filter.render('value', ctx))).toBe('value') }) it('should call filter impl with correct arguments', async function () { const spy = jest.fn() const thirty = new NumberToken('30', 0, 2, undefined) - const filter = new Filter('foo', spy, [thirty], liquid) + const filter = new Filter({ name: 'foo', args: [thirty] } as any, spy, liquid) await toPromise(filter.render('foo', ctx)) expect(spy).toHaveBeenCalledWith('foo', 30) }) @@ -24,37 +24,37 @@ describe('filter', function () { return `${this.liquid.testVersion}: ${val + diff}` }) const ten = new NumberToken('10', 0, 2, undefined) - const filter = new Filter('add', spy, [ten], liquid) + const filter = new Filter({ name: 'add', args: [ten] } as any, spy, liquid) const val = await toPromise(filter.render('thirty', ctx)) expect(val).toEqual('1.0: 40') }) it('should render a simple filter', async function () { - expect(await toPromise(new Filter('upcase', (x: string) => x.toUpperCase(), [], liquid).render('foo', ctx))).toBe('FOO') + expect(await toPromise(new Filter({ name: 'upcase', args: [] } as any, (x: string) => x.toUpperCase(), liquid).render('foo', ctx))).toBe('FOO') }) it('should reject promise when filter throws', async function () { - const filter = new Filter('foo', function * () { throw new Error('intended') }, [], liquid) + const filter = new Filter({ name: 'foo', args: [] } as any, function * () { throw new Error('intended') }, liquid) expect(toPromise(filter.render('foo', ctx))).rejects.toMatchObject({ message: 'intended' }) }) it('should render filters with argument', async function () { const two = new NumberToken('2', 0, 1, undefined) - expect(await toPromise(new Filter('add', (a: number, b: number) => a + b, [two], liquid).render(3, ctx))).toBe(5) + expect(await toPromise(new Filter({ name: 'add', args: [two] } as any, (a: number, b: number) => a + b, liquid).render(3, ctx))).toBe(5) }) it('should render filters with multiple arguments', async function () { const two = new NumberToken('2', 0, 1, undefined) const c = new QuotedToken('"c"', 0, 3) - expect(await toPromise(new Filter('add', (a: number, b: number, c: number) => a + b + c, [two, c], liquid).render(3, ctx))).toBe('5c') + expect(await toPromise(new Filter({ name: 'add', args: [two, c] } as any, (a: number, b: number, c: number) => a + b + c, liquid).render(3, ctx))).toBe('5c') }) it('should pass Objects/Drops as it is', async function () { class Foo {} - expect(await toPromise(new Filter('name', (a: any) => a.constructor.name, [], liquid).render(new Foo(), ctx))).toBe('Foo') + expect(await toPromise(new Filter({ name: 'name', args: [] } as any, (a: any) => a.constructor.name, liquid).render(new Foo(), ctx))).toBe('Foo') }) it('should support key value pairs', async function () { const two = new NumberToken('2', 0, 1, undefined) - expect(await toPromise(new Filter('add', (a: number, b: number[]) => b[0] + ':' + (a + b[1]), [['num', two]], liquid).render(3, ctx))).toBe('num:5') + expect(await toPromise(new Filter({ name: 'add', args: [['num', two]] } as any, (a: number, b: number[]) => b[0] + ':' + (a + b[1]), liquid).render(3, ctx))).toBe('num:5') }) }) diff --git a/src/template/filter.ts b/src/template/filter.ts index 5d60197b12..cb0f9caaa6 100644 --- a/src/template/filter.ts +++ b/src/template/filter.ts @@ -4,6 +4,7 @@ import { identify, isFunction } from '../util/underscore' import { FilterHandler, FilterImplOptions } from './filter-impl-options' import { FilterArg, isKeyValuePair } from '../parser/filter-arg' import { Liquid } from '../liquid' +import { FilterToken } from '../tokens' export class Filter { public name: string @@ -11,14 +12,16 @@ export class Filter { public readonly raw: boolean private handler: FilterHandler private liquid: Liquid + private token: FilterToken - public constructor (name: string, options: FilterImplOptions | undefined, args: FilterArg[], liquid: Liquid) { - this.name = name + public constructor (token: FilterToken, options: FilterImplOptions | undefined, liquid: Liquid) { + this.token = token + this.name = token.name this.handler = isFunction(options) ? options : (isFunction(options?.handler) ? options!.handler : identify) this.raw = !isFunction(options) && !!options?.raw - this.args = args + this.args = token.args this.liquid = liquid } public * render (value: any, context: Context): IterableIterator { @@ -27,6 +30,6 @@ export class Filter { if (isKeyValuePair(arg)) argv.push([arg[0], yield evalToken(arg[1], context)]) else argv.push(yield evalToken(arg, context)) } - return yield this.handler.apply({ context, liquid: this.liquid }, [value, ...argv]) + return yield this.handler.apply({ context, token: this.token, liquid: this.liquid }, [value, ...argv]) } } diff --git a/src/template/output.ts b/src/template/output.ts index 8fffd49006..dfb37877a8 100644 --- a/src/template/output.ts +++ b/src/template/output.ts @@ -6,6 +6,7 @@ import { OutputToken } from '../tokens/output-token' import { Tokenizer } from '../parser' import { Liquid } from '../liquid' import { Filter } from './filter' +import { FilterToken } from '../tokens' export class Output extends TemplateImpl implements Template { value: Value @@ -16,7 +17,8 @@ export class Output extends TemplateImpl implements Template { const filters = this.value.filters const outputEscape = liquid.options.outputEscape if (!filters[filters.length - 1]?.raw && outputEscape) { - filters.push(new Filter(toString.call(outputEscape), outputEscape, [], liquid)) + const token = new FilterToken(toString.call(outputEscape), [], '', 0, 0) + filters.push(new Filter(token, outputEscape, liquid)) } } public * render (ctx: Context, emitter: Emitter): IterableIterator { diff --git a/src/template/value.ts b/src/template/value.ts index 3658d244e9..fb41bd7b68 100644 --- a/src/template/value.ts +++ b/src/template/value.ts @@ -18,7 +18,7 @@ export class Value { ? new Tokenizer(input, liquid.options.operators).readFilteredValue() : input this.initial = token.initial - this.filters = token.filters.map(({ name, args }) => new Filter(name, this.getFilter(liquid, name), args, liquid)) + this.filters = token.filters.map(token => new Filter(token, this.getFilter(liquid, token.name), liquid)) } public * value (ctx: Context, lenient?: boolean): Generator { lenient = lenient || (ctx.opts.lenientIf && this.filters.length > 0 && this.filters[0].name === 'default') diff --git a/test/e2e/issues.spec.ts b/test/e2e/issues.spec.ts index d37d91104c..554d4c0b52 100644 --- a/test/e2e/issues.spec.ts +++ b/test/e2e/issues.spec.ts @@ -515,4 +515,13 @@ describe('Issues', function () { expect(LiquidError.is(err) && err.originalError).toHaveProperty('message', 'intended') } }) + it('getting current line number and template name from a filter #762', () => { + const engine = new Liquid() + engine.registerFilter('pos', function (val: string) { + const [line, col] = this.token.getPosition() + return `[${line},${col}] ${val}` + }) + const result = engine.parseAndRenderSync(`\n{{ "foo" | pos }}`) + expect(result).toEqual('\n[2,12] foo') + }) })