Skip to content

Commit

Permalink
Fix nested variable segments array
Browse files Browse the repository at this point in the history
  • Loading branch information
jg-rp committed Dec 22, 2024
1 parent f73f0d1 commit e9b11f4
Show file tree
Hide file tree
Showing 4 changed files with 61 additions and 16 deletions.
17 changes: 12 additions & 5 deletions docs/source/tutorials/static-analysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const template = engine.parse(`\
- {{ email }}
{% endfor %}
{% endif %}
{{ a[b.c].d }}
<p>
`)

Expand All @@ -38,10 +39,10 @@ console.log(engine.variablesSync(template))
**Output**

```javascript
[ 'user', 'title', 'email' ]
[ 'user', 'title', 'email', 'a', 'b' ]
```

Alternatively, use `Liquid.fullVariables(template)` to get a list of variables including their properties. Notice that variables from tag and filter arguments are included too.
Notice that variables from tag and filter arguments are included, as well as nested variables like `b` in the example. Alternatively, use `Liquid.fullVariables(template)` to get a list of variables including their properties as strings.

```javascript
// continued from above
Expand All @@ -61,7 +62,9 @@ engine.fullVariables(template).then(console.log)
'user.email_addresses[0]',
'user.email_addresses',
'title',
'email'
'email',
'a[b.c].d',
'b.c'
]
```

Expand All @@ -85,7 +88,9 @@ engine.variableSegments(template).then(console.log)
[ 'user', 'email_addresses', 0 ],
[ 'user', 'email_addresses' ],
[ 'title' ],
[ 'email' ]
[ 'email' ],
[ 'a', [ 'b', 'c' ], 'd' ],
[ 'b', 'c' ]
]
```

Expand All @@ -111,7 +116,9 @@ engine.globalVariableSegments(template).then(console.log)
[ 'user', 'address' ],
[ 'user', 'address', 'line1' ],
[ 'user', 'email_addresses', 0 ],
[ 'user', 'email_addresses' ]
[ 'user', 'email_addresses' ],
[ 'a', [ 'b', 'c' ], 'd' ],
[ 'b', 'c' ]
]
```

Expand Down
20 changes: 9 additions & 11 deletions src/liquid.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Context } from './context'
import { toPromise, toValueSync, isFunction, forOwn, isString, strictUniq } from './util'
import { TagClass, createTagClass, TagImplOptions, FilterImplOptions, Template, Value, StaticAnalysisOptions, StaticAnalysis, analyze, analyzeSync, Variable } from './template'
import { TagClass, createTagClass, TagImplOptions, FilterImplOptions, Template, Value, StaticAnalysisOptions, StaticAnalysis, analyze, analyzeSync, SegmentArray } from './template'
import { LookupType } from './fs/loader'
import { Render } from './render'
import { Parser } from './parser'
Expand Down Expand Up @@ -139,8 +139,6 @@ export class Liquid {
return analyzeSync(this.parse(html, filename), options)
}

// TODO: deduplicate paths if they are used more than once

/** Return an array of all variables without their properties. */
public async variables (template: string | Template[], options: StaticAnalysisOptions = {}): Promise<string[]> {
const analysis = await analyze(isString(template) ? this.parse(template) : template, options)
Expand All @@ -166,15 +164,15 @@ export class Liquid {
}

/** Return an array of all variables, each as an array of properties/segments. */
public async variableSegments (template: string | Template[], options: StaticAnalysisOptions = {}): Promise<Array<Array<string | number | Variable>>> {
public async variableSegments (template: string | Template[], options: StaticAnalysisOptions = {}): Promise<Array<SegmentArray>> {
const analysis = await analyze(isString(template) ? this.parse(template) : template, options)
return Array.from(strictUniq(Object.values(analysis.variables).flatMap((a) => a.map((v) => v.segments))))
return Array.from(strictUniq(Object.values(analysis.variables).flatMap((a) => a.map((v) => v.toArray()))))
}

/** Return an array of all variables, each as an array of properties/segments. */
public variableSegmentsSync (template: string | Template[], options: StaticAnalysisOptions = {}): Array<Array<string | number | Variable>> {
public variableSegmentsSync (template: string | Template[], options: StaticAnalysisOptions = {}): Array<SegmentArray> {
const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options)
return Array.from(strictUniq(Object.values(analysis.variables).flatMap((a) => a.map((v) => v.segments))))
return Array.from(strictUniq(Object.values(analysis.variables).flatMap((a) => a.map((v) => v.toArray()))))
}

/** Return an array of all expected context variables without their properties. */
Expand Down Expand Up @@ -202,14 +200,14 @@ export class Liquid {
}

/** Return an array of all expected context variables, each as an array of properties/segments. */
public async globalVariableSegments (template: string | Template[], options: StaticAnalysisOptions = {}): Promise<Array<Array<string | number | Variable>>> {
public async globalVariableSegments (template: string | Template[], options: StaticAnalysisOptions = {}): Promise<Array<SegmentArray>> {
const analysis = await analyze(isString(template) ? this.parse(template) : template, options)
return Array.from(strictUniq(Object.values(analysis.globals).flatMap((a) => a.map((v) => v.segments))))
return Array.from(strictUniq(Object.values(analysis.globals).flatMap((a) => a.map((v) => v.toArray()))))
}

/** Return an array of all expected context variables, each as an array of properties/segments. */
public globalVariableSegmentsSync (template: string | Template[], options: StaticAnalysisOptions = {}): Array<Array<string | number | Variable>> {
public globalVariableSegmentsSync (template: string | Template[], options: StaticAnalysisOptions = {}): Array<SegmentArray> {
const analysis = analyzeSync(isString(template) ? this.parse(template) : template, options)
return Array.from(strictUniq(Object.values(analysis.globals).flatMap((a) => a.map((v) => v.segments))))
return Array.from(strictUniq(Object.values(analysis.globals).flatMap((a) => a.map((v) => v.toArray()))))
}
}
20 changes: 20 additions & 0 deletions src/template/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ export interface VariableLocation {
file?: string;
}

/**
* A variable's segments as an array, possibly with nested arrays of segments.
*/
export type SegmentArray = Array<string | number | SegmentArray>

/**
* A variable's segments and location, which can be coerced to a string.
*/
Expand All @@ -34,6 +39,20 @@ export class Variable {
public toString (): string {
return segmentsString(this.segments, true)
}

/** Return this variable's segments as an array, possibly with nested arrays for nested paths. */
public toArray (): SegmentArray {
function * _visit (...segments: Array<string | number | Variable>): Generator<string | number | SegmentArray> {
for (const segment of segments) {
if (segment instanceof Variable) {
yield Array.from(_visit(...segment.segments))
} else {
yield segment
}
}
}
return Array.from(_visit(...this.segments))
}
}

/**
Expand Down Expand Up @@ -132,6 +151,7 @@ function * _analyze (templates: Template[], partials: boolean, sync: boolean): G

if (aliased !== undefined) {
const root = aliased.segments[0]
// TODO: What if a a template renders a rendered template? Do we need scope.parent?
if (isString(root) && !rootScope.has(root)) {
globals.push(aliased)
}
Expand Down
20 changes: 20 additions & 0 deletions test/integration/liquid/liquid.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,16 @@ describe('Liquid', function () {
expect(engine.variableSegmentsSync(engine.parse('{% assign c = 1 %}{{ a.b }}{{ c }}{{ c }}'))).toStrictEqual([['a', 'b'], ['c']])
})

it('should list all variables as an array of segments with nested variables as arrays', () => {
expect(engine.variableSegments('{{ a[b.c].d }}')).resolves.toStrictEqual([['a', ['b', 'c'], 'd'], ['b', 'c']])
expect(engine.variableSegments(engine.parse('{{ a[b.c].d }}'))).resolves.toStrictEqual([['a', ['b', 'c'], 'd'], ['b', 'c']])
})

it('should list all variables synchronously as an array of segments with nested variables as arrays', () => {
expect(engine.variableSegmentsSync('{{ a[b.c].d }}')).toStrictEqual([['a', ['b', 'c'], 'd'], ['b', 'c']])
expect(engine.variableSegmentsSync(engine.parse('{{ a[b.c].d }}'))).toStrictEqual([['a', ['b', 'c'], 'd'], ['b', 'c']])
})

it('should list global variables as an array of segments', () => {
expect(engine.globalVariableSegments('{% assign c = 1 %}{{ a.b }}{{ c }}{{ c }}')).resolves.toStrictEqual([['a', 'b']])
expect(engine.globalVariableSegments(engine.parse('{% assign c = 1 %}{{ a.b }}{{ c }}{{ c }}'))).resolves.toStrictEqual([['a', 'b']])
Expand All @@ -317,5 +327,15 @@ describe('Liquid', function () {
expect(engine.globalVariableSegmentsSync('{% assign c = 1 %}{{ a.b }}{{ c }}{{ c }}')).toStrictEqual([['a', 'b']])
expect(engine.globalVariableSegmentsSync(engine.parse('{% assign c = 1 %}{{ a.b }}{{ c }}{{ c }}'))).toStrictEqual([['a', 'b']])
})

it('should list global variables as an array of segments with nested variables as arrays', () => {
expect(engine.globalVariableSegments('{{ a[b.c].d }}')).resolves.toStrictEqual([['a', ['b', 'c'], 'd'], ['b', 'c']])
expect(engine.globalVariableSegments(engine.parse('{{ a[b.c].d }}'))).resolves.toStrictEqual([['a', ['b', 'c'], 'd'], ['b', 'c']])
})

it('should list global variables synchronously as an array of segments with nested variables as arrays', () => {
expect(engine.globalVariableSegmentsSync('{{ a[b.c].d }}')).toStrictEqual([['a', ['b', 'c'], 'd'], ['b', 'c']])
expect(engine.globalVariableSegmentsSync(engine.parse('{{ a[b.c].d }}'))).toStrictEqual([['a', ['b', 'c'], 'd'], ['b', 'c']])
})
})
})

0 comments on commit e9b11f4

Please sign in to comment.