diff --git a/docs/source/tags/include.md b/docs/source/tags/include.md index 19c46a243d..02e88265cb 100644 --- a/docs/source/tags/include.md +++ b/docs/source/tags/include.md @@ -53,12 +53,12 @@ When filename is specified as literal string, it supports Liquid output and filt ``` {% note info Escaping %} -In LiquidJS, `"` within quoted string literals need to be escaped. Adding a slash before the quote, e.g. `\"`. Using Jekyll-like filenames can make this easier, see below. +In LiquidJS, `"` within quoted string literals need to be escaped by adding a slash before the quote, e.g. `\"`. Using Jekyll-like filenames can make this easier, see below. {% endnote %} ## Jekyll-like filenames -Setting [dynamicPartials][dynamicPartials] to `false` will enable Jekyll-like includes, file names are specified as literal string. And it also supports Liquid outputs and filters. +Setting [dynamicPartials][dynamicPartials] to `false` will enable Jekyll-like filenames, where file names are specified as literal string without surrounding quotes. Liquid outputs and filters are also supported within that, for example: ```liquid {% include prefix/{{ page.my_variable }}/suffix %} @@ -70,6 +70,35 @@ This way, you don't need to escape `"` in the filename expression. {% include prefix/{{name | append: ".html"}} %} ``` +## Jekyll include + +[jekyllInclude][jekyllInclude] is used to enable Jekyll-like include syntax. Defaults to `false`, when set to `true`: + +- Filename will be static: `dynamicPartials` now defaults to `false` (instead of `true`). And you can set `dynamicPartials` back to `true`. +- Use `=` instead of `:` to separate parameter key-values. +- Parameters are under `include` variable instead of current scope. + +For example, the following template: + +```liquid +{% include name.html header="HEADER" content="CONTENT" %} +``` + +`name.html` with following content: + +```liquid +
{{include.header}}
+{{include.content}} +``` + +Note that we're referencing the first parameter by `include.header` instead of `header`. Will output following: + +```html +
HEADER
+CONTENT +``` + [extname]: ../api/interfaces/liquid_options_.liquidoptions.html#Optional-extname [root]: ../api/interfaces/liquid_options_.liquidoptions.html#Optional-root [dynamicPartials]: ../api/interfaces/liquid_options_.liquidoptions.html#dynamicPartials +[jekyllInclude]: ../api/interfaces/liquid_options_.liquidoptions.html#jekyllInclude diff --git a/docs/source/zh-cn/tags/include.md b/docs/source/zh-cn/tags/include.md index f62babcbcc..934b57085a 100644 --- a/docs/source/zh-cn/tags/include.md +++ b/docs/source/zh-cn/tags/include.md @@ -44,5 +44,61 @@ title: Include 上面的例子中,子模板中 `product` 会保有父模板中的 `featured_product` 变量的值。 +## 输出和过滤器 + +文件名为字符串字面量时,支持 Liquid 输出和过滤器。在拼接文件名时很方便: + +```liquid +{% include "prefix/{{name | append: \".html\"}}" %} +``` + +{% note info 转义 %} +字符串字面量里的 `"` 需要转义为 `\"`,使用静态文件名可以避免这个问题,见下面的 Jekyll-like 文件名。 +{% endnote %} + +## Jekyll-like 文件名 + +设置 [dynamicPartials][dynamicPartials] 为 `false` 来启用 Jekyll-like 文件名,这时文件名不需要用引号包含,会被当作字面量处理。 这样的字符串里面仍然支持 Liquid 输出和过滤器,例如: + +```liquid +{% include prefix/{{ page.my_variable }}/suffix %} +``` + +这样文件名里的 `"` 就不用转义了。 + +```liquid +{% include prefix/{{name | append: ".html"}} %} +``` + +## Jekyll include + +[jekyllInclude][jekyllInclude] 用来启用 Jekyll-like include 语法。默认为 `false`,当设置为 `true` 时: + +- 默认启用静态文件名:`dynamicPartials` 的默认值变为 `false`(而非 `true`)。但你也可以把它设置回 `true`。 +- 参数的键和值之间由 `=` 分隔(本来是 `:`)。 +- 参数放到了 `include` 变量下,而非当前作用域。 + +例如下面的模板: + +```liquid +{% include name.html header="HEADER" content="CONTENT" %} +``` + +其中 `name.html` 的内容是: + +```liquid +
{{include.header}}
+{{include.content}} +``` + +注意我们通过 `include.header` 引用第一个参数,而不是 `header`。输出如下: + +```html +
HEADER
+CONTENT +``` + [extname]: ../../api/interfaces/liquid_options_.liquidoptions.html#Optional-extname [root]: ../../api/interfaces/liquid_options_.liquidoptions.html#Optional-root +[dynamicPartials]: ../../api/interfaces/liquid_options_.liquidoptions.html#dynamicPartials +[jekyllInclude]: ../../api/interfaces/liquid_options_.liquidoptions.html#jekyllInclude diff --git a/src/builtin/tags/include.ts b/src/builtin/tags/include.ts index fd1538a41d..a8d026983e 100644 --- a/src/builtin/tags/include.ts +++ b/src/builtin/tags/include.ts @@ -20,7 +20,7 @@ export default { } else tokenizer.p = begin } else tokenizer.p = begin - this.hash = new Hash(tokenizer.remaining()) + this.hash = new Hash(tokenizer.remaining(), this.liquid.options.jekyllInclude) }, render: function * (ctx: Context, emitter: Emitter) { const { liquid, hash, withVar } = this @@ -34,7 +34,7 @@ export default { const scope = yield hash.render(ctx) if (withVar) scope[filepath] = evalToken(withVar, ctx) const templates = yield liquid._parsePartialFile(filepath, ctx.sync, this['currentFile']) - ctx.push(scope) + ctx.push(ctx.opts.jekyllInclude ? { include: scope } : scope) yield renderer.renderTemplates(templates, ctx, emitter) ctx.pop() ctx.restoreRegister(saved) diff --git a/src/liquid-options.ts b/src/liquid-options.ts index 0603e3e23d..fe98d86c3c 100644 --- a/src/liquid-options.ts +++ b/src/liquid-options.ts @@ -17,6 +17,8 @@ export interface LiquidOptions { layouts?: string | string[]; /** Allow refer to layouts/partials by relative pathname. To avoid arbitrary filesystem read, paths been referenced also need to be within corresponding root, partials, layouts. Defaults to `true`. */ relativeReference?: boolean; + /** Use jekyll style include, pass parameters to `include` variable of current scope. Defaults to `false`. */ + jekyllInclude?: boolean; /** Add a extname (if filepath doesn't include one) before template file lookup. Eg: setting to `".html"` will allow including file by basename. Defaults to `""`. */ extname?: string; /** Whether or not to cache resolved templates. Defaults to `false`. */ @@ -93,6 +95,7 @@ export interface NormalizedFullOptions extends NormalizedOptions { partials: string[]; layouts: string[]; relativeReference: boolean; + jekyllInclude: boolean; extname: string; cache: undefined | Cache>; jsTruthy: boolean; @@ -122,6 +125,7 @@ export const defaultOptions: NormalizedFullOptions = { layouts: ['.'], partials: ['.'], relativeReference: true, + jekyllInclude: false, cache: undefined, extname: '', fs: fs, @@ -161,7 +165,7 @@ export function normalize (options: LiquidOptions): NormalizedFullOptions { else cache = options.cache ? new LRU(1024) : undefined options.cache = cache } - options = { ...defaultOptions, ...options } + options = { ...defaultOptions, ...(options.jekyllInclude ? { dynamicPartials: false } : {}), ...options } if (!options.fs!.dirname && options.relativeReference) { console.warn('[LiquidJS] `fs.dirname` is required for relativeReference, set relativeReference to `false` to suppress this warning, or provide implementation for `fs.dirname`') options.relativeReference = false diff --git a/src/parser/tokenizer.ts b/src/parser/tokenizer.ts index 511d7af62a..f0399c1a38 100644 --- a/src/parser/tokenizer.ts +++ b/src/parser/tokenizer.ts @@ -233,16 +233,16 @@ export class Tokenizer { return new IdentifierToken(this.input, begin, this.p, this.file) } - readHashes () { + readHashes (jekyllStyle?: boolean) { const hashes = [] while (true) { - const hash = this.readHash() + const hash = this.readHash(jekyllStyle) if (!hash) return hashes hashes.push(hash) } } - readHash (): HashToken | undefined { + readHash (jekyllStyle?: boolean): HashToken | undefined { this.skipBlank() if (this.peek() === ',') ++this.p const begin = this.p @@ -251,7 +251,8 @@ export class Tokenizer { let value this.skipBlank() - if (this.peek() === ':') { + const sep = jekyllStyle ? '=' : ':' + if (this.peek() === sep) { ++this.p value = this.readValue() } diff --git a/src/template/tag/hash.ts b/src/template/tag/hash.ts index e7fe3f34a8..27394528eb 100644 --- a/src/template/tag/hash.ts +++ b/src/template/tag/hash.ts @@ -16,9 +16,9 @@ export interface HashValue { */ export class Hash { hash: HashValue = {} - constructor (markup: string) { + constructor (markup: string, jekyllStyle?: boolean) { const tokenizer = new Tokenizer(markup, {}) - for (const hash of tokenizer.readHashes()) { + for (const hash of tokenizer.readHashes(jekyllStyle)) { this.hash[hash.name.content] = hash.value } } diff --git a/test/integration/builtin/tags/include.ts b/test/integration/builtin/tags/include.ts index bae15a24b9..800bb0905f 100644 --- a/test/integration/builtin/tags/include.ts +++ b/test/integration/builtin/tags/include.ts @@ -248,4 +248,44 @@ describe('tags/include', function () { return expect(html).to.equal('Xchild with redY') }) }) + + describe('Jekyll include', function () { + before(function () { + liquid = new Liquid({ + root: '/', + extname: '.html', + jekyllInclude: true + }) + }) + it('should support Jekyll style include', function () { + mock({ + '/current.html': '{% include bar/foo.html content="FOO" %}', + '/bar/foo.html': '{{include.content}}-{{content}}' + }) + const html = liquid.renderFileSync('/current.html') + return expect(html).to.equal('FOO-') + }) + it('should support multiple parameters', function () { + mock({ + '/current.html': '{% include bar/foo.html header="HEADER" content="CONTENT" %}', + '/bar/foo.html': '

{{include.header}}

{{include.content}}' + }) + const html = liquid.renderFileSync('/current.html') + return expect(html).to.equal('

HEADER

CONTENT') + }) + it('should support dynamicPartials=true', function () { + mock({ + '/current.html': '{% include "bar/foo.html" content="FOO" %}', + '/bar/foo.html': '{{include.content}}-{{content}}' + }) + liquid = new Liquid({ + root: '/', + extname: '.html', + jekyllInclude: true, + dynamicPartials: true + }) + const html = liquid.renderFileSync('/current.html') + return expect(html).to.equal('FOO-') + }) + }) })