mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-16 21:00:40 -07:00
chore(TypeScript): refactor objects into classes
fix: `Nil`(null, undefined) now renders as empty string change: `parser.parseValue()` renamed to `parser.parseOutput` change: registered tags/filters become static and shared across different liquid instances
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
import Token from './token'
|
||||
|
||||
export default class DelimitedToken extends Token {
|
||||
trim_left: boolean
|
||||
trim_right: boolean
|
||||
constructor(raw, pos, input, file, line) {
|
||||
super(raw, pos, input, file, line)
|
||||
this.trim_left = raw[2] === '-'
|
||||
this.trim_right = raw[raw.length - 3] === '-'
|
||||
this.value = raw.slice(this.trim_left ? 3 : 2, this.trim_right ? -3 : -2).trim()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import Token from './token'
|
||||
|
||||
export default class HTMLToken extends Token {
|
||||
constructor(str, begin, input, file, line) {
|
||||
super(str, begin, input, file, line)
|
||||
this.type = 'html'
|
||||
this.value = str
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// quote related
|
||||
const singleQuoted = /'[^']*'/
|
||||
const doubleQuoted = /"[^"]*"/
|
||||
export const quoted = new RegExp(`${singleQuoted.source}|${doubleQuoted.source}`)
|
||||
export const quoteBalanced = new RegExp(`(?:${quoted.source}|[^'"])*`)
|
||||
|
||||
// basic types
|
||||
export const integer = /-?\d+/
|
||||
export const number = /-?\d+\.?\d*|\.?\d+/
|
||||
export const bool = /true|false/
|
||||
|
||||
// property access
|
||||
export const identifier = /[\w-]+[?]?/
|
||||
export const subscript = new RegExp(`\\[(?:${quoted.source}|[\\w-\\.]+)\\]`)
|
||||
export const literal = new RegExp(`(?:${quoted.source}|${bool.source}|${number.source})`)
|
||||
export const variable = new RegExp(`${identifier.source}(?:\\.${identifier.source}|${subscript.source})*`)
|
||||
|
||||
// range related
|
||||
export const rangeLimit = new RegExp(`(?:${variable.source}|${number.source})`)
|
||||
export const range = new RegExp(`\\(${rangeLimit.source}\\.\\.${rangeLimit.source}\\)`)
|
||||
export const rangeCapture = new RegExp(`\\((${rangeLimit.source})\\.\\.(${rangeLimit.source})\\)`)
|
||||
|
||||
export const value = new RegExp(`(?:${variable.source}|${literal.source}|${range.source})`)
|
||||
|
||||
// hash related
|
||||
export const hash = new RegExp(`(?:${identifier.source})\\s*:\\s*(?:${value.source})`)
|
||||
export const hashCapture = new RegExp(`(${identifier.source})\\s*:\\s*(${value.source})`, 'g')
|
||||
|
||||
// full match
|
||||
export const tagLine = new RegExp(`^\\s*(${identifier.source})\\s*([\\s\\S]*?)\\s*$`)
|
||||
export const literalLine = new RegExp(`^${literal.source}$`, 'i')
|
||||
export const variableLine = new RegExp(`^${variable.source}$`)
|
||||
export const numberLine = new RegExp(`^${number.source}$`)
|
||||
export const boolLine = new RegExp(`^${bool.source}$`, 'i')
|
||||
export const quotedLine = new RegExp(`^${quoted.source}$`)
|
||||
export const rangeLine = new RegExp(`^${rangeCapture.source}$`)
|
||||
export const integerLine = new RegExp(`^${integer.source}$`)
|
||||
|
||||
// filter related
|
||||
export const valueDeclaration = new RegExp(`(?:${identifier.source}\\s*:\\s*)?${value.source}`)
|
||||
export const valueList = new RegExp(`${valueDeclaration.source}(\\s*,\\s*${valueDeclaration.source})*`)
|
||||
export const filter = new RegExp(`${identifier.source}(?:\\s*:\\s*${valueList.source})?`, 'g')
|
||||
export const filterCapture = new RegExp(`(${identifier.source})(?:\\s*:\\s*(${valueList.source}))?`)
|
||||
export const filterLine = new RegExp(`^${filterCapture.source}$`)
|
||||
|
||||
export const operators = [
|
||||
/\s+or\s+/,
|
||||
/\s+and\s+/,
|
||||
/==|!=|<=|>=|<|>|\s+contains\s+/
|
||||
]
|
||||
|
||||
export function isInteger (str) {
|
||||
return integerLine.test(str)
|
||||
}
|
||||
|
||||
export function isLiteral (str) {
|
||||
return literalLine.test(str)
|
||||
}
|
||||
|
||||
export function isRange (str) {
|
||||
return rangeLine.test(str)
|
||||
}
|
||||
|
||||
export function isVariable (str) {
|
||||
return variableLine.test(str)
|
||||
}
|
||||
|
||||
export function matchValue (str) {
|
||||
return value.exec(str)
|
||||
}
|
||||
|
||||
export function parseLiteral (str) {
|
||||
let res = str.match(numberLine)
|
||||
if (res) {
|
||||
return Number(str)
|
||||
}
|
||||
res = str.match(boolLine)
|
||||
if (res) {
|
||||
return str.toLowerCase() === 'true'
|
||||
}
|
||||
res = str.match(quotedLine)
|
||||
if (res) {
|
||||
return str.slice(1, -1)
|
||||
}
|
||||
throw new TypeError(`cannot parse '${str}' as literal`)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import DelimitedToken from './delimited-token'
|
||||
|
||||
export default class OutputToken extends DelimitedToken {
|
||||
constructor(raw, pos, input, file, line) {
|
||||
super(raw, pos, input, file, line)
|
||||
this.type = 'output'
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import Token from 'src/parser/token'
|
||||
import ITemplate from 'src/template/itemplate'
|
||||
|
||||
type parseToken = (token: Token, remainTokens: Array<Token>) => ITemplate
|
||||
type eventHandler = ((arg?: Token | ITemplate) => void)
|
||||
|
||||
export default class ParseStream {
|
||||
private tokens: Array<Token>
|
||||
private handlers: {[key: string]: eventHandler} = {}
|
||||
private stopRequested: boolean
|
||||
private parseToken: parseToken
|
||||
|
||||
constructor (tokens: Array<Token>, parseToken: parseToken) {
|
||||
this.tokens = tokens
|
||||
this.parseToken = parseToken
|
||||
}
|
||||
on (name: string, cb: eventHandler) {
|
||||
this.handlers[name] = cb
|
||||
return this
|
||||
}
|
||||
trigger (event: string, arg?: Token | ITemplate) {
|
||||
const h = this.handlers[event]
|
||||
if (typeof h === 'function') {
|
||||
h(arg)
|
||||
return true
|
||||
}
|
||||
}
|
||||
start () {
|
||||
this.trigger('start')
|
||||
let token
|
||||
while (!this.stopRequested && (token = this.tokens.shift())) {
|
||||
if (this.trigger('token', token)) continue
|
||||
if (token.type === 'tag' &&
|
||||
this.trigger(`tag:${token.name}`, token)) {
|
||||
continue
|
||||
}
|
||||
const template = this.parseToken(token, this.tokens)
|
||||
this.trigger('template', template)
|
||||
}
|
||||
if (!this.stopRequested) this.trigger('end')
|
||||
return this
|
||||
}
|
||||
stop () {
|
||||
this.stopRequested = true
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { ParseError } from '../util/error'
|
||||
import Liquid from 'src/liquid'
|
||||
import ParseStream from './parse-stream'
|
||||
import Token from './token'
|
||||
import Tag from 'src/template/tag/tag'
|
||||
import HTMLToken from './html-token'
|
||||
import TagToken from './tag-token'
|
||||
import OutputToken from './output-token'
|
||||
import Output from 'src/template/output'
|
||||
import HTML from 'src/template/html'
|
||||
import Value from 'src/template/value'
|
||||
|
||||
export default class Parser {
|
||||
liquid: Liquid
|
||||
|
||||
constructor(liquid: Liquid) {
|
||||
this.liquid = liquid
|
||||
}
|
||||
parse (tokens: Array<Token>) {
|
||||
let token
|
||||
const templates = []
|
||||
while ((token = tokens.shift())) {
|
||||
templates.push(this.parseToken(token, tokens))
|
||||
}
|
||||
return templates
|
||||
}
|
||||
parseToken (token: Token, remainTokens: Array<Token>) {
|
||||
try {
|
||||
if (token.type === 'tag') {
|
||||
return new Tag(token, remainTokens, this.liquid)
|
||||
}
|
||||
if (token.type === 'output') {
|
||||
return new Output(token, this.liquid.options.strict_filters)
|
||||
}
|
||||
return new HTML(token)
|
||||
} catch (e) {
|
||||
throw new ParseError(e, token)
|
||||
}
|
||||
}
|
||||
parseStream (tokens: Array<Token>) {
|
||||
return new ParseStream(tokens, (token, tokens) => this.parseToken(token, tokens))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import DelimitedToken from './delimited-token'
|
||||
import { TokenizationError } from 'src/util/error'
|
||||
import * as lexical from './lexical'
|
||||
|
||||
export default class TagToken extends DelimitedToken {
|
||||
name: string
|
||||
args: string
|
||||
constructor(raw, pos, input, file, line) {
|
||||
super(raw, pos, input, file, line)
|
||||
this.type = 'tag'
|
||||
const match = this.value.match(lexical.tagLine)
|
||||
if (!match) {
|
||||
throw new TokenizationError(`illegal tag syntax`, this)
|
||||
}
|
||||
this.name = match[1]
|
||||
this.args = match[2]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import { last, isArray } from '../util/underscore'
|
||||
|
||||
function domResolve (root, path) {
|
||||
const base = document.createElement('base')
|
||||
base.href = root
|
||||
|
||||
const head = document.getElementsByTagName('head')[0]
|
||||
head.insertBefore(base, head.firstChild)
|
||||
|
||||
const a = document.createElement('a')
|
||||
a.href = path
|
||||
const resolved = a.href
|
||||
head.removeChild(base)
|
||||
|
||||
return resolved
|
||||
}
|
||||
|
||||
export function resolve (filepath, root, options) {
|
||||
root = root || options.root
|
||||
if (isArray(root)) {
|
||||
root = root[0]
|
||||
}
|
||||
if (root.length && last(root) !== '/') {
|
||||
root += '/'
|
||||
}
|
||||
const url = domResolve(root, filepath)
|
||||
return url.replace(/^(\w+:\/\/[^/]+)(\/[^?]+)/, (str, origin, path) => {
|
||||
const last = path.split('/').pop()
|
||||
if (/\.\w+$/.test(last)) {
|
||||
return str
|
||||
}
|
||||
return origin + path + options.extname
|
||||
})
|
||||
}
|
||||
|
||||
export async function read (url: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest()
|
||||
xhr.onload = () => {
|
||||
if (xhr.status >= 200 && xhr.status < 300) {
|
||||
resolve(xhr.responseText as string)
|
||||
} else {
|
||||
reject(new Error(xhr.statusText))
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
reject(new Error('An error occurred whilst receiving the response.'))
|
||||
}
|
||||
xhr.open('GET', url)
|
||||
xhr.send()
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import * as _ from '../util/underscore'
|
||||
import * as path from 'path'
|
||||
import { anySeries } from '../util/promise'
|
||||
import * as fs from 'fs'
|
||||
|
||||
const statFileAsync = <(filepath: string) => Promise<object>>_.promisify(fs.stat)
|
||||
const readFileAsync = <(filepath: string, encoding: string) => Promise<string>>_.promisify(fs.readFile)
|
||||
|
||||
export async function resolve (filepath, root, options) {
|
||||
if (!path.extname(filepath)) {
|
||||
filepath += options.extname
|
||||
}
|
||||
root = options.root.concat(root || [])
|
||||
root = _.uniq(root)
|
||||
const paths = root.map(root => path.resolve(root, filepath))
|
||||
return anySeries(paths, async path => {
|
||||
try {
|
||||
await statFileAsync(path)
|
||||
return path
|
||||
} catch (e) {
|
||||
e.message = `${e.code}: Failed to lookup ${filepath} in: ${root}`
|
||||
throw e
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export async function read (filepath): Promise<string> {
|
||||
return readFileAsync(filepath, 'utf8')
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
export default class Token {
|
||||
type: string
|
||||
line: number
|
||||
raw: string
|
||||
input: string
|
||||
file: string
|
||||
value: string
|
||||
constructor(raw, pos, input, file, line) {
|
||||
this.line = line
|
||||
this.raw = raw
|
||||
this.input = input
|
||||
this.file = file
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import whiteSpaceCtrl from './whitespace-ctrl'
|
||||
import HTMLToken from './html-token'
|
||||
import TagToken from './tag-token'
|
||||
import OutputToken from './output-token'
|
||||
|
||||
enum ParseState { HTML, OUTPUT, TAG }
|
||||
|
||||
export function parse (input: string, file?: string, options?) {
|
||||
const tokens = []
|
||||
let p = 0
|
||||
let line = 1
|
||||
let state = ParseState.HTML
|
||||
let buffer = ''
|
||||
let bufferBegin = 0
|
||||
|
||||
while(p < input.length) {
|
||||
if (input[p] === '\n') line++
|
||||
const bin = input.substr(p, 2)
|
||||
if (state === ParseState.HTML) {
|
||||
if (bin === '{{' || bin === '{%') {
|
||||
if (buffer) tokens.push(new HTMLToken(buffer, bufferBegin, input, file, line))
|
||||
buffer = bin
|
||||
bufferBegin = p
|
||||
p += 2
|
||||
state = bin === '{{' ? ParseState.OUTPUT : ParseState.TAG
|
||||
continue
|
||||
}
|
||||
}
|
||||
else if (state === ParseState.OUTPUT && bin === '}}') {
|
||||
buffer += '}}'
|
||||
tokens.push(new OutputToken(buffer, bufferBegin, input, file, line))
|
||||
p += 2
|
||||
buffer = ''
|
||||
bufferBegin = p
|
||||
state = ParseState.HTML
|
||||
continue
|
||||
}
|
||||
else if (bin === '%}') {
|
||||
buffer += '%}'
|
||||
tokens.push(new TagToken(buffer, bufferBegin, input, file, line))
|
||||
p += 2
|
||||
buffer = ''
|
||||
bufferBegin = p
|
||||
state = ParseState.HTML
|
||||
continue
|
||||
}
|
||||
buffer += input[p++]
|
||||
}
|
||||
if (buffer) tokens.push(new HTMLToken(buffer, bufferBegin, input, file, line))
|
||||
|
||||
whiteSpaceCtrl(tokens, options)
|
||||
return tokens
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import { assign } from 'src/util/underscore'
|
||||
import TagToken from './tag-token'
|
||||
import OutputToken from './output-token'
|
||||
import HTMLToken from './html-token'
|
||||
|
||||
export default function whiteSpaceCtrl (tokens, options) {
|
||||
options = assign({ greedy: true }, options)
|
||||
let inRaw = false
|
||||
|
||||
tokens.forEach((token, i) => {
|
||||
if (shouldTrimLeft(token, inRaw, options)) {
|
||||
trimLeft(tokens[i - 1], options.greedy)
|
||||
}
|
||||
|
||||
if (token.type === 'tag' && token.name === 'raw') inRaw = true
|
||||
if (token.type === 'tag' && token.name === 'endraw') inRaw = false
|
||||
|
||||
if (shouldTrimRight(token, inRaw, options)) {
|
||||
trimRight(tokens[i + 1], options.greedy)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function shouldTrimLeft (token, inRaw, options) {
|
||||
if (inRaw) return false
|
||||
if (token.type === 'tag') return token.trim_left || options.trim_tag_left
|
||||
if (token.type === 'output') return token.trim_left || options.trim_value_left
|
||||
}
|
||||
|
||||
function shouldTrimRight (token, inRaw, options) {
|
||||
if (inRaw) return false
|
||||
if (token.type === 'tag') return token.trim_right || options.trim_tag_right
|
||||
if (token.type === 'output') return token.trim_right || options.trim_value_right
|
||||
}
|
||||
|
||||
function trimLeft (token, greedy) {
|
||||
if (!token || token.type !== 'html') return
|
||||
|
||||
const rLeft = greedy ? /\s+$/g : /[\t\r ]*$/g
|
||||
token.value = token.value.replace(rLeft, '')
|
||||
}
|
||||
|
||||
function trimRight (token, greedy) {
|
||||
if (!token || token.type !== 'html') return
|
||||
|
||||
const rRight = greedy ? /^\s+/g : /^[\t\r ]*\n?/g
|
||||
token.value = token.value.replace(rRight, '')
|
||||
}
|
||||
Reference in New Issue
Block a user