diff --git a/demo/template/README.md b/demo/template/README.md new file mode 100644 index 000000000..d70afb158 --- /dev/null +++ b/demo/template/README.md @@ -0,0 +1,9 @@ +# LiquidJS for Node.js + +## Get Started + +```bash +cd demo/nodejs +npm install +npm start +``` \ No newline at end of file diff --git a/demo/template/get-outputs.ts b/demo/template/get-outputs.ts new file mode 100644 index 000000000..ca61dc521 --- /dev/null +++ b/demo/template/get-outputs.ts @@ -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 { + 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 + } + } +} diff --git a/demo/template/index.ts b/demo/template/index.ts new file mode 100644 index 000000000..2beb2d6c8 --- /dev/null +++ b/demo/template/index.ts @@ -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}`) +} diff --git a/demo/template/package.json b/demo/template/package.json new file mode 100644 index 000000000..9eb728ad2 --- /dev/null +++ b/demo/template/package.json @@ -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" + } +} diff --git a/demo/template/todolist.liquid b/demo/template/todolist.liquid new file mode 100644 index 000000000..8564cc3a2 --- /dev/null +++ b/demo/template/todolist.liquid @@ -0,0 +1,13 @@ +{% layout 'html.liquid' %} + +

{{ title | capitalize }}

+ + {% if authed %} Sign out {{ username }} + {% else %} Sign in + {% endif %} + + \ No newline at end of file diff --git a/demo/template/tsconfig.json b/demo/template/tsconfig.json new file mode 100644 index 000000000..7a6be845e --- /dev/null +++ b/demo/template/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "downlevelIteration": true, + "types": [ + "node" + ] + } +} \ No newline at end of file diff --git a/demo/template/type-guards.ts b/demo/template/type-guards.ts new file mode 100644 index 000000000..47f12fe26 --- /dev/null +++ b/demo/template/type-guards.ts @@ -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' +} diff --git a/src/index.ts b/src/index.ts index a73cb973e..56e39d7fd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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' diff --git a/src/tags/block.ts b/src/tags/block.ts index b59c600bd..450d6c136 100644 --- a/src/tags/block.ts +++ b/src/tags/block.ts @@ -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 diff --git a/src/tags/capture.ts b/src/tags/capture.ts index 95a7c40c5..5abf02720 100644 --- a/src/tags/capture.ts +++ b/src/tags/capture.ts @@ -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) diff --git a/src/tags/case.ts b/src/tags/case.ts index 8cad4bbd2..a7608f6b5 100644 --- a/src/tags/case.ts +++ b/src/tags/case.ts @@ -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 { 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 } diff --git a/src/tags/for.ts b/src/tags/for.ts index 110d04ee2..1ed0f5509 100644 --- a/src/tags/for.ts +++ b/src/tags/for.ts @@ -7,11 +7,11 @@ const MODIFIERS = ['offset', 'limit', 'reversed'] type valueof = 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) diff --git a/src/tags/if.ts b/src/tags/if.ts index c619c9142..8e6651793 100644 --- a/src/tags/if.ts +++ b/src/tags/if.ts @@ -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 { 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 } diff --git a/src/tags/index.ts b/src/tags/index.ts index b9763a67c..9edf91f51 100644 --- a/src/tags/index.ts +++ b/src/tags/index.ts @@ -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 = { - 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 } diff --git a/src/tags/layout.ts b/src/tags/layout.ts index b1700feb0..7ffc04da1 100644 --- a/src/tags/layout.ts +++ b/src/tags/layout.ts @@ -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 { - 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() } diff --git a/src/tags/liquid.ts b/src/tags/liquid.ts index d733529b8..18a4ff7df 100644 --- a/src/tags/liquid.ts +++ b/src/tags/liquid.ts @@ -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 { - yield this.liquid.renderer.renderTemplates(this.tpls, ctx, emitter) + yield this.liquid.renderer.renderTemplates(this.templates, ctx, emitter) } } diff --git a/src/tags/tablerow.ts b/src/tags/tablerow.ts index 23ed63ad0..c69fe8d9f 100644 --- a/src/tags/tablerow.ts +++ b/src/tags/tablerow.ts @@ -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 { let collection = toEnumerable(yield evalToken(this.collection, ctx)) - const hash = (yield this.hash.render(ctx)) as Record - const offset = hash.offset || 0 - const limit = (hash.limit === undefined) ? collection.length : hash.limit + const args = (yield this.args.render(ctx)) as Record + 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) diff --git a/src/tags/unless.ts b/src/tags/unless.ts index 15f03cc06..390d7931b 100644 --- a/src/tags/unless.ts +++ b/src/tags/unless.ts @@ -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 { 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 } diff --git a/src/template/output.ts b/src/template/output.ts index 9c20bfff8..f97313eb9 100644 --- a/src/template/output.ts +++ b/src/template/output.ts @@ -7,7 +7,7 @@ import { Liquid } from '../liquid' import { Filter } from './filter' export class Output extends TemplateImpl implements Template { - private value: Value + value: Value public constructor (token: OutputToken, liquid: Liquid) { super(token) this.value = new Value(token.content, liquid)