refactor: more consistent tags to make it easier to iterate over, #524

This commit is contained in:
Jun Yang
2022-11-27 17:29:44 +08:00
parent 4e1a30a20c
commit 1380ac931a
19 changed files with 225 additions and 82 deletions
+9
View File
@@ -0,0 +1,9 @@
# LiquidJS for Node.js
## Get Started
```bash
cd demo/nodejs
npm install
npm start
```
+25
View File
@@ -0,0 +1,25 @@
import { Output, Template, Tag } from 'liquidjs'
import { isLayoutTag, isIfTag, isUnlessTag, isLiquidTag, isCaseTag, isCaptureTag, isTablerowTag, isForTag } from './type-guards'
/**
* iterate over all `{{ output }}`
*/
export function * getOutputs (templates: Template[]): Generator<Output, void> {
for (const template of templates) {
if (template instanceof Tag) {
if (isIfTag(template) || isUnlessTag(template) || isCaseTag(template)) {
for (const branch of template.branches) {
yield * getOutputs(branch.templates)
}
yield * getOutputs(template.elseTemplates)
} else if (isForTag(template)) {
yield * getOutputs(template.templates)
yield * getOutputs(template.elseTemplates)
} else if (isLiquidTag(template) || isCaptureTag(template) || isTablerowTag(template) || isLayoutTag(template)) {
yield * getOutputs(template.templates)
}
} else if (template instanceof Output) {
yield template
}
}
}
+16
View File
@@ -0,0 +1,16 @@
import { Liquid } from 'liquidjs'
import { getOutputs } from './get-outputs'
const engine = new Liquid({
root: __dirname,
extname: '.liquid'
})
const templates = engine.parseFileSync('todolist')
for (const output of getOutputs(templates)) {
const token = output.token
const [line, col] = token.getPosition()
const text = token.getText()
console.log(`[${line}:${col}] ${text}`)
}
+17
View File
@@ -0,0 +1,17 @@
{
"name": "liquidjs-demo-template",
"private": true,
"description": "liquid template parsing",
"main": "index.ts",
"scripts": {
"start": "ts-node index.ts"
},
"dependencies": {
"liquidjs": "latest",
"ts-node": "^8.10.2",
"typescript": "^4.2.4"
},
"devDependencies": {
"@types/node": "^14.14.37"
}
}
+13
View File
@@ -0,0 +1,13 @@
{% layout 'html.liquid' %}
<h1>{{ title | capitalize }}</h1>
<a>
{% if authed %} Sign out {{ username }}
{% else %} Sign in
{% endif %}
</a>
<ul>
{% for todo in todos %}
<p>{{ todo }}</p>
{% endfor %}
</ul>
+8
View File
@@ -0,0 +1,8 @@
{
"compilerOptions": {
"downlevelIteration": true,
"types": [
"node"
]
}
}
+33
View File
@@ -0,0 +1,33 @@
import { LayoutTag, ForTag, LiquidTag, CaptureTag, CaseTag, UnlessTag, TablerowTag, Tag, IfTag } from 'liquidjs'
export function isIfTag (tag: Tag): tag is IfTag {
return tag.name === 'if'
}
export function isUnlessTag (tag: Tag): tag is UnlessTag {
return tag.name === 'unless'
}
export function isLiquidTag (tag: Tag): tag is LiquidTag {
return tag.name === 'liquid'
}
export function isCaseTag (tag: Tag): tag is CaseTag {
return tag.name === 'case'
}
export function isCaptureTag (tag: Tag): tag is CaptureTag {
return tag.name === 'capture'
}
export function isTablerowTag (tag: Tag): tag is TablerowTag {
return tag.name === 'tablerow'
}
export function isForTag (tag: Tag): tag is ForTag {
return tag.name === 'for'
}
export function isLayoutTag (tag: Tag): tag is LayoutTag {
return tag.name === 'layout'
}
+2 -2
View File
@@ -6,10 +6,10 @@ export { Drop } from './drop'
export { Emitter } from './emitters'
export { defaultOperators, Operators, evalToken, evalQuotedToken, Expression, isFalsy, isTruthy } from './render'
export { Context, Scope } from './context'
export { Value, Hash, Template, FilterImplOptions, Tag, Filter } from './template'
export { Value, Hash, Template, FilterImplOptions, Tag, Filter, Output } from './template'
export { Token, TopLevelToken, TagToken, ValueToken } from './tokens'
export { TokenKind, Tokenizer, ParseStream } from './parser'
export { filters } from './filters'
export { tags } from './tags'
export * from './tags'
export { defaultOptions } from './liquid-options'
export { Liquid } from './liquid'
+5 -5
View File
@@ -4,8 +4,8 @@ import { BlockDrop } from '../drop'
import { Liquid, TagToken, TopLevelToken, Template, Context, Emitter, Tag } from '..'
export default class extends Tag {
private block: string
private tpls: Template[] = []
block: string
templates: Template[] = []
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
const match = /\w+/.exec(token.args)
@@ -14,7 +14,7 @@ export default class extends Tag {
const token = remainTokens.shift()!
if (isTagToken(token) && token.name === 'endblock') return
const template = liquid.parser.parseToken(token, remainTokens)
this.tpls.push(template)
this.templates.push(template)
}
throw new Error(`tag ${token.getText()} not closed`)
}
@@ -29,12 +29,12 @@ export default class extends Tag {
}
private getBlockRender (ctx: Context) {
const { liquid, tpls } = this
const { liquid, templates } = this
const renderChild = ctx.getRegister('blocks')[this.block]
const renderCurrent = function * (superBlock: BlockDrop, emitter: Emitter) {
// add {{ block.super }} support when rendering
ctx.push({ block: superBlock })
yield liquid.renderer.renderTemplates(tpls, ctx, emitter)
yield liquid.renderer.renderTemplates(templates, ctx, emitter)
ctx.pop()
}
return renderChild
+2 -2
View File
@@ -3,8 +3,8 @@ import { evalQuotedToken } from '../render'
import { isTagToken } from '../util'
export default class extends Tag {
private variable: string
private templates: Template[] = []
variable: string
templates: Template[] = []
constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
const tokenizer = new Tokenizer(tagToken.args, this.liquid.options.operators)
+10 -10
View File
@@ -1,12 +1,12 @@
import { ValueToken, Liquid, Tokenizer, toValue, evalToken, Value, Emitter, TagToken, TopLevelToken, Context, Template, Tag, ParseStream } from '..'
export default class extends Tag {
private cond: Value
private cases: { val?: ValueToken, templates: Template[] }[] = []
private elseTemplates: Template[] = []
value: Value
branches: { value?: ValueToken, templates: Template[] }[] = []
elseTemplates: Template[] = []
constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
this.cond = new Value(tagToken.args, this.liquid)
this.value = new Value(tagToken.args, this.liquid)
this.elseTemplates = []
let p: Template[] = []
@@ -18,8 +18,8 @@ export default class extends Tag {
while (!tokenizer.end()) {
const value = tokenizer.readValue()
this.cases.push({
val: value,
this.branches.push({
value: value,
templates: p
})
tokenizer.readTo(',')
@@ -37,10 +37,10 @@ export default class extends Tag {
* render (ctx: Context, emitter: Emitter): Generator<unknown, unknown, unknown> {
const r = this.liquid.renderer
const cond = toValue(yield this.cond.value(ctx, ctx.opts.lenientIf))
for (const branch of this.cases) {
const val = yield evalToken(branch.val, ctx, ctx.opts.lenientIf)
if (val === cond) {
const value = toValue(yield this.value.value(ctx, ctx.opts.lenientIf))
for (const branch of this.branches) {
const target = yield evalToken(branch.value, ctx, ctx.opts.lenientIf)
if (target === value) {
yield r.renderTemplates(branch.templates, ctx, emitter)
return
}
+5 -5
View File
@@ -7,11 +7,11 @@ const MODIFIERS = ['offset', 'limit', 'reversed']
type valueof<T> = T[keyof T]
export default class extends Tag {
private variable: string
private collection: ValueToken
private hash: Hash
private templates: Template[]
private elseTemplates: Template[]
variable: string
collection: ValueToken
hash: Hash
templates: Template[]
elseTemplates: Template[]
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
+7 -7
View File
@@ -1,19 +1,19 @@
import { Liquid, Tag, Value, Emitter, isTruthy, TagToken, TopLevelToken, Context, Template } from '..'
export default class extends Tag {
private branches: { predicate: Value, templates: Template[] }[] = []
private elseTemplates: Template[] = []
branches: { value: Value, templates: Template[] }[] = []
elseTemplates: Template[] = []
constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
let p
liquid.parser.parseStream(remainTokens)
.on('start', () => this.branches.push({
predicate: new Value(tagToken.args, this.liquid),
value: new Value(tagToken.args, this.liquid),
templates: (p = [])
}))
.on('tag:elsif', (token: TagToken) => this.branches.push({
predicate: new Value(token.args, this.liquid),
value: new Value(token.args, this.liquid),
templates: (p = [])
}))
.on('tag:else', () => (p = this.elseTemplates))
@@ -26,9 +26,9 @@ export default class extends Tag {
* render (ctx: Context, emitter: Emitter): Generator<unknown, void, string> {
const r = this.liquid.renderer
for (const { predicate, templates } of this.branches) {
const value = yield predicate.value(ctx, ctx.opts.lenientIf)
if (isTruthy(value, ctx)) {
for (const { value, templates } of this.branches) {
const v = yield value.value(ctx, ctx.opts.lenientIf)
if (isTruthy(v, ctx)) {
yield r.renderTemplates(templates, ctx, emitter)
return
}
+44 -22
View File
@@ -1,26 +1,48 @@
import assign from './assign'
import For from './for'
import capture from './capture'
import Case from './case'
import comment from './comment'
import include from './include'
import render from './render'
import decrement from './decrement'
import cycle from './cycle'
import If from './if'
import increment from './increment'
import layout from './layout'
import block from './block'
import raw from './raw'
import tablerow from './tablerow'
import unless from './unless'
import Break from './break'
import Continue from './continue'
import echo from './echo'
import liquid from './liquid'
import inlineComment from './inline-comment'
import AssignTag from './assign'
import ForTag from './for'
import CaptureTag from './capture'
import CaseTag from './case'
import CommentTag from './comment'
import IncludeTag from './include'
import RenderTag from './render'
import DecrementTag from './decrement'
import CycleTag from './cycle'
import IfTag from './if'
import IncrementTag from './increment'
import LayoutTag from './layout'
import BlockTag from './block'
import RawTag from './raw'
import TablerowTag from './tablerow'
import UnlessTag from './unless'
import BreakTag from './break'
import ContinueTag from './continue'
import EchoTag from './echo'
import LiquidTag from './liquid'
import InlineCommentTag from './inline-comment'
import type { TagClass } from '../template/tag'
export const tags: Record<string, TagClass> = {
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, '#': inlineComment
assign: AssignTag,
'for': ForTag,
capture: CaptureTag,
'case': CaseTag,
comment: CommentTag,
include: IncludeTag,
render: RenderTag,
decrement: DecrementTag,
increment: IncrementTag,
cycle: CycleTag,
'if': IfTag,
layout: LayoutTag,
block: BlockTag,
raw: RawTag,
tablerow: TablerowTag,
unless: UnlessTag,
'break': BreakTag,
'continue': ContinueTag,
echo: EchoTag,
liquid: LiquidTag,
'#': InlineCommentTag
}
export { AssignTag, ForTag, CaptureTag, CaseTag, CommentTag, IncludeTag, RenderTag, DecrementTag, IncrementTag, CycleTag, IfTag, LayoutTag, BlockTag, RawTag, TablerowTag, UnlessTag, BreakTag, ContinueTag, EchoTag, LiquidTag, InlineCommentTag }
+9 -9
View File
@@ -4,23 +4,23 @@ import { parseFilePath, renderFilePath, ParsedFileName } from './render'
import { BlankDrop } from '../drop'
export default class extends Tag {
private hash: Hash
private tpls: Template[]
private file?: ParsedFileName
args: Hash
templates: Template[]
file?: ParsedFileName
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
const tokenizer = new Tokenizer(token.args, this.liquid.options.operators)
this.file = parseFilePath(tokenizer, this.liquid)
this['currentFile'] = token.file
this.hash = new Hash(tokenizer.remaining())
this.tpls = this.liquid.parser.parseTokens(remainTokens)
this.args = new Hash(tokenizer.remaining())
this.templates = this.liquid.parser.parseTokens(remainTokens)
}
* render (ctx: Context, emitter: Emitter): Generator<unknown, unknown, unknown> {
const { liquid, hash, file } = this
const { liquid, args, file } = this
const { renderer } = liquid
if (file === undefined) {
ctx.setRegister('blockMode', BlockMode.OUTPUT)
yield renderer.renderTemplates(this.tpls, ctx, emitter)
yield renderer.renderTemplates(this.templates, ctx, emitter)
return
}
const filepath = (yield renderFilePath(this.file, ctx, liquid)) as string
@@ -29,7 +29,7 @@ export default class extends Tag {
// render remaining contents and store rendered results
ctx.setRegister('blockMode', BlockMode.STORE)
const html = yield renderer.renderTemplates(this.tpls, ctx)
const html = yield renderer.renderTemplates(this.templates, ctx)
const blocks = ctx.getRegister('blocks')
// set whole content to anonymous block if anonymous doesn't specified
@@ -37,7 +37,7 @@ export default class extends Tag {
ctx.setRegister('blockMode', BlockMode.OUTPUT)
// render the layout file use stored blocks
ctx.push((yield hash.render(ctx)) as Scope)
ctx.push((yield args.render(ctx)) as Scope)
yield renderer.renderTemplates(templates, ctx, emitter)
ctx.pop()
}
+3 -3
View File
@@ -1,14 +1,14 @@
import { Template, Tokenizer, Emitter, Liquid, TopLevelToken, TagToken, Context, Tag } from '..'
export default class extends Tag {
private tpls: Template[]
templates: Template[]
constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(token, remainTokens, liquid)
const tokenizer = new Tokenizer(token.args, this.liquid.options.operators)
const tokens = tokenizer.readLiquidTagTokens(this.liquid.options)
this.tpls = this.liquid.parser.parseTokens(tokens)
this.templates = this.liquid.parser.parseTokens(tokens)
}
* render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
yield this.liquid.renderer.renderTemplates(this.tpls, ctx, emitter)
yield this.liquid.renderer.renderTemplates(this.templates, ctx, emitter)
}
}
+9 -9
View File
@@ -4,10 +4,10 @@ import { TablerowloopDrop } from '../drop/tablerowloop-drop'
import { Tokenizer } from '../parser/tokenizer'
export default class extends Tag {
private variable: string
private hash: Hash
private templates: Template[]
private collection: ValueToken
variable: string
args: Hash
templates: Template[]
collection: ValueToken
constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
const tokenizer = new Tokenizer(tagToken.args, this.liquid.options.operators)
@@ -23,7 +23,7 @@ export default class extends Tag {
this.variable = variable.content
this.collection = collectionToken
this.hash = new Hash(tokenizer.remaining())
this.args = new Hash(tokenizer.remaining())
this.templates = []
let p
@@ -40,12 +40,12 @@ export default class extends Tag {
* render (ctx: Context, emitter: Emitter): Generator<unknown, void, unknown> {
let collection = toEnumerable(yield evalToken(this.collection, ctx))
const hash = (yield this.hash.render(ctx)) as Record<string, any>
const offset = hash.offset || 0
const limit = (hash.limit === undefined) ? collection.length : hash.limit
const args = (yield this.args.render(ctx)) as Record<string, any>
const offset = args.offset || 0
const limit = (args.limit === undefined) ? collection.length : args.limit
collection = collection.slice(offset, offset + limit)
const cols = hash.cols || collection.length
const cols = args.cols || collection.length
const r = this.liquid.renderer
const tablerowloop = new TablerowloopDrop(collection.length, cols, this.collection.getText(), this.variable)
+7 -7
View File
@@ -1,19 +1,19 @@
import { Liquid, Tag, Value, TopLevelToken, Template, Emitter, isTruthy, isFalsy, Context, TagToken } from '..'
export default class extends Tag {
private branches: { predicate: Value, test: (val: any, ctx: Context) => boolean, templates: Template[] }[] = []
private elseTemplates: Template[] = []
branches: { value: Value, test: (val: any, ctx: Context) => boolean, templates: Template[] }[] = []
elseTemplates: Template[] = []
constructor (tagToken: TagToken, remainTokens: TopLevelToken[], liquid: Liquid) {
super(tagToken, remainTokens, liquid)
let p
this.liquid.parser.parseStream(remainTokens)
.on('start', () => this.branches.push({
predicate: new Value(tagToken.args, this.liquid),
value: new Value(tagToken.args, this.liquid),
test: isFalsy,
templates: (p = [])
}))
.on('tag:elsif', (token: TagToken) => this.branches.push({
predicate: new Value(token.args, this.liquid),
value: new Value(token.args, this.liquid),
test: isTruthy,
templates: (p = [])
}))
@@ -27,9 +27,9 @@ export default class extends Tag {
* render (ctx: Context, emitter: Emitter): Generator<unknown, unknown, unknown> {
const r = this.liquid.renderer
for (const { predicate, test, templates } of this.branches) {
const value = yield predicate.value(ctx, ctx.opts.lenientIf)
if (test(value, ctx)) {
for (const { value, test, templates } of this.branches) {
const v = yield value.value(ctx, ctx.opts.lenientIf)
if (test(v, ctx)) {
yield r.renderTemplates(templates, ctx, emitter)
return
}
+1 -1
View File
@@ -7,7 +7,7 @@ import { Liquid } from '../liquid'
import { Filter } from './filter'
export class Output extends TemplateImpl<OutputToken> implements Template {
private value: Value
value: Value
public constructor (token: OutputToken, liquid: Liquid) {
super(token)
this.value = new Value(token.content, liquid)