mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-18 14:00:39 -07:00
feat: relativeReference for render/include/layout, #395
- `relativeReference` is enabled by default, set to `false` to disable
- Referenced files are still constrained within root/partias/layouts
- fix: relative filenames are not constrained (which allows arbitrary filesystem read)
Example Usage:
{% render "../foo/bar.html" %}
Note:
../foo/bar.html' should also be within `partials` (or `root` if `partials` not set)
This commit is contained in:
@@ -9,6 +9,7 @@ export default {
|
||||
const args = token.args
|
||||
const tokenizer = new Tokenizer(args, this.liquid.options.operatorsTrie)
|
||||
this['file'] = this.parseFilePath(tokenizer, this.liquid)
|
||||
this['currentFile'] = token.file
|
||||
|
||||
const begin = tokenizer.p
|
||||
const withStr = tokenizer.readIdentifier()
|
||||
@@ -32,7 +33,7 @@ export default {
|
||||
ctx.setRegister('blockMode', BlockMode.OUTPUT)
|
||||
const scope = yield hash.render(ctx)
|
||||
if (withVar) scope[filepath] = evalToken(withVar, ctx)
|
||||
const templates = yield liquid._parsePartialFile(filepath, ctx.sync)
|
||||
const templates = yield liquid._parsePartialFile(filepath, ctx.sync, this['currentFile'])
|
||||
ctx.push(scope)
|
||||
yield renderer.renderTemplates(templates, ctx, emitter)
|
||||
ctx.pop()
|
||||
|
||||
@@ -9,6 +9,7 @@ export default {
|
||||
parse: function (token: TagToken, remainTokens: TopLevelToken[]) {
|
||||
const tokenizer = new Tokenizer(token.args, this.liquid.options.operatorsTrie)
|
||||
this['file'] = this.parseFilePath(tokenizer, this.liquid)
|
||||
this['currentFile'] = token.file
|
||||
this.hash = new Hash(tokenizer.remaining())
|
||||
this.tpls = this.liquid.parser.parseTokens(remainTokens)
|
||||
},
|
||||
@@ -22,7 +23,7 @@ export default {
|
||||
}
|
||||
const filepath = yield this.renderFilePath(this['file'], ctx, liquid)
|
||||
assert(filepath, () => `illegal filename "${filepath}"`)
|
||||
const templates = yield liquid._parseLayoutFile(filepath, ctx.sync)
|
||||
const templates = yield liquid._parseLayoutFile(filepath, ctx.sync, this['currentFile'])
|
||||
|
||||
// render remaining contents and store rendered results
|
||||
ctx.setRegister('blockMode', BlockMode.STORE)
|
||||
|
||||
@@ -11,7 +11,7 @@ export default {
|
||||
const args = token.args
|
||||
const tokenizer = new Tokenizer(args, this.liquid.options.operatorsTrie)
|
||||
this['file'] = this.parseFilePath(tokenizer, this.liquid)
|
||||
|
||||
this['currentFile'] = token.file
|
||||
while (!tokenizer.end()) {
|
||||
tokenizer.skipBlank()
|
||||
const begin = tokenizer.p
|
||||
@@ -65,12 +65,12 @@ export default {
|
||||
scope['forloop'] = new ForloopDrop(collection.length, value.getText(), alias)
|
||||
for (const item of collection) {
|
||||
scope[alias] = item
|
||||
const templates = yield liquid._parsePartialFile(filepath, childCtx.sync)
|
||||
const templates = yield liquid._parsePartialFile(filepath, childCtx.sync, this['currentFile'])
|
||||
yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
|
||||
scope.forloop.next()
|
||||
}
|
||||
} else {
|
||||
const templates = yield liquid._parsePartialFile(filepath, childCtx.sync)
|
||||
const templates = yield liquid._parsePartialFile(filepath, childCtx.sync, this['currentFile'])
|
||||
yield liquid.renderer.renderTemplates(templates, childCtx, emitter)
|
||||
}
|
||||
}
|
||||
|
||||
+32
-4
@@ -6,6 +6,7 @@ interface LoaderOptions {
|
||||
root: string[];
|
||||
partials: string[];
|
||||
layouts: string[];
|
||||
relativeReference: boolean;
|
||||
}
|
||||
export enum LookupType {
|
||||
Partials = 'partials',
|
||||
@@ -19,19 +20,40 @@ export class Loader {
|
||||
this.options = options
|
||||
}
|
||||
|
||||
public * lookup (file: string, type: LookupType, sync?: boolean) {
|
||||
public * lookup (file: string, type: LookupType, sync?: boolean, currentFile?: string) {
|
||||
const { fs } = this.options
|
||||
const dirs = this.options[type]
|
||||
for (const filepath of this.candidates(file, dirs)) {
|
||||
for (const filepath of this.candidates(file, dirs, currentFile)) {
|
||||
if (sync ? fs.existsSync(filepath) : yield fs.exists(filepath)) return filepath
|
||||
}
|
||||
throw this.lookupError(file, dirs)
|
||||
}
|
||||
|
||||
private * candidates (file: string, dirs: string[]) {
|
||||
public shouldLoadRelative (currentFile: string) {
|
||||
return this.options.relativeReference && this.isRelativePath(currentFile)
|
||||
}
|
||||
|
||||
public isRelativePath (path: string) {
|
||||
return path.startsWith('./') || path.startsWith('../')
|
||||
}
|
||||
|
||||
public * candidates (file: string, dirs: string[], currentFile?: string) {
|
||||
const { fs, extname } = this.options
|
||||
if (this.shouldLoadRelative(file) && currentFile) {
|
||||
const referenced = fs.resolve(this.dirname(currentFile), file, extname)
|
||||
for (const dir of dirs) {
|
||||
if (referenced.startsWith(dir)) {
|
||||
// the relatively referenced file is within one of root dirs
|
||||
yield referenced
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const dir of dirs) {
|
||||
yield fs.resolve(dir, file, extname)
|
||||
const referenced = fs.resolve(dir, file, extname)
|
||||
if (referenced.startsWith(dir)) {
|
||||
yield referenced
|
||||
}
|
||||
}
|
||||
if (fs.fallback !== undefined) {
|
||||
const filepath = fs.fallback(file)
|
||||
@@ -39,6 +61,12 @@ export class Loader {
|
||||
}
|
||||
}
|
||||
|
||||
private dirname (path: string) {
|
||||
const segments = path.split('/')
|
||||
segments.pop()
|
||||
return segments.join('/')
|
||||
}
|
||||
|
||||
private lookupError (file: string, roots: string[]) {
|
||||
const err = new Error('ENOENT') as any
|
||||
err.message = `ENOENT: Failed to lookup "${file}" in "${roots}"`
|
||||
|
||||
@@ -14,6 +14,8 @@ export interface LiquidOptions {
|
||||
partials?: string | string[];
|
||||
/** A directory or an array of directories from where to resolve layout templates. If it's an array, the files are looked up in the order they occur in the array. Defaults to `root` */
|
||||
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;
|
||||
/** 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`. */
|
||||
@@ -74,6 +76,7 @@ export interface NormalizedFullOptions extends NormalizedOptions {
|
||||
root: string[];
|
||||
partials: string[];
|
||||
layouts: string[];
|
||||
relativeReference: boolean;
|
||||
extname: string;
|
||||
cache: undefined | Cache<Template[]>;
|
||||
jsTruthy: boolean;
|
||||
@@ -102,6 +105,7 @@ export const defaultOptions: NormalizedFullOptions = {
|
||||
root: ['.'],
|
||||
layouts: ['.'],
|
||||
partials: ['.'],
|
||||
relativeReference: true,
|
||||
cache: undefined,
|
||||
extname: '',
|
||||
fs: fs,
|
||||
|
||||
+4
-4
@@ -64,11 +64,11 @@ export class Liquid {
|
||||
return toValue(this._parseAndRender(html, scope, true))
|
||||
}
|
||||
|
||||
public _parsePartialFile (file: string, sync?: boolean) {
|
||||
return this.parser.parseFile(file, sync, LookupType.Partials)
|
||||
public _parsePartialFile (file: string, sync?: boolean, currentFile?: string) {
|
||||
return this.parser.parseFile(file, sync, LookupType.Partials, currentFile)
|
||||
}
|
||||
public _parseLayoutFile (file: string, sync?: boolean) {
|
||||
return this.parser.parseFile(file, sync, LookupType.Layouts)
|
||||
public _parseLayoutFile (file: string, sync?: boolean, currentFile?: string) {
|
||||
return this.parser.parseFile(file, sync, LookupType.Layouts, currentFile)
|
||||
}
|
||||
public async parseFile (file: string): Promise<Template[]> {
|
||||
return toPromise(this.parser.parseFile(file, false))
|
||||
|
||||
@@ -13,7 +13,7 @@ import { Loader, LookupType } from '../fs/loader'
|
||||
import { FS } from '../fs/fs'
|
||||
|
||||
export default class Parser {
|
||||
public parseFile: (file: string, sync?: boolean, type?: LookupType) => Iterator<Template[]>
|
||||
public parseFile: (file: string, sync?: boolean, type?: LookupType, currentFile?: string) => Iterator<Template[]>
|
||||
|
||||
private liquid: Liquid
|
||||
private fs: FS
|
||||
@@ -56,17 +56,19 @@ export default class Parser {
|
||||
public parseStream (tokens: TopLevelToken[]) {
|
||||
return new ParseStream(tokens, (token, tokens) => this.parseToken(token, tokens))
|
||||
}
|
||||
private * _parseFileCached (file: string, sync?: boolean, type: LookupType = LookupType.Root) {
|
||||
const key = type + ':' + file
|
||||
private * _parseFileCached (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string) {
|
||||
const key = this.loader.shouldLoadRelative(file)
|
||||
? currentFile + ',' + file
|
||||
: type + ':' + file
|
||||
let templates = yield this.cache!.read(key)
|
||||
if (templates) return templates
|
||||
|
||||
templates = yield this._parseFile(file, sync)
|
||||
templates = yield this._parseFile(file, sync, type, currentFile)
|
||||
this.cache!.write(key, templates)
|
||||
return templates
|
||||
}
|
||||
private * _parseFile (file: string, sync?: boolean, type: LookupType = LookupType.Root) {
|
||||
const filepath = yield this.loader.lookup(file, type, sync)
|
||||
private * _parseFile (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string) {
|
||||
const filepath = yield this.loader.lookup(file, type, sync, currentFile)
|
||||
return this.liquid.parse(sync ? this.fs.readFileSync(filepath) : yield this.fs.readFile(filepath), filepath)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user