feat: exported Drop interface for #107

Deprecate snake_cased options and APIs, sed #109
This commit is contained in:
harttle
2019-02-28 00:05:08 +08:00
parent b69c3a3203
commit 7bee9fc92d
26 changed files with 227 additions and 179 deletions
+1 -1
View File
@@ -18,6 +18,6 @@
"prefer-const": 2, "prefer-const": 2,
"no-unused-vars": "off", "no-unused-vars": "off",
"import/export": "off", "import/export": "off",
"@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }] "@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "off", "ignoreRestSiblings": false }]
} }
} }
+12 -12
View File
@@ -24,11 +24,11 @@ This is a liquid implementation for both Node.js and browsers. Website: <http://
Though being compatible with [Ruby Liquid](https://github.com/shopify/liquid) is one of our priorities, there're still certain differences. You may need some configuration to get it compatible in these senarios: Though being compatible with [Ruby Liquid](https://github.com/shopify/liquid) is one of our priorities, there're still certain differences. You may need some configuration to get it compatible in these senarios:
* Dynamic file locating (enabled by default), which means layout/partial name can be an variable in liquidjs. See [#51](https://github.com/harttle/liquidjs/issues/51). * Dynamic file locating (enabled by default), that means layout/partial names are treated as variables in liquidjs. See [#51](https://github.com/harttle/liquidjs/issues/51).
* Truthy and Falsy. All values except `undefined`, `null`, `false` are truthy, whereas in Ruby Liquid all except `nil` and `false` are truthy. See [#26](https://github.com/harttle/liquidjs/pull/26). * Truthy and Falsy. All values except `undefined`, `null`, `false` are truthy, whereas in Ruby Liquid all except `nil` and `false` are truthy. See [#26](https://github.com/harttle/liquidjs/pull/26).
* Number Rendering. Since JavaScript do not distinguish `float` and `integer`, we cannot either convert between them nor render regarding to their type. See [#59](https://github.com/harttle/liquidjs/issues/59). * Number Rendering. Since JavaScript do not distinguish `float` and `integer`, we cannot either convert between them nor render regarding to their type. See [#59](https://github.com/harttle/liquidjs/issues/59).
* [.to_liquid()](https://github.com/Shopify/liquid/wiki/Introduction-to-Drops) has a `.toLiquid()` alias and and the JavaScript `.toString()` is aliased to `.to_s()`. * [.to_liquid()](https://github.com/Shopify/liquid/wiki/Introduction-to-Drops) is replaced by `.toLiquid()`
* [.to_s()](https://www.rubydoc.info/gems/liquid/Liquid/Drop) uses `JSON.prototype.stringify` as default, rather than Ruby's inspect. * [.to_s()](https://www.rubydoc.info/gems/liquid/Liquid/Drop) is replaced by JavaScript `.toString()`
## TOC ## TOC
@@ -182,25 +182,25 @@ Defaults to `["."]`
* `dynamicPartials`: if set, treat `<filepath>` parameter in `{%include filepath %}`, `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`. * `dynamicPartials`: if set, treat `<filepath>` parameter in `{%include filepath %}`, `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`.
* `strict_filters` is used to enable strict filter existence. If set to `false`, undefined filters will be rendered as empty string. Otherwise, undefined filters will cause an exception. Defaults to `false`. * `strictFilters` is used to enable strict filter existence. If set to `false`, undefined filters will be rendered as empty string. Otherwise, undefined filters will cause an exception. Defaults to `false`.
* `strict_variables` is used to enable strict variable derivation. * `strictVariables` is used to enable strict variable derivation.
If set to `false`, undefined variables will be rendered as empty string. If set to `false`, undefined variables will be rendered as empty string.
Otherwise, undefined variables will cause an exception. Defaults to `false`. Otherwise, undefined variables will cause an exception. Defaults to `false`.
* `trim_tag_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`. * `trimTagRight` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`.
* `trim_tag_left` is similiar to `trim_tag_right`, whereas the `\n` is exclusive. Defaults to `false`. See [Whitespace Control][whitespace control] for details. * `trimTagLeft` is similiar to `trimTagRight`, whereas the `\n` is exclusive. Defaults to `false`. See [Whitespace Control][whitespace control] for details.
* `trim_output_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of values (`{{ }}`) until `\n` (inclusive). Defaults to `false`. * `trimOutputRight` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of values (`{{ }}`) until `\n` (inclusive). Defaults to `false`.
* `trim_output_left` is similiar to `trim_output_right`, whereas the `\n` is exclusive. Defaults to `false`. See [Whitespace Control][whitespace control] for details. * `trimOutputLeft` is similiar to `trimOutputRight`, whereas the `\n` is exclusive. Defaults to `false`. See [Whitespace Control][whitespace control] for details.
* `tag_delimiter_left` and `tag_delimiter_right` are used to override the delimiter for liquid tags. * `tagDelimiterLeft` and `tagDelimiterRight` are used to override the delimiter for liquid tags.
* `output_delimiter_left` and `output_delimiter_right` are used to override the delimiter for liquid outputs. * `outputDelimiterLeft` and `outputDelimiterRight` are used to override the delimiter for liquid outputs.
* `greedy` is used to specify whether `trim_left`/`trim_right` is greedy. When set to `true`, all consecutive blank characters including `\n` will be trimed regardless of line breaks. Defaults to `true`. * `greedy` is used to specify whether `trim*Left`/`trim*Right` is greedy. When set to `true`, all consecutive blank characters including `\n` will be trimed regardless of line breaks. Defaults to `true`.
## Register Filters ## Register Filters
+8 -3
View File
@@ -1,6 +1,11 @@
export abstract class Drop { import { deprecate } from '../util/deprecate'
abstract valueOf(): any;
liquid_method_missing (name: string) { // eslint-disable-line export abstract class Drop {
valueOf(): any {
return undefined
}
liquidMethodMissing (key: string): string | undefined {
return undefined
} }
} }
+47 -41
View File
@@ -1,5 +1,4 @@
/* eslint-disable camelcase */ import { deprecate } from './util/deprecate'
import * as _ from './util/underscore' import * as _ from './util/underscore'
export interface LiquidOptions { export interface LiquidOptions {
@@ -11,25 +10,25 @@ export interface LiquidOptions {
cache?: boolean cache?: boolean
/** `dynamicPartials`: if set, treat `<filepath>` parameter in `{%include filepath %}`, `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`. */ /** `dynamicPartials`: if set, treat `<filepath>` parameter in `{%include filepath %}`, `{%layout filepath%}` as a variable, otherwise as a literal value. Defaults to `true`. */
dynamicPartials?: boolean dynamicPartials?: boolean
/** `strict_filters` is used to enable strict filter existence. If set to `false`, undefined filters will be rendered as empty string. Otherwise, undefined filters will cause an exception. Defaults to `false`. */ /** `strictFilters` is used to enable strict filter existence. If set to `false`, undefined filters will be rendered as empty string. Otherwise, undefined filters will cause an exception. Defaults to `false`. */
strict_filters?: boolean strictFilters?: boolean
/** `strict_variables` is used to enable strict variable derivation. If set to `false`, undefined variables will be rendered as empty string. Otherwise, undefined variables will cause an exception. Defaults to `false`. */ /** `strictVariables` is used to enable strict variable derivation. If set to `false`, undefined variables will be rendered as empty string. Otherwise, undefined variables will cause an exception. Defaults to `false`. */
strict_variables?: boolean strictVariables?: boolean
/** `trim_tag_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`. */ /** `trimTagRight` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of tags (`{% %}`) until `\n` (inclusive). Defaults to `false`. */
trim_tag_right?: boolean trimTagRight?: boolean
/** `trim_tag_left` is similar to `trim_tag_right`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */ /** `trimTagLeft` is similar to `trimTagRight`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
trim_tag_left?: boolean trimTagLeft?: boolean
/** ``trim_output_right` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of values (`{{ }}`) until `\n` (inclusive). Defaults to `false`. */ /** ``trimOutputRight` is used to strip blank characters (including ` `, `\t`, and `\r`) from the right of values (`{{ }}`) until `\n` (inclusive). Defaults to `false`. */
trim_output_right?: boolean trimOutputRight?: boolean
/** `trim_output_left` is similar to `trim_output_right`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */ /** `trimOutputLeft` is similar to `trimOutputRight`, whereas the `\n` is exclusive. Defaults to `false`. See Whitespace Control for details. */
trim_output_left?: boolean trimOutputLeft?: boolean
/** `tag_delimiter_left` and `tag_delimiter_right` are used to override the delimiter for liquid tags **/ /** `tagDelimiterLeft` and `tagDelimiterRight` are used to override the delimiter for liquid tags **/
tag_delimiter_left?: string, tagDelimiterLeft?: string,
tag_delimiter_right?: string, tagDelimiterRight?: string,
/** `output_delimiter_left` and `output_delimiter_right` are used to override the delimiter for liquid outputs **/ /** `outputDelimiterLeft` and `outputDelimiterRight` are used to override the delimiter for liquid outputs **/
output_delimiter_left?: string, outputDelimiterLeft?: string,
output_delimiter_right?: string, outputDelimiterRight?: string,
/** `greedy` is used to specify whether `trim_left`/`trim_right` is greedy. When set to `true`, all consecutive blank characters including `\n` will be trimed regardless of line breaks. Defaults to `true`. */ /** `greedy` is used to specify whether `trim*Left`/`trim*Right` is greedy. When set to `true`, all consecutive blank characters including `\n` will be trimed regardless of line breaks. Defaults to `true`. */
greedy?: boolean greedy?: boolean
} }
@@ -42,16 +41,16 @@ export interface NormalizedFullOptions extends NormalizedOptions {
extname: string extname: string
cache: boolean cache: boolean
dynamicPartials: boolean dynamicPartials: boolean
strict_filters: boolean strictFilters: boolean
strict_variables: boolean strictVariables: boolean
trim_tag_right: boolean trimTagRight: boolean
trim_tag_left: boolean trimTagLeft: boolean
trim_output_right: boolean trimOutputRight: boolean
trim_output_left: boolean trimOutputLeft: boolean
tag_delimiter_left: string, tagDelimiterLeft: string,
tag_delimiter_right: string, tagDelimiterRight: string,
output_delimiter_left: string, outputDelimiterLeft: string,
output_delimiter_right: string, outputDelimiterRight: string,
greedy: boolean greedy: boolean
} }
@@ -60,17 +59,17 @@ const defaultOptions: NormalizedFullOptions = {
cache: false, cache: false,
extname: '', extname: '',
dynamicPartials: true, dynamicPartials: true,
trim_tag_right: false, trimTagRight: false,
trim_tag_left: false, trimTagLeft: false,
trim_output_right: false, trimOutputRight: false,
trim_output_left: false, trimOutputLeft: false,
greedy: true, greedy: true,
tag_delimiter_left: '{%', tagDelimiterLeft: '{%',
tag_delimiter_right: '%}', tagDelimiterRight: '%}',
output_delimiter_left: '{{', outputDelimiterLeft: '{{',
output_delimiter_right: '}}', outputDelimiterRight: '}}',
strict_filters: false, strictFilters: false,
strict_variables: false strictVariables: false
} }
export function normalize (options?: LiquidOptions): NormalizedOptions { export function normalize (options?: LiquidOptions): NormalizedOptions {
@@ -78,6 +77,13 @@ export function normalize (options?: LiquidOptions): NormalizedOptions {
if (options.hasOwnProperty('root')) { if (options.hasOwnProperty('root')) {
options.root = normalizeStringArray(options.root) options.root = normalizeStringArray(options.root)
} }
for (const key of Object.keys(options)) {
if (key.indexOf('_') > -1) {
const newKey = key.replace(/_([a-z])/g, (_, ch) => ch.toUpperCase())
deprecate(`${key} is deprecated, use ${newKey} instead.`, 109)
options[newKey] = options[key]
}
}
return options as NormalizedOptions return options as NormalizedOptions
} }
+1 -1
View File
@@ -70,7 +70,7 @@ export default class Liquid {
return this.render(templates, ctx, opts) return this.render(templates, ctx, opts)
} }
evalValue (str: string, scope: Scope) { evalValue (str: string, scope: Scope) {
return new Value(str, this.options.strict_filters).value(scope) return new Value(str, this.options.strictFilters).value(scope)
} }
registerFilter (name: string, filter: FilterImpl) { registerFilter (name: string, filter: FilterImpl) {
return Filter.register(name, filter) return Filter.register(name, filter)
+1 -1
View File
@@ -29,7 +29,7 @@ export default class Parser {
return new Tag(token as TagToken, remainTokens, this.liquid) return new Tag(token as TagToken, remainTokens, this.liquid)
} }
if (token.type === 'output') { if (token.type === 'output') {
return new Output(token as OutputToken, this.liquid.options.strict_filters) return new Output(token as OutputToken, this.liquid.options.strictFilters)
} }
return new HTML(token) return new HTML(token)
} catch (e) { } catch (e) {
+4 -4
View File
@@ -15,10 +15,10 @@ export default class Tokenizer {
} }
tokenize (input: string, file?: string) { tokenize (input: string, file?: string) {
const tokens: Token[] = [] const tokens: Token[] = []
const tagL = this.options.tag_delimiter_left const tagL = this.options.tagDelimiterLeft
const tagR = this.options.tag_delimiter_right const tagR = this.options.tagDelimiterRight
const outputL = this.options.output_delimiter_left const outputL = this.options.outputDelimiterLeft
const outputR = this.options.output_delimiter_right const outputR = this.options.outputDelimiterRight
let p = 0 let p = 0
let curLine = 1 let curLine = 1
let state = ParseState.HTML let state = ParseState.HTML
+4 -4
View File
@@ -23,14 +23,14 @@ export default function whiteSpaceCtrl (tokens: Token[], options: NormalizedFull
function shouldTrimLeft (token: DelimitedToken, inRaw: boolean, options: NormalizedFullOptions) { function shouldTrimLeft (token: DelimitedToken, inRaw: boolean, options: NormalizedFullOptions) {
if (inRaw) return false if (inRaw) return false
if (token.type === 'tag') return token.trimLeft || options.trim_tag_left if (token.type === 'tag') return token.trimLeft || options.trimTagLeft
if (token.type === 'output') return token.trimLeft || options.trim_output_left if (token.type === 'output') return token.trimLeft || options.trimOutputLeft
} }
function shouldTrimRight (token: DelimitedToken, inRaw: boolean, options: NormalizedFullOptions) { function shouldTrimRight (token: DelimitedToken, inRaw: boolean, options: NormalizedFullOptions) {
if (inRaw) return false if (inRaw) return false
if (token.type === 'tag') return token.trimRight || options.trim_tag_right if (token.type === 'tag') return token.trimRight || options.trimTagRight
if (token.type === 'output') return token.trimRight || options.trim_output_right if (token.type === 'output') return token.trimRight || options.trimOutputRight
} }
function trimLeft (token: Token, greedy: boolean) { function trimLeft (token: Token, greedy: boolean) {
+16 -25
View File
@@ -1,4 +1,5 @@
import * as _ from '../util/underscore' import * as _ from '../util/underscore'
import { Drop } from '../drop/drop'
import { __assign } from 'tslib' import { __assign } from 'tslib'
import assert from '../util/assert' import assert from '../util/assert'
import { NormalizedFullOptions, applyDefault } from '../liquid-options' import { NormalizedFullOptions, applyDefault } from '../liquid-options'
@@ -27,7 +28,13 @@ export default class Scope {
get (path: string): any { get (path: string): any {
const paths = this.propertyAccessSeq(path) const paths = this.propertyAccessSeq(path)
const scope = this.findContextFor(paths[0]) || _.last(this.contexts) const scope = this.findContextFor(paths[0]) || _.last(this.contexts)
return paths.reduce((value, key) => this.readProperty(value, key), scope) return paths.reduce((value, key) => {
const val = this.readProperty(value, key)
if (_.isNil(val) && this.opts.strictVariables) {
throw new TypeError(`undefined variable: ${key}`)
}
return val
}, scope)
} }
set (path: string, v: any): void { set (path: string, v: any): void {
const paths = this.propertyAccessSeq(path) const paths = this.propertyAccessSeq(path)
@@ -74,26 +81,20 @@ export default class Scope {
return null return null
} }
private readProperty (obj: Context, key: string) { private readProperty (obj: Context, key: string) {
let val if (_.isNil(obj)) return obj
if (_.isNil(obj)) { obj = _.toLiquid(obj)
val = obj if (obj instanceof Drop) {
} else { if (_.isFunction(obj[key])) return obj[key]()
obj = toLiquid(obj) if (obj.hasOwnProperty(key)) return obj[key]
val = key === 'size' ? readSize(obj) : obj[key] return obj.liquidMethodMissing(key)
if (_.isFunction(obj.liquid_method_missing)) {
val = obj.liquid_method_missing!(key)
}
} }
if (_.isNil(val) && this.opts.strict_variables) { return key === 'size' ? readSize(obj) : obj[key]
throw new TypeError(`undefined variable: ${key}`)
}
return val
} }
/* /*
* Parse property access sequence from access string * Parse property access sequence from access string
* @example * @example
* accessSeq("foo.bar") // ['foo', 'bar'] * accessSeq("foo.bar") // ['foo', 'bar']
* accessSeq("foo['bar']") // ['foo', 'bar'] * accessSeq("foo['bar']") // ['foo', 'bar']
* accessSeq("foo['b]r']") // ['foo', 'b]r'] * accessSeq("foo['b]r']") // ['foo', 'b]r']
* accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar' * accessSeq("foo[bar.coo]") // ['foo', 'bar'], for bar.coo == 'bar'
@@ -150,16 +151,6 @@ export default class Scope {
} }
} }
function toLiquid (obj: Context) {
if (_.isFunction(obj.to_liquid)) {
return obj.to_liquid()
}
if (_.isFunction(obj.toLiquid)) {
return obj.toLiquid()
}
return obj
}
function readSize (obj: Context) { function readSize (obj: Context) {
if (!_.isNil(obj.size)) return obj.size if (!_.isNil(obj.size)) return obj.size
if (_.isArray(obj) || _.isString(obj)) return obj.length if (_.isArray(obj) || _.isString(obj)) return obj.length
+1
View File
@@ -1,2 +1,3 @@
export { AssignScope, CaptureScope, IncrementScope, DecrementScope } from './scope/scopes' export { AssignScope, CaptureScope, IncrementScope, DecrementScope } from './scope/scopes'
export { ParseError, TokenizationError, RenderBreakError, AssertionError } from './util/error' export { ParseError, TokenizationError, RenderBreakError, AssertionError } from './util/error'
export { Drop } from './drop/drop'
+7
View File
@@ -0,0 +1,7 @@
const reported:{[key: string]: boolean} = {}
export function deprecate(msg: string, issue: number) {
if (reported[msg]) return
console.warn(msg + ` See: https://github.com/harttle/liquidjs/issues/${issue}`)
reported[msg] = true
}
+14 -18
View File
@@ -1,5 +1,5 @@
import { deprecate } from './deprecate'
const toStr = Object.prototype.toString const toStr = Object.prototype.toString
const arrToStr = Array.prototype.toString
/* /*
* Checks if value is classified as a String primitive or object. * Checks if value is classified as a String primitive or object.
@@ -28,25 +28,21 @@ export function promisify (fn: any) {
export function stringify (value: any): string { export function stringify (value: any): string {
if (isNil(value)) return '' if (isNil(value)) return ''
if (isFunction(value.to_liquid)) return stringify(value.to_liquid()) value = toLiquid(value)
if (isFunction(value.toLiquid)) return stringify(value.toLiquid()) if (isFunction(value.to_s)) {
if (isFunction(value.to_s)) return value.to_s() deprecate('to_s is deprecated, use toString instead.', 109)
if ([toStr, arrToStr].indexOf(value.toString) > -1) return defaultToString(value) return value.to_s()
if (isFunction(value.toString)) return value.toString() }
return toStr.call(value) return String(value)
} }
function defaultToString (value: any): string { export function toLiquid (value: any): any {
const cache: any[] = [] if (isFunction(value.to_liquid)) {
return JSON.stringify(value, (key, value) => { deprecate('to_liquid is deprecated, use toLiquid instead.', 109)
if (isObject(value)) { return toLiquid(value.to_liquid())
if (cache.indexOf(value) !== -1) { }
return if (isFunction(value.toLiquid)) return toLiquid(value.toLiquid())
} return value
cache.push(value)
}
return value
})
} }
export function create<T1 extends object, T2 extends T1 = T1> (proto: T1): T2 { export function create<T1 extends object, T2 extends T1 = T1> (proto: T1): T2 {
+27 -14
View File
@@ -4,25 +4,38 @@ import * as chaiAsPromised from 'chai-as-promised'
use(chaiAsPromised) use(chaiAsPromised)
class SettingsDrop extends Liquid.Types.Drop {
foo: string = 'FOO'
bar() {
return 'BAR'
}
liquidMethodMissing(key: string) {
return key.toUpperCase()
}
}
describe('drop', function () { describe('drop', function () {
var engine: Liquid const settings = new SettingsDrop()
let engine: Liquid
beforeEach(function () { beforeEach(function () {
engine = new Liquid() engine = new Liquid()
}) })
it('should support liquid_method_missing', async function () { it('should support liquidMethodMissing', async function () {
let i = 0 let i = 0
const src = `{{settings.foo}},{{settings.foo}},{{settings.foo}}` const src = `{{settings.foo}},{{settings.bar}},{{settings.coo}}`
const ctx = { settings: { liquid_method_missing: () => i++ } } const html = await engine.parseAndRender(src, { settings })
const html = await engine.parseAndRender(src, ctx) return expect(html).to.equal('FOO,BAR,COO')
return expect(html).to.equal('0,1,2')
}) })
it('should test blank strings', async function () {
const src = ` describe('BlandDrop', function () {
{% unless settings.fp_heading == blank %} it('should test blank strings', async function () {
<h1>{{ settings.fp_heading }}</h1> const src = `
{% endunless %}` {% unless settings.fp_heading == blank %}
var ctx = { settings: { fp_heading: '' } } <h1>{{ settings.fp_heading }}</h1>
const html = await engine.parseAndRender(src, ctx) {% endunless %}`
return expect(html).to.match(/^\s+$/) var ctx = { settings: { fp_heading: '' } }
const html = await engine.parseAndRender(src, ctx)
return expect(html).to.match(/^\s+$/)
})
}) })
}) })
+7 -12
View File
@@ -9,18 +9,13 @@ describe('.parseAndRender()', function () {
beforeEach(function () { beforeEach(function () {
engine = new Liquid() engine = new Liquid()
strictEngine = new Liquid({ strictEngine = new Liquid({
strict_filters: true strictFilters: true
}) })
}) })
it('should stringify object', async function () {
var ctx = { obj: { foo: 'bar' } }
const html = await engine.parseAndRender('{{obj}}', ctx)
return expect(html).to.equal('{"foo":"bar"}')
})
it('should stringify array ', async function () { it('should stringify array ', async function () {
var ctx = { arr: [-2, 'a'] } var ctx = { arr: [-2, 'a'] }
const html = await engine.parseAndRender('{{arr}}', ctx) const html = await engine.parseAndRender('{{arr}}', ctx)
return expect(html).to.equal('[-2,"a"]') return expect(html).to.equal('-2,a')
}) })
it('should render undefined as empty', async function () { it('should render undefined as empty', async function () {
const html = await engine.parseAndRender('foo{{zzz}}bar', {}) const html = await engine.parseAndRender('foo{{zzz}}bar', {})
@@ -30,7 +25,7 @@ describe('.parseAndRender()', function () {
const html = await engine.parseAndRender('{{"foo" | filter1}}', {}) const html = await engine.parseAndRender('{{"foo" | filter1}}', {})
return expect(html).to.equal('foo') return expect(html).to.equal('foo')
}) })
it('should throw upon undefined filter when strict_filters set', function () { it('should throw upon undefined filter when strictFilters set', function () {
return expect(strictEngine.parseAndRender('{{"foo" | filter1}}', {})).to return expect(strictEngine.parseAndRender('{{"foo" | filter1}}', {})).to
.be.rejectedWith(/undefined filter: filter1/) .be.rejectedWith(/undefined filter: filter1/)
}) })
@@ -42,13 +37,13 @@ describe('.parseAndRender()', function () {
engine.parse('<html><head>{{obj}}</head></html>') engine.parse('<html><head>{{obj}}</head></html>')
}).to.not.throw() }).to.not.throw()
}) })
it('should render template multiple times', async function () { it('template should be able to be rendered multiple times', async function () {
const ctx = { obj: { foo: 'bar' } } const ctx = { obj: [1, 2] }
const template = engine.parse('{{obj}}') const template = engine.parse('{{obj}}')
const result = await engine.render(template, ctx) const result = await engine.render(template, ctx)
expect(result).to.equal('{"foo":"bar"}') expect(result).to.equal('1,2')
const result2 = await engine.render(template, ctx) const result2 = await engine.render(template, ctx)
expect(result2).to.equal('{"foo":"bar"}') expect(result2).to.equal('1,2')
}) })
it('should render filters', async function () { it('should render filters', async function () {
var ctx = { names: ['alice', 'bob'] } var ctx = { names: ['alice', 'bob'] }
+1 -1
View File
@@ -19,7 +19,7 @@ describe('filters/array', function () {
return test(src, 'tiger') return test(src, 'tiger')
}) })
it('should support map', function () { it('should support map', function () {
return test('{{posts | map: "category"}}', '["foo","bar"]') return test('{{posts | map: "category"}}', 'foo,bar')
}) })
it('should support reverse', function () { it('should support reverse', function () {
return test('{{ "Ground control to Major Tom." | split: "" | reverse | join: "" }}', return test('{{ "Ground control to Major Tom." | split: "" | reverse | join: "" }}',
+1 -1
View File
@@ -15,6 +15,6 @@ describe('filters/date', function () {
return test('{{ "foo" | date: "%Y"}}', 'foo') return test('{{ "foo" | date: "%Y"}}', 'foo')
}) })
it('should render object as string if not valid', function () { it('should render object as string if not valid', function () {
return test('{{ obj | date: "%Y"}}', '{"foo":"bar"}') return test('{{ obj | date: "%Y"}}', '[object Object]')
}) })
}) })
+1 -1
View File
@@ -31,7 +31,7 @@ describe('tags/assign', function () {
it('should assign as array', async function () { it('should assign as array', async function () {
const src = '{% assign foo=(1..3) %}{{foo}}' const src = '{% assign foo=(1..3) %}{{foo}}'
const html = await liquid.parseAndRender(src) const html = await liquid.parseAndRender(src)
return expect(html).to.equal('[1,2,3]') return expect(html).to.equal('1,2,3')
}) })
it('should assign as filter result', async function () { it('should assign as filter result', async function () {
const src = '{% assign foo="a b" | capitalize | split: " " | first %}{{foo}}' const src = '{% assign foo="a b" | capitalize | split: " " | first %}{{foo}}'
+35
View File
@@ -0,0 +1,35 @@
import { expect } from 'chai'
import Liquid from '../../../src/liquid'
describe('drop/drop', function () {
let liquid: Liquid
before(() => (liquid = new Liquid()))
class CustomDrop extends Liquid.Types.Drop {
name: string = 'NAME'
getName() {
return 'GETNAME'
}
}
class CustomDropWithMethodMissing extends CustomDrop {
liquidMethodMissing(key: string) {
return key.toUpperCase()
}
}
it('should call corresponding method', async function () {
const html = await liquid.parseAndRender(`{{obj.getName}}`, {obj: new CustomDrop()})
expect(html).to.equal('GETNAME')
})
it('should read corresponding property', async function () {
const html = await liquid.parseAndRender(`{{obj.name}}`, {obj: new CustomDrop()})
expect(html).to.equal('NAME')
})
it('should output empty string if not exist', async function () {
const html = await liquid.parseAndRender(`{{obj.foo}}`, {obj: new CustomDrop()})
expect(html).to.equal('')
})
it('should respect liquidMethodMissing', async function () {
const html = await liquid.parseAndRender(`{{obj.foo}}`, {obj: new CustomDropWithMethodMissing()})
expect(html).to.equal('FOO')
})
})
+8 -8
View File
@@ -4,26 +4,26 @@ import Liquid from '../../../src/liquid'
describe('LiquidOptions#*_delimiter_*', function () { describe('LiquidOptions#*_delimiter_*', function () {
it('should respect tag_delimiter_*', async function () { it('should respect tag_delimiter_*', async function () {
const engine = new Liquid({ const engine = new Liquid({
tag_delimiter_left: '<%=', tagDelimiterLeft: '<%=',
tag_delimiter_right: '%>' tagDelimiterRight: '%>'
}) })
const html = await engine.parseAndRender('<%=if true%>foo<%=endif%> ') const html = await engine.parseAndRender('<%=if true%>foo<%=endif%> ')
return expect(html).to.equal('foo ') return expect(html).to.equal('foo ')
}) })
it('should respect output_delimiter_*', async function () { it('should respect output_delimiter_*', async function () {
const engine = new Liquid({ const engine = new Liquid({
output_delimiter_left: '<<', outputDelimiterLeft: '<<',
output_delimiter_right: '>>' outputDelimiterRight: '>>'
}) })
const html = await engine.parseAndRender('<< "liquid" | capitalize >>') const html = await engine.parseAndRender('<< "liquid" | capitalize >>')
return expect(html).to.equal('Liquid') return expect(html).to.equal('Liquid')
}) })
it('should support trimming with tag_delimiter_* set', async function () { it('should support trimming with tag_delimiter_* set', async function () {
const engine = new Liquid({ const engine = new Liquid({
tag_delimiter_left: '<%=', tagDelimiterLeft: '<%=',
tag_delimiter_right: '%>', tagDelimiterRight: '%>',
trim_tag_left: true, trimTagLeft: true,
trim_tag_right: true trimTagRight: true
}) })
const html = await engine.parseAndRender(' <%=if true%> \tfoo\t <%=endif%> ') const html = await engine.parseAndRender(' <%=if true%> \tfoo\t <%=endif%> ')
return expect(html).to.equal('foo') return expect(html).to.equal('foo')
+5 -5
View File
@@ -10,22 +10,22 @@ describe('LiquidOptions#strict_*', function () {
extname: '.html' extname: '.html'
}) })
}) })
it('should not throw when strict_variables false (default)', async function () { it('should not throw when strictVariables false (default)', async function () {
const html = await engine.parseAndRender('before{{notdefined}}after', ctx) const html = await engine.parseAndRender('before{{notdefined}}after', ctx)
return expect(html).to.equal('beforeafter') return expect(html).to.equal('beforeafter')
}) })
it('should throw when strict_variables true', function () { it('should throw when strictVariables true', function () {
const tpl = engine.parse('before{{notdefined}}after') const tpl = engine.parse('before{{notdefined}}after')
const opts = { const opts = {
strict_variables: true strictVariables: true
} }
return expect(engine.render(tpl, ctx, opts)).to return expect(engine.render(tpl, ctx, opts)).to
.be.rejectedWith(/undefined variable: notdefined/) .be.rejectedWith(/undefined variable: notdefined/)
}) })
it('should pass strict_variables to render by parseAndRender', function () { it('should pass strictVariables to render by parseAndRender', function () {
const html = 'before{{notdefined}}after' const html = 'before{{notdefined}}after'
const opts = { const opts = {
strict_variables: true strictVariables: true
} }
return expect(engine.parseAndRender(html, ctx, opts)).to return expect(engine.parseAndRender(html, ctx, opts)).to
.be.rejectedWith(/undefined variable: notdefined/) .be.rejectedWith(/undefined variable: notdefined/)
+7 -7
View File
@@ -6,34 +6,34 @@ describe('LiquidOptions#trimming', function () {
describe('tag trimming', function () { describe('tag trimming', function () {
it('should respect trim_tag_left', async function () { it('should respect trim_tag_left', async function () {
const engine = new Liquid({ trim_tag_left: true }) const engine = new Liquid({ trim_tag_left: true } as any)
const html = await engine.parseAndRender(' \n \t{%if true%}foo{%endif%} ') const html = await engine.parseAndRender(' \n \t{%if true%}foo{%endif%} ')
return expect(html).to.equal('foo ') return expect(html).to.equal('foo ')
}) })
it('should respect trim_tag_right', async function () { it('should respect trim_tag_right', async function () {
const engine = new Liquid({ trim_tag_right: true }) const engine = new Liquid({ trim_tag_right: true } as any)
const html = await engine.parseAndRender('\t{%if true%}foo{%endif%} \n') const html = await engine.parseAndRender('\t{%if true%}foo{%endif%} \n')
return expect(html).to.equal('\tfoo') return expect(html).to.equal('\tfoo')
}) })
it('should not trim value', async function () { it('should not trim value', async function () {
const engine = new Liquid({ trim_tag_left: true, trim_tag_right: true }) const engine = new Liquid({ trim_tag_left: true, trim_tag_right: true } as any)
const html = await engine.parseAndRender('{%if true%}a {{name}} b{%endif%}', ctx) const html = await engine.parseAndRender('{%if true%}a {{name}} b{%endif%}', ctx)
return expect(html).to.equal('a harttle b') return expect(html).to.equal('a harttle b')
}) })
}) })
describe('value trimming', function () { describe('value trimming', function () {
it('should respect trim_output_left', async function () { it('should respect trim_output_left', async function () {
const engine = new Liquid({ trim_output_left: true }) const engine = new Liquid({ trim_output_left: true } as any)
const html = await engine.parseAndRender(' \n \t{{name}} ', ctx) const html = await engine.parseAndRender(' \n \t{{name}} ', ctx)
return expect(html).to.equal('harttle ') return expect(html).to.equal('harttle ')
}) })
it('should respect trim_output_right', async function () { it('should respect trim_output_right', async function () {
const engine = new Liquid({ trim_output_right: true }) const engine = new Liquid({ trim_output_right: true } as any)
const html = await engine.parseAndRender(' \n \t{{name}} ', ctx) const html = await engine.parseAndRender(' \n \t{{name}} ', ctx)
return expect(html).to.equal(' \n \tharttle') return expect(html).to.equal(' \n \tharttle')
}) })
it('should respect not trim tag', async function () { it('should respect not trim tag', async function () {
const engine = new Liquid({ trim_output_left: true, trim_output_right: true }) const engine = new Liquid({ trim_output_left: true, trim_output_right: true } as any)
const html = await engine.parseAndRender('\t{% if true %} aha {%endif%}\t') const html = await engine.parseAndRender('\t{% if true %} aha {%endif%}\t')
return expect(html).to.equal('\t aha \t') return expect(html).to.equal('\t aha \t')
}) })
@@ -46,7 +46,7 @@ describe('LiquidOptions#trimming', function () {
return expect(html).to.equal('aharttle') return expect(html).to.equal('aharttle')
}) })
it('should respect to greedy:false by default', async function () { it('should respect to greedy:false by default', async function () {
const engine = new Liquid({ greedy: false }) const engine = new Liquid({ greedy: false } as any)
const html = await engine.parseAndRender(src, ctx) const html = await engine.parseAndRender(src, ctx)
return expect(html).to.equal('\n a \nharttle ') return expect(html).to.equal('\n a \nharttle ')
}) })
+2 -2
View File
@@ -5,8 +5,8 @@ import { mock, restore } from '../../stub/mockfs'
let engine = new Liquid() let engine = new Liquid()
const strictEngine = new Liquid({ const strictEngine = new Liquid({
strict_variables: true, strictVariables: true,
strict_filters: true strictFilters: true
}) })
describe('error', function () { describe('error', function () {
+10
View File
@@ -0,0 +1,10 @@
import { expect } from 'chai'
import { Drop } from '../../../src/drop/drop'
describe('drop/drop', function () {
class CustomDrop extends Drop { }
it('.valueOf() should return undefined by default', async function () {
expect(new CustomDrop().valueOf()).to.be.undefined
})
})
+2 -2
View File
@@ -181,11 +181,11 @@ describe('scope', function () {
expect(scope.get('foo')).to.equal('bar') expect(scope.get('foo')).to.equal('bar')
}) })
}) })
describe('strict_variables', function () { describe('strictVariables', function () {
let scope: Scope let scope: Scope
beforeEach(function () { beforeEach(function () {
scope = new Scope(ctx, { scope = new Scope(ctx, {
strict_variables: true strictVariables: true
} as any) } as any)
}) })
it('should throw when variable not defined', function () { it('should throw when variable not defined', function () {
+5 -12
View File
@@ -25,20 +25,13 @@ describe('Output', function () {
}) })
const output = new Output({ value: 'foo' } as OutputToken, false) const output = new Output({ value: 'foo' } as OutputToken, false)
const html = await output.render(scope) const html = await output.render(scope)
return expect(html).to.equal('{"obj":{"arr":["a",2]}}') return expect(html).to.equal('[object Object]')
})
it('should skip circular property', async function () {
const ctx = { foo: { num: 2 }, bar: 'bar' } as any
ctx.foo.circular = ctx
const output = new Output({ value: 'foo' } as OutputToken, false)
const html = await output.render(new Scope(ctx))
return expect(html).equal('{"num":2,"circular":{"bar":"bar"}}')
}) })
it('should skip function property', async function () { it('should skip function property', async function () {
const scope = new Scope({ obj: { foo: 'foo', bar: (x: any) => x } }) const scope = new Scope({ obj: { foo: 'foo', bar: (x: any) => x } })
const output = new Output({ value: 'obj' } as OutputToken, false) const output = new Output({ value: 'obj' } as OutputToken, false)
const html = await output.render(scope) const html = await output.render(scope)
return expect(html).to.equal('{"foo":"foo"}') return expect(html).to.equal('[object Object]')
}) })
it('should respect to .toString()', async () => { it('should respect to .toString()', async () => {
const scope = new Scope({ obj: { toString: () => 'FOO' } }) const scope = new Scope({ obj: { toString: () => 'FOO' } })
@@ -52,9 +45,9 @@ describe('Output', function () {
const str = await output.render(scope) const str = await output.render(scope)
return expect(str).to.equal('FOO') return expect(str).to.equal('FOO')
}) })
it('should respect to .liquid_method_missing()', async () => { it('should respect to .toString()', async () => {
const scope = new Scope({ obj: { liquid_method_missing: (x: string) => x.toUpperCase() } }) const scope = new Scope({ obj: { toString: () => 'FOO' } })
const output = new Output({ value: 'obj.foo' } as OutputToken, false) const output = new Output({ value: 'obj' } as OutputToken, false)
const str = await output.render(scope) const str = await output.render(scope)
return expect(str).to.equal('FOO') return expect(str).to.equal('FOO')
}) })
-4
View File
@@ -34,10 +34,6 @@ describe('util/underscore', function () {
it('should return "" for undefined', function () { it('should return "" for undefined', function () {
expect(_.stringify(undefined)).to.equal('') expect(_.stringify(undefined)).to.equal('')
}) })
it('should use Object.prototype.toString if no toString method exists', function () {
const obj = { toString: undefined }
expect(_.stringify(obj)).to.equal('[object Object]')
})
it('should return regex string for RegExp', function () { it('should return regex string for RegExp', function () {
const reg = /foo/g const reg = /foo/g
expect(_.stringify(reg)).to.equal('/foo/g') expect(_.stringify(reg)).to.equal('/foo/g')