mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-14 20:00:39 -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
|
||||
plus: plus.html
|
||||
prepend: prepend.html
|
||||
raw: raw.html
|
||||
remove: remove.html
|
||||
remove_first: remove_first.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
|
||||
Array | slice, map, sort, sort_natural, uniq, where, first, last, join, reverse, concat, compact, size
|
||||
Date | date
|
||||
Misc | default, json
|
||||
Misc | default, json, raw
|
||||
|
||||
[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 { isArray, isString, toValue } from '../../util/underscore'
|
||||
import { identify, isArray, isString, toValue } from '../../util/underscore'
|
||||
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 {
|
||||
@@ -12,3 +12,5 @@ export function Default<T1 extends boolean, T2> (this: FilterImpl, value: T1, de
|
||||
export function json (value: any) {
|
||||
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 { Cache } from './cache/cache'
|
||||
import { LRU } from './cache/lru'
|
||||
@@ -7,6 +7,16 @@ import * as fs from './fs/node'
|
||||
import { defaultOperators, Operators } from './render/operator'
|
||||
import { createTrie, Trie } from './util/operator-trie'
|
||||
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 {
|
||||
/** 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;
|
||||
/** Whether or not to keep value type when writing the Output, not working for streamed rendering. Defaults to `false`. */
|
||||
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. */
|
||||
operators?: Operators;
|
||||
/** Respect parameter order when using filters like "for ... reversed limit", Defaults to `false`. */
|
||||
@@ -93,6 +105,7 @@ interface NormalizedOptions extends LiquidOptions {
|
||||
partials?: string[];
|
||||
layouts?: string[];
|
||||
cache?: Cache<Thenable<Template[]>>;
|
||||
outputEscape?: OutputEscape;
|
||||
operatorsTrie?: Trie;
|
||||
}
|
||||
|
||||
@@ -181,12 +194,24 @@ export function normalize (options: LiquidOptions): NormalizedFullOptions {
|
||||
options.root = normalizeDirectoryList(options.root)
|
||||
options.partials = normalizeDirectoryList(options.partials)
|
||||
options.layouts = normalizeDirectoryList(options.layouts)
|
||||
options.outputEscape = options.outputEscape && getOutputEscapeFunction(options.outputEscape)
|
||||
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[] {
|
||||
let list: string[] = []
|
||||
if (_.isArray(value)) list = value
|
||||
if (_.isString(value)) list = [value]
|
||||
if (isArray(value)) list = value
|
||||
if (isString(value)) list = [value]
|
||||
return list
|
||||
}
|
||||
|
||||
@@ -5,12 +5,20 @@ import { Context } from '../context/context'
|
||||
import { Emitter } from '../emitters/emitter'
|
||||
import { OutputToken } from '../tokens/output-token'
|
||||
import { Liquid } from '../liquid'
|
||||
import { Filter } from './filter/filter'
|
||||
|
||||
export class Output extends TemplateImpl<OutputToken> implements Template {
|
||||
private value: Value
|
||||
public constructor (token: OutputToken, liquid: Liquid) {
|
||||
super(token)
|
||||
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> {
|
||||
const val = yield this.value.value(ctx, false)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Drop } from '../drop/drop'
|
||||
|
||||
const toStr = Object.prototype.toString
|
||||
export const toString = Object.prototype.toString
|
||||
const toLowerCase = String.prototype.toLowerCase
|
||||
|
||||
export const hasOwnProperty = Object.hasOwnProperty
|
||||
@@ -57,7 +57,7 @@ export function isNil (value: any): boolean {
|
||||
|
||||
export function isArray (value: any): value is any[] {
|
||||
// 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