Skip to content

Commit

Permalink
Merge 87a2928 into 24f5346
Browse files Browse the repository at this point in the history
  • Loading branch information
harttle committed Sep 29, 2021
2 parents 24f5346 + 87a2928 commit 80b295f
Show file tree
Hide file tree
Showing 36 changed files with 232 additions and 126 deletions.
5 changes: 1 addition & 4 deletions .github/workflows/release.yml
@@ -1,8 +1,5 @@
name: Release
on:
push:
branches:
- master
on: workflow_dispatch
jobs:
release:
name: Release
Expand Down
2 changes: 1 addition & 1 deletion README.md
Expand Up @@ -12,7 +12,7 @@
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/harttle/liquidjs)

A simple, expressive, safe and [Shopify][shopify/liquid] compatible template engine in pure JavaScript.
A simple, expressive and safe [Shopify][shopify/liquid] / Github Pages compatible template engine in pure JavaScript.
**The purpose of this repo** is to provide a standard Liquid implementation for the JavaScript community so that [Jekyll sites](https://jekyllrb.com), [Github Pages](https://pages.github.com/) and [Shopify templates](https://themes.shopify.com/) can be ported to Node.js without pain.

* [Documentation][doc]
Expand Down
2 changes: 1 addition & 1 deletion docs/_config.yml
@@ -1,6 +1,6 @@
title: LiquidJS
subtitle: "A simple, expressive and safe template engine."
description: "LiquidJS is a simple, expressive and safe template engine."
description: "LiquidJS is a simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript."
author: Harttle
language: en
timezone: UTC
Expand Down
2 changes: 1 addition & 1 deletion docs/source/index.pug
@@ -1,5 +1,5 @@
layout: index
description: LiquidJS is a simple, expressive and safe template engine.
description: LiquidJS is a simple, expressive and safe Shopify / Github Pages compatible template engine in pure JavaScript.
subtitle: A simple, expressive and safe template engine.
---
ul#intro-feature-list
Expand Down
2 changes: 1 addition & 1 deletion docs/source/tutorials/intro-to-liquid.md
Expand Up @@ -2,7 +2,7 @@
title: Introduction to Liquid Template Language
---

LiquidJS is a simple, expressive, safe and shopify compatible template engine in pure JavaScript. The purpose of this repo is to provide a standard Liquid implementation for the JavaScript community. Liquid is originally implemented in Ruby and used by Github Pages, Jekyll and Shopify, see [Differences with Shopify/liquid][diff].
LiquidJS is a simple, expressive and safe [Shopify][shopify/liquid] / Github Pages compatible template engine in pure JavaScript. The purpose of this repo is to provide a standard Liquid implementation for the JavaScript community. Liquid is originally implemented in Ruby and used by Github Pages, Jekyll and Shopify, see [Differences with Shopify/liquid][diff].

LiquidJS syntax is relatively simple. There're 2 types of markups in LiquidJS:

Expand Down
2 changes: 1 addition & 1 deletion docs/source/zh-cn/index.pug
@@ -1,5 +1,5 @@
layout: index
description: LiquidJS is a simple, expressive and safe template engine.
description: LiquidJS 是一个纯 JavaScript 实现的,简洁的、安全的模板引擎,兼容 Shopify / Github Pages。
subtitle: 简单安全的 Liquid 模板引擎
---
ul#intro-feature-list
Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,7 +1,7 @@
{
"name": "liquidjs",
"version": "9.25.1",
"description": "A simple, expressive, safe and Shopify compatible template engine in pure JavaScript.",
"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",
"es2015": "dist/liquid.browser.esm.js",
Expand Down
2 changes: 1 addition & 1 deletion src/builtin/tags/break.ts
Expand Up @@ -2,6 +2,6 @@ import { Emitter, Context } from '../../types'

export default {
render: function (ctx: Context, emitter: Emitter) {
emitter.break = true
emitter['break'] = true
}
}
2 changes: 1 addition & 1 deletion src/builtin/tags/continue.ts
Expand Up @@ -2,6 +2,6 @@ import { Emitter, Context } from '../../types'

export default {
render: function (ctx: Context, emitter: Emitter) {
emitter.continue = true
emitter['continue'] = true
}
}
6 changes: 3 additions & 3 deletions src/builtin/tags/for.ts
Expand Up @@ -55,11 +55,11 @@ export default {
for (const item of collection) {
scope[this.variable] = item
yield r.renderTemplates(this.templates, ctx, emitter)
if (emitter.break) {
emitter.break = false
if (emitter['break']) {
emitter['break'] = false
break
}
emitter.continue = false
emitter['continue'] = false
scope.forloop.next()
}
ctx.pop()
Expand Down
21 changes: 8 additions & 13 deletions src/builtin/tags/include.ts
@@ -1,14 +1,14 @@
import { assert, evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
import { assert, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
import BlockMode from '../../context/block-mode'
import { parseFilePath, renderFilePath } from './render'

export default {
parseFilePath,
renderFilePath,
parse: function (token: TagToken) {
const args = token.args
const tokenizer = new Tokenizer(args, this.liquid.options.operatorsTrie)
this.file = this.liquid.options.dynamicPartials
? tokenizer.readValue()
: tokenizer.readFileName()
assert(this.file, () => `illegal argument "${token.args}"`)
this['file'] = this.parseFilePath(tokenizer, this.liquid)

const begin = tokenizer.p
const withStr = tokenizer.readIdentifier()
Expand All @@ -22,15 +22,10 @@ export default {
this.hash = new Hash(tokenizer.remaining())
},
render: function * (ctx: Context, emitter: Emitter) {
const { liquid, hash, withVar, file } = this
const { liquid, hash, withVar } = this
const { renderer } = liquid
// TODO try move all liquid.parse calls into parse() section
const filepath = ctx.opts.dynamicPartials
? (TypeGuards.isQuotedToken(file)
? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx)
: yield evalToken(file, ctx))
: file.getText()
assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`)
const filepath = yield this.renderFilePath(this['file'], ctx, liquid)
assert(filepath, () => `illegal filename "${filepath}"`)

const saved = ctx.saveRegister('blocks', 'blockMode')
ctx.setRegister('blocks', {})
Expand Down
20 changes: 8 additions & 12 deletions src/builtin/tags/layout.ts
@@ -1,31 +1,27 @@
import { assert, evalQuotedToken, TypeGuards, evalToken, Tokenizer, Emitter, Hash, TagToken, TopLevelToken, Context, TagImplOptions } from '../../types'
import { assert, Tokenizer, Emitter, Hash, TagToken, TopLevelToken, Context, TagImplOptions } from '../../types'
import BlockMode from '../../context/block-mode'
import { parseFilePath, renderFilePath } from './render'

export default {
parseFilePath,
renderFilePath,
parse: function (token: TagToken, remainTokens: TopLevelToken[]) {
const tokenizer = new Tokenizer(token.args, this.liquid.options.operatorsTrie)
const file = this.liquid.options.dynamicPartials ? tokenizer.readValue() : tokenizer.readFileName()
assert(file, () => `illegal argument "${token.args}"`)

this.file = file
this['file'] = this.parseFilePath(tokenizer, this.liquid)
this.hash = new Hash(tokenizer.remaining())
this.tpls = this.liquid.parser.parse(remainTokens)
},
render: function * (ctx: Context, emitter: Emitter) {
const { liquid, hash, file } = this
const { renderer } = liquid
if (file.getText() === 'none') {
if (file === null) {
ctx.setRegister('blockMode', BlockMode.OUTPUT)
const html = yield renderer.renderTemplates(this.tpls, ctx)
emitter.write(html)
return
}
const filepath = ctx.opts.dynamicPartials
? (TypeGuards.isQuotedToken(file)
? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx)
: evalToken(this.file, ctx))
: file.getText()
assert(filepath, () => `file "${file.getText()}"("${filepath}") not available`)
const filepath = yield this.renderFilePath(this['file'], ctx, liquid)
assert(filepath, () => `illegal filename "${filepath}"`)
const templates = yield liquid.parseFileImpl(filepath, ctx.sync)

// render remaining contents and store rendered results
Expand Down
60 changes: 45 additions & 15 deletions src/builtin/tags/render.ts
@@ -1,16 +1,16 @@
import { assert } from '../../util/assert'
import { ForloopDrop } from '../../drop/forloop-drop'
import { toEnumerable } from '../../util/collection'
import { evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'
import { Liquid } from '../../liquid'
import { Token, Template, evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, TagImplOptions } from '../../types'

export default {
parseFilePath,
renderFilePath,
parse: function (token: TagToken) {
const args = token.args
const tokenizer = new Tokenizer(args, this.liquid.options.operatorsTrie)
this.file = this.liquid.options.dynamicPartials
? tokenizer.readValue()
: tokenizer.readFileName()
assert(this.file, () => `illegal argument "${token.args}"`)
this['file'] = this.parseFilePath(tokenizer, this.liquid)

while (!tokenizer.end()) {
tokenizer.skipBlank()
Expand Down Expand Up @@ -40,14 +40,9 @@ export default {
this.hash = new Hash(tokenizer.remaining())
},
render: function * (ctx: Context, emitter: Emitter) {
const { liquid, file, hash } = this
const { renderer } = liquid
const filepath = ctx.opts.dynamicPartials
? (TypeGuards.isQuotedToken(file)
? yield renderer.renderTemplates(liquid.parse(evalQuotedToken(file)), ctx)
: evalToken(file, ctx))
: file.getText()
assert(filepath, () => `illegal filename "${file.getText()}":"${filepath}"`)
const { liquid, hash } = this
const filepath = yield this.renderFilePath(this['file'], ctx, liquid)
assert(filepath, () => `illegal filename "${filepath}"`)

const childCtx = new Context({}, ctx.opts, ctx.sync)
const scope = yield hash.render(ctx)
Expand All @@ -65,12 +60,47 @@ export default {
for (const item of collection) {
scope[alias] = item
const templates = yield liquid.parseFileImpl(filepath, childCtx.sync)
yield renderer.renderTemplates(templates, childCtx, emitter)
yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
scope.forloop.next()
}
} else {
const templates = yield liquid.parseFileImpl(filepath, childCtx.sync)
yield renderer.renderTemplates(templates, childCtx, emitter)
yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
}
}
} as TagImplOptions

type ParsedFileName = Template[] | Token | string | undefined

/**
* @return null for "none",
* @return Template[] for quoted with tags and/or filters
* @return Token for expression (not quoted)
* @throws TypeError if cannot read next token
*/
export function parseFilePath (tokenizer: Tokenizer, liquid: Liquid): ParsedFileName | null {
if (liquid.options.dynamicPartials) {
const file = tokenizer.readValue()
if (file === undefined) throw new TypeError(`illegal argument "${tokenizer.input}"`)
if (file.getText() === 'none') return null
// for filenames like "files/{{file}}", eval as liquid template
if (TypeGuards.isQuotedToken(file)) {
const tpls = liquid.parse(evalQuotedToken(file))
// for filenames like "files/file.liquid", extract the string directly
if (tpls.length === 1) {
const first = tpls[0]
if (TypeGuards.isHTMLToken(first)) return first.getText()
}
return tpls
}
return file
}
const filepath = tokenizer.readFileName().getText()
return filepath === 'none' ? null : filepath
}

export function renderFilePath (file: ParsedFileName, ctx: Context, liquid: Liquid) {
if (typeof file === 'string') return file
if (Array.isArray(file)) return liquid.renderer.renderTemplates(file, ctx)
return evalToken(file, ctx)
}
4 changes: 4 additions & 0 deletions src/emitters/emitter.ts
@@ -0,0 +1,4 @@
export interface Emitter {
write (html: any): void;
end (): void;
}
22 changes: 22 additions & 0 deletions src/emitters/keeping-type-emitter.ts
@@ -0,0 +1,22 @@
import { stringify, toValue } from '../util/underscore'

export class KeepingTypeEmitter {
public html: any = '';

public write (html: any) {
html = toValue(html)
// This will only preserve the type if the value is isolated.
// I.E:
// {{ my-port }} -> 42
// {{ my-host }}:{{ my-port }} -> 'host:42'
if (typeof html !== 'string' && this.html === '') {
this.html = html
} else {
this.html = stringify(this.html) + stringify(html)
}
}

public end () {
return this.html
}
}
14 changes: 14 additions & 0 deletions src/emitters/simple-emitter.ts
@@ -0,0 +1,14 @@
import { stringify } from '../util/underscore'
import { Emitter } from './emitter'

export class SimpleEmitter implements Emitter {
public html: any = '';

public write (html: any) {
this.html += stringify(html)
}

public end () {
return this.html
}
}
12 changes: 12 additions & 0 deletions src/emitters/streamed-emitter.ts
@@ -0,0 +1,12 @@
import { stringify } from '../util/underscore'

export class StreamedEmitter {
public html: any = '';
public stream = new (require('stream').PassThrough)()
public write (html: any) {
this.stream.write(stringify(html))
}
public end () {
this.stream.end()
}
}
2 changes: 1 addition & 1 deletion src/liquid-options.ts
Expand Up @@ -51,7 +51,7 @@ export interface LiquidOptions {
fs?: FS;
/** the global environment passed down to all partial templates, i.e. templates included by `include`, `layout` and `render` tags. */
globals?: object;
/** Whether or not to keep value type when writing the Output. Defaults to `false`. */
/** Whether or not to keep value type when writing the Output, not working for streamed rendering. Defaults to `false`. */
keepOutputType?: boolean;
/** An object of operators for conditional statements. Defaults to the regular Liquid operators. */
operators?: Operators;
Expand Down
10 changes: 6 additions & 4 deletions src/liquid.ts
Expand Up @@ -13,7 +13,6 @@ import { FilterMap } from './template/filter/filter-map'
import { LiquidOptions, normalizeStringArray, NormalizedFullOptions, applyDefault, normalize } from './liquid-options'
import { FilterImplOptions } from './template/filter/filter-impl-options'
import { toPromise, toValue } from './util/async'
import { Emitter } from './render/emitter'

export * from './util/error'
export * from './types'
Expand All @@ -24,7 +23,7 @@ export class Liquid {
public parser: Parser
public filters: FilterMap
public tags: TagMap
private parseFileImpl: (file: string, sync?: boolean) => Iterator<Template[]>
public parseFileImpl: (file: string, sync?: boolean) => Iterator<Template[]>

public constructor (opts: LiquidOptions = {}) {
this.options = applyDefault(normalize(opts))
Expand All @@ -45,15 +44,18 @@ export class Liquid {

public _render (tpl: Template[], scope?: object, sync?: boolean): IterableIterator<any> {
const ctx = new Context(scope, this.options, sync)
const emitter = new Emitter(this.options.keepOutputType)
return this.renderer.renderTemplates(tpl, ctx, emitter)
return this.renderer.renderTemplates(tpl, ctx)
}
public async render (tpl: Template[], scope?: object): Promise<any> {
return toPromise(this._render(tpl, scope, false))
}
public renderSync (tpl: Template[], scope?: object): any {
return toValue(this._render(tpl, scope, true))
}
public renderToNodeStream (tpl: Template[], scope?: object): NodeJS.ReadableStream {
const ctx = new Context(scope, this.options)
return this.renderer.renderTemplatesToNodeStream(tpl, ctx)
}

public _parseAndRender (html: string, scope?: object, sync?: boolean): IterableIterator<any> {
const tpl = this.parse(html)
Expand Down
2 changes: 1 addition & 1 deletion src/parser/tokenizer.ts
Expand Up @@ -31,7 +31,7 @@ export class Tokenizer {
private rawBeginAt = -1

constructor (
private input: string,
public input: string,
private trie: Trie,
private file: string = ''
) {
Expand Down

0 comments on commit 80b295f

Please sign in to comment.