import * as _ from './underscore' import { Token } from '../tokens/token' import { Template } from '../template/template' /** * targeting ES5, extends Error won't create a proper prototype chain, need a trait to keep track of classes */ const TRAIT = '__liquidClass__' export abstract class LiquidError extends Error { public token!: Token public context = '' public originalError?: Error public constructor (err: Error | string, token: Token) { /** * note: for ES5 targeting, `this` will be replaced by return value of Error(), * thus everything on `this` will be lost, avoid calling `LiquidError` methods here */ super(typeof err === 'string' ? err : err.message) if (typeof err !== 'string') Object.defineProperty(this, 'originalError', { value: err, enumerable: false }) Object.defineProperty(this, 'token', { value: token, enumerable: false }) Object.defineProperty(this, TRAIT, { value: 'LiquidError', enumerable: false }) } protected update () { Object.defineProperty(this, 'context', { value: mkContext(this.token), enumerable: false }) this.message = mkMessage(this.message, this.token) this.stack = this.message + '\n' + this.context + '\n' + this.stack if (this.originalError) this.stack += '\nFrom ' + this.originalError.stack } static is (obj: unknown): obj is LiquidError { return obj?.[TRAIT] === 'LiquidError' } } export class TokenizationError extends LiquidError { public constructor (message: string, token: Token) { super(message, token) this.name = 'TokenizationError' super.update() } } export class ParseError extends LiquidError { public constructor (err: Error, token: Token) { super(err, token) this.name = 'ParseError' this.message = err.message super.update() } } export class RenderError extends LiquidError { public constructor (err: Error, tpl: Template) { super(err, tpl.token) this.name = 'RenderError' this.message = err.message super.update() } public static is (obj: any): obj is RenderError { return obj.name === 'RenderError' } } export class LiquidErrors extends LiquidError { public constructor (public errors: RenderError[]) { super(errors[0], errors[0].token) this.name = 'LiquidErrors' const s = errors.length > 1 ? 's' : '' this.message = `${errors.length} error${s} found` super.update() } public static is (obj: any): obj is LiquidErrors { return obj.name === 'LiquidErrors' } } export class UndefinedVariableError extends LiquidError { public constructor (err: Error, token: Token) { super(err, token) this.name = 'UndefinedVariableError' this.message = err.message super.update() } } // only used internally; raised where we don't have token information, // so it can't be an UndefinedVariableError. export class InternalUndefinedVariableError extends Error { variableName: string public constructor (variableName: string) { super(`undefined variable: ${variableName}`) this.name = 'InternalUndefinedVariableError' this.variableName = variableName } } export class AssertionError extends Error { public constructor (message: string) { super(message) this.name = 'AssertionError' this.message = message + '' } } function mkContext (token: Token) { const [line, col] = token.getPosition() const lines = token.input.split('\n') const begin = Math.max(line - 2, 1) const end = Math.min(line + 3, lines.length) const context = _ .range(begin, end + 1) .map(lineNumber => { const rowIndicator = (lineNumber === line) ? '>> ' : ' ' const num = _.padStart(String(lineNumber), String(end).length) let text = `${rowIndicator}${num}| ` const colIndicator = lineNumber === line ? '\n' + _.padStart('^', col + text.length) : '' text += lines[lineNumber - 1] text += colIndicator return text }) .join('\n') return context } function mkMessage (msg: string, token: Token) { if (token.file) msg += `, file:${token.file}` const [line, col] = token.getPosition() msg += `, line:${line}, col:${col}` return msg }