mirror of
https://github.com/harttle/liquidjs.git
synced 2026-09-16 04:40:39 -07:00
feat: throw an Error if delimiter not matched
This commit is contained in:
+3
-1
@@ -1,11 +1,13 @@
|
|||||||
export default class Token {
|
export default class Token {
|
||||||
type: string
|
type: string
|
||||||
line: number
|
line: number
|
||||||
|
col: number
|
||||||
raw: string
|
raw: string
|
||||||
input: string
|
input: string
|
||||||
file: string
|
file: string
|
||||||
value: string
|
value: string
|
||||||
constructor (raw, pos, input, file, line) {
|
constructor (raw, col, input, file, line) {
|
||||||
|
this.col = col
|
||||||
this.line = line
|
this.line = line
|
||||||
this.raw = raw
|
this.raw = raw
|
||||||
this.input = input
|
this.input = input
|
||||||
|
|||||||
+28
-10
@@ -1,7 +1,9 @@
|
|||||||
import whiteSpaceCtrl from './whitespace-ctrl'
|
import whiteSpaceCtrl from './whitespace-ctrl'
|
||||||
import HTMLToken from './html-token'
|
import HTMLToken from './html-token'
|
||||||
import TagToken from './tag-token'
|
import TagToken from './tag-token'
|
||||||
|
import Token from './token'
|
||||||
import OutputToken from './output-token'
|
import OutputToken from './output-token'
|
||||||
|
import { TokenizationError } from 'src/util/error'
|
||||||
import { LiquidOptions, defaultOptions } from 'src/liquid-options'
|
import { LiquidOptions, defaultOptions } from 'src/liquid-options'
|
||||||
|
|
||||||
enum ParseState { HTML, OUTPUT, TAG }
|
enum ParseState { HTML, OUTPUT, TAG }
|
||||||
@@ -14,43 +16,59 @@ export default class Tokenizer {
|
|||||||
tokenize (input: string, file?: string) {
|
tokenize (input: string, file?: string) {
|
||||||
const tokens = []
|
const tokens = []
|
||||||
let p = 0
|
let p = 0
|
||||||
let line = 1
|
let curLine = 1
|
||||||
let state = ParseState.HTML
|
let state = ParseState.HTML
|
||||||
let buffer = ''
|
let buffer = ''
|
||||||
let bufferBegin = 0
|
let lineBegin = 0
|
||||||
|
let line = 1
|
||||||
|
let col = 1
|
||||||
|
|
||||||
while (p < input.length) {
|
while (p < input.length) {
|
||||||
if (input[p] === '\n') line++
|
if (input[p] === '\n') {
|
||||||
|
curLine++
|
||||||
|
lineBegin = p + 1
|
||||||
|
}
|
||||||
const bin = input.substr(p, 2)
|
const bin = input.substr(p, 2)
|
||||||
if (state === ParseState.HTML) {
|
if (state === ParseState.HTML) {
|
||||||
if (bin === '{{' || bin === '{%') {
|
if (bin === '{{' || bin === '{%') {
|
||||||
if (buffer) tokens.push(new HTMLToken(buffer, bufferBegin, input, file, line))
|
if (buffer) tokens.push(new HTMLToken(buffer, col, input, file, line))
|
||||||
buffer = bin
|
buffer = bin
|
||||||
bufferBegin = p
|
line = curLine
|
||||||
|
col = p - lineBegin + 1
|
||||||
p += 2
|
p += 2
|
||||||
state = bin === '{{' ? ParseState.OUTPUT : ParseState.TAG
|
state = bin === '{{' ? ParseState.OUTPUT : ParseState.TAG
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
} else if (state === ParseState.OUTPUT && bin === '}}') {
|
} else if (state === ParseState.OUTPUT && bin === '}}') {
|
||||||
buffer += '}}'
|
buffer += '}}'
|
||||||
tokens.push(new OutputToken(buffer, bufferBegin, input, file, line))
|
tokens.push(new OutputToken(buffer, col, input, file, line))
|
||||||
p += 2
|
p += 2
|
||||||
buffer = ''
|
buffer = ''
|
||||||
bufferBegin = p
|
line = curLine
|
||||||
|
col = p - lineBegin + 1
|
||||||
state = ParseState.HTML
|
state = ParseState.HTML
|
||||||
continue
|
continue
|
||||||
} else if (bin === '%}') {
|
} else if (bin === '%}') {
|
||||||
buffer += '%}'
|
buffer += '%}'
|
||||||
tokens.push(new TagToken(buffer, bufferBegin, input, file, line))
|
tokens.push(new TagToken(buffer, col, input, file, line))
|
||||||
p += 2
|
p += 2
|
||||||
buffer = ''
|
buffer = ''
|
||||||
bufferBegin = p
|
line = curLine
|
||||||
|
col = p - lineBegin + 1
|
||||||
state = ParseState.HTML
|
state = ParseState.HTML
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
buffer += input[p++]
|
buffer += input[p++]
|
||||||
}
|
}
|
||||||
if (buffer) tokens.push(new HTMLToken(buffer, bufferBegin, input, file, line))
|
if (state !== ParseState.HTML) {
|
||||||
|
const t = state === ParseState.OUTPUT ? 'output' : 'tag'
|
||||||
|
const str = buffer.length > 16 ? buffer.slice(0, 13) + '...' : buffer
|
||||||
|
throw new TokenizationError(
|
||||||
|
new Error(`${t} "${str}" not closed`),
|
||||||
|
new Token(buffer, col, input, file, line)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (buffer) tokens.push(new HTMLToken(buffer, col, input, file, line))
|
||||||
|
|
||||||
whiteSpaceCtrl(tokens, this.options)
|
whiteSpaceCtrl(tokens, this.options)
|
||||||
return tokens
|
return tokens
|
||||||
|
|||||||
+2
-4
@@ -11,14 +11,12 @@ abstract class LiquidError {
|
|||||||
name: string
|
name: string
|
||||||
message: string
|
message: string
|
||||||
stack: string
|
stack: string
|
||||||
private line: string
|
|
||||||
private file: string
|
private file: string
|
||||||
private input: string
|
private input: string
|
||||||
private token: Token
|
private token: Token
|
||||||
private originalError: Error
|
private originalError: Error
|
||||||
constructor (err, token) {
|
constructor (err, token) {
|
||||||
this.input = token.input
|
this.input = token.input
|
||||||
this.line = token.line
|
|
||||||
this.file = token.file
|
this.file = token.file
|
||||||
this.originalError = err
|
this.originalError = err
|
||||||
this.token = token
|
this.token = token
|
||||||
@@ -28,7 +26,7 @@ abstract class LiquidError {
|
|||||||
|
|
||||||
captureStack.call(obj)
|
captureStack.call(obj)
|
||||||
const err = this.originalError
|
const err = this.originalError
|
||||||
const context = mkContext(this.input, this.line)
|
const context = mkContext(this.input, this.token.line)
|
||||||
this.message = mkMessage(err.message, this.token)
|
this.message = mkMessage(err.message, this.token)
|
||||||
this.stack = this.message + '\n' + context +
|
this.stack = this.message + '\n' + context +
|
||||||
'\n' + (this.stack || this.message) +
|
'\n' + (this.stack || this.message) +
|
||||||
@@ -110,7 +108,7 @@ function mkMessage (msg, token) {
|
|||||||
msg += ', file:' + token.file
|
msg += ', file:' + token.file
|
||||||
}
|
}
|
||||||
if (token.line) {
|
if (token.line) {
|
||||||
msg += ', line:' + token.line
|
msg += `, line:${token.line}, col:${token.col}`
|
||||||
}
|
}
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import HTMLToken from 'src/parser/html-token'
|
|||||||
|
|
||||||
describe('tokenizer', function () {
|
describe('tokenizer', function () {
|
||||||
const tokenizer = new Tokenizer()
|
const tokenizer = new Tokenizer()
|
||||||
describe('parse', function () {
|
describe('#tokenize()', function () {
|
||||||
it('should handle plain HTML', function () {
|
it('should handle plain HTML', function () {
|
||||||
const html = '<html><body><p>Lorem Ipsum</p></body></html>'
|
const html = '<html><body><p>Lorem Ipsum</p></body></html>'
|
||||||
const tokens = tokenizer.tokenize(html)
|
const tokens = tokenizer.tokenize(html)
|
||||||
@@ -66,5 +66,15 @@ describe('tokenizer', function () {
|
|||||||
expect(tokens[0]).instanceOf(OutputToken)
|
expect(tokens[0]).instanceOf(OutputToken)
|
||||||
expect(tokens[0].raw).to.equal('{{foo\n|date:\n"%Y-%m-%d"\n}}')
|
expect(tokens[0].raw).to.equal('{{foo\n|date:\n"%Y-%m-%d"\n}}')
|
||||||
})
|
})
|
||||||
|
it('should throw if tag not closed', function () {
|
||||||
|
expect(() => {
|
||||||
|
tokenizer.tokenize('{% assign foo = bar {{foo}}')
|
||||||
|
}).to.throw(/tag "{% assign foo..." not closed/)
|
||||||
|
})
|
||||||
|
it('should throw if output not closed', function () {
|
||||||
|
expect(() => {
|
||||||
|
tokenizer.tokenize('{{name}')
|
||||||
|
}).to.throw(/output "{{name}" not closed/)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+18
-12
@@ -28,7 +28,7 @@ describe('error', function () {
|
|||||||
'TokenizationError'
|
'TokenizationError'
|
||||||
]
|
]
|
||||||
const err = await expect(engine.parseAndRender(html.join('\n'))).be.rejected
|
const err = await expect(engine.parseAndRender(html.join('\n'))).be.rejected
|
||||||
expect(err.message).to.equal('illegal tag syntax, line:3')
|
expect(err.message).to.equal('illegal tag syntax, line:3, col:2')
|
||||||
expect(err.stack).to.contain(message.join('\n'))
|
expect(err.stack).to.contain(message.join('\n'))
|
||||||
expect(err.name).to.equal('TokenizationError')
|
expect(err.name).to.equal('TokenizationError')
|
||||||
})
|
})
|
||||||
@@ -37,10 +37,10 @@ describe('error', function () {
|
|||||||
const err = await expect(engine.parseAndRender(html)).be.rejected
|
const err = await expect(engine.parseAndRender(html)).be.rejected
|
||||||
expect(err.input).to.equal(html)
|
expect(err.input).to.equal(html)
|
||||||
})
|
})
|
||||||
it('should contain line number in err.line', async function () {
|
it('should contain line number in err.token.line', async function () {
|
||||||
const err = await expect(engine.parseAndRender('1\n2\n{% . a %}\n4')).be.rejected
|
const err = await expect(engine.parseAndRender('1\n2\n{% . a %}\n4')).be.rejected
|
||||||
expect(err.name).to.equal('TokenizationError')
|
expect(err.name).to.equal('TokenizationError')
|
||||||
expect(err.line).to.equal(3)
|
expect(err.token.line).to.equal(3)
|
||||||
})
|
})
|
||||||
it('should contain stack in err.stack', async function () {
|
it('should contain stack in err.stack', async function () {
|
||||||
const err = await expect(engine.parseAndRender('{% . a %}')).be.rejected
|
const err = await expect(engine.parseAndRender('{% . a %}')).be.rejected
|
||||||
@@ -68,6 +68,12 @@ describe('error', function () {
|
|||||||
expect(err.name).to.equal('TokenizationError')
|
expect(err.name).to.equal('TokenizationError')
|
||||||
expect(err.file).to.equal(path.resolve('/foo.html'))
|
expect(err.file).to.equal(path.resolve('/foo.html'))
|
||||||
})
|
})
|
||||||
|
it('should throw error with line and pos if tag unmatched', async function () {
|
||||||
|
const err = await expect(engine.parseAndRender('1\n2\nfoo{% assign a = 4 }\n4')).be.rejected
|
||||||
|
expect(err.name).to.equal('TokenizationError')
|
||||||
|
expect(err.token.line).to.equal(3)
|
||||||
|
expect(err.token.col).to.equal(4)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('RenderError', function () {
|
describe('RenderError', function () {
|
||||||
@@ -128,7 +134,7 @@ describe('error', function () {
|
|||||||
'RenderError'
|
'RenderError'
|
||||||
]
|
]
|
||||||
const err = await expect(engine.parseAndRender(html.join('\n'))).be.rejected
|
const err = await expect(engine.parseAndRender(html.join('\n'))).be.rejected
|
||||||
expect(err.message).to.equal('intended render error, line:4')
|
expect(err.message).to.equal('intended render error, line:4, col:2')
|
||||||
expect(err.stack).to.contain(message.join('\n'))
|
expect(err.stack).to.contain(message.join('\n'))
|
||||||
expect(err.name).to.equal('RenderError')
|
expect(err.name).to.equal('RenderError')
|
||||||
})
|
})
|
||||||
@@ -157,7 +163,7 @@ describe('error', function () {
|
|||||||
const err = await expect(engine.parseAndRender(html)).be.rejected
|
const err = await expect(engine.parseAndRender(html)).be.rejected
|
||||||
console.log(err.message)
|
console.log(err.message)
|
||||||
console.log(err.stack)
|
console.log(err.stack)
|
||||||
expect(err.message).to.equal(`intended render error, file:${path.resolve('/throwing-tag.html')}, line:4`)
|
expect(err.message).to.equal(`intended render error, file:${path.resolve('/throwing-tag.html')}, line:4, col:2`)
|
||||||
expect(err.stack).to.contain(message.join('\n'))
|
expect(err.stack).to.contain(message.join('\n'))
|
||||||
expect(err.name).to.equal('RenderError')
|
expect(err.name).to.equal('RenderError')
|
||||||
})
|
})
|
||||||
@@ -177,7 +183,7 @@ describe('error', function () {
|
|||||||
'RenderError'
|
'RenderError'
|
||||||
]
|
]
|
||||||
const err = await expect(engine.parseAndRender(html)).be.rejected
|
const err = await expect(engine.parseAndRender(html)).be.rejected
|
||||||
expect(err.message).to.equal(`intended render error, file:${path.resolve('/throwing-tag.html')}, line:4`)
|
expect(err.message).to.equal(`intended render error, file:${path.resolve('/throwing-tag.html')}, line:4, col:2`)
|
||||||
expect(err.stack).to.contain(message.join('\n'))
|
expect(err.stack).to.contain(message.join('\n'))
|
||||||
expect(err.name).to.equal('RenderError')
|
expect(err.name).to.equal('RenderError')
|
||||||
})
|
})
|
||||||
@@ -187,10 +193,10 @@ describe('error', function () {
|
|||||||
expect(err.input).to.equal(html)
|
expect(err.input).to.equal(html)
|
||||||
expect(err.name).to.equal('RenderError')
|
expect(err.name).to.equal('RenderError')
|
||||||
})
|
})
|
||||||
it('should contain line number in err.line', async function () {
|
it('should contain line number in err.token.line', async function () {
|
||||||
const src = '1\n2\n{{1|throwingFilter}}\n4'
|
const src = '1\n2\n{{1|throwingFilter}}\n4'
|
||||||
const err = await expect(engine.parseAndRender(src)).be.rejected
|
const err = await expect(engine.parseAndRender(src)).be.rejected
|
||||||
expect(err.line).to.equal(3)
|
expect(err.token.line).to.equal(3)
|
||||||
expect(err.name).to.equal('RenderError')
|
expect(err.name).to.equal('RenderError')
|
||||||
})
|
})
|
||||||
it('should contain stack in err.stack', async function () {
|
it('should contain stack in err.stack', async function () {
|
||||||
@@ -260,7 +266,7 @@ describe('error', function () {
|
|||||||
'ParseError: tag a not found'
|
'ParseError: tag a not found'
|
||||||
]
|
]
|
||||||
const err = await expect(engine.parseAndRender(html.join('\n'))).be.rejected
|
const err = await expect(engine.parseAndRender(html.join('\n'))).be.rejected
|
||||||
expect(err.message).to.equal('tag a not found, line:4')
|
expect(err.message).to.equal('tag a not found, line:4, col:2')
|
||||||
expect(err.stack).to.contain(message.join('\n'))
|
expect(err.stack).to.contain(message.join('\n'))
|
||||||
expect(err.name).to.equal('ParseError')
|
expect(err.name).to.equal('ParseError')
|
||||||
})
|
})
|
||||||
@@ -275,14 +281,14 @@ describe('error', function () {
|
|||||||
'ParseError: tag a not found'
|
'ParseError: tag a not found'
|
||||||
]
|
]
|
||||||
const err = await expect(engine.parseAndRender(html.join('\n'))).be.rejected
|
const err = await expect(engine.parseAndRender(html.join('\n'))).be.rejected
|
||||||
expect(err.message).to.equal('tag a not found, line:2')
|
expect(err.message).to.equal('tag a not found, line:2, col:2')
|
||||||
expect(err.stack).to.contain(message.join('\n'))
|
expect(err.stack).to.contain(message.join('\n'))
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should contain line number in err.line', async function () {
|
it('should contain line number in err.token.line', async function () {
|
||||||
const html = '<html>\n<head>\n\n{% raw %}\n\n'
|
const html = '<html>\n<head>\n\n{% raw %}\n\n'
|
||||||
const err = await expect(engine.parseAndRender(html)).be.rejected
|
const err = await expect(engine.parseAndRender(html)).be.rejected
|
||||||
expect(err.line).to.equal(4)
|
expect(err.token.line).to.equal(4)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should contain stack in err.stack', async function () {
|
it('should contain stack in err.stack', async function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user