mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
feat: automatic output escaping, closes #500
This commit is contained in:
@@ -51,6 +51,7 @@ filters:
|
|||||||
newline_to_br: newline_to_br.html
|
newline_to_br: newline_to_br.html
|
||||||
plus: plus.html
|
plus: plus.html
|
||||||
prepend: prepend.html
|
prepend: prepend.html
|
||||||
|
raw: raw.html
|
||||||
remove: remove.html
|
remove: remove.html
|
||||||
remove_first: remove_first.html
|
remove_first: remove_first.html
|
||||||
replace: replace.html
|
replace: replace.html
|
||||||
|
|||||||
@@ -14,6 +14,6 @@ String | append, prepend, capitalize, upcase, downcase, strip, lstrip, rstrip, s
|
|||||||
HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br
|
HTML/URI | escape, escape_once, url_encode, url_decode, strip_html, newline_to_br
|
||||||
Array | slice, map, sort, sort_natural, uniq, where, first, last, join, reverse, concat, compact, size
|
Array | slice, map, sort, sort_natural, uniq, where, first, last, join, reverse, concat, compact, size
|
||||||
Date | date
|
Date | date
|
||||||
Misc | default, json
|
Misc | default, json, raw
|
||||||
|
|
||||||
[shopify/liquid]: https://github.com/Shopify/liquid
|
[shopify/liquid]: https://github.com/Shopify/liquid
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
---
|
||||||
|
title: raw
|
||||||
|
---
|
||||||
|
|
||||||
|
{% since %}v9.37.0{% endsince %}
|
||||||
|
|
||||||
|
Liquid filter that directly returns the value of the variable. Useful when [outputEscape](/api/interfaces/liquid_options_.liquidoptions.html#Optional-outputEscape) is set.
|
||||||
|
|
||||||
|
{% note info Auto escape %}
|
||||||
|
By default `outputEscape` is not set. That means LiquidJS output is not escaped by default, thus `raw` filter is not useful until `outputEscape` is set.
|
||||||
|
{% endnote %}
|
||||||
|
|
||||||
|
Input (`outputEscape` not set)
|
||||||
|
```liquid
|
||||||
|
{{ "<" }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Output
|
||||||
|
```text
|
||||||
|
<
|
||||||
|
```
|
||||||
|
|
||||||
|
Input (`outputEscape="escape"`)
|
||||||
|
```liquid
|
||||||
|
{{ "<" }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Output
|
||||||
|
```text
|
||||||
|
<
|
||||||
|
```
|
||||||
|
|
||||||
|
Input (`outputEscape="json"`)
|
||||||
|
```liquid
|
||||||
|
{{ "<" }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Output
|
||||||
|
```text
|
||||||
|
"<"
|
||||||
|
```
|
||||||
|
|
||||||
|
Input (`outputEscape="escape"`)
|
||||||
|
```liquid
|
||||||
|
{{ "<" | raw }}
|
||||||
|
```
|
||||||
|
|
||||||
|
Output
|
||||||
|
```text
|
||||||
|
<
|
||||||
|
```
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
---
|
||||||
|
title: raw
|
||||||
|
---
|
||||||
|
|
||||||
|
{% since %}v9.37.0{% endsince %}
|
||||||
|
|
||||||
|
直接返回变量的值。配合 [outputEscape](/api/interfaces/liquid_options_.liquidoptions.html#Optional-outputEscape) 参数使用。
|
||||||
|
|
||||||
|
{% note info 自动转义 %}
|
||||||
|
默认情况下 `outputEscape` 为 `undefined`,这意味着 LiquidJS 输出不会默认转义,因此这时使用 `raw` 没有意义。
|
||||||
|
{% endnote %}
|
||||||
|
|
||||||
|
输入(未设置 `outputEscape`)
|
||||||
|
```liquid
|
||||||
|
{{ "<" }}
|
||||||
|
```
|
||||||
|
|
||||||
|
输出
|
||||||
|
```text
|
||||||
|
<
|
||||||
|
```
|
||||||
|
|
||||||
|
输入(`outputEscape="escape"`)
|
||||||
|
```liquid
|
||||||
|
{{ "<" }}
|
||||||
|
```
|
||||||
|
|
||||||
|
输出
|
||||||
|
```text
|
||||||
|
<
|
||||||
|
```
|
||||||
|
|
||||||
|
输入(`outputEscape="json"`)
|
||||||
|
```liquid
|
||||||
|
{{ "<" }}
|
||||||
|
```
|
||||||
|
|
||||||
|
输出
|
||||||
|
```text
|
||||||
|
"<"
|
||||||
|
```
|
||||||
|
|
||||||
|
输入(`outputEscape="escape"`)
|
||||||
|
```liquid
|
||||||
|
{{ "<" | raw }}
|
||||||
|
```
|
||||||
|
|
||||||
|
输出
|
||||||
|
```text
|
||||||
|
<
|
||||||
|
```
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isFalsy } from '../../render/boolean'
|
import { isFalsy } from '../../render/boolean'
|
||||||
import { isArray, isString, toValue } from '../../util/underscore'
|
import { identify, isArray, isString, toValue } from '../../util/underscore'
|
||||||
import { FilterImpl } from '../../template/filter/filter-impl'
|
import { FilterImpl } from '../../template/filter/filter-impl'
|
||||||
|
|
||||||
export function Default<T1 extends boolean, T2> (this: FilterImpl, value: T1, defaultValue: T2, ...args: Array<[string, any]>): T1 | T2 {
|
export function Default<T1 extends boolean, T2> (this: FilterImpl, value: T1, defaultValue: T2, ...args: Array<[string, any]>): T1 | T2 {
|
||||||
@@ -12,3 +12,5 @@ export function Default<T1 extends boolean, T2> (this: FilterImpl, value: T1, de
|
|||||||
export function json (value: any) {
|
export function json (value: any) {
|
||||||
return JSON.stringify(value)
|
return JSON.stringify(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const raw = identify
|
||||||
|
|||||||
+28
-3
@@ -1,4 +1,4 @@
|
|||||||
import * as _ from './util/underscore'
|
import { snakeCase, forOwn, isArray, isString, isFunction } from './util/underscore'
|
||||||
import { Template } from './template/template'
|
import { Template } from './template/template'
|
||||||
import { Cache } from './cache/cache'
|
import { Cache } from './cache/cache'
|
||||||
import { LRU } from './cache/lru'
|
import { LRU } from './cache/lru'
|
||||||
@@ -7,6 +7,16 @@ import * as fs from './fs/node'
|
|||||||
import { defaultOperators, Operators } from './render/operator'
|
import { defaultOperators, Operators } from './render/operator'
|
||||||
import { createTrie, Trie } from './util/operator-trie'
|
import { createTrie, Trie } from './util/operator-trie'
|
||||||
import { Thenable } from './util/async'
|
import { Thenable } from './util/async'
|
||||||
|
import * as builtinFilters from './builtin/filters'
|
||||||
|
import { assert, FilterImplOptions } from './types'
|
||||||
|
|
||||||
|
const filters = new Map()
|
||||||
|
forOwn(builtinFilters, (conf: FilterImplOptions, name: string) => {
|
||||||
|
filters.set(snakeCase(name), conf)
|
||||||
|
})
|
||||||
|
|
||||||
|
type OutputEscape = (value: any) => string
|
||||||
|
type OutputEscapeOption = 'escape' | 'json' | OutputEscape
|
||||||
|
|
||||||
export interface LiquidOptions {
|
export interface LiquidOptions {
|
||||||
/** A directory or an array of directories from where to resolve layout and include templates, and the filename passed to `.renderFile()`. If it's an array, the files are looked up in the order they occur in the array. Defaults to `["."]` */
|
/** A directory or an array of directories from where to resolve layout and include templates, and the filename passed to `.renderFile()`. If it's an array, the files are looked up in the order they occur in the array. Defaults to `["."]` */
|
||||||
@@ -63,6 +73,8 @@ export interface LiquidOptions {
|
|||||||
globals?: object;
|
globals?: object;
|
||||||
/** Whether or not to keep value type when writing the Output, not working for streamed rendering. Defaults to `false`. */
|
/** Whether or not to keep value type when writing the Output, not working for streamed rendering. Defaults to `false`. */
|
||||||
keepOutputType?: boolean;
|
keepOutputType?: boolean;
|
||||||
|
/** Default escape filter applied to output values, when set, you'll have to add `| raw` for values don't need to be escaped. Defaults to `undefined`. */
|
||||||
|
outputEscape?: OutputEscapeOption;
|
||||||
/** An object of operators for conditional statements. Defaults to the regular Liquid operators. */
|
/** An object of operators for conditional statements. Defaults to the regular Liquid operators. */
|
||||||
operators?: Operators;
|
operators?: Operators;
|
||||||
/** Respect parameter order when using filters like "for ... reversed limit", Defaults to `false`. */
|
/** Respect parameter order when using filters like "for ... reversed limit", Defaults to `false`. */
|
||||||
@@ -93,6 +105,7 @@ interface NormalizedOptions extends LiquidOptions {
|
|||||||
partials?: string[];
|
partials?: string[];
|
||||||
layouts?: string[];
|
layouts?: string[];
|
||||||
cache?: Cache<Thenable<Template[]>>;
|
cache?: Cache<Thenable<Template[]>>;
|
||||||
|
outputEscape?: OutputEscape;
|
||||||
operatorsTrie?: Trie;
|
operatorsTrie?: Trie;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,12 +194,24 @@ export function normalize (options: LiquidOptions): NormalizedFullOptions {
|
|||||||
options.root = normalizeDirectoryList(options.root)
|
options.root = normalizeDirectoryList(options.root)
|
||||||
options.partials = normalizeDirectoryList(options.partials)
|
options.partials = normalizeDirectoryList(options.partials)
|
||||||
options.layouts = normalizeDirectoryList(options.layouts)
|
options.layouts = normalizeDirectoryList(options.layouts)
|
||||||
|
options.outputEscape = options.outputEscape && getOutputEscapeFunction(options.outputEscape)
|
||||||
return options as NormalizedFullOptions
|
return options as NormalizedFullOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOutputEscapeFunction (nameOrFunction: OutputEscapeOption) {
|
||||||
|
if (isString(nameOrFunction)) {
|
||||||
|
const filterImpl = filters.get(nameOrFunction)
|
||||||
|
assert(isFunction(filterImpl), `filter "${nameOrFunction}" not found`)
|
||||||
|
return filterImpl
|
||||||
|
} else {
|
||||||
|
assert(isFunction(nameOrFunction), '`outputEscape` need to be of type string or function')
|
||||||
|
return nameOrFunction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function normalizeDirectoryList (value: any): string[] {
|
export function normalizeDirectoryList (value: any): string[] {
|
||||||
let list: string[] = []
|
let list: string[] = []
|
||||||
if (_.isArray(value)) list = value
|
if (isArray(value)) list = value
|
||||||
if (_.isString(value)) list = [value]
|
if (isString(value)) list = [value]
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,12 +5,20 @@ import { Context } from '../context/context'
|
|||||||
import { Emitter } from '../emitters/emitter'
|
import { Emitter } from '../emitters/emitter'
|
||||||
import { OutputToken } from '../tokens/output-token'
|
import { OutputToken } from '../tokens/output-token'
|
||||||
import { Liquid } from '../liquid'
|
import { Liquid } from '../liquid'
|
||||||
|
import { Filter } from './filter/filter'
|
||||||
|
|
||||||
export class Output extends TemplateImpl<OutputToken> implements Template {
|
export class Output extends TemplateImpl<OutputToken> implements Template {
|
||||||
private value: Value
|
private value: Value
|
||||||
public constructor (token: OutputToken, liquid: Liquid) {
|
public constructor (token: OutputToken, liquid: Liquid) {
|
||||||
super(token)
|
super(token)
|
||||||
this.value = new Value(token.content, liquid)
|
this.value = new Value(token.content, liquid)
|
||||||
|
const filters = this.value.filters
|
||||||
|
const outputEscape = liquid.options.outputEscape
|
||||||
|
if (filters.length && filters[filters.length - 1].name === 'raw') {
|
||||||
|
filters.pop()
|
||||||
|
} else if (outputEscape) {
|
||||||
|
filters.push(new Filter(toString.call(outputEscape), outputEscape, [], liquid))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
public * render (ctx: Context, emitter: Emitter): IterableIterator<unknown> {
|
public * render (ctx: Context, emitter: Emitter): IterableIterator<unknown> {
|
||||||
const val = yield this.value.value(ctx, false)
|
const val = yield this.value.value(ctx, false)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Drop } from '../drop/drop'
|
import { Drop } from '../drop/drop'
|
||||||
|
|
||||||
const toStr = Object.prototype.toString
|
export const toString = Object.prototype.toString
|
||||||
const toLowerCase = String.prototype.toLowerCase
|
const toLowerCase = String.prototype.toLowerCase
|
||||||
|
|
||||||
export const hasOwnProperty = Object.hasOwnProperty
|
export const hasOwnProperty = Object.hasOwnProperty
|
||||||
@@ -57,7 +57,7 @@ export function isNil (value: any): boolean {
|
|||||||
|
|
||||||
export function isArray (value: any): value is any[] {
|
export function isArray (value: any): value is any[] {
|
||||||
// be compatible with IE 8
|
// be compatible with IE 8
|
||||||
return toStr.call(value) === '[object Array]'
|
return toString.call(value) === '[object Array]'
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { expect } from 'chai'
|
||||||
|
import { Liquid } from '../../../src/liquid'
|
||||||
|
|
||||||
|
describe('LiquidOptions#*outputEscape*', function () {
|
||||||
|
it('when outputEscape is not set', async function () {
|
||||||
|
const engine = new Liquid()
|
||||||
|
const html = await engine.parseAndRender('{{"<"}}')
|
||||||
|
expect(html).to.equal('<')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should escape when outputEscape="escape"', async function () {
|
||||||
|
const engine = new Liquid({
|
||||||
|
outputEscape: 'escape'
|
||||||
|
})
|
||||||
|
const html = await engine.parseAndRender('{{"<"}}')
|
||||||
|
expect(html).to.equal('<')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should json stringify when outputEscape="json"', async function () {
|
||||||
|
const engine = new Liquid({
|
||||||
|
outputEscape: 'json'
|
||||||
|
})
|
||||||
|
const html = await engine.parseAndRender('{{"<"}}')
|
||||||
|
expect(html).to.equal('"<"')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should support outputEscape=Function', async function () {
|
||||||
|
const engine = new Liquid({
|
||||||
|
outputEscape: (v: any) => `{${v}}`
|
||||||
|
})
|
||||||
|
const html = await engine.parseAndRender('{{"<"}}')
|
||||||
|
expect(html).to.equal('{<}')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should skip escape for output with filter "| raw"', async function () {
|
||||||
|
const engine = new Liquid({
|
||||||
|
outputEscape: 'escape'
|
||||||
|
})
|
||||||
|
const html = await engine.parseAndRender('{{"<" | raw}}')
|
||||||
|
expect(html).to.equal('<')
|
||||||
|
})
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user