|
|
@ -24,7 +24,13 @@ import Tokenizer, { |
|
|
|
isWhitespace, |
|
|
|
toCharCodes |
|
|
|
} from './Tokenizer' |
|
|
|
import { CompilerCompatOptions } from '../compat/compatConfig' |
|
|
|
import { |
|
|
|
CompilerCompatOptions, |
|
|
|
CompilerDeprecationTypes, |
|
|
|
checkCompatEnabled, |
|
|
|
isCompatEnabled, |
|
|
|
warnDeprecation |
|
|
|
} from '../compat/compatConfig' |
|
|
|
import { NO, extend } from '@vue/shared' |
|
|
|
import { |
|
|
|
ErrorCodes, |
|
|
@ -32,7 +38,7 @@ import { |
|
|
|
defaultOnError, |
|
|
|
defaultOnWarn |
|
|
|
} from '../errors' |
|
|
|
import { forAliasRE, isCoreComponent } from '../utils' |
|
|
|
import { forAliasRE, isCoreComponent, isStaticArgOf } from '../utils' |
|
|
|
import { decodeHTML } from 'entities/lib/decode.js' |
|
|
|
|
|
|
|
type OptionalOptions = |
|
|
@ -42,7 +48,10 @@ type OptionalOptions = |
|
|
|
| 'isBuiltInComponent' |
|
|
|
| keyof CompilerCompatOptions |
|
|
|
|
|
|
|
type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> & |
|
|
|
export type MergedParserOptions = Omit< |
|
|
|
Required<ParserOptions>, |
|
|
|
OptionalOptions |
|
|
|
> & |
|
|
|
Pick<ParserOptions, OptionalOptions> |
|
|
|
|
|
|
|
export const defaultParserOptions: MergedParserOptions = { |
|
|
@ -63,7 +72,7 @@ let currentRoot: RootNode | null = null |
|
|
|
|
|
|
|
// parser state
|
|
|
|
let currentInput = '' |
|
|
|
let currentElement: ElementNode | null = null |
|
|
|
let currentOpenTag: ElementNode | null = null |
|
|
|
let currentProp: AttributeNode | DirectiveNode | null = null |
|
|
|
let currentAttrValue = '' |
|
|
|
let currentAttrStartIndex = -1 |
|
|
@ -118,7 +127,7 @@ const tokenizer = new Tokenizer(stack, { |
|
|
|
const startIndex = tokenizer.inSFCRoot |
|
|
|
? end + fastForward(end, CharCodes.Gt) + 1 |
|
|
|
: start - 1 |
|
|
|
currentElement = { |
|
|
|
currentOpenTag = { |
|
|
|
type: NodeTypes.ELEMENT, |
|
|
|
tag: name, |
|
|
|
ns: currentOptions.getNamespace(name, stack[0], currentOptions.ns), |
|
|
@ -159,7 +168,7 @@ const tokenizer = new Tokenizer(stack, { |
|
|
|
}, |
|
|
|
|
|
|
|
onselfclosingtag(end) { |
|
|
|
const name = currentElement!.tag |
|
|
|
const name = currentOpenTag!.tag |
|
|
|
endOpenTag(end) |
|
|
|
if (stack[0]?.tag === name) { |
|
|
|
onCloseTag(stack.shift()!, end) |
|
|
@ -213,9 +222,9 @@ const tokenizer = new Tokenizer(stack, { |
|
|
|
} |
|
|
|
if (name === 'pre') { |
|
|
|
inVPre = true |
|
|
|
currentVPreBoundary = currentElement |
|
|
|
currentVPreBoundary = currentOpenTag |
|
|
|
// convert dirs before this one to attributes
|
|
|
|
const props = currentElement!.props |
|
|
|
const props = currentOpenTag!.props |
|
|
|
for (let i = 0; i < props.length; i++) { |
|
|
|
if (props[i].type === NodeTypes.DIRECTIVE) { |
|
|
|
props[i] = dirToAttr(props[i] as DirectiveNode) |
|
|
@ -279,7 +288,7 @@ const tokenizer = new Tokenizer(stack, { |
|
|
|
} |
|
|
|
// check duplicate attrs
|
|
|
|
if ( |
|
|
|
currentElement!.props.some( |
|
|
|
currentOpenTag!.props.some( |
|
|
|
p => (p.type === NodeTypes.DIRECTIVE ? p.rawName : p.name) === name |
|
|
|
) |
|
|
|
) { |
|
|
@ -288,7 +297,10 @@ const tokenizer = new Tokenizer(stack, { |
|
|
|
}, |
|
|
|
|
|
|
|
onattribend(quote, end) { |
|
|
|
if (currentElement && currentProp) { |
|
|
|
if (currentOpenTag && currentProp) { |
|
|
|
// finalize end pos
|
|
|
|
currentProp.loc.end = tokenizer.getPos(end) |
|
|
|
|
|
|
|
if (quote !== QuoteType.NoValue) { |
|
|
|
if (__BROWSER__ && currentAttrValue.includes('&')) { |
|
|
|
currentAttrValue = currentOptions.decodeEntities!( |
|
|
@ -296,6 +308,7 @@ const tokenizer = new Tokenizer(stack, { |
|
|
|
true |
|
|
|
) |
|
|
|
} |
|
|
|
|
|
|
|
if (currentProp.type === NodeTypes.ATTRIBUTE) { |
|
|
|
// assign value
|
|
|
|
|
|
|
@ -318,7 +331,7 @@ const tokenizer = new Tokenizer(stack, { |
|
|
|
} |
|
|
|
if ( |
|
|
|
tokenizer.inSFCRoot && |
|
|
|
currentElement.tag === 'template' && |
|
|
|
currentOpenTag.tag === 'template' && |
|
|
|
currentProp.name === 'lang' && |
|
|
|
currentAttrValue && |
|
|
|
currentAttrValue !== 'html' |
|
|
@ -338,14 +351,29 @@ const tokenizer = new Tokenizer(stack, { |
|
|
|
if (currentProp.name === 'for') { |
|
|
|
currentProp.forParseResult = parseForExpression(currentProp.exp) |
|
|
|
} |
|
|
|
// 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) |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
currentProp.loc.end = tokenizer.getPos(end) |
|
|
|
if ( |
|
|
|
currentProp.type !== NodeTypes.DIRECTIVE || |
|
|
|
currentProp.name !== 'pre' |
|
|
|
) { |
|
|
|
currentElement.props.push(currentProp) |
|
|
|
currentOpenTag.props.push(currentProp) |
|
|
|
} |
|
|
|
} |
|
|
|
currentAttrValue = '' |
|
|
@ -503,20 +531,20 @@ function getSlice(start: number, end: number) { |
|
|
|
} |
|
|
|
|
|
|
|
function endOpenTag(end: number) { |
|
|
|
addNode(currentElement!) |
|
|
|
const { tag, ns } = currentElement! |
|
|
|
addNode(currentOpenTag!) |
|
|
|
const { tag, ns } = currentOpenTag! |
|
|
|
if (ns === Namespaces.HTML && currentOptions.isPreTag(tag)) { |
|
|
|
inPre++ |
|
|
|
} |
|
|
|
if (currentOptions.isVoidTag(tag)) { |
|
|
|
onCloseTag(currentElement!, end) |
|
|
|
onCloseTag(currentOpenTag!, end) |
|
|
|
} else { |
|
|
|
stack.unshift(currentElement!) |
|
|
|
stack.unshift(currentOpenTag!) |
|
|
|
if (ns === Namespaces.SVG || ns === Namespaces.MATH_ML) { |
|
|
|
tokenizer.inXML = true |
|
|
|
} |
|
|
|
} |
|
|
|
currentElement = null |
|
|
|
currentOpenTag = null |
|
|
|
} |
|
|
|
|
|
|
|
function onText(content: string, start: number, end: number) { |
|
|
@ -586,6 +614,81 @@ function onCloseTag(el: ElementNode, end: number, isImplied = false) { |
|
|
|
) { |
|
|
|
tokenizer.inXML = false |
|
|
|
} |
|
|
|
|
|
|
|
// 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 |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function fastForward(start: number, c: number) { |
|
|
@ -641,32 +744,30 @@ function isComponent({ tag, props }: ElementNode): boolean { |
|
|
|
if (p.name === 'is' && p.value) { |
|
|
|
if (p.value.content.startsWith('vue:')) { |
|
|
|
return true |
|
|
|
} else if ( |
|
|
|
__COMPAT__ && |
|
|
|
checkCompatEnabled( |
|
|
|
CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT, |
|
|
|
currentOptions, |
|
|
|
p.loc |
|
|
|
) |
|
|
|
) { |
|
|
|
return true |
|
|
|
} |
|
|
|
// TODO else if (
|
|
|
|
// __COMPAT__ &&
|
|
|
|
// checkCompatEnabled(
|
|
|
|
// CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
|
|
|
|
// context,
|
|
|
|
// p.loc
|
|
|
|
// )
|
|
|
|
// ) {
|
|
|
|
// return true
|
|
|
|
// }
|
|
|
|
} |
|
|
|
} 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 |
|
|
|
} |
|
|
|
// TODO 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,
|
|
|
|
// context,
|
|
|
|
// p.loc
|
|
|
|
// )
|
|
|
|
// ) {
|
|
|
|
// return true
|
|
|
|
// }
|
|
|
|
} |
|
|
|
return false |
|
|
|
} |
|
|
@ -818,7 +919,7 @@ function emitError(code: ErrorCodes, index: number) { |
|
|
|
|
|
|
|
function reset() { |
|
|
|
tokenizer.reset() |
|
|
|
currentElement = null |
|
|
|
currentOpenTag = null |
|
|
|
currentProp = null |
|
|
|
currentAttrValue = '' |
|
|
|
currentAttrStartIndex = -1 |
|
|
@ -829,7 +930,17 @@ function reset() { |
|
|
|
export function baseParse(input: string, options?: ParserOptions): RootNode { |
|
|
|
reset() |
|
|
|
currentInput = input |
|
|
|
currentOptions = extend({}, defaultParserOptions, options) |
|
|
|
currentOptions = extend({}, defaultParserOptions) |
|
|
|
|
|
|
|
if (options) { |
|
|
|
let key: keyof ParserOptions |
|
|
|
for (key in options) { |
|
|
|
if (options[key] != null) { |
|
|
|
// @ts-ignore
|
|
|
|
currentOptions[key] = options[key] |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
if (__DEV__) { |
|
|
|
if (!__BROWSER__ && currentOptions.decodeEntities) { |
|
|
|