mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-15 04:10:40 -07:00
feat: support in-memory template mapping, inspired by @jg-rp #714
This commit is contained in:
@@ -100,6 +100,23 @@ var engine = new Liquid({
|
|||||||
|
|
||||||
{% note warn Path Traversal Vulnerability %}The default value of <code>contains()</code> always returns true. That means when specifying an abstract file system, you'll need to provide a proper <code>contains()</code> to avoid expose such vulnerabilities.{% endnote %}
|
{% note warn Path Traversal Vulnerability %}The default value of <code>contains()</code> always returns true. That means when specifying an abstract file system, you'll need to provide a proper <code>contains()</code> to avoid expose such vulnerabilities.{% endnote %}
|
||||||
|
|
||||||
|
## In-memory Template
|
||||||
|
|
||||||
|
To facilitate rendering w/o files, there's a `templates` option to specify a mapping of filenames and their content. LiquidJS will read templates from the mapping.
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
const engine = new Liquid({
|
||||||
|
templates: {
|
||||||
|
'views/entry': 'header {% include "../partials/footer" %}',
|
||||||
|
'partials/footer': 'footer'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
engine.renderFileSync('views/entry'))
|
||||||
|
// Result: 'header footer'
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that file system options like `root`, `layouts`, `partials`, `relativeReference` will be ignored when `templates` is specified.
|
||||||
|
|
||||||
[fs]: /api/interfaces/LiquidOptions.html#fs
|
[fs]: /api/interfaces/LiquidOptions.html#fs
|
||||||
[ifs]: /api/interfaces/FS.html
|
[ifs]: /api/interfaces/FS.html
|
||||||
[fs-node]: https://github.com/harttle/liquidjs/blob/master/src/fs/fs-impl.ts
|
[fs-node]: https://github.com/harttle/liquidjs/blob/master/src/fs/fs-impl.ts
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { MapFS } from './map-fs'
|
||||||
|
|
||||||
|
describe('MapFS', () => {
|
||||||
|
const fs = new MapFS({})
|
||||||
|
it('should resolve relative file paths', () => {
|
||||||
|
expect(fs.resolve('foo/bar', 'coo', '')).toEqual('foo/bar/coo')
|
||||||
|
})
|
||||||
|
it('should resolve to parent', () => {
|
||||||
|
expect(fs.resolve('foo/bar', '../coo', '')).toEqual('foo/coo')
|
||||||
|
})
|
||||||
|
it('should resolve to root', () => {
|
||||||
|
expect(fs.resolve('foo/bar', '../../coo', '')).toEqual('coo')
|
||||||
|
})
|
||||||
|
it('should resolve exceeding root', () => {
|
||||||
|
expect(fs.resolve('foo/bar', '../../../coo', '')).toEqual('coo')
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { isNil } from '../util'
|
||||||
|
|
||||||
|
export class MapFS {
|
||||||
|
constructor (private mapping: {[key: string]: string}) {}
|
||||||
|
|
||||||
|
public sep = '/'
|
||||||
|
|
||||||
|
async exists (filepath: string) {
|
||||||
|
return this.existsSync(filepath)
|
||||||
|
}
|
||||||
|
|
||||||
|
existsSync (filepath: string) {
|
||||||
|
return !isNil(this.mapping[filepath])
|
||||||
|
}
|
||||||
|
|
||||||
|
async readFile (filepath: string) {
|
||||||
|
return this.readFileSync(filepath)
|
||||||
|
}
|
||||||
|
|
||||||
|
readFileSync (filepath: string) {
|
||||||
|
const content = this.mapping[filepath]
|
||||||
|
if (isNil(content)) throw new Error(`ENOENT: ${filepath}`)
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
dirname (filepath: string) {
|
||||||
|
const segments = filepath.split(this.sep)
|
||||||
|
segments.pop()
|
||||||
|
return segments.join(this.sep)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve (dir: string, file: string, ext: string) {
|
||||||
|
file += ext
|
||||||
|
if (dir === '.') return file
|
||||||
|
const segments = dir.split(this.sep)
|
||||||
|
for (const segment of file.split(this.sep)) {
|
||||||
|
if (segment === '.' || segment === '') continue
|
||||||
|
else if (segment === '..') segments.pop()
|
||||||
|
else segments.push(segment)
|
||||||
|
}
|
||||||
|
return segments.join(this.sep)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import * as fs from './fs/fs-impl'
|
|||||||
import { defaultOperators, Operators } from './render'
|
import { defaultOperators, Operators } from './render'
|
||||||
import misc from './filters/misc'
|
import misc from './filters/misc'
|
||||||
import { escape } from './filters/html'
|
import { escape } from './filters/html'
|
||||||
|
import { MapFS } from './fs/map-fs'
|
||||||
|
|
||||||
type OutputEscape = (value: any) => string
|
type OutputEscape = (value: any) => string
|
||||||
type OutputEscapeOption = 'escape' | 'json' | OutputEscape
|
type OutputEscapeOption = 'escape' | 'json' | OutputEscape
|
||||||
@@ -64,6 +65,8 @@ export interface LiquidOptions {
|
|||||||
greedy?: boolean;
|
greedy?: boolean;
|
||||||
/** `fs` is used to override the default file-system module with a custom implementation. */
|
/** `fs` is used to override the default file-system module with a custom implementation. */
|
||||||
fs?: FS;
|
fs?: FS;
|
||||||
|
/** Render from in-memory `templates` mapping instead of file system. File system related options like `fs`, 'root', and `relativeReference` will be ignored when `templates` is specified. */
|
||||||
|
templates?: {[key: string]: string};
|
||||||
/** the global scope passed down to all partial and layout templates, i.e. templates included by `include`, `layout` and `render` tags. */
|
/** the global scope passed down to all partial and layout templates, i.e. templates included by `include`, `layout` and `render` tags. */
|
||||||
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`. */
|
||||||
@@ -190,6 +193,11 @@ export function normalize (options: LiquidOptions): NormalizedFullOptions {
|
|||||||
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)
|
options.outputEscape = options.outputEscape && getOutputEscapeFunction(options.outputEscape)
|
||||||
|
if (options.templates) {
|
||||||
|
options.fs = new MapFS(options.templates)
|
||||||
|
options.relativeReference = true
|
||||||
|
options.root = options.partials = options.layouts = '.'
|
||||||
|
}
|
||||||
return options as NormalizedFullOptions
|
return options as NormalizedFullOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,4 +49,34 @@ describe('LiquidOptions#fs', function () {
|
|||||||
} as any)
|
} as any)
|
||||||
expect(engine.options.relativeReference).toBe(false)
|
expect(engine.options.relativeReference).toBe(false)
|
||||||
})
|
})
|
||||||
|
it('should render from in-memory templates', () => {
|
||||||
|
const engine = new Liquid({
|
||||||
|
templates: {
|
||||||
|
entry: '{% layout "main" %}entry',
|
||||||
|
main: 'header {% block %}{% endblock %} footer'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(engine.renderFileSync('entry')).toEqual('header entry footer')
|
||||||
|
})
|
||||||
|
it('should render relative in-memory templates', () => {
|
||||||
|
const engine = new Liquid({
|
||||||
|
templates: {
|
||||||
|
'views/entry': 'header {% include "../partials/footer" %}',
|
||||||
|
'partials/footer': 'footer'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(engine.renderFileSync('views/entry')).toEqual('header footer')
|
||||||
|
})
|
||||||
|
it('should ignore root/layouts/partials', () => {
|
||||||
|
const engine = new Liquid({
|
||||||
|
root: '/foo/bar/',
|
||||||
|
layouts: '/foo/bar/',
|
||||||
|
partials: '/foo/bar/',
|
||||||
|
templates: {
|
||||||
|
entry: '{% layout "main" %}entry',
|
||||||
|
main: 'header {% block %}{% endblock %} footer'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
expect(engine.renderFileSync('entry')).toEqual('header entry footer')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user