-
-
Notifications
You must be signed in to change notification settings - Fork 238
/
Copy pathparser.ts
87 lines (83 loc) · 3.63 KB
/
parser.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
import { Limiter, toPromise, assert, isTagToken, isOutputToken, ParseError } from '../util'
import { Tokenizer } from './tokenizer'
import { ParseStream } from './parse-stream'
import { TopLevelToken, OutputToken } from '../tokens'
import { Template, Output, HTML } from '../template'
import { LiquidCache } from '../cache'
import { FS, Loader, LookupType } from '../fs'
import { LiquidError, LiquidErrors } from '../util/error'
import type { Liquid } from '../liquid'
export class Parser {
public parseFile: (file: string, sync?: boolean, type?: LookupType, currentFile?: string) => Generator<unknown, Template[], Template[] | string>
private liquid: Liquid
private fs: FS
private cache?: LiquidCache
private loader: Loader
private parseLimit: Limiter
public constructor (liquid: Liquid) {
this.liquid = liquid
this.cache = this.liquid.options.cache
this.fs = this.liquid.options.fs
this.parseFile = this.cache ? this._parseFileCached : this._parseFile
this.loader = new Loader(this.liquid.options)
this.parseLimit = new Limiter('parse length', liquid.options.parseLimit)
}
public parse (html: string, filepath?: string): Template[] {
html = String(html)
this.parseLimit.use(html.length)
const tokenizer = new Tokenizer(html, this.liquid.options.operators, filepath)
const tokens = tokenizer.readTopLevelTokens(this.liquid.options)
return this.parseTokens(tokens)
}
public parseTokens (tokens: TopLevelToken[]) {
let token
const templates: Template[] = []
const errors: LiquidError[] = []
while ((token = tokens.shift())) {
try {
templates.push(this.parseToken(token, tokens))
} catch (err) {
if (this.liquid.options.catchAllErrors) errors.push(err as LiquidError)
else throw err
}
}
if (errors.length) throw new LiquidErrors(errors)
return templates
}
public parseToken (token: TopLevelToken, remainTokens: TopLevelToken[]) {
try {
if (isTagToken(token)) {
const TagClass = this.liquid.tags[token.name]
assert(TagClass, `tag "${token.name}" not found`)
return new TagClass(token, remainTokens, this.liquid, this)
}
if (isOutputToken(token)) {
return new Output(token as OutputToken, this.liquid)
}
return new HTML(token)
} catch (e) {
if (LiquidError.is(e)) throw e
throw new ParseError(e as Error, token)
}
}
public parseStream (tokens: TopLevelToken[]) {
return new ParseStream(tokens, (token, tokens) => this.parseToken(token, tokens))
}
private * _parseFileCached (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string): Generator<unknown, Template[], Template[]> {
const cache = this.cache!
const key = this.loader.shouldLoadRelative(file) ? currentFile + ',' + file : type + ':' + file
const tpls = yield cache.read(key)
if (tpls) return tpls
const task = this._parseFile(file, sync, type, currentFile)
// sync mode: exec the task and cache the result
// async mode: cache the task before exec
const taskOrTpl = sync ? yield task : toPromise(task)
cache.write(key, taskOrTpl as any)
// note: concurrent tasks will be reused, cache for failed task is removed until its end
try { return yield taskOrTpl } catch (err) { cache.remove(key); throw err }
}
private * _parseFile (file: string, sync?: boolean, type: LookupType = LookupType.Root, currentFile?: string): Generator<unknown, Template[], string> {
const filepath = yield this.loader.lookup(file, type, sync, currentFile)
return this.parse(sync ? this.fs.readFileSync(filepath) : yield this.fs.readFile(filepath), filepath)
}
}