vue3-core/packages/compiler-core/src/parser.ts

993 lines
27 KiB
TypeScript
Raw Normal View History

2023-11-13 21:03:39 +08:00
import {
2023-11-14 18:03:00 +08:00
AttributeNode,
2023-11-14 20:39:08 +08:00
ConstantTypes,
2023-11-14 18:03:00 +08:00
DirectiveNode,
2023-11-13 21:03:39 +08:00
ElementNode,
ElementTypes,
ForParseResult,
2023-11-14 20:39:08 +08:00
Namespaces,
2023-11-13 21:03:39 +08:00
NodeTypes,
RootNode,
2023-11-16 01:31:52 +08:00
SimpleExpressionNode,
2023-11-15 17:45:42 +08:00
SourceLocation,
2023-11-13 21:03:39 +08:00
TemplateChildNode,
createRoot,
createSimpleExpression
2023-11-24 21:53:35 +08:00
} from './ast'
import { ParserOptions } from './options'
2023-11-17 09:22:12 +08:00
import Tokenizer, {
CharCodes,
ParseMode,
QuoteType,
2023-11-22 13:58:50 +08:00
Sequences,
State,
2023-11-17 09:22:12 +08:00
isWhitespace,
toCharCodes
2023-11-24 21:53:35 +08:00
} from './tokenizer'
2023-11-22 15:19:06 +08:00
import {
CompilerCompatOptions,
CompilerDeprecationTypes,
checkCompatEnabled,
isCompatEnabled,
warnDeprecation
2023-11-24 21:53:35 +08:00
} from './compat/compatConfig'
2023-11-14 21:55:16 +08:00
import { NO, extend } from '@vue/shared'
2023-11-20 17:38:00 +08:00
import {
ErrorCodes,
createCompilerError,
defaultOnError,
defaultOnWarn
2023-11-24 21:53:35 +08:00
} from './errors'
import { forAliasRE, isCoreComponent, isStaticArgOf } from './utils'
2023-11-19 10:39:11 +08:00
import { decodeHTML } from 'entities/lib/decode.js'
2023-11-13 21:03:39 +08:00
2023-11-14 20:39:08 +08:00
type OptionalOptions =
2023-11-18 21:39:31 +08:00
| 'decodeEntities'
2023-11-14 20:39:08 +08:00
| 'whitespace'
| 'isNativeTag'
| 'isBuiltInComponent'
| keyof CompilerCompatOptions
2023-11-22 15:19:06 +08:00
export type MergedParserOptions = Omit<
Required<ParserOptions>,
OptionalOptions
> &
2023-11-14 20:39:08 +08:00
Pick<ParserOptions, OptionalOptions>
export const defaultParserOptions: MergedParserOptions = {
2023-11-17 09:22:12 +08:00
parseMode: 'base',
ns: Namespaces.HTML,
2023-11-14 20:39:08 +08:00
delimiters: [`{{`, `}}`],
getNamespace: () => Namespaces.HTML,
isVoidTag: NO,
isPreTag: NO,
isCustomElement: NO,
onError: defaultOnError,
onWarn: defaultOnWarn,
comments: __DEV__
}
let currentOptions: MergedParserOptions = defaultParserOptions
let currentRoot: RootNode | null = null
2023-11-13 21:03:39 +08:00
// parser state
let currentInput = ''
2023-11-22 15:19:06 +08:00
let currentOpenTag: ElementNode | null = null
2023-11-14 18:03:00 +08:00
let currentProp: AttributeNode | DirectiveNode | null = null
let currentAttrValue = ''
2023-11-15 17:45:42 +08:00
let currentAttrStartIndex = -1
let currentAttrEndIndex = -1
2023-11-13 21:03:39 +08:00
let inPre = 0
2023-11-16 01:31:52 +08:00
let inVPre = false
2023-11-17 17:46:47 +08:00
let currentVPreBoundary: ElementNode | null = null
2023-11-14 18:03:00 +08:00
const stack: ElementNode[] = []
2023-11-13 21:03:39 +08:00
2023-11-17 09:22:12 +08:00
const tokenizer = new Tokenizer(stack, {
2023-11-22 13:58:50 +08:00
onerr: emitError,
2023-11-15 23:33:57 +08:00
ontext(start, end) {
onText(getSlice(start, end), start, end)
},
2023-11-13 21:03:39 +08:00
2023-11-19 10:39:11 +08:00
ontextentity(char, start, end) {
onText(char, start, end)
2023-11-15 23:33:57 +08:00
},
2023-11-15 19:36:05 +08:00
2023-11-15 23:33:57 +08:00
oninterpolation(start, end) {
2023-11-16 01:31:52 +08:00
if (inVPre) {
return onText(getSlice(start, end), start, end)
}
2023-11-15 23:33:57 +08:00
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--
}
2023-11-19 10:39:11 +08:00
let exp = getSlice(innerStart, innerEnd)
// decode entities for backwards compat
if (exp.includes('&')) {
if (__BROWSER__) {
exp = currentOptions.decodeEntities!(exp, false)
} else {
exp = decodeHTML(exp)
}
}
2023-11-15 23:33:57 +08:00
addNode({
type: NodeTypes.INTERPOLATION,
2023-11-19 10:39:11 +08:00
content: createSimpleExpression(exp, false, getLoc(innerStart, innerEnd)),
2023-11-15 23:33:57 +08:00
loc: getLoc(start, end)
})
},
onopentagname(start, end) {
2023-11-16 01:31:52 +08:00
const name = getSlice(start, end)
2023-11-22 15:19:06 +08:00
currentOpenTag = {
2023-11-16 01:31:52 +08:00
type: NodeTypes.ELEMENT,
tag: name,
ns: currentOptions.getNamespace(name, stack[0], currentOptions.ns),
2023-11-16 10:54:54 +08:00
tagType: ElementTypes.ELEMENT, // will be refined on tag close
2023-11-16 01:31:52 +08:00
props: [],
children: [],
loc: getLoc(start - 1, end),
2023-11-16 01:31:52 +08:00
codegenNode: undefined
}
if (tokenizer.inSFCRoot) {
// in SFC mode, generate locations for root-level tags' inner content.
currentOpenTag.innerLoc = getLoc(
end + fastForward(end, CharCodes.Gt) + 1,
end
)
}
2023-11-15 23:33:57 +08:00
},
onopentagend(end) {
endOpenTag(end)
},
onclosetag(start, end) {
const name = getSlice(start, end)
if (!currentOptions.isVoidTag(name)) {
2023-11-22 13:58:50 +08:00
let found = false
2023-11-17 17:46:47 +08:00
for (let i = 0; i < stack.length; i++) {
const e = stack[i]
if (e.tag.toLowerCase() === name.toLowerCase()) {
2023-11-22 13:58:50 +08:00
found = true
if (i > 0) {
emitError(ErrorCodes.X_MISSING_END_TAG, stack[0].loc.start.offset)
}
2023-11-17 17:46:47 +08:00
for (let j = 0; j <= i; j++) {
2023-11-22 13:58:50 +08:00
const el = stack.shift()!
onCloseTag(el, end, j < i)
2023-11-17 17:46:47 +08:00
}
break
2023-11-15 23:33:57 +08:00
}
2023-11-15 19:36:05 +08:00
}
2023-11-22 13:58:50 +08:00
if (!found) {
emitError(ErrorCodes.X_INVALID_END_TAG, backTrack(start, CharCodes.Lt))
}
2023-11-15 23:33:57 +08:00
}
},
onselfclosingtag(end) {
2023-11-22 15:19:06 +08:00
const name = currentOpenTag!.tag
currentOpenTag!.isSelfClosing = true
2023-11-20 17:38:00 +08:00
endOpenTag(end)
if (stack[0]?.tag === name) {
onCloseTag(stack.shift()!, end)
}
2023-11-15 23:33:57 +08:00
},
onattribname(start, end) {
// plain attribute
currentProp = {
type: NodeTypes.ATTRIBUTE,
name: getSlice(start, end),
nameLoc: getLoc(start, end),
2023-11-15 23:33:57 +08:00
value: undefined,
loc: getLoc(start)
}
},
ondirname(start, end) {
const raw = getSlice(start, end)
2023-11-22 13:58:50 +08:00
const name =
raw === '.' || raw === ':'
? 'bind'
: raw === '@'
? 'on'
: raw === '#'
? 'slot'
: raw.slice(2)
if (!inVPre && name === '') {
emitError(ErrorCodes.X_MISSING_DIRECTIVE_NAME, start)
}
if (inVPre || name === '') {
2023-11-16 01:31:52 +08:00
currentProp = {
type: NodeTypes.ATTRIBUTE,
name: raw,
nameLoc: getLoc(start, end),
2023-11-16 01:31:52 +08:00
value: undefined,
loc: getLoc(start)
}
} else {
currentProp = {
type: NodeTypes.DIRECTIVE,
name,
rawName: raw,
2023-11-16 01:31:52 +08:00
exp: undefined,
arg: undefined,
2023-11-17 17:46:47 +08:00
modifiers: raw === '.' ? ['prop'] : [],
2023-11-16 01:31:52 +08:00
loc: getLoc(start)
}
if (name === 'pre') {
inVPre = true
2023-11-22 15:19:06 +08:00
currentVPreBoundary = currentOpenTag
2023-11-16 01:31:52 +08:00
// convert dirs before this one to attributes
2023-11-22 15:19:06 +08:00
const props = currentOpenTag!.props
2023-11-16 01:31:52 +08:00
for (let i = 0; i < props.length; i++) {
if (props[i].type === NodeTypes.DIRECTIVE) {
props[i] = dirToAttr(props[i] as DirectiveNode)
}
}
}
2023-11-15 23:33:57 +08:00
}
},
ondirarg(start, end) {
const arg = getSlice(start, end)
2023-11-16 01:31:52 +08:00
if (inVPre) {
;(currentProp as AttributeNode).name += arg
setLocEnd((currentProp as AttributeNode).nameLoc, end)
2023-11-16 01:31:52 +08:00
} else {
const isStatic = arg[0] !== `[`
;(currentProp as DirectiveNode).arg = createSimpleExpression(
2023-11-17 17:46:47 +08:00
isStatic ? arg : arg.slice(1, -1),
2023-11-16 01:31:52 +08:00
isStatic,
getLoc(start, end),
isStatic ? ConstantTypes.CAN_STRINGIFY : ConstantTypes.NOT_CONSTANT
)
2023-11-15 23:33:57 +08:00
}
},
ondirmodifier(start, end) {
2023-11-16 01:31:52 +08:00
const mod = getSlice(start, end)
if (inVPre) {
;(currentProp as AttributeNode).name += '.' + mod
setLocEnd((currentProp as AttributeNode).nameLoc, end)
2023-11-17 17:46:47 +08:00
} else if ((currentProp as DirectiveNode).name === 'slot') {
// slot has no modifiers, special case for edge cases like
// https://github.com/vuejs/language-tools/issues/2710
const arg = (currentProp as DirectiveNode).arg
if (arg) {
;(arg as SimpleExpressionNode).content += '.' + mod
setLocEnd(arg.loc, end)
2023-11-17 17:46:47 +08:00
}
2023-11-16 01:31:52 +08:00
} else {
;(currentProp as DirectiveNode).modifiers.push(mod)
}
2023-11-15 23:33:57 +08:00
},
onattribdata(start, end) {
currentAttrValue += getSlice(start, end)
if (currentAttrStartIndex < 0) currentAttrStartIndex = start
currentAttrEndIndex = end
},
2023-11-19 10:39:11 +08:00
onattribentity(char, start, end) {
2023-11-18 21:39:31 +08:00
currentAttrValue += char
2023-11-19 10:39:11 +08:00
if (currentAttrStartIndex < 0) currentAttrStartIndex = start
currentAttrEndIndex = end
2023-11-15 23:33:57 +08:00
},
onattribnameend(end) {
const start = currentProp!.loc.start.offset
const name = getSlice(start, end)
2023-11-16 01:31:52 +08:00
if (currentProp!.type === NodeTypes.DIRECTIVE) {
currentProp!.rawName = name
2023-11-16 01:31:52 +08:00
}
2023-11-18 12:01:55 +08:00
// check duplicate attrs
if (
2023-11-22 15:19:06 +08:00
currentOpenTag!.props.some(
2023-11-18 12:01:55 +08:00
p => (p.type === NodeTypes.DIRECTIVE ? p.rawName : p.name) === name
)
) {
2023-11-22 13:58:50 +08:00
emitError(ErrorCodes.DUPLICATE_ATTRIBUTE, start)
2023-11-15 23:33:57 +08:00
}
},
onattribend(quote, end) {
2023-11-22 15:19:06 +08:00
if (currentOpenTag && currentProp) {
// finalize end pos
setLocEnd(currentProp.loc, end)
2023-11-22 15:19:06 +08:00
2023-11-17 17:46:47 +08:00
if (quote !== QuoteType.NoValue) {
2023-11-18 21:39:31 +08:00
if (__BROWSER__ && currentAttrValue.includes('&')) {
currentAttrValue = currentOptions.decodeEntities!(
currentAttrValue,
true
)
}
2023-11-22 15:19:06 +08:00
2023-11-15 23:33:57 +08:00
if (currentProp.type === NodeTypes.ATTRIBUTE) {
// assign value
2023-11-17 17:46:47 +08:00
// condense whitespaces in class
if (currentProp!.name === 'class') {
currentAttrValue = condense(currentAttrValue).trim()
}
2023-11-22 13:58:50 +08:00
if (quote === QuoteType.Unquoted && !currentAttrValue) {
emitError(ErrorCodes.MISSING_ATTRIBUTE_VALUE, end)
}
2023-11-15 23:33:57 +08:00
currentProp!.value = {
type: NodeTypes.TEXT,
content: currentAttrValue,
loc:
quote === QuoteType.Unquoted
? getLoc(currentAttrStartIndex, currentAttrEndIndex)
: getLoc(currentAttrStartIndex - 1, currentAttrEndIndex + 1)
}
if (
tokenizer.inSFCRoot &&
2023-11-22 15:19:06 +08:00
currentOpenTag.tag === 'template' &&
currentProp.name === 'lang' &&
currentAttrValue &&
currentAttrValue !== 'html'
) {
// SFC root template with preprocessor lang, force tokenizer to
// RCDATA mode
tokenizer.enterRCDATA(toCharCodes(`</template`), 0)
}
2023-11-18 10:49:29 +08:00
} else {
2023-11-15 23:33:57 +08:00
// directive
currentProp.exp = createSimpleExpression(
currentAttrValue,
false,
getLoc(currentAttrStartIndex, currentAttrEndIndex)
)
if (currentProp.name === 'for') {
currentProp.forParseResult = parseForExpression(currentProp.exp)
2023-11-15 17:45:42 +08:00
}
2023-11-22 15:19:06 +08:00
// 2.x compat v-bind:foo.sync -> v-model:foo
let syncIndex = -1
if (
__COMPAT__ &&
currentProp.name === 'bind' &&
(syncIndex = currentProp.modifiers.indexOf('sync')) > -1 &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_V_BIND_SYNC,
currentOptions,
currentProp.loc,
currentProp.rawName
)
) {
currentProp.name = 'model'
currentProp.modifiers.splice(syncIndex, 1)
}
2023-11-15 17:45:42 +08:00
}
2023-11-13 21:03:39 +08:00
}
2023-11-16 01:31:52 +08:00
if (
currentProp.type !== NodeTypes.DIRECTIVE ||
currentProp.name !== 'pre'
) {
2023-11-22 15:19:06 +08:00
currentOpenTag.props.push(currentProp)
2023-11-16 01:31:52 +08:00
}
2023-11-13 21:03:39 +08:00
}
2023-11-15 23:33:57 +08:00
currentAttrValue = ''
currentAttrStartIndex = currentAttrEndIndex = -1
},
2023-11-16 11:05:31 +08:00
oncomment(start, end) {
if (currentOptions.comments) {
addNode({
type: NodeTypes.COMMENT,
content: getSlice(start, end),
loc: getLoc(start - 4, end + 3)
})
}
2023-11-15 23:33:57 +08:00
},
onend() {
2023-11-22 13:58:50 +08:00
const end = currentInput.length
// EOF ERRORS
if ((__DEV__ || !__BROWSER__) && tokenizer.state !== State.Text) {
switch (tokenizer.state) {
case State.BeforeTagName:
case State.BeforeClosingTagName:
emitError(ErrorCodes.EOF_BEFORE_TAG_NAME, end)
break
case State.Interpolation:
case State.InterpolationClose:
emitError(
ErrorCodes.X_MISSING_INTERPOLATION_END,
tokenizer.sectionStart
)
break
case State.InCommentLike:
if (tokenizer.currentSequence === Sequences.CdataEnd) {
emitError(ErrorCodes.EOF_IN_CDATA, end)
} else {
emitError(ErrorCodes.EOF_IN_COMMENT, end)
}
break
case State.InTagName:
case State.InSelfClosingTag:
case State.InClosingTagName:
2023-11-22 14:01:20 +08:00
case State.BeforeAttrName:
case State.InAttrName:
case State.InDirName:
case State.InDirArg:
case State.InDirDynamicArg:
case State.InDirModifier:
case State.AfterAttrName:
case State.BeforeAttrValue:
case State.InAttrValueDq: // "
case State.InAttrValueSq: // '
case State.InAttrValueNq:
2023-11-22 13:58:50 +08:00
emitError(ErrorCodes.EOF_IN_TAG, end)
break
default:
// console.log(tokenizer.state)
break
}
2023-11-20 17:38:00 +08:00
}
2023-11-15 23:33:57 +08:00
for (let index = 0; index < stack.length; index++) {
2023-11-22 13:58:50 +08:00
onCloseTag(stack[index], end - 1)
emitError(ErrorCodes.X_MISSING_END_TAG, stack[index].loc.start.offset)
2023-11-15 23:33:57 +08:00
}
},
2023-11-16 11:05:31 +08:00
oncdata(start, end) {
2023-11-19 10:39:11 +08:00
if (stack[0].ns !== Namespaces.HTML) {
onText(getSlice(start, end), start, end)
} else {
2023-11-22 13:58:50 +08:00
emitError(ErrorCodes.CDATA_IN_HTML_CONTENT, start - 9)
}
},
onprocessinginstruction(start) {
// ignore as we do not have runtime handling for this, only check error
if ((stack[0] ? stack[0].ns : currentOptions.ns) === Namespaces.HTML) {
emitError(
ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
start - 1
)
2023-11-19 10:39:11 +08:00
}
2023-11-13 21:03:39 +08:00
}
2023-11-15 23:33:57 +08:00
})
2023-11-13 21:03:39 +08:00
// This regex doesn't cover the case if key or index aliases have destructuring,
// but those do not make sense in the first place, so this works in practice.
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
function parseForExpression(
input: SimpleExpressionNode
): ForParseResult | undefined {
const loc = input.loc
const exp = input.content
const inMatch = exp.match(forAliasRE)
if (!inMatch) return
const [, LHS, RHS] = inMatch
const createAliasExpression = (content: string, offset: number) => {
const start = loc.start.offset + offset
const end = start + content.length
return createSimpleExpression(content, false, getLoc(start, end))
}
const result: ForParseResult = {
source: createAliasExpression(RHS.trim(), exp.indexOf(RHS, LHS.length)),
value: undefined,
key: undefined,
index: undefined,
finalized: false
}
let valueContent = LHS.trim().replace(stripParensRE, '').trim()
const trimmedOffset = LHS.indexOf(valueContent)
const iteratorMatch = valueContent.match(forIteratorRE)
if (iteratorMatch) {
valueContent = valueContent.replace(forIteratorRE, '').trim()
const keyContent = iteratorMatch[1].trim()
let keyOffset: number | undefined
if (keyContent) {
keyOffset = exp.indexOf(keyContent, trimmedOffset + valueContent.length)
result.key = createAliasExpression(keyContent, keyOffset)
}
if (iteratorMatch[2]) {
const indexContent = iteratorMatch[2].trim()
if (indexContent) {
result.index = createAliasExpression(
indexContent,
exp.indexOf(
indexContent,
result.key
? keyOffset! + keyContent.length
: trimmedOffset + valueContent.length
)
)
}
}
}
if (valueContent) {
result.value = createAliasExpression(valueContent, trimmedOffset)
}
return result
}
2023-11-13 21:03:39 +08:00
function getSlice(start: number, end: number) {
return currentInput.slice(start, end)
}
2023-11-14 20:39:08 +08:00
function endOpenTag(end: number) {
2023-11-22 15:19:06 +08:00
addNode(currentOpenTag!)
const { tag, ns } = currentOpenTag!
2023-11-19 11:46:44 +08:00
if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) {
2023-11-15 23:55:50 +08:00
inPre++
}
2023-11-19 11:46:44 +08:00
if (currentOptions.isVoidTag(tag)) {
2023-11-22 15:19:06 +08:00
onCloseTag(currentOpenTag!, end)
2023-11-15 23:55:50 +08:00
} else {
2023-11-22 15:19:06 +08:00
stack.unshift(currentOpenTag!)
2023-11-19 11:46:44 +08:00
if (ns === Namespaces.SVG || ns === Namespaces.MATH_ML) {
tokenizer.inXML = true
}
2023-11-13 21:03:39 +08:00
}
2023-11-22 15:19:06 +08:00
currentOpenTag = null
2023-11-13 21:03:39 +08:00
}
2023-11-14 01:14:33 +08:00
function onText(content: string, start: number, end: number) {
2023-11-22 13:58:50 +08:00
if (__BROWSER__) {
const tag = stack[0]?.tag
if (tag !== 'script' && tag !== 'style' && content.includes('&')) {
content = currentOptions.decodeEntities!(content, false)
}
2023-11-18 21:39:31 +08:00
}
2023-11-19 10:39:11 +08:00
const parent = stack[0] || currentRoot
2023-11-13 21:03:39 +08:00
const lastNode = parent.children[parent.children.length - 1]
if (lastNode?.type === NodeTypes.TEXT) {
// merge
lastNode.content += content
setLocEnd(lastNode.loc, end)
2023-11-13 21:03:39 +08:00
} else {
parent.children.push({
type: NodeTypes.TEXT,
content,
loc: getLoc(start, end)
2023-11-13 21:03:39 +08:00
})
}
}
2023-11-22 13:58:50 +08:00
function onCloseTag(el: ElementNode, end: number, isImplied = false) {
2023-11-14 18:03:00 +08:00
// attach end position
if (isImplied) {
2023-11-22 13:58:50 +08:00
// implied close, end should be backtracked to close
setLocEnd(el.loc, backTrack(end, CharCodes.Lt))
2023-11-20 17:38:00 +08:00
} else {
setLocEnd(el.loc, end + fastForward(end, CharCodes.Gt) + 1)
2023-11-13 21:03:39 +08:00
}
2023-11-16 10:54:54 +08:00
if (tokenizer.inSFCRoot) {
// SFC root tag, resolve inner end
if (el.children.length) {
el.innerLoc!.end = extend({}, el.children[el.children.length - 1].loc.end)
} else {
el.innerLoc!.end = extend({}, el.innerLoc!.start)
}
el.innerLoc!.source = getSlice(
el.innerLoc!.start.offset,
el.innerLoc!.end.offset
)
}
2023-11-16 10:54:54 +08:00
// refine element type
2023-11-19 11:46:44 +08:00
const { tag, ns } = el
2023-11-16 10:54:54 +08:00
if (!inVPre) {
if (tag === 'slot') {
el.tagType = ElementTypes.SLOT
} else if (isFragmentTemplate(el)) {
el.tagType = ElementTypes.TEMPLATE
} else if (isComponent(el)) {
el.tagType = ElementTypes.COMPONENT
}
}
2023-11-13 21:03:39 +08:00
// whitepsace management
2023-11-17 17:46:47 +08:00
if (!tokenizer.inRCDATA) {
el.children = condenseWhitespace(el.children, el.tag)
}
2023-11-19 11:46:44 +08:00
if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) {
2023-11-15 23:55:50 +08:00
inPre--
}
2023-11-17 17:46:47 +08:00
if (currentVPreBoundary === el) {
2023-11-16 01:31:52 +08:00
inVPre = false
2023-11-17 17:46:47 +08:00
currentVPreBoundary = null
2023-11-16 01:31:52 +08:00
}
2023-11-19 11:46:44 +08:00
if (
tokenizer.inXML &&
(stack[0] ? stack[0].ns : currentOptions.ns) === Namespaces.HTML
) {
tokenizer.inXML = false
}
2023-11-22 15:19:06 +08:00
// 2.x compat / deprecation checks
if (__COMPAT__) {
const props = el.props
if (
__DEV__ &&
isCompatEnabled(
CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE,
currentOptions
)
) {
let hasIf = false
let hasFor = false
for (let i = 0; i < props.length; i++) {
const p = props[i]
if (p.type === NodeTypes.DIRECTIVE) {
if (p.name === 'if') {
hasIf = true
} else if (p.name === 'for') {
hasFor = true
}
}
if (hasIf && hasFor) {
warnDeprecation(
CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE,
currentOptions,
el.loc
)
break
}
}
}
if (
isCompatEnabled(
CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE,
currentOptions
) &&
el.tag === 'template' &&
!isFragmentTemplate(el)
) {
__DEV__ &&
warnDeprecation(
CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE,
currentOptions,
el.loc
)
// unwrap
const parent = stack[0] || currentRoot
const index = parent.children.indexOf(el)
parent.children.splice(index, 1, ...el.children)
}
const inlineTemplateProp = props.find(
p => p.type === NodeTypes.ATTRIBUTE && p.name === 'inline-template'
) as AttributeNode
if (
inlineTemplateProp &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_INLINE_TEMPLATE,
currentOptions,
inlineTemplateProp.loc
) &&
el.children.length
) {
inlineTemplateProp.value = {
type: NodeTypes.TEXT,
content: getSlice(
el.children[0].loc.start.offset,
el.children[el.children.length - 1].loc.end.offset
),
loc: inlineTemplateProp.loc
}
}
}
2023-11-14 01:14:33 +08:00
}
2023-11-20 17:38:00 +08:00
function fastForward(start: number, c: number) {
let offset = 0
while (
currentInput.charCodeAt(start + offset) !== CharCodes.Gt &&
start + offset < currentInput.length
) {
offset++
}
return offset
}
2023-11-22 13:58:50 +08:00
function backTrack(index: number, c: number) {
let i = index
while (currentInput.charCodeAt(i) !== c && i >= 0) i--
return i
}
2023-11-16 10:54:54 +08:00
const specialTemplateDir = new Set(['if', 'else', 'else-if', 'for', 'slot'])
function isFragmentTemplate({ tag, props }: ElementNode): boolean {
if (tag === 'template') {
for (let i = 0; i < props.length; i++) {
if (
props[i].type === NodeTypes.DIRECTIVE &&
specialTemplateDir.has((props[i] as DirectiveNode).name)
2023-11-16 10:54:54 +08:00
) {
return true
}
}
}
return false
}
2023-11-14 01:14:33 +08:00
2023-11-16 10:54:54 +08:00
function isComponent({ tag, props }: ElementNode): boolean {
if (currentOptions.isCustomElement(tag)) {
return false
}
if (
tag === 'component' ||
isUpperCase(tag.charCodeAt(0)) ||
isCoreComponent(tag) ||
currentOptions.isBuiltInComponent?.(tag) ||
2023-11-17 17:46:47 +08:00
(currentOptions.isNativeTag && !currentOptions.isNativeTag(tag))
2023-11-16 10:54:54 +08:00
) {
return true
}
// at this point the tag should be a native tag, but check for potential "is"
// casting
for (let i = 0; i < props.length; i++) {
const p = props[i]
if (p.type === NodeTypes.ATTRIBUTE) {
if (p.name === 'is' && p.value) {
if (p.value.content.startsWith('vue:')) {
return true
2023-11-22 15:19:06 +08:00
} else if (
__COMPAT__ &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
currentOptions,
p.loc
)
) {
return true
2023-11-16 10:54:54 +08:00
}
}
2023-11-22 15:19:06 +08:00
} else if (
__COMPAT__ &&
// :is on plain element - only treat as component in compat mode
p.name === 'bind' &&
isStaticArgOf(p.arg, 'is') &&
checkCompatEnabled(
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
currentOptions,
p.loc
)
) {
return true
2023-11-16 10:54:54 +08:00
}
}
return false
}
function isUpperCase(c: number) {
return c > 64 && c < 91
}
const windowsNewlineRE = /\r\n/g
2023-11-17 17:46:47 +08:00
function condenseWhitespace(
nodes: TemplateChildNode[],
tag?: string
): TemplateChildNode[] {
2023-11-13 21:03:39 +08:00
const shouldCondense = currentOptions.whitespace !== 'preserve'
let removedWhitespace = false
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.type === NodeTypes.TEXT) {
if (!inPre) {
2023-11-14 21:55:16 +08:00
if (isAllWhitespace(node.content)) {
const prev = nodes[i - 1]?.type
const next = nodes[i + 1]?.type
2023-11-13 21:03:39 +08:00
// Remove if:
// - the whitespace is the first or last node, or:
2023-11-14 21:55:16 +08:00
// - (condense mode) the whitespace is between two comments, or:
2023-11-13 21:03:39 +08:00
// - (condense mode) the whitespace is between comment and element, or:
// - (condense mode) the whitespace is between two elements AND contains newline
if (
!prev ||
!next ||
(shouldCondense &&
2023-11-14 21:55:16 +08:00
((prev === NodeTypes.COMMENT &&
(next === NodeTypes.COMMENT || next === NodeTypes.ELEMENT)) ||
(prev === NodeTypes.ELEMENT &&
(next === NodeTypes.COMMENT ||
(next === NodeTypes.ELEMENT &&
hasNewlineChar(node.content))))))
2023-11-13 21:03:39 +08:00
) {
removedWhitespace = true
nodes[i] = null as any
} else {
// Otherwise, the whitespace is condensed into a single space
node.content = ' '
}
} else if (shouldCondense) {
// in condense mode, consecutive whitespaces in text are condensed
// down to a single space.
2023-11-14 21:55:16 +08:00
node.content = condense(node.content)
2023-11-13 21:03:39 +08:00
}
} else {
// #6410 normalize windows newlines in <pre>:
// in SSR, browsers normalize server-rendered \r\n into a single \n
// in the DOM
2023-11-14 01:14:33 +08:00
node.content = node.content.replace(windowsNewlineRE, '\n')
2023-11-13 21:03:39 +08:00
}
}
}
2023-11-17 17:46:47 +08:00
if (inPre && tag && currentOptions.isPreTag(tag)) {
// remove leading newline per html spec
// https://html.spec.whatwg.org/multipage/grouping-content.html#the-pre-element
const first = nodes[0]
if (first && first.type === NodeTypes.TEXT) {
first.content = first.content.replace(/^\r?\n/, '')
}
}
2023-11-14 01:14:33 +08:00
return removedWhitespace ? nodes.filter(Boolean) : nodes
2023-11-13 21:03:39 +08:00
}
2023-11-14 21:55:16 +08:00
function isAllWhitespace(str: string) {
for (let i = 0; i < str.length; i++) {
if (!isWhitespace(str.charCodeAt(i))) {
return false
}
}
return true
}
function hasNewlineChar(str: string) {
for (let i = 0; i < str.length; i++) {
const c = str.charCodeAt(i)
if (c === CharCodes.NewLine || c === CharCodes.CarriageReturn) {
return true
}
}
return false
}
function condense(str: string) {
let ret = ''
let prevCharIsWhitespace = false
for (let i = 0; i < str.length; i++) {
if (isWhitespace(str.charCodeAt(i))) {
if (!prevCharIsWhitespace) {
ret += ' '
prevCharIsWhitespace = true
}
} else {
ret += str[i]
prevCharIsWhitespace = false
}
}
return ret
}
2023-11-13 21:03:39 +08:00
function addNode(node: TemplateChildNode) {
2023-11-19 10:39:11 +08:00
;(stack[0] || currentRoot).children.push(node)
2023-11-13 21:03:39 +08:00
}
2023-11-15 17:45:42 +08:00
function getLoc(start: number, end?: number): SourceLocation {
return {
2023-11-15 19:36:05 +08:00
start: tokenizer.getPos(start),
2023-11-15 17:45:42 +08:00
// @ts-expect-error allow late attachment
end: end == null ? end : tokenizer.getPos(end),
// @ts-expect-error allow late attachment
source: end == null ? end : getSlice(start, end)
2023-11-14 21:55:16 +08:00
}
}
function setLocEnd(loc: SourceLocation, end: number) {
loc.end = tokenizer.getPos(end)
loc.source = getSlice(loc.start.offset, end)
}
2023-11-16 01:31:52 +08:00
function dirToAttr(dir: DirectiveNode): AttributeNode {
const attr: AttributeNode = {
type: NodeTypes.ATTRIBUTE,
name: dir.rawName!,
nameLoc: getLoc(
dir.loc.start.offset,
dir.loc.start.offset + dir.rawName!.length
),
2023-11-16 01:31:52 +08:00
value: undefined,
loc: dir.loc
}
if (dir.exp) {
// account for quotes
const loc = dir.exp.loc
if (loc.end.offset < dir.loc.end.offset) {
loc.start.offset--
loc.start.column--
loc.end.offset++
loc.end.column++
}
attr.value = {
type: NodeTypes.TEXT,
content: (dir.exp as SimpleExpressionNode).content,
loc
}
}
return attr
}
2023-11-22 13:58:50 +08:00
function emitError(code: ErrorCodes, index: number) {
currentOptions.onError(createCompilerError(code, getLoc(index, index)))
}
2023-11-13 21:03:39 +08:00
function reset() {
tokenizer.reset()
2023-11-22 15:19:06 +08:00
currentOpenTag = null
2023-11-14 18:03:00 +08:00
currentProp = null
currentAttrValue = ''
2023-11-15 17:45:42 +08:00
currentAttrStartIndex = -1
currentAttrEndIndex = -1
2023-11-13 21:03:39 +08:00
stack.length = 0
}
2023-11-12 21:42:27 +08:00
2023-11-14 20:39:08 +08:00
export function baseParse(input: string, options?: ParserOptions): RootNode {
2023-11-13 21:03:39 +08:00
reset()
2023-11-17 09:22:12 +08:00
currentInput = input
2023-11-22 15:19:06 +08:00
currentOptions = extend({}, defaultParserOptions)
if (options) {
let key: keyof ParserOptions
for (key in options) {
if (options[key] != null) {
// @ts-ignore
currentOptions[key] = options[key]
}
}
}
2023-11-17 09:22:12 +08:00
2023-11-18 21:39:31 +08:00
if (__DEV__) {
if (!__BROWSER__ && currentOptions.decodeEntities) {
console.warn(
`[@vue/compiler-core] decodeEntities option is passed but will be ` +
`ignored in non-browser builds.`
)
} else if (__BROWSER__ && !currentOptions.decodeEntities) {
throw new Error(
`[@vue/compiler-core] decodeEntities option is required in browser builds.`
)
}
}
2023-11-17 09:22:12 +08:00
tokenizer.mode =
currentOptions.parseMode === 'html'
? ParseMode.HTML
: currentOptions.parseMode === 'sfc'
? ParseMode.SFC
: ParseMode.BASE
2023-11-17 09:22:12 +08:00
2023-11-15 23:33:57 +08:00
const delimiters = options?.delimiters
if (delimiters) {
tokenizer.delimiterOpen = toCharCodes(delimiters[0])
tokenizer.delimiterClose = toCharCodes(delimiters[1])
}
2023-11-17 09:22:12 +08:00
const root = (currentRoot = createRoot([], input))
2023-11-13 21:03:39 +08:00
tokenizer.parse(currentInput)
2023-11-18 10:49:29 +08:00
root.loc = getLoc(0, input.length)
2023-11-14 01:14:33 +08:00
root.children = condenseWhitespace(root.children)
currentRoot = null
2023-11-12 16:58:24 +08:00
return root
}