Skip to content

Commit

Permalink
Test analysis of standard tags
Browse files Browse the repository at this point in the history
  • Loading branch information
jg-rp committed Nov 18, 2024
1 parent e7b8559 commit a69f33c
Show file tree
Hide file tree
Showing 15 changed files with 369 additions and 79 deletions.
9 changes: 6 additions & 3 deletions src/tags/assign.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
import { Value, Liquid, TopLevelToken, TagToken, Context, Tag } from '..'
import { Arguments } from '../template'
import { IdentifierToken } from '../tokens'

export default class extends Tag {
private key: string
private value: Value
private identifier: IdentifierToken

constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
this.key = this.tokenizer.readIdentifier().content
this.identifier = this.tokenizer.readIdentifier()
this.key = this.identifier.content
this.tokenizer.assert(this.key, 'expected variable name')

this.tokenizer.skipBlank()
Expand All @@ -24,7 +27,7 @@ export default class extends Tag {
yield this.value
}

public * localScope (): Iterable<string> {
yield this.key
public * localScope (): Iterable<IdentifierToken> {
yield this.identifier
}
}
26 changes: 15 additions & 11 deletions src/tags/capture.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import { Liquid, Tag, Template, Context, TagToken, TopLevelToken } from '..'
import { Parser } from '../parser'
import { evalQuotedToken } from '../render'
import { IdentifierToken, QuotedToken } from '../tokens'
import { isTagToken } from '../util'

export default class extends Tag {
identifier: IdentifierToken | QuotedToken
variable: string
templates: Template[] = []
constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid, parser: Parser) {
super(tagToken, remainTokens, liquid)
this.variable = this.readVariableName()
this.identifier = this.readVariable()
this.variable = this.identifier.content

while (remainTokens.length) {
const token = remainTokens.shift()!
Expand All @@ -17,24 +19,26 @@ export default class extends Tag {
}
throw new Error(`tag ${tagToken.getText()} not closed`)
}

private readVariable (): IdentifierToken | QuotedToken {
let ident: IdentifierToken | QuotedToken | undefined = this.tokenizer.readIdentifier()
if (ident.content) return ident
ident = this.tokenizer.readQuoted()
if (ident) return ident
throw this.tokenizer.error('invalid capture name')
}

* render (ctx: Context): Generator<unknown, void, string> {
const r = this.liquid.renderer
const html = yield r.renderTemplates(this.templates, ctx)
ctx.bottom()[this.variable] = html
}
private readVariableName () {
const word = this.tokenizer.readIdentifier().content
if (word) return word
const quoted = this.tokenizer.readQuoted()
if (quoted) return evalQuotedToken(quoted)
throw this.tokenizer.error('invalid capture name')
}

public children (): Iterable<Template> {
return this.templates
}

public * localScope (): Iterable<string> {
yield this.variable
public * localScope (): Iterable<string | IdentifierToken | QuotedToken> {
yield this.identifier
}
}
5 changes: 3 additions & 2 deletions src/tags/case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,9 @@ export default class extends Tag {
}
}

public arguments (): Arguments {
return this.branches.flatMap(b => b.values)
public * arguments (): Arguments {
yield this.value
yield * this.branches.flatMap(b => b.values)
}

public * children (): Iterable<Template> {
Expand Down
14 changes: 6 additions & 8 deletions src/tags/decrement.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Tag, Liquid, TopLevelToken, Emitter, TagToken, Context } from '..'
import { Arguments } from '../template'
import { IdentifierToken } from '../tokens'
import { isNumber, stringify } from '../util'

export default class extends Tag {
private identifier: IdentifierToken
private variable: string
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
this.variable = this.tokenizer.readIdentifier().content
this.identifier = this.tokenizer.readIdentifier()
this.variable = this.identifier.content
}
render (context: Context, emitter: Emitter) {
const scope = context.environments
Expand All @@ -16,11 +18,7 @@ export default class extends Tag {
emitter.write(stringify(--scope[this.variable]))
}

public * arguments (): Arguments {
yield this.variable
}

public * localScope (): Iterable<string> {
yield this.variable
public * localScope (): Iterable<string | IdentifierToken> {
yield this.identifier
}
}
4 changes: 2 additions & 2 deletions src/tags/if.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ export default class extends Tag {
let p: Template[] = []
parser.parseStream(remainTokens)
.on('start', () => this.branches.push({
value: new Value(tagToken.args, this.liquid),
value: new Value(tagToken, this.liquid),
templates: (p = [])
}))
.on('tag:elsif', (token: TagToken) => {
assert(!this.elseTemplates, 'unexpected elsif after else')
this.branches.push({
value: new Value(token.args, this.liquid),
value: new Value(token, this.liquid),
templates: (p = [])
})
})
Expand Down
14 changes: 6 additions & 8 deletions src/tags/increment.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { isNumber, stringify } from '../util'
import { Tag, Liquid, TopLevelToken, Emitter, TagToken, Context } from '..'
import { Arguments } from '../template'
import { IdentifierToken } from '../tokens'

export default class extends Tag {
private identifier: IdentifierToken
private variable: string
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
this.variable = this.tokenizer.readIdentifier().content
this.identifier = this.tokenizer.readIdentifier()
this.variable = this.identifier.content
}
render (context: Context, emitter: Emitter) {
const scope = context.environments
Expand All @@ -18,11 +20,7 @@ export default class extends Tag {
emitter.write(stringify(val))
}

public * arguments (): Arguments {
yield this.variable
}

public * localScope (): Iterable<string> {
yield this.variable
public * localScope (): Iterable<string | IdentifierToken> {
yield this.identifier
}
}
10 changes: 8 additions & 2 deletions src/tags/tablerow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,14 @@ export default class extends Tag {
return this.templates
}

public arguments (): Arguments {
return Object.values(this.args.hash).filter(isValueToken)
public * arguments (): Arguments {
yield this.collection

for (const v of Object.values(this.args.hash)) {
if (isValueToken(v)) {
yield v
}
}
}

public blockScope (): string[] {
Expand Down
4 changes: 2 additions & 2 deletions src/tags/unless.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export default class extends Tag {
let elseCount = 0
parser.parseStream(remainTokens)
.on('start', () => this.branches.push({
value: new Value(tagToken.args, this.liquid),
value: new Value(tagToken, this.liquid),
test: isFalsy,
templates: (p = [])
}))
Expand All @@ -21,7 +21,7 @@ export default class extends Tag {
return
}
this.branches.push({
value: new Value(token.args, this.liquid),
value: new Value(token, this.liquid),
test: isTruthy,
templates: (p = [])
})
Expand Down
29 changes: 17 additions & 12 deletions src/template/analysis.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Argument, Template, Value } from '.'
import { isKeyValuePair } from '../parser/filter-arg'
import { PropertyAccessToken, ValueToken } from '../tokens'
import { IdentifierToken, PropertyAccessToken, ValueToken } from '../tokens'
import {
isNumberToken,
isPropertyAccessToken,
Expand Down Expand Up @@ -139,12 +139,17 @@ export function analyze (templates: Template[]): StaticAnalysis {
}

if (template.localScope) {
for (const key of template.localScope()) {
// Names added to the scope by tags like `assign`, `capture` and `increment`.
templateScope.add(key)
// XXX: This is the row and col of the node as some names (like 'tablerow') don't have a token.
const [row, col] = template.token.getPosition()
locals.push(new Variable([key], { row, col, file: template.token.file }))
for (const ident of template.localScope()) {
if (isString(ident)) {
templateScope.add(ident)
// XXX: This is the row and col of the node as some names (like 'tablerow') don't have a token.
const [row, col] = template.token.getPosition()
locals.push(new Variable([ident], { row, col, file: template.token.file }))
} else {
templateScope.add(ident.content)
const [row, col] = ident.getPosition()
locals.push(new Variable([ident.content], { row, col, file: template.token.file }))
}
}
}

Expand Down Expand Up @@ -203,7 +208,10 @@ class DummyScope {
}

function * extractVariables (value: Argument): Generator<Variable> {
if (isValueToken(value)) {
if (value instanceof IdentifierToken) {
const [row, col] = value.getPosition()
yield new Variable([value.content], { row, col, file: value.file })
} else if (isValueToken(value)) {
yield * extractValueTokenVariables(value)
} else if (value instanceof Value) {
yield * extractFilteredValueVariables(value)
Expand Down Expand Up @@ -234,10 +242,7 @@ function * extractValueTokenVariables (token: ValueToken): Generator<Variable> {
yield * extractValueTokenVariables(token.rhs)
} else if (isPropertyAccessToken(token)) {
yield extractPropertyAccessVariable(token)
} else if (
isNumberToken(token) ||
isWordToken(token)
) {
} else if (isWordToken(token)) {
const [row, col] = token.getPosition()
yield new Variable([token.content], {
row,
Expand Down
6 changes: 3 additions & 3 deletions src/template/template.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Context } from '../context/context'
import { Token } from '../tokens/token'
import { Emitter } from '../emitters/emitter'
import { ValueToken } from '../tokens'
import { IdentifierToken, QuotedToken, ValueToken } from '../tokens'
import { Value } from './value'

export type Argument = string | Value | ValueToken
export type Argument = IdentifierToken | Value | ValueToken
export type Arguments = Iterable<Argument>

export interface Template {
Expand All @@ -13,5 +13,5 @@ export interface Template {
children?(): Iterable<Template>;
arguments?(): Arguments;
blockScope?(): Iterable<string>;
localScope?(): Iterable<string>;
localScope?(): Iterable<string | IdentifierToken | QuotedToken>;
}
24 changes: 18 additions & 6 deletions src/template/value.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Filter } from './filter'
import { Expression } from '../render'
import { Tokenizer } from '../parser'
import { assert } from '../util'
import type { FilteredValueToken } from '../tokens'
import { assert, isString, isTagToken } from '../util'
import type { FilteredValueToken, TagToken } from '../tokens'
import type { Liquid } from '../liquid'
import type { Context } from '../context'

Expand All @@ -13,13 +13,12 @@ export class Value {
/**
* @param str the value to be valuated, eg.: "foobar" | truncate: 3
*/
public constructor (input: string | FilteredValueToken, liquid: Liquid) {
const token: FilteredValueToken = typeof input === 'string'
? new Tokenizer(input, liquid.options.operators).readFilteredValue()
: input
public constructor (input: string | FilteredValueToken | TagToken, liquid: Liquid) {
const token = this.getToken(input, liquid)
this.initial = token.initial
this.filters = token.filters.map(token => new Filter(token, this.getFilter(liquid, token.name), liquid))
}

public * value (ctx: Context, lenient?: boolean): Generator<unknown, unknown, unknown> {
lenient = lenient || (ctx.opts.lenientIf && this.filters.length > 0 && this.filters[0].name === 'default')
let val = yield this.initial.evaluate(ctx, lenient)
Expand All @@ -29,9 +28,22 @@ export class Value {
}
return val
}

private getFilter (liquid: Liquid, name: string) {
const impl = liquid.filters[name]
assert(impl || !liquid.options.strictFilters, () => `undefined filter: ${name}`)
return impl
}

private getToken (input: string | FilteredValueToken | TagToken, liquid: Liquid): FilteredValueToken {
if (isString(input)) {
return new Tokenizer(input, liquid.options.operators).readFilteredValue()
}

if (isTagToken(input)) {
return new Tokenizer(input.input, liquid.options.operators, input.file, input.argsRange()).readFilteredValue()
}

return input
}
}
6 changes: 4 additions & 2 deletions src/tokens/liquid-tag-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ export class LiquidTagToken extends DelimitedToken {
file?: string
) {
super(TokenKind.Tag, [begin, end], input, begin, end, false, false, file)

this.tokenizer = new Tokenizer(input, options.operators, file, this.contentRange)
this.name = this.tokenizer.readTagName()
this.tokenizer.assert(this.name, 'illegal liquid tag syntax')

this.tokenizer.skipBlank()
this.args = this.tokenizer.remaining()
}

public argsRange (): [number, number] {
return [this.tokenizer.p, this.contentRange[1]]
}
}
5 changes: 5 additions & 0 deletions src/tokens/tag-token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ export class TagToken extends DelimitedToken {
this.tokenizer.assert(this.name, `illegal tag syntax, tag name expected`)
this.tokenizer.skipBlank()
}

get args (): string {
return this.tokenizer.input.slice(this.tokenizer.p, this.contentRange[1])
}

public argsRange (): [number, number] {
return [this.tokenizer.p, this.contentRange[1]]
}
}
Loading

0 comments on commit a69f33c

Please sign in to comment.