Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: harttle/liquidjs
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v9.38.0
Choose a base ref
...
head repository: harttle/liquidjs
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v9.39.0
Choose a head ref
  • 4 commits
  • 17 files changed
  • 3 contributors

Commits on Jul 9, 2022

  1. feat: iteration protocols

    jg-rp authored and harttle committed Jul 9, 2022

    Verified

    This commit was signed with the committer’s verified signature.
    dtolnay David Tolnay
    Copy the full SHA
    a19feea View commit details
  2. Unverified

    This commit is not signed, but one or more authors requires that any commit attributed to them is signed.
    Copy the full SHA
    c3e51ca View commit details
  3. Copy the full SHA
    4289b4e View commit details
  4. chore(release): 9.39.0 [skip ci]

    # [9.39.0](v9.38.0...v9.39.0) (2022-07-09)
    
    ### Bug Fixes
    
    * for tag not respecting Drop#valueOf(), fixes [#515](#515) ([c3e51ca](c3e51ca))
    
    ### Features
    
    * iteration protocols ([a19feea](a19feea))
    semantic-release-bot committed Jul 9, 2022
    Copy the full SHA
    0aca2dd View commit details
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
# [9.39.0](https://github.com/harttle/liquidjs/compare/v9.38.0...v9.39.0) (2022-07-09)


### Bug Fixes

* for tag not respecting Drop#valueOf(), fixes [#515](https://github.com/harttle/liquidjs/issues/515) ([c3e51ca](https://github.com/harttle/liquidjs/commit/c3e51caa701fd4449ed5257e23569a37ef12dea2))


### Features

* iteration protocols ([a19feea](https://github.com/harttle/liquidjs/commit/a19feea7c46fc476139a150bda051f485328afe8))

# [9.38.0](https://github.com/harttle/liquidjs/compare/v9.37.0...v9.38.0) (2022-07-07)


2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "liquidjs",
"version": "9.38.0",
"version": "9.39.0",
"description": "A simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript.",
"main": "dist/liquid.node.cjs.js",
"module": "dist/liquid.node.esm.js",
4 changes: 0 additions & 4 deletions src/drop/drop.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
export abstract class Drop {
public valueOf (): any {
return undefined
}

public liquidMethodMissing (key: string | number): Promise<string | undefined> | string | undefined {
return undefined
}
10 changes: 5 additions & 5 deletions src/liquid.ts
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import { TagMap } from './template/tag/tag-map'
import { FilterMap } from './template/filter/filter-map'
import { LiquidOptions, normalizeDirectoryList, NormalizedFullOptions, normalize, RenderOptions } from './liquid-options'
import { FilterImplOptions } from './template/filter/filter-impl-options'
import { toPromise, toValue } from './util/async'
import { toPromise, toValueSync } from './util/async'

export * from './util/error'
export * from './types'
@@ -47,7 +47,7 @@ export class Liquid {
return toPromise(this._render(tpl, scope, { ...renderOptions, sync: false }))
}
public renderSync (tpl: Template[], scope?: object, renderOptions?: RenderOptions): any {
return toValue(this._render(tpl, scope, { ...renderOptions, sync: true }))
return toValueSync(this._render(tpl, scope, { ...renderOptions, sync: true }))
}
public renderToNodeStream (tpl: Template[], scope?: object, renderOptions: RenderOptions = {}): NodeJS.ReadableStream {
const ctx = new Context(scope, this.options, renderOptions)
@@ -62,7 +62,7 @@ export class Liquid {
return toPromise(this._parseAndRender(html, scope, { ...renderOptions, sync: false }))
}
public parseAndRenderSync (html: string, scope?: object, renderOptions?: RenderOptions): any {
return toValue(this._parseAndRender(html, scope, { ...renderOptions, sync: true }))
return toValueSync(this._parseAndRender(html, scope, { ...renderOptions, sync: true }))
}

public _parsePartialFile (file: string, sync?: boolean, currentFile?: string) {
@@ -75,7 +75,7 @@ export class Liquid {
return toPromise<Template[]>(this.parser.parseFile(file, false))
}
public parseFileSync (file: string): Template[] {
return toValue<Template[]>(this.parser.parseFile(file, true))
return toValueSync<Template[]>(this.parser.parseFile(file, true))
}
public async renderFile (file: string, ctx?: object, renderOptions?: RenderOptions) {
const templates = await this.parseFile(file)
@@ -98,7 +98,7 @@ export class Liquid {
return toPromise(this._evalValue(str, ctx))
}
public evalValueSync (str: string, ctx: Context): any {
return toValue(this._evalValue(str, ctx))
return toValueSync(this._evalValue(str, ctx))
}

public registerFilter (name: string, filter: FilterImplOptions) {
35 changes: 10 additions & 25 deletions src/util/async.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,8 @@
import { isFunction } from './underscore'

type resolver = (x?: any) => any

export interface Thenable<T> {
then (resolve: resolver, reject?: resolver): Thenable<T>;
catch (reject: resolver): Thenable<T>;
}

function isThenable<T> (val: any): val is Thenable<T> {
return val && isFunction(val.then)
}

function isAsyncIterator (val: any): val is IterableIterator<any> {
return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return)
}
import { isPromise, isIterator } from './underscore'

// convert an async iterator to a Promise
export async function toPromise<T> (val: Generator<unknown, T, unknown> | Thenable<T> | T): Promise<T> {
if (!isAsyncIterator(val)) return val
export async function toPromise<T> (val: Generator<unknown, T, unknown> | Promise<T> | T): Promise<T> {
if (!isIterator(val)) return val
let value: unknown
let done = false
let next = 'next'
@@ -27,8 +12,8 @@ export async function toPromise<T> (val: Generator<unknown, T, unknown> | Thenab
value = state.value
next = 'next'
try {
if (isAsyncIterator(value)) value = toPromise(value)
if (isThenable(value)) value = await value
if (isIterator(value)) value = toPromise(value)
if (isPromise(value)) value = await value
} catch (err) {
next = 'throw'
value = err
@@ -37,9 +22,9 @@ export async function toPromise<T> (val: Generator<unknown, T, unknown> | Thenab
return value as T
}

// convert an async iterator to a value in a synchronous maner
export function toValue<T> (val: Generator<unknown, T, unknown> | T): T {
if (!isAsyncIterator(val)) return val
// convert an async iterator to a value in a synchronous manner
export function toValueSync<T> (val: Generator<unknown, T, unknown> | T): T {
if (!isIterator(val)) return val
let value: any
let done = false
let next = 'next'
@@ -48,9 +33,9 @@ export function toValue<T> (val: Generator<unknown, T, unknown> | T): T {
done = state.done
value = state.value
next = 'next'
if (isAsyncIterator(value)) {
if (isIterator(value)) {
try {
value = toValue(value)
value = toValueSync(value)
} catch (err) {
next = 'throw'
value = err
4 changes: 3 additions & 1 deletion src/util/collection.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { isNil, isString, isObject, isArray } from './underscore'
import { isNil, isString, isObject, isArray, isIterable, toValue } from './underscore'

export function toEnumerable (val: any) {
val = toValue(val)
if (isArray(val)) return val
if (isString(val) && val.length > 0) return [val]
if (isIterable(val)) return Array.from(val)
if (isObject(val)) return Object.keys(val).map((key) => [key, val[key]])
return []
}
14 changes: 13 additions & 1 deletion src/util/underscore.ts
Original file line number Diff line number Diff line change
@@ -14,6 +14,14 @@ export function isFunction (value: any): value is Function {
return typeof value === 'function'
}

export function isPromise<T> (val: any): val is Promise<T> {
return val && isFunction(val.then)
}

export function isIterator (val: any): val is IterableIterator<any> {
return val && isFunction(val.next) && isFunction(val.throw) && isFunction(val.return)
}

export function escapeRegex (str: string) {
return str.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')
}
@@ -39,7 +47,7 @@ export function stringify (value: any): string {
}

export function toValue (value: any): any {
return value instanceof Drop ? value.valueOf() : value
return (value instanceof Drop && isFunction(value.valueOf)) ? value.valueOf() : value
}

export function isNumber (value: any): value is number {
@@ -60,6 +68,10 @@ export function isArray (value: any): value is any[] {
return toString.call(value) === '[object Array]'
}

export function isIterable (value: any): value is Iterable<any> {
return isObject(value) && Symbol.iterator in value
}

/*
* Iterates over own enumerable string keyed properties of an object and invokes iteratee for each property.
* The iteratee is invoked with three arguments: (value, key, object).
63 changes: 62 additions & 1 deletion test/integration/builtin/tags/for.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Liquid } from '../../../../src/liquid'
import { Drop, Liquid } from '../../../../src/liquid'
import { expect, use } from 'chai'
import * as chaiAsPromised from 'chai-as-promised'
import { Scope } from '../../../../src/context/scope'
@@ -304,4 +304,65 @@ describe('tags/for', function () {
return expect(html).to.equal('b')
})
})

describe('iterables', function () {
class MockIterable {
* [Symbol.iterator] () {
yield 'a'
yield 'b'
yield 'c'
}
}

class MockEmptyIterable {
* [Symbol.iterator] () {}
}

class MockIterableDrop extends Drop {
* [Symbol.iterator] () {
yield 'a'
yield 'b'
yield 'c'
}
toString () {
return 'MockIterableDrop'
}
}

it('should loop over iterable objects', function () {
const src = '{% for i in someIterable %}{{i}}{%endfor%}'
const html = liquid.parseAndRenderSync(src, { someIterable: new MockIterable() })
return expect(html).to.equal('abc')
})
it('should loop over iterable drops', function () {
const src = '{{ someDrop }}: {% for i in someDrop %}{{i}}{%endfor%}'
const html = liquid.parseAndRenderSync(src, { someDrop: new MockIterableDrop() })
return expect(html).to.equal('MockIterableDrop: abc')
})
it('should loop over iterable objects with a limit', function () {
const src = '{% for i in someIterable limit:2 %}{{i}}{%endfor%}'
const html = liquid.parseAndRenderSync(src, { someIterable: new MockIterable() })
return expect(html).to.equal('ab')
})
it('should loop over iterable objects with an offset', function () {
const src = '{% for i in someIterable offset:1 %}{{i}}{%endfor%}'
const html = liquid.parseAndRenderSync(src, { someIterable: new MockIterable() })
return expect(html).to.equal('bc')
})
it('should loop over iterable objects in reverse', function () {
const src = '{% for i in someIterable reversed %}{{i}}{%endfor%}'
const html = liquid.parseAndRenderSync(src, { someIterable: new MockIterable() })
return expect(html).to.equal('cba')
})
it('should go to else for an empty iterable', function () {
const src = '{% for i in emptyIterable reversed %}{{i}}{%else%}EMPTY{%endfor%}'
const html = liquid.parseAndRenderSync(src, { emptyIterable: new MockEmptyIterable() })
return expect(html).to.equal('EMPTY')
})
it('should support iterable names', function () {
const src = '{% for i in someDrop %}{{forloop.name}} {%else%}EMPTY{%endfor%}'
const html = liquid.parseAndRenderSync(src, { someDrop: new MockIterableDrop() })
return expect(html).to.equal('i-someDrop i-someDrop i-someDrop ')
})
})
})
14 changes: 14 additions & 0 deletions test/integration/builtin/tags/render.ts
Original file line number Diff line number Diff line change
@@ -155,6 +155,20 @@ describe('tags/render', function () {
const html = await liquid.renderFile('index.html', { colors: ['red', 'green'] })
expect(html).to.equal('1: red\n2: green\n')
})
it('should support for <iterable> as', async function () {
class MockIterable {
* [Symbol.iterator] () {
yield 'red'
yield 'green'
}
}
mock({
'/index.html': '{% render "item" for colors as color %}',
'/item.html': '{{forloop.index}}: {{color}}\n'
})
const html = await liquid.renderFile('index.html', { colors: new MockIterable() })
expect(html).to.equal('1: red\n2: green\n')
})
it('should support for <non-array> as', async function () {
mock({
'/index.html': '{% render "item" for "green" as color %}',
17 changes: 17 additions & 0 deletions test/integration/builtin/tags/tablerow.ts
Original file line number Diff line number Diff line change
@@ -24,6 +24,23 @@ describe('tags/tablerow', function () {
return expect(html).to.equal(dst)
})

it('should support iterables', async function () {
class MockIterable {
* [Symbol.iterator] () {
yield 1
yield 2
yield 3
}
}
const src = '{% tablerow i in someIterable %}{{ i }}{% endtablerow %}'
const ctx = {
someIterable: new MockIterable()
}
const dst = '<tr class="row1"><td class="col1">1</td><td class="col2">2</td><td class="col3">3</td></tr>'
const html = await liquid.parseAndRender(src, ctx)
return expect(html).to.equal(dst)
})

it('should support cols', async function () {
const src = '{% tablerow i in alpha cols:2 %}{{ i }}{% endtablerow %}'
const ctx = {
11 changes: 11 additions & 0 deletions test/integration/drop/drop.ts
Original file line number Diff line number Diff line change
@@ -61,4 +61,15 @@ describe('drop/drop', function () {
const html = await liquid.parseAndRender(`{{obj.foo}}`, { obj: new PromiseDrop() })
expect(html).to.equal('FOO')
})
it('should respect valueOf', async () => {
class CustomDrop extends Drop {
prop = 'not enumerable'
valueOf () {
return ['foo', 'bar']
}
}
const tpl = '{{drop}}: {% for field in drop %}{{ field }};{% endfor %}'
const html = await liquid.parseAndRender(tpl, { drop: new CustomDrop() })
expect(html).to.equal('foobar: foo;bar;')
})
})
27 changes: 27 additions & 0 deletions test/integration/liquid/liquid.ts
Original file line number Diff line number Diff line change
@@ -58,6 +58,33 @@ describe('Liquid', function () {
expect(html).to.equal('FOO')
})
})
describe('#parseAndRenderSync', function () {
const engine = new Liquid()
it('should parse and render variable output', function () {
const html = engine.parseAndRenderSync('{{"foo"}}')
expect(html).to.equal('foo')
})
it('should parse and render complex output', function () {
const tpl = '{{ "Welcome|to]Liquid" | split: "|" | join: "("}}'
const html = engine.parseAndRenderSync(tpl)
expect(html).to.equal('Welcome(to]Liquid')
})
it('should support for-in with variable', function () {
const src = '{% assign total = 3 | minus: 1 %}' +
'{% for i in (1..total) %}{{ i }}{% endfor %}'
const html = engine.parseAndRenderSync(src, {})
return expect(html).to.equal('12')
})
it('should support `globals` render option', function () {
const src = '{{ foo }}'
const html = engine.parseAndRenderSync(src, {}, { globals: { foo: 'FOO' } })
return expect(html).to.equal('FOO')
})
it('should support `strictVariables` render option', function () {
const src = '{{ foo }}'
return expect(() => engine.parseAndRenderSync(src, {}, { strictVariables: true })).throw(/undefined variable/)
})
})
describe('#express()', function () {
const liquid = new Liquid({ root: '/root' })
const render = liquid.express()
10 changes: 0 additions & 10 deletions test/unit/drop/drop.ts

This file was deleted.

Loading