diff --git a/rollup.config.mjs b/rollup.config.mjs index 328505fdc..ca7261f10 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -23,7 +23,8 @@ const tsconfig = (target) => ({ compilerOptions: { target, module: 'ES2015', - rootDir: 'src' + rootDir: 'src', + downlevelIteration: true } } }) diff --git a/src/context/context.ts b/src/context/context.ts index 083a17cfe..b8951056f 100644 --- a/src/context/context.ts +++ b/src/context/context.ts @@ -13,7 +13,7 @@ export class Context { * for tags like `{% capture %}` `{% assign %}` to operate */ private scopes: Scope[] = [createScope()] - private registers = {} + private registers: Record = {} /** * user passed in scope * `{% increment %}`, `{% decrement %}` changes this scope, diff --git a/src/drop/drop.ts b/src/drop/drop.ts index c62768dc2..f83f5576e 100644 --- a/src/drop/drop.ts +++ b/src/drop/drop.ts @@ -1,6 +1,7 @@ import { Context } from '../context' export abstract class Drop { + [key: string]: any public liquidMethodMissing (key: string | number, context: Context): Promise | any { return undefined } diff --git a/src/filters/html.ts b/src/filters/html.ts index 21cb8c4a9..807c42373 100644 --- a/src/filters/html.ts +++ b/src/filters/html.ts @@ -1,14 +1,14 @@ import { FilterImpl } from '../template' import { stringify } from '../util/underscore' -const escapeMap = { +const escapeMap: Record = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' } -const unescapeMap = { +const unescapeMap: Record = { '&': '&', '<': '<', '>': '>', diff --git a/src/parser/tokenizer.ts b/src/parser/tokenizer.ts index 0c2d86df7..be8ac136b 100644 --- a/src/parser/tokenizer.ts +++ b/src/parser/tokenizer.ts @@ -1,6 +1,6 @@ import { FilteredValueToken, TagToken, HTMLToken, HashToken, QuotedToken, LiquidTagToken, OutputToken, ValueToken, Token, RangeToken, FilterToken, TopLevelToken, PropertyAccessToken, OperatorToken, LiteralToken, IdentifierToken, NumberToken } from '../tokens' import { OperatorHandler } from '../render/operator' -import { TrieNode, LiteralValue, Trie, createTrie, ellipsis, literalValues, TokenizationError, TYPES, QUOTE, BLANK, NUMBER, SIGN, isWord, isString } from '../util' +import { LiteralValue, Trie, createTrie, ellipsis, literalValues, TokenizationError, TYPES, QUOTE, BLANK, NUMBER, SIGN, isWord, isString } from '../util' import { Operators, Expression } from '../render' import { NormalizedFullOptions, defaultOptions } from '../liquid-options' import { FilterArg } from './filter-arg' @@ -51,11 +51,11 @@ export class Tokenizer { return new OperatorToken(this.input, this.p, (this.p = end), this.file) } matchTrie (trie: Trie) { - let node: TrieNode = trie + let node: Trie = trie let i = this.p - let info - while (node[this.input[i]] && i < this.N) { - node = node[this.input[i++]] + let info: Trie | undefined + while ((node as Trie)[this.input[i]] && i < this.N) { + node = (node as Trie)[this.input[i++]] as Trie if (node['end']) info = node } if (!info) return -1 diff --git a/src/render/string.ts b/src/render/string.ts index c1e8f1a3e..91b726367 100644 --- a/src/render/string.ts +++ b/src/render/string.ts @@ -1,6 +1,6 @@ const rHex = /[\da-fA-F]/ const rOct = /[0-7]/ -const escapeChar = { +const escapeChar: Record = { b: '\b', f: '\f', n: '\n', diff --git a/src/tags/for.ts b/src/tags/for.ts index fec0b0603..198750116 100644 --- a/src/tags/for.ts +++ b/src/tags/for.ts @@ -52,7 +52,7 @@ export default class extends Tag { const continueKey = 'continue-' + this.variable + '-' + this.collection.getText() ctx.push(createScope({ continue: ctx.getRegister(continueKey, {}) })) - const hash = yield this.hash.render(ctx) + const hash = (yield this.hash.render(ctx)) as Record ctx.pop() const modifiers = this.liquid.options.orderedFilterParameters diff --git a/src/tags/include.ts b/src/tags/include.ts index 0db1ee749..a00dda4e2 100644 --- a/src/tags/include.ts +++ b/src/tags/include.ts @@ -3,16 +3,18 @@ import { BlockMode, createScope, Scope } from '../context' import { Parser } from '../parser' import { Argument, Arguments, PartialScope } from '../template' import { isString, isValueToken } from '../util' -import { parseFilePath, renderFilePath } from './render' +import { parseFilePath, renderFilePath, ParsedFileName } from './render' export default class extends Tag { + private file: ParsedFileName + private currentFile?: string private withVar?: ValueToken private hash: Hash constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid, parser: Parser) { super(token, remainTokens, liquid) const { tokenizer } = token - this['file'] = parseFilePath(tokenizer, this.liquid, parser) - this['currentFile'] = token.file + this.file = parseFilePath(tokenizer, this.liquid, parser) + this.currentFile = token.file const begin = tokenizer.p const withStr = tokenizer.readIdentifier() @@ -28,7 +30,7 @@ export default class extends Tag { * render (ctx: Context, emitter: Emitter): Generator { const { liquid, hash, withVar } = this const { renderer } = liquid - const filepath = (yield renderFilePath(this['file'], ctx, liquid)) as string + const filepath = (yield renderFilePath(this.file, ctx, liquid)) as string assert(filepath, () => `illegal file path "${filepath}"`) const saved = ctx.saveRegister('blocks', 'blockMode') @@ -36,7 +38,7 @@ export default class extends Tag { ctx.setRegister('blockMode', BlockMode.OUTPUT) const scope = createScope((yield hash.render(ctx)) as Scope) if (withVar) scope[filepath] = yield evalToken(withVar, ctx) - const templates = (yield liquid._parsePartialFile(filepath, ctx.sync, this['currentFile'])) as Template[] + const templates = (yield liquid._parsePartialFile(filepath, ctx.sync, this.currentFile)) as Template[] ctx.push(ctx.opts.jekyllInclude ? createScope({ include: scope }) : scope) yield renderer.renderTemplates(templates, ctx, emitter) ctx.pop() @@ -44,14 +46,14 @@ export default class extends Tag { } public * children (partials: boolean, sync: boolean): Generator { - if (partials && isString(this['file'])) { - return (yield this.liquid._parsePartialFile(this['file'], sync, this['currentFile'])) as Template[] + if (partials && isString(this.file)) { + return (yield this.liquid._parsePartialFile(this.file, sync, this.currentFile)) as Template[] } return [] } public partialScope (): PartialScope | undefined { - if (isString(this['file'])) { + if (isString(this.file)) { let names: Array if (this.liquid.options.jekyllInclude) { @@ -59,19 +61,19 @@ export default class extends Tag { } else { names = Object.keys(this.hash.hash) if (this.withVar) { - names.push([this['file'], this.withVar]) + names.push([this.file, this.withVar]) } } - return { name: this['file'], isolated: false, scope: names } + return { name: this.file, isolated: false, scope: names } } } public * arguments (): Arguments { yield * Object.values(this.hash.hash).filter(isValueToken) - if (isValueToken(this['file'])) { - yield this['file'] + if (isValueToken(this.file)) { + yield this.file } if (isValueToken(this.withVar)) { diff --git a/src/tags/layout.ts b/src/tags/layout.ts index ab2b3b539..156ab0ee4 100644 --- a/src/tags/layout.ts +++ b/src/tags/layout.ts @@ -10,10 +10,11 @@ export default class extends Tag { args: Hash templates: Template[] file?: ParsedFileName + private currentFile?: string constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid, parser: Parser) { super(token, remainTokens, liquid) this.file = parseFilePath(this.tokenizer, this.liquid, parser) - this['currentFile'] = token.file + this.currentFile = token.file this.args = new Hash(this.tokenizer, liquid.options.keyValueSeparator) this.templates = parser.parseTokens(remainTokens) } @@ -27,7 +28,7 @@ export default class extends Tag { } const filepath = (yield renderFilePath(this.file, ctx, liquid)) as string assert(filepath, () => `illegal file path "${filepath}"`) - const templates = (yield liquid._parseLayoutFile(filepath, ctx.sync, this['currentFile'])) as Template[] + const templates = (yield liquid._parseLayoutFile(filepath, ctx.sync, this.currentFile)) as Template[] // render remaining contents and store rendered results ctx.setRegister('blockMode', BlockMode.STORE) @@ -48,7 +49,7 @@ export default class extends Tag { const templates = this.templates.slice() if (partials && isString(this.file)) { - templates.push(...(yield this.liquid._parsePartialFile(this.file, true, this['currentFile'])) as Template[]) + templates.push(...(yield this.liquid._parsePartialFile(this.file, true, this.currentFile)) as Template[]) } return templates diff --git a/src/tags/render.ts b/src/tags/render.ts index 123e73a93..bf86a28cb 100644 --- a/src/tags/render.ts +++ b/src/tags/render.ts @@ -1,16 +1,20 @@ import { __assign } from 'tslib' import { ForloopDrop } from '../drop' import { isString, isValueToken, toEnumerable } from '../util' -import { TopLevelToken, assert, Liquid, Token, Template, evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, Tag } from '..' +import { TopLevelToken, assert, Liquid, Token, ValueToken, Template, evalQuotedToken, TypeGuards, Tokenizer, evalToken, Hash, Emitter, TagToken, Context, Tag } from '..' import { Parser } from '../parser' import { Argument, Arguments, PartialScope } from '../template' export type ParsedFileName = Template[] | Token | string | undefined +type RenderBinding = { value: ValueToken; alias?: string } + export default class extends Tag { private file: ParsedFileName private currentFile?: string private hash: Hash + private with?: RenderBinding + private forBinding?: RenderBinding constructor (token: TagToken, remainTokens: TopLevelToken[], liquid: Liquid, parser: Parser) { super(token, remainTokens, liquid) const tokenizer = this.tokenizer @@ -33,7 +37,9 @@ export default class extends Tag { if (asStr.content === 'as') alias = tokenizer.readIdentifier() else tokenizer.p = beforeAs - this[keyword.content] = { value, alias: alias && alias.content } + const binding: RenderBinding = { value, alias: alias && alias.content } + if (keyword.content === 'with') this.with = binding + else this.forBinding = binding tokenizer.skipBlank() if (tokenizer.peek() === ',') tokenizer.advance() continue // matched! @@ -50,46 +56,46 @@ export default class extends Tag { } * render (ctx: Context, emitter: Emitter): Generator { const { liquid, hash } = this - const filepath = (yield renderFilePath(this['file'], ctx, liquid)) as string + const filepath = (yield renderFilePath(this.file, ctx, liquid)) as string assert(filepath, () => `illegal file path "${filepath}"`) const childCtx = ctx.spawn() const scope = childCtx.bottom() __assign(scope, yield hash.render(ctx)) - if (this['with']) { - const { value, alias } = this['with'] + if (this.with) { + const { value, alias } = this.with scope[alias || filepath] = yield evalToken(value, ctx) } - if (this['for']) { - const { value, alias } = this['for'] + if (this.forBinding) { + const { value, alias } = this.forBinding const collection = toEnumerable(yield evalToken(value, ctx)) - scope['forloop'] = new ForloopDrop(collection.length, value.getText(), alias) + scope['forloop'] = new ForloopDrop(collection.length, value.getText(), alias as string) for (const item of collection) { - scope[alias] = item - const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this['currentFile'])) as Template[] + scope[alias as string] = item + const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this.currentFile)) as Template[] yield liquid.renderer.renderTemplates(templates, childCtx, emitter) scope['forloop'].next() } } else { - const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this['currentFile'])) as Template[] + const templates = (yield liquid._parsePartialFile(filepath, childCtx.sync, this.currentFile)) as Template[] yield liquid.renderer.renderTemplates(templates, childCtx, emitter) } } public * children (partials: boolean, sync: boolean): Generator { - if (partials && isString(this['file'])) { - return (yield this.liquid._parsePartialFile(this['file'], sync, this['currentFile'])) as Template[] + if (partials && isString(this.file)) { + return (yield this.liquid._parsePartialFile(this.file, sync, this.currentFile)) as Template[] } return [] } public partialScope (): PartialScope | undefined { - if (isString(this['file'])) { + if (isString(this.file)) { const names: Array = Object.keys(this.hash.hash) - if (this['with']) { - const { value, alias } = this['with'] + if (this.with) { + const { value, alias } = this.with if (isString(alias)) { names.push([alias, value]) } else if (isString(this.file)) { @@ -97,8 +103,8 @@ export default class extends Tag { } } - if (this['for']) { - const { value, alias } = this['for'] + if (this.forBinding) { + const { value, alias } = this.forBinding if (isString(alias)) { names.push([alias, value]) } else if (isString(this.file)) { @@ -106,7 +112,7 @@ export default class extends Tag { } } - return { name: this['file'], isolated: true, scope: names } + return { name: this.file, isolated: true, scope: names } } } @@ -117,15 +123,15 @@ export default class extends Tag { } } - if (this['with']) { - const { value } = this['with'] + if (this.with) { + const { value } = this.with if (isValueToken(value)) { yield value } } - if (this['for']) { - const { value } = this['for'] + if (this.forBinding) { + const { value } = this.forBinding if (isValueToken(value)) { yield value } diff --git a/src/template/hash.ts b/src/template/hash.ts index 4899a8045..4ef67c1a2 100644 --- a/src/template/hash.ts +++ b/src/template/hash.ts @@ -24,7 +24,7 @@ export class Hash { } * render (ctx: Context): Generator, unknown> { - const hash = {} + const hash: Record = {} for (const key of Object.keys(this.hash)) { hash[key] = this.hash[key] === undefined ? true : yield evalToken(this.hash[key], ctx) } diff --git a/src/tokens/literal-token.ts b/src/tokens/literal-token.ts index 2852e2e02..ce60fdfc5 100644 --- a/src/tokens/literal-token.ts +++ b/src/tokens/literal-token.ts @@ -1,10 +1,10 @@ import { Token } from './token' import { TokenKind } from '../parser' -import { literalValues, LiteralValue } from '../util' +import { literalValues, LiteralValue, LiteralKey } from '../util' export class LiteralToken extends Token { public content: LiteralValue - public literal: string + public literal: LiteralKey public constructor ( public input: string, public begin: number, @@ -12,7 +12,7 @@ export class LiteralToken extends Token { public file?: string ) { super(TokenKind.Literal, input, begin, end, file) - this.literal = this.getText() + this.literal = this.getText() as LiteralKey this.content = literalValues[this.literal] } } diff --git a/src/tokens/operator-token.ts b/src/tokens/operator-token.ts index ae6724e22..358878278 100644 --- a/src/tokens/operator-token.ts +++ b/src/tokens/operator-token.ts @@ -32,8 +32,10 @@ export const operatorTypes = { 'or': OperatorType.Binary } +export type OperatorKey = keyof typeof operatorPrecedences + export class OperatorToken extends Token { - public operator: string + public operator: OperatorKey public constructor ( public input: string, public begin: number, @@ -41,10 +43,9 @@ export class OperatorToken extends Token { public file?: string ) { super(TokenKind.Operator, input, begin, end, file) - this.operator = this.getText() + this.operator = this.getText() as OperatorKey } getPrecedence () { - const key = this.getText() - return key in operatorPrecedences ? operatorPrecedences[key] : 1 + return this.operator in operatorPrecedences ? operatorPrecedences[this.operator] : 1 } } diff --git a/src/util/async.ts b/src/util/async.ts index c190cd860..137753ded 100644 --- a/src/util/async.ts +++ b/src/util/async.ts @@ -18,10 +18,10 @@ export async function toPromise (val: Generator | Promis if (!isIterator(val)) return val let value: unknown let done = false - let next = 'next' + let next: 'next' | 'throw' = 'next' do { const state = val[next](value) - done = state.done + done = !!state.done value = state.value next = 'next' try { @@ -40,10 +40,10 @@ export function toValueSync (val: Generator | T): T { if (!isIterator(val)) return val let value: any let done = false - let next = 'next' + let next: 'next' | 'throw' = 'next' do { const state = val[next](value) - done = state.done + done = !!state.done value = state.value next = 'next' if (isIterator(value)) { diff --git a/src/util/error.ts b/src/util/error.ts index 8fe76061d..3b5534bab 100644 --- a/src/util/error.ts +++ b/src/util/error.ts @@ -29,7 +29,7 @@ export abstract class LiquidError extends Error { if (this.originalError) this.stack += '\nFrom ' + this.originalError.stack } static is (obj: unknown): obj is LiquidError { - return obj?.[TRAIT] === 'LiquidError' + return (obj as Record | null | undefined)?.[TRAIT] === 'LiquidError' } } diff --git a/src/util/operator-trie.ts b/src/util/operator-trie.ts index e09c90ef0..f94d7138a 100644 --- a/src/util/operator-trie.ts +++ b/src/util/operator-trie.ts @@ -4,22 +4,16 @@ interface TrieInput { [key: string]: T } -interface TrieLeafNode { - data: T; - end: true; - needBoundary?: true; -} - -export interface Trie { - [key: string]: Trie | TrieLeafNode; -} - -export type TrieNode = Trie | TrieLeafNode +export type Trie = { + data?: T + end?: true + needBoundary?: true +} & Record export function createTrie (input: TrieInput): Trie { const trie: Trie = {} for (const [name, data] of Object.entries(input)) { - let node: Trie | TrieLeafNode = trie + let node = trie for (let i = 0; i < name.length; i++) { const c = name[i] diff --git a/src/util/strftime.ts b/src/util/strftime.ts index e6ac04ebf..8bf8b09d1 100644 --- a/src/util/strftime.ts +++ b/src/util/strftime.ts @@ -4,7 +4,7 @@ import type { Limiter } from './limiter' const rFormat = /%([-_0^#:]+)?(\d+)?([EO])?(.)/ interface FormatOptions { - flags: object; + flags: Record; width?: string; modifier?: string; memoryLimit?: Pick; @@ -50,7 +50,7 @@ function century (d: LiquidDate) { } // default to 0 -const padWidths = { +const padWidths: Record = { d: 2, e: 2, H: 2, @@ -77,7 +77,9 @@ function getTimezoneOffset (d: LiquidDate, opts: FormatOptions) { (opts.flags[':'] ? ':' : '') + padStart(m, 2, '0') } -const formatCodes = { +type FormatCodeHandler = (d: LiquidDate, opts: FormatOptions) => unknown + +const formatCodes: Record = { a: (d: LiquidDate) => d.getShortWeekdayName(), A: (d: LiquidDate) => d.getLongWeekdayName(), b: (d: LiquidDate) => d.getShortMonthName(), @@ -118,8 +120,8 @@ const formatCodes = { 't': () => '\t', 'n': () => '\n', '%': () => '%' -}; -(formatCodes as any).h = formatCodes.b +} +formatCodes.h = formatCodes.b export function strftime (d: LiquidDate, formatStr: string, memoryLimit?: Pick) { let output = '' @@ -137,11 +139,11 @@ function format (d: LiquidDate, match: RegExpExecArray, memoryLimit?: Pick = {} for (const flag of flagStr) flags[flag] = true let ret = String(convert(d, { flags, width, modifier, memoryLimit })) let padChar = padSpaceChars.has(conversion) ? ' ' : '0' - let padWidth = width || padWidths[conversion] || 0 + let padWidth = Number(width) || padWidths[conversion] || 0 if (flags['^']) ret = ret.toUpperCase() else if (flags['#']) ret = changeCase(ret) if (flags['_']) padChar = ' ' diff --git a/test/integration/liquid/liquid.spec.ts b/test/integration/liquid/liquid.spec.ts index 918b1aabe..e229940a2 100644 --- a/test/integration/liquid/liquid.spec.ts +++ b/test/integration/liquid/liquid.spec.ts @@ -1,6 +1,7 @@ import { Liquid, Context, isFalsy } from '../../../src' import { mock, restore } from '../../stub/mockfs' import { drainStream } from '../../stub/stream' +import { resolve } from 'path' describe('Liquid', function () { describe('#plugin()', function () { @@ -144,7 +145,7 @@ describe('Liquid', function () { }) it('should fallback to require.resolve in Node.js', async function () { const engine = new Liquid({ - root: [process.cwd()], + root: [resolve(__dirname, '../../..')], extname: '.html' }) const tpls = await engine.parseFileSync('jest') diff --git a/tsconfig.json b/tsconfig.json index 258f536b4..0646fbcfe 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,9 +9,7 @@ "skipLibCheck": true, "allowSyntheticDefaultImports": true, "resolveJsonModule": true, - "downlevelIteration": true, - "strict": true, - "suppressImplicitAnyIndexErrors": true + "strict": true }, "all": true, "exclude": [ "node_modules", "dist", "demo", "test" ]