mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 20:30:39 -07:00
feat: implement liquid and echo tags, see #428
This commit is contained in:
@@ -0,0 +1,13 @@
|
|||||||
|
import { Value } from '../../template/value'
|
||||||
|
import { Emitter } from '../../emitters/emitter'
|
||||||
|
import { TagImplOptions, TagToken, Context } from '../../types'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
parse: function (token: TagToken) {
|
||||||
|
this.value = new Value(token.args, this.liquid)
|
||||||
|
},
|
||||||
|
render: function * (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
|
||||||
|
const val = yield this.value.value(ctx, false)
|
||||||
|
emitter.write(val)
|
||||||
|
}
|
||||||
|
} as TagImplOptions
|
||||||
@@ -16,10 +16,12 @@ import tablerow from './tablerow'
|
|||||||
import unless from './unless'
|
import unless from './unless'
|
||||||
import Break from './break'
|
import Break from './break'
|
||||||
import Continue from './continue'
|
import Continue from './continue'
|
||||||
|
import echo from './echo'
|
||||||
|
import liquid from './liquid'
|
||||||
import { TagImplOptions } from '../../template/tag/tag-impl-options'
|
import { TagImplOptions } from '../../template/tag/tag-impl-options'
|
||||||
|
|
||||||
const tags: { [key: string]: TagImplOptions } = {
|
const tags: { [key: string]: TagImplOptions } = {
|
||||||
assign, 'for': For, capture, 'case': Case, comment, include, render, decrement, increment, cycle, 'if': If, layout, block, raw, tablerow, unless, 'break': Break, 'continue': Continue
|
assign, 'for': For, capture, 'case': Case, comment, include, render, decrement, increment, cycle, 'if': If, layout, block, raw, tablerow, unless, 'break': Break, 'continue': Continue, echo, liquid
|
||||||
}
|
}
|
||||||
|
|
||||||
export default tags
|
export default tags
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { Emitter } from '../../emitters/emitter'
|
||||||
|
import { TagImplOptions, TagToken, Context } from '../../types'
|
||||||
|
import { Tokenizer } from '../../parser/tokenizer'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
parse: function (token: TagToken) {
|
||||||
|
const tokenizer = new Tokenizer(token.args, this.liquid.options.operatorsTrie)
|
||||||
|
const tokens = tokenizer.readLiquidTagTokens(this.liquid.options)
|
||||||
|
this.tpls = this.liquid.parser.parseTokens(tokens)
|
||||||
|
},
|
||||||
|
render: function * (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
|
||||||
|
yield this.liquid.renderer.renderTemplates(this.tpls, ctx, emitter)
|
||||||
|
}
|
||||||
|
} as TagImplOptions
|
||||||
@@ -24,6 +24,7 @@ import { TYPES, QUOTE, BLANK, IDENTIFIER } from '../util/character'
|
|||||||
import { matchOperator } from './match-operator'
|
import { matchOperator } from './match-operator'
|
||||||
import { Trie } from '../util/operator-trie'
|
import { Trie } from '../util/operator-trie'
|
||||||
import { Expression } from '../render/expression'
|
import { Expression } from '../render/expression'
|
||||||
|
import { LiquidTagToken } from '../tokens/liquid-tag-token'
|
||||||
|
|
||||||
export class Tokenizer {
|
export class Tokenizer {
|
||||||
p = 0
|
p = 0
|
||||||
@@ -191,6 +192,24 @@ export class Tokenizer {
|
|||||||
throw this.mkError(`raw ${this.snapshot(this.rawBeginAt)} not closed`, begin)
|
throw this.mkError(`raw ${this.snapshot(this.rawBeginAt)} not closed`, begin)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readLiquidTagTokens (options: NormalizedFullOptions = defaultOptions): LiquidTagToken[] {
|
||||||
|
const tokens: LiquidTagToken[] = []
|
||||||
|
while (this.p < this.N) {
|
||||||
|
const token = this.readLiquidTagToken(options)
|
||||||
|
if (token.name) tokens.push(token)
|
||||||
|
}
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
readLiquidTagToken (options: NormalizedFullOptions): LiquidTagToken {
|
||||||
|
const { file, input } = this
|
||||||
|
const begin = this.p
|
||||||
|
let end = this.N
|
||||||
|
if (this.readToDelimiter('\n') !== -1) end = this.p
|
||||||
|
const token = new LiquidTagToken(input, begin, end, options, file)
|
||||||
|
return token
|
||||||
|
}
|
||||||
|
|
||||||
mkError (msg: string, begin: number) {
|
mkError (msg: string, begin: number) {
|
||||||
return new TokenizationError(msg, new IdentifierToken(this.input, begin, this.N, this.file))
|
return new TokenizationError(msg, new IdentifierToken(this.input, begin, this.N, this.file))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { DelimitedToken } from './delimited-token'
|
||||||
|
import { TokenizationError } from '../util/error'
|
||||||
|
import { NormalizedFullOptions } from '../liquid-options'
|
||||||
|
import { TokenKind } from '../parser/token-kind'
|
||||||
|
import { Tokenizer } from '../parser/tokenizer'
|
||||||
|
|
||||||
|
export class LiquidTagToken extends DelimitedToken {
|
||||||
|
public name: string
|
||||||
|
public args: string
|
||||||
|
public constructor (
|
||||||
|
input: string,
|
||||||
|
begin: number,
|
||||||
|
end: number,
|
||||||
|
options: NormalizedFullOptions,
|
||||||
|
file?: string
|
||||||
|
) {
|
||||||
|
const value = input.slice(begin, end)
|
||||||
|
super(TokenKind.Tag, value, input, begin, end, false, false, file)
|
||||||
|
|
||||||
|
if (!/\S/.test(value)) {
|
||||||
|
// A line that contains only whitespace.
|
||||||
|
this.name = ''
|
||||||
|
this.args = ''
|
||||||
|
} else {
|
||||||
|
const tokenizer = new Tokenizer(this.content, options.operatorsTrie)
|
||||||
|
this.name = tokenizer.readIdentifier().getText()
|
||||||
|
if (!this.name) throw new TokenizationError(`illegal liquid tag syntax`, this)
|
||||||
|
|
||||||
|
tokenizer.skipBlank()
|
||||||
|
this.args = tokenizer.remaining()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -177,4 +177,21 @@ describe('Issues', function () {
|
|||||||
const html = await engine.render(tpl, { my_variable: 'foo' })
|
const html = await engine.render(tpl, { my_variable: 'foo' })
|
||||||
expect(html).to.equal('CONTENT for /tmp/prefix/foo-bar/suffix')
|
expect(html).to.equal('CONTENT for /tmp/prefix/foo-bar/suffix')
|
||||||
})
|
})
|
||||||
|
it('#428 Implement liquid/echo tags', () => {
|
||||||
|
const template = `{%- liquid
|
||||||
|
for value in array
|
||||||
|
assign double_value = value | times: 2
|
||||||
|
echo double_value | times: 2
|
||||||
|
unless forloop.last
|
||||||
|
echo '#'
|
||||||
|
endunless
|
||||||
|
endfor
|
||||||
|
|
||||||
|
echo '#'
|
||||||
|
echo double_value
|
||||||
|
-%}`
|
||||||
|
const engine = new Liquid()
|
||||||
|
const html = engine.parseAndRenderSync(template, { array: [1, 2, 3] })
|
||||||
|
expect(html).to.equal('4#8#12#6')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Liquid } from '../../../../src/liquid'
|
||||||
|
import { expect, use } from 'chai'
|
||||||
|
import * as chaiAsPromised from 'chai-as-promised'
|
||||||
|
|
||||||
|
use(chaiAsPromised)
|
||||||
|
|
||||||
|
describe('tags/echo', function () {
|
||||||
|
const liquid = new Liquid()
|
||||||
|
|
||||||
|
it('should output literals', async function () {
|
||||||
|
const src = '{% echo 1 %} {% echo "1" %} {% echo 1.1 %}'
|
||||||
|
const html = await liquid.parseAndRender(src)
|
||||||
|
return expect(html).to.equal('1 1 1.1')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should output variables', async function () {
|
||||||
|
const src = '{% echo people.users[0].name %}'
|
||||||
|
const html = await liquid.parseAndRender(src, { people: { users: [ { name: 'Sally' } ] } })
|
||||||
|
return expect(html).to.equal('Sally')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should apply filters before output', async function () {
|
||||||
|
const src = '{% echo user.name | upcase | prepend: "Hello, " | append: "!" %}'
|
||||||
|
const html = await liquid.parseAndRender(src, { user: { name: 'Sally' } })
|
||||||
|
return expect(html).to.equal('Hello, SALLY!')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty tag', async function () {
|
||||||
|
const src = '{% echo %}'
|
||||||
|
const html = await liquid.parseAndRender(src)
|
||||||
|
return expect(html).to.equal('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle extra whitespace', async function () {
|
||||||
|
const src = `{% echo
|
||||||
|
user.name
|
||||||
|
| upcase | prepend:
|
||||||
|
"Hello, " | append: "!"
|
||||||
|
%}`
|
||||||
|
const html = await liquid.parseAndRender(src, { user: { name: 'Sally' } })
|
||||||
|
return expect(html).to.equal('Hello, SALLY!')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import { Liquid } from '../../../../src/liquid'
|
||||||
|
import { expect, use } from 'chai'
|
||||||
|
import * as chaiAsPromised from 'chai-as-promised'
|
||||||
|
|
||||||
|
use(chaiAsPromised)
|
||||||
|
|
||||||
|
describe('tags/liquid', function () {
|
||||||
|
const liquid = new Liquid()
|
||||||
|
|
||||||
|
it('should support shorthand syntax', async function () {
|
||||||
|
const src = `
|
||||||
|
{%- liquid
|
||||||
|
for value in array
|
||||||
|
echo value
|
||||||
|
unless forloop.last
|
||||||
|
echo '#'
|
||||||
|
endunless
|
||||||
|
endfor
|
||||||
|
-%}
|
||||||
|
`
|
||||||
|
const html = await liquid.parseAndRender(src, { array: [1, 2, 3] })
|
||||||
|
return expect(html).to.equal('1#2#3')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support shorthand syntax with assignments and filters', async function () {
|
||||||
|
const src = `
|
||||||
|
{%- liquid
|
||||||
|
for value in array
|
||||||
|
assign double_value = value | times: 2
|
||||||
|
echo double_value | times: 2
|
||||||
|
unless forloop.last
|
||||||
|
echo '#'
|
||||||
|
endunless
|
||||||
|
endfor
|
||||||
|
|
||||||
|
echo '#'
|
||||||
|
echo double_value
|
||||||
|
-%}
|
||||||
|
`
|
||||||
|
const html = await liquid.parseAndRender(src, { array: [1, 2, 3] })
|
||||||
|
return expect(html).to.equal('4#8#12#6')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty tag', async function () {
|
||||||
|
const src = '{% liquid %}'
|
||||||
|
const html = await liquid.parseAndRender(src)
|
||||||
|
return expect(html).to.equal('')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle lines containing only whitespace', async function () {
|
||||||
|
const src = `{% liquid
|
||||||
|
echo 'hello '
|
||||||
|
|
||||||
|
|
||||||
|
\t
|
||||||
|
echo 'goodbye'
|
||||||
|
%}`
|
||||||
|
const html = await liquid.parseAndRender(src)
|
||||||
|
return expect(html).to.equal('hello goodbye')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should fail with carriage return terminated tags', async function () {
|
||||||
|
const src = [
|
||||||
|
'{%- liquid',
|
||||||
|
' for value in array',
|
||||||
|
' echo value',
|
||||||
|
' unless forloop.last',
|
||||||
|
' echo "#"',
|
||||||
|
' endunless',
|
||||||
|
'endfor',
|
||||||
|
'-%}'
|
||||||
|
].join('\r')
|
||||||
|
return expect(liquid.parseAndRender(src))
|
||||||
|
.to.be.rejectedWith(/not closed/)
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -11,6 +11,7 @@ import { OutputToken } from '../../../src/tokens/output-token'
|
|||||||
import { HTMLToken } from '../../../src/tokens/html-token'
|
import { HTMLToken } from '../../../src/tokens/html-token'
|
||||||
import { createTrie } from '../../../src/util/operator-trie'
|
import { createTrie } from '../../../src/util/operator-trie'
|
||||||
import { defaultOperators } from '../../../src/types'
|
import { defaultOperators } from '../../../src/types'
|
||||||
|
import { LiquidTagToken } from '../../../src/tokens/liquid-tag-token'
|
||||||
|
|
||||||
describe('Tokenizer', function () {
|
describe('Tokenizer', function () {
|
||||||
const trie = createTrie(defaultOperators)
|
const trie = createTrie(defaultOperators)
|
||||||
@@ -500,4 +501,28 @@ describe('Tokenizer', function () {
|
|||||||
expect(rhs.getText()).to.deep.equal('"\\""')
|
expect(rhs.getText()).to.deep.equal('"\\""')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
describe('#readLiquidTagTokens', () => {
|
||||||
|
it('should read newline terminated tokens', () => {
|
||||||
|
const tokenizer = new Tokenizer('echo \'hello\'', trie)
|
||||||
|
const tokens = tokenizer.readLiquidTagTokens()
|
||||||
|
expect(tokens.length).to.equal(1)
|
||||||
|
const tag = tokens[0]
|
||||||
|
expect(tag).instanceOf(LiquidTagToken)
|
||||||
|
expect(tag.name).to.equal('echo')
|
||||||
|
expect(tag.args).to.equal('\'hello\'')
|
||||||
|
})
|
||||||
|
it('should gracefully handle empty lines', () => {
|
||||||
|
const tokenizer = new Tokenizer(`
|
||||||
|
echo 'hello'
|
||||||
|
|
||||||
|
decrement foo
|
||||||
|
`, trie)
|
||||||
|
const tokens = tokenizer.readLiquidTagTokens()
|
||||||
|
expect(tokens.length).to.equal(2)
|
||||||
|
})
|
||||||
|
it('should throw if line does not start with an identifier', () => {
|
||||||
|
const tokenizer = new Tokenizer('!', trie)
|
||||||
|
expect(() => tokenizer.readLiquidTagTokens()).to.throw(/illegal liquid tag syntax/)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user