wip: parse interpolation

This commit is contained in:
Evan You 2023-11-15 23:33:57 +08:00
parent 70edd1c61e
commit 5762288bdf
2 changed files with 235 additions and 168 deletions

View File

@ -65,9 +65,15 @@ export const enum CharCodes {
RightSquare = 93 // "]"
}
const defaultDelimitersOpen = [123, 123] // "{{"
const defaultDelimitersClose = [125, 125] // "}}"
/** All the states the tokenizer can be in. */
const enum State {
Text = 1,
Interpolation,
// Tags
BeforeTagName, // After <
InTagName,
InSelfClosingTag,
@ -134,6 +140,8 @@ export interface Callbacks {
ontext(start: number, endIndex: number): void
ontextentity(codepoint: number, endIndex: number): void
oninterpolation(start: number, endIndex: number): void
onopentagname(start: number, endIndex: number): void
onopentagend(endIndex: number): void
onselfclosingtag(endIndex: number): void
@ -190,14 +198,9 @@ export default class Tokenizer {
/** Reocrd newline positions for fast line / column calculation */
private newlines: number[] = []
private readonly decodeEntities: boolean
private readonly entityDecoder: EntityDecoder
constructor(
{ decodeEntities = true }: { decodeEntities?: boolean },
private readonly cbs: Callbacks
) {
this.decodeEntities = decodeEntities
constructor(private readonly cbs: Callbacks) {
this.entityDecoder = new EntityDecoder(htmlDecodeTree, (cp, consumed) =>
this.emitCodePoint(cp, consumed)
)
@ -211,6 +214,8 @@ export default class Tokenizer {
this.baseState = State.Text
this.currentSequence = undefined!
this.newlines.length = 0
this.delimiterOpen = defaultDelimitersOpen
this.delimiterClose = defaultDelimitersClose
}
/**
@ -238,17 +243,45 @@ export default class Tokenizer {
}
private stateText(c: number): void {
if (
c === CharCodes.Lt ||
(!this.decodeEntities && this.fastForwardTo(CharCodes.Lt))
) {
if (c === CharCodes.Lt) {
if (this.index > this.sectionStart) {
this.cbs.ontext(this.sectionStart, this.index)
}
this.state = State.BeforeTagName
this.sectionStart = this.index
} else if (this.decodeEntities && c === CharCodes.Amp) {
} else if (c === CharCodes.Amp) {
this.startEntity()
} else if (this.matchDelimiter(c, this.delimiterOpen)) {
if (this.index > this.sectionStart) {
this.cbs.ontext(this.sectionStart, this.index)
}
this.state = State.Interpolation
this.sectionStart = this.index
this.index += this.delimiterOpen.length
}
}
public delimiterOpen: number[] = defaultDelimitersOpen
public delimiterClose: number[] = defaultDelimitersClose
private matchDelimiter(c: number, delimiter: number[]): boolean {
if (c === delimiter[0]) {
const l = delimiter.length
for (let i = 1; i < l; i++) {
if (this.buffer.charCodeAt(this.index + i) !== delimiter[i]) {
return false
}
}
return true
}
return false
}
private stateInterpolation(c: number): void {
if (this.matchDelimiter(c, this.delimiterClose)) {
this.index += this.delimiterClose.length
this.cbs.oninterpolation(this.sectionStart, this.index)
this.state = State.Text
this.sectionStart = this.index
}
}
@ -302,7 +335,7 @@ export default class Tokenizer {
} else if (this.sequenceIndex === 0) {
if (this.currentSequence === Sequences.TitleEnd) {
// We have to parse entities in <title> tags.
if (this.decodeEntities && c === CharCodes.Amp) {
if (c === CharCodes.Amp) {
this.startEntity()
}
} else if (this.fastForwardTo(CharCodes.Lt)) {
@ -592,7 +625,7 @@ export default class Tokenizer {
}
}
private handleInAttributeValue(c: number, quote: number) {
if (c === quote || (!this.decodeEntities && this.fastForwardTo(quote))) {
if (c === quote) {
this.cbs.onattribdata(this.sectionStart, this.index)
this.sectionStart = -1
this.cbs.onattribend(
@ -600,7 +633,7 @@ export default class Tokenizer {
this.index + 1
)
this.state = State.BeforeAttributeName
} else if (this.decodeEntities && c === CharCodes.Amp) {
} else if (c === CharCodes.Amp) {
this.startEntity()
}
}
@ -617,7 +650,7 @@ export default class Tokenizer {
this.cbs.onattribend(QuoteType.Unquoted, this.index)
this.state = State.BeforeAttributeName
this.stateBeforeAttributeName(c)
} else if (this.decodeEntities && c === CharCodes.Amp) {
} else if (c === CharCodes.Amp) {
this.startEntity()
}
}
@ -715,6 +748,10 @@ export default class Tokenizer {
this.stateText(c)
break
}
case State.Interpolation: {
this.stateInterpolation(c)
break
}
case State.SpecialStartSequence: {
this.stateSpecialStartSequence(c)
break

View File

@ -47,6 +47,7 @@ export const defaultParserOptions: MergedParserOptions = {
isVoidTag: NO,
isPreTag: NO,
isCustomElement: NO,
// TODO handle entities
decodeEntities: (rawText: string): string =>
rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),
onError: defaultOnError,
@ -69,162 +70,182 @@ let inPre = 0
// let inVPre = 0
const stack: ElementNode[] = []
const tokenizer = new Tokenizer(
// TODO handle entities
{ decodeEntities: true },
{
ontext(start, end) {
onText(getSlice(start, end), start, end)
},
const tokenizer = new Tokenizer({
ontext(start, end) {
onText(getSlice(start, end), start, end)
},
ontextentity(cp, end) {
onText(fromCodePoint(cp), end - 1, end)
},
ontextentity(cp, end) {
onText(fromCodePoint(cp), end - 1, end)
},
onopentagname(start, end) {
emitOpenTag(getSlice(start, end), start)
},
onopentagend(end) {
endOpenTag(end)
},
onclosetag(start, end) {
const name = getSlice(start, end)
if (!currentOptions.isVoidTag(name)) {
const pos = stack.findIndex(e => e.tag === name)
if (pos !== -1) {
for (let index = 0; index <= pos; index++) {
onCloseTag(stack.shift()!, end)
}
}
}
},
onselfclosingtag(end) {
closeCurrentTag(end)
},
onattribname(start, end) {
// plain attribute
currentProp = {
type: NodeTypes.ATTRIBUTE,
name: getSlice(start, end),
value: undefined,
loc: getLoc(start)
}
},
ondirname(start, end) {
const raw = getSlice(start, end)
const name =
raw === '.' || raw === ':'
? 'bind'
: raw === '@'
? 'on'
: raw === '#'
? 'slot'
: raw.slice(2)
currentProp = {
type: NodeTypes.DIRECTIVE,
name,
exp: undefined,
arg: undefined,
modifiers: [],
loc: getLoc(start)
}
},
ondirarg(start, end) {
const arg = getSlice(start, end)
const isStatic = arg[0] !== `[`
;(currentProp as DirectiveNode).arg = {
type: NodeTypes.SIMPLE_EXPRESSION,
content: arg,
isStatic,
constType: isStatic
? ConstantTypes.CAN_STRINGIFY
: ConstantTypes.NOT_CONSTANT,
loc: getLoc(start, end)
}
},
ondirmodifier(start, end) {
;(currentProp as DirectiveNode).modifiers.push(getSlice(start, end))
},
onattribdata(start, end) {
currentAttrValue += getSlice(start, end)
if (currentAttrStartIndex < 0) currentAttrStartIndex = start
currentAttrEndIndex = end
},
onattribentity(codepoint) {
currentAttrValue += fromCodePoint(codepoint)
},
onattribnameend(end) {
// check duplicate attrs
const start = currentProp!.loc.start.offset
const name = getSlice(start, end)
if (currentAttrs.has(name)) {
currentProp = null
// TODO emit error DUPLICATE_ATTRIBUTE
throw new Error(`duplicate attr ${name}`)
} else {
currentAttrs.add(name)
}
},
onattribend(quote, end) {
if (currentElement && currentProp) {
if (currentAttrValue) {
if (currentProp.type === NodeTypes.ATTRIBUTE) {
// assign value
currentProp!.value = {
type: NodeTypes.TEXT,
content: currentAttrValue,
loc:
quote === QuoteType.Unquoted
? getLoc(currentAttrStartIndex, currentAttrEndIndex)
: getLoc(currentAttrStartIndex - 1, currentAttrEndIndex + 1)
}
} else {
// directive
currentProp.exp = {
type: NodeTypes.SIMPLE_EXPRESSION,
content: currentAttrValue,
isStatic: false,
// Treat as non-constant by default. This can be potentially set
// to other values by `transformExpression` to make it eligible
// for hoisting.
constType: ConstantTypes.NOT_CONSTANT,
loc: getLoc(currentAttrStartIndex, currentAttrEndIndex)
}
}
}
currentProp.loc.end = tokenizer.getPos(end)
currentElement.props.push(currentProp!)
}
currentAttrValue = ''
currentAttrStartIndex = currentAttrEndIndex = -1
},
oncomment(start, end, offset) {
// TODO oncomment
},
onend() {
const end = currentInput.length - 1
for (let index = 0; index < stack.length; index++) {
onCloseTag(stack[index], end)
}
},
oncdata(start, end, offset) {
// TODO throw error
oninterpolation(start, end) {
let innerStart = start + tokenizer.delimiterOpen.length
let innerEnd = end - tokenizer.delimiterClose.length
while (isWhitespace(currentInput.charCodeAt(innerStart))) {
innerStart++
}
while (isWhitespace(currentInput.charCodeAt(innerEnd - 1))) {
innerEnd--
}
addNode({
type: NodeTypes.INTERPOLATION,
content: {
type: NodeTypes.SIMPLE_EXPRESSION,
isStatic: false,
// Set `isConstant` to false by default and will decide in transformExpression
constType: ConstantTypes.NOT_CONSTANT,
content: getSlice(innerStart, innerEnd),
loc: getLoc(innerStart, innerEnd)
},
loc: getLoc(start, end)
})
},
onopentagname(start, end) {
emitOpenTag(getSlice(start, end), start)
},
onopentagend(end) {
endOpenTag(end)
},
onclosetag(start, end) {
const name = getSlice(start, end)
if (!currentOptions.isVoidTag(name)) {
const pos = stack.findIndex(e => e.tag === name)
if (pos !== -1) {
for (let index = 0; index <= pos; index++) {
onCloseTag(stack.shift()!, end)
}
}
}
},
onselfclosingtag(end) {
closeCurrentTag(end)
},
onattribname(start, end) {
// plain attribute
currentProp = {
type: NodeTypes.ATTRIBUTE,
name: getSlice(start, end),
value: undefined,
loc: getLoc(start)
}
},
ondirname(start, end) {
const raw = getSlice(start, end)
const name =
raw === '.' || raw === ':'
? 'bind'
: raw === '@'
? 'on'
: raw === '#'
? 'slot'
: raw.slice(2)
currentProp = {
type: NodeTypes.DIRECTIVE,
name,
exp: undefined,
arg: undefined,
modifiers: [],
loc: getLoc(start)
}
},
ondirarg(start, end) {
const arg = getSlice(start, end)
const isStatic = arg[0] !== `[`
;(currentProp as DirectiveNode).arg = {
type: NodeTypes.SIMPLE_EXPRESSION,
content: arg,
isStatic,
constType: isStatic
? ConstantTypes.CAN_STRINGIFY
: ConstantTypes.NOT_CONSTANT,
loc: getLoc(start, end)
}
},
ondirmodifier(start, end) {
;(currentProp as DirectiveNode).modifiers.push(getSlice(start, end))
},
onattribdata(start, end) {
currentAttrValue += getSlice(start, end)
if (currentAttrStartIndex < 0) currentAttrStartIndex = start
currentAttrEndIndex = end
},
onattribentity(codepoint) {
currentAttrValue += fromCodePoint(codepoint)
},
onattribnameend(end) {
// check duplicate attrs
const start = currentProp!.loc.start.offset
const name = getSlice(start, end)
if (currentAttrs.has(name)) {
currentProp = null
// TODO emit error DUPLICATE_ATTRIBUTE
throw new Error(`duplicate attr ${name}`)
} else {
currentAttrs.add(name)
}
},
onattribend(quote, end) {
if (currentElement && currentProp) {
if (currentAttrValue) {
if (currentProp.type === NodeTypes.ATTRIBUTE) {
// assign value
currentProp!.value = {
type: NodeTypes.TEXT,
content: currentAttrValue,
loc:
quote === QuoteType.Unquoted
? getLoc(currentAttrStartIndex, currentAttrEndIndex)
: getLoc(currentAttrStartIndex - 1, currentAttrEndIndex + 1)
}
} else {
// directive
currentProp.exp = {
type: NodeTypes.SIMPLE_EXPRESSION,
content: currentAttrValue,
isStatic: false,
// Treat as non-constant by default. This can be potentially set
// to other values by `transformExpression` to make it eligible
// for hoisting.
constType: ConstantTypes.NOT_CONSTANT,
loc: getLoc(currentAttrStartIndex, currentAttrEndIndex)
}
}
}
currentProp.loc.end = tokenizer.getPos(end)
currentElement.props.push(currentProp!)
}
currentAttrValue = ''
currentAttrStartIndex = currentAttrEndIndex = -1
},
oncomment(start, end, offset) {
// TODO oncomment
},
onend() {
const end = currentInput.length - 1
for (let index = 0; index < stack.length; index++) {
onCloseTag(stack[index], end)
}
},
oncdata(start, end, offset) {
// TODO throw error
}
)
})
function getSlice(start: number, end: number) {
return currentInput.slice(start, end)
@ -283,7 +304,7 @@ function onText(content: string, start: number, end: number) {
loc: {
start: tokenizer.getPos(start),
end: tokenizer.getPos(end),
source: content
source: ''
}
})
}
@ -413,8 +434,17 @@ function reset() {
stack.length = 0
}
function toCharCodes(str: string): number[] {
return str.split('').map(c => c.charCodeAt(0))
}
export function baseParse(input: string, options?: ParserOptions): RootNode {
reset()
const delimiters = options?.delimiters
if (delimiters) {
tokenizer.delimiterOpen = toCharCodes(delimiters[0])
tokenizer.delimiterClose = toCharCodes(delimiters[1])
}
currentInput = input
currentOptions = extend({}, defaultParserOptions, options)
const root = (currentRoot = createRoot([]))