mirror of https://github.com/vuejs/core.git
wip: parse directives
This commit is contained in:
parent
f131a4723b
commit
9aa8effec3
|
|
@ -1,9 +1,12 @@
|
|||
import { fromCodePoint } from 'entities/lib/decode.js'
|
||||
import {
|
||||
AttributeNode,
|
||||
ConstantTypes,
|
||||
DirectiveNode,
|
||||
ElementNode,
|
||||
ElementTypes,
|
||||
ExpressionNode,
|
||||
Namespaces,
|
||||
NodeTypes,
|
||||
RootNode,
|
||||
TemplateChildNode,
|
||||
|
|
@ -11,28 +14,50 @@ import {
|
|||
} from '../ast'
|
||||
import { ParserOptions } from '../options'
|
||||
import Tokenizer, { CharCodes } from './Tokenizer'
|
||||
import { CompilerCompatOptions } from '../compat/compatConfig'
|
||||
import { NO, extend, hasOwn } from '@vue/shared'
|
||||
import { defaultOnError, defaultOnWarn } from '../errors'
|
||||
|
||||
const voidElements = new Set([
|
||||
'area',
|
||||
'base',
|
||||
'basefont',
|
||||
'br',
|
||||
'col',
|
||||
'command',
|
||||
'embed',
|
||||
'frame',
|
||||
'hr',
|
||||
'img',
|
||||
'input',
|
||||
'isindex',
|
||||
'keygen',
|
||||
'link',
|
||||
'meta',
|
||||
'param',
|
||||
'source',
|
||||
'track',
|
||||
'wbr'
|
||||
])
|
||||
type OptionalOptions =
|
||||
| 'htmlMode'
|
||||
| 'getTextMode' // TODO
|
||||
| 'whitespace'
|
||||
| 'isNativeTag'
|
||||
| 'isBuiltInComponent'
|
||||
| keyof CompilerCompatOptions
|
||||
|
||||
type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
|
||||
Pick<ParserOptions, OptionalOptions>
|
||||
|
||||
// The default decoder only provides escapes for characters reserved as part of
|
||||
// the template syntax, and is only used if the custom renderer did not provide
|
||||
// a platform-specific decoder.
|
||||
const decodeRE = /&(gt|lt|amp|apos|quot);/g
|
||||
const decodeMap: Record<string, string> = {
|
||||
gt: '>',
|
||||
lt: '<',
|
||||
amp: '&',
|
||||
apos: "'",
|
||||
quot: '"'
|
||||
}
|
||||
|
||||
export const defaultParserOptions: MergedParserOptions = {
|
||||
delimiters: [`{{`, `}}`],
|
||||
getNamespace: () => Namespaces.HTML,
|
||||
// getTextMode: () => TextModes.DATA,
|
||||
isVoidTag: NO,
|
||||
isPreTag: NO,
|
||||
isCustomElement: NO,
|
||||
decodeEntities: (rawText: string): string =>
|
||||
rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),
|
||||
onError: defaultOnError,
|
||||
onWarn: defaultOnWarn,
|
||||
comments: __DEV__
|
||||
}
|
||||
|
||||
const directiveTestRE = /^(v-[A-Za-z0-9-]|:|\.|@|#)/
|
||||
const directiveParseRE =
|
||||
/(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i
|
||||
|
||||
const foreignContextElements = new Set(['math', 'svg'])
|
||||
|
||||
|
|
@ -48,7 +73,7 @@ const htmlIntegrationElements = new Set([
|
|||
'title'
|
||||
])
|
||||
|
||||
let currentOptions: ParserOptions = {}
|
||||
let currentOptions: MergedParserOptions = defaultParserOptions
|
||||
let currentRoot: RootNode = createRoot([])
|
||||
|
||||
// parser state
|
||||
|
|
@ -57,8 +82,9 @@ let currentInput = ''
|
|||
let currentElement: ElementNode | null = null
|
||||
let currentProp: AttributeNode | DirectiveNode | null = null
|
||||
let currentAttrValue = ''
|
||||
let currentAttrs: Record<string, true> | null = null
|
||||
let inPre = 0
|
||||
// let inVPre = 0
|
||||
let inVPre = 0
|
||||
const stack: ElementNode[] = []
|
||||
const foreignContext: boolean[] = [false]
|
||||
|
||||
|
|
@ -79,7 +105,7 @@ const tokenizer = new Tokenizer(
|
|||
},
|
||||
|
||||
onopentagend(end) {
|
||||
endOpenTag()
|
||||
endOpenTag(end)
|
||||
},
|
||||
|
||||
onclosetag(start, end) {
|
||||
|
|
@ -92,7 +118,7 @@ const tokenizer = new Tokenizer(
|
|||
foreignContext.shift()
|
||||
}
|
||||
|
||||
if (!voidElements.has(name)) {
|
||||
if (!currentOptions.isVoidTag?.(name)) {
|
||||
const pos = stack.findIndex(e => e.tag === name)
|
||||
if (pos !== -1) {
|
||||
for (let index = 0; index <= pos; index++) {
|
||||
|
|
@ -116,16 +142,114 @@ const tokenizer = new Tokenizer(
|
|||
},
|
||||
|
||||
onattribname(start, end) {
|
||||
// TODO directives
|
||||
currentProp = {
|
||||
type: NodeTypes.ATTRIBUTE,
|
||||
name: getSlice(start, end),
|
||||
value: undefined,
|
||||
loc: {
|
||||
start: tokenizer.getPositionForIndex(start),
|
||||
// @ts-expect-error to be attached on attribute end
|
||||
end: undefined,
|
||||
source: ''
|
||||
const name = getSlice(start, end)
|
||||
if (hasOwn(currentAttrs!, name)) {
|
||||
// TODO emit error DUPLICATE_ATTRIBUTE
|
||||
} else {
|
||||
currentAttrs![name] = true
|
||||
}
|
||||
if (!inVPre && directiveTestRE.test(name)) {
|
||||
// directive
|
||||
const match = directiveParseRE.exec(name)!
|
||||
const firstChar = name[0]
|
||||
const isPropShorthand = firstChar === '.'
|
||||
const dirName =
|
||||
match[1] ||
|
||||
(isPropShorthand || firstChar === ':'
|
||||
? 'bind'
|
||||
: firstChar === '@'
|
||||
? 'on'
|
||||
: 'slot')
|
||||
|
||||
let arg: ExpressionNode | undefined
|
||||
if (match[2]) {
|
||||
const isSlot = dirName === 'slot'
|
||||
// const startOffset = name.lastIndexOf(
|
||||
// match[2],
|
||||
// name.length - (match[3]?.length || 0)
|
||||
// )
|
||||
let content = match[2]
|
||||
let isStatic = true
|
||||
|
||||
if (content.startsWith('[')) {
|
||||
isStatic = false
|
||||
|
||||
if (!content.endsWith(']')) {
|
||||
// TODO emitError(
|
||||
// context,
|
||||
// ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END
|
||||
// )
|
||||
content = content.slice(1)
|
||||
} else {
|
||||
content = content.slice(1, content.length - 1)
|
||||
}
|
||||
} else if (isSlot) {
|
||||
// #1241 special case for v-slot: vuetify relies extensively on slot
|
||||
// names containing dots. v-slot doesn't have any modifiers and Vue 2.x
|
||||
// supports such usage so we are keeping it consistent with 2.x.
|
||||
content += match[3] || ''
|
||||
}
|
||||
|
||||
arg = {
|
||||
type: NodeTypes.SIMPLE_EXPRESSION,
|
||||
content,
|
||||
isStatic,
|
||||
constType: isStatic
|
||||
? ConstantTypes.CAN_STRINGIFY
|
||||
: ConstantTypes.NOT_CONSTANT,
|
||||
// @ts-expect-error TODO
|
||||
loc: {}
|
||||
}
|
||||
}
|
||||
|
||||
const modifiers = match[3] ? match[3].slice(1).split('.') : []
|
||||
if (isPropShorthand) modifiers.push('prop')
|
||||
|
||||
// 2.x compat v-bind:foo.sync -> v-model:foo
|
||||
if (__COMPAT__ && dirName === 'bind' && arg) {
|
||||
// TODO
|
||||
// if (
|
||||
// modifiers.includes('sync') &&
|
||||
// checkCompatEnabled(
|
||||
// CompilerDeprecationTypes.COMPILER_V_BIND_SYNC,
|
||||
// context,
|
||||
// loc,
|
||||
// arg.loc.source
|
||||
// )
|
||||
// ) {
|
||||
// dirName = 'model'
|
||||
// modifiers.splice(modifiers.indexOf('sync'), 1)
|
||||
// }
|
||||
// if (__DEV__ && modifiers.includes('prop')) {
|
||||
// checkCompatEnabled(
|
||||
// CompilerDeprecationTypes.COMPILER_V_BIND_PROP,
|
||||
// context,
|
||||
// loc
|
||||
// )
|
||||
// }
|
||||
}
|
||||
|
||||
currentProp = {
|
||||
type: NodeTypes.DIRECTIVE,
|
||||
name: dirName,
|
||||
exp: undefined,
|
||||
arg,
|
||||
modifiers,
|
||||
// @ts-expect-error TODO
|
||||
loc: {}
|
||||
}
|
||||
} else {
|
||||
// plain attribute
|
||||
currentProp = {
|
||||
type: NodeTypes.ATTRIBUTE,
|
||||
name,
|
||||
value: undefined,
|
||||
loc: {
|
||||
start: tokenizer.getPositionForIndex(start),
|
||||
// @ts-expect-error to be attached on attribute end
|
||||
end: undefined,
|
||||
source: ''
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -137,16 +261,28 @@ const tokenizer = new Tokenizer(
|
|||
},
|
||||
onattribend(_quote, end) {
|
||||
if (currentElement) {
|
||||
if (currentProp!.type === NodeTypes.ATTRIBUTE) {
|
||||
// assign value
|
||||
currentProp!.value = {
|
||||
type: NodeTypes.TEXT,
|
||||
content: currentAttrValue,
|
||||
// @ts-expect-error TODO
|
||||
loc: {}
|
||||
if (currentAttrValue) {
|
||||
if (currentProp!.type === NodeTypes.ATTRIBUTE) {
|
||||
// assign value
|
||||
currentProp!.value = {
|
||||
type: NodeTypes.TEXT,
|
||||
content: currentAttrValue,
|
||||
// @ts-expect-error TODO
|
||||
loc: {}
|
||||
}
|
||||
} 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,
|
||||
// @ts-expect-error TODO
|
||||
loc: {}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO
|
||||
}
|
||||
currentProp!.loc.end = tokenizer.getPositionForIndex(end)
|
||||
currentElement.props.push(currentProp!)
|
||||
|
|
@ -179,8 +315,7 @@ function emitOpenTag(name: string, start: number) {
|
|||
currentElement = {
|
||||
type: NodeTypes.ELEMENT,
|
||||
tag: name,
|
||||
// TODO refine namespace
|
||||
ns: 0,
|
||||
ns: currentOptions.getNamespace(name, getParent()),
|
||||
// TODO refine tag type
|
||||
tagType: ElementTypes.ELEMENT,
|
||||
props: [],
|
||||
|
|
@ -193,12 +328,13 @@ function emitOpenTag(name: string, start: number) {
|
|||
},
|
||||
codegenNode: undefined
|
||||
}
|
||||
currentAttrs = {}
|
||||
}
|
||||
|
||||
function endOpenTag() {
|
||||
function endOpenTag(end: number) {
|
||||
addNode(currentElement!)
|
||||
const name = currentElement!.tag
|
||||
if (!voidElements.has(name)) {
|
||||
if (!currentOptions.isVoidTag(name)) {
|
||||
stack.unshift(currentElement!)
|
||||
if (htmlMode) {
|
||||
if (foreignContextElements.has(name)) {
|
||||
|
|
@ -207,13 +343,16 @@ function endOpenTag() {
|
|||
foreignContext.unshift(false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
onCloseTag(currentElement!, end)
|
||||
}
|
||||
currentElement = null
|
||||
currentAttrs = null
|
||||
}
|
||||
|
||||
function closeCurrentTag(end: number) {
|
||||
const name = currentElement!.tag
|
||||
endOpenTag()
|
||||
endOpenTag(end)
|
||||
if (stack[0].tag === name) {
|
||||
onCloseTag(stack.shift()!, end)
|
||||
}
|
||||
|
|
@ -321,20 +460,18 @@ function reset() {
|
|||
tokenizer.reset()
|
||||
currentElement = null
|
||||
currentProp = null
|
||||
currentAttrs = null
|
||||
currentAttrValue = ''
|
||||
stack.length = 0
|
||||
foreignContext.length = 1
|
||||
foreignContext[0] = false
|
||||
}
|
||||
|
||||
export function baseParse(
|
||||
input: string,
|
||||
options: ParserOptions = {}
|
||||
): RootNode {
|
||||
export function baseParse(input: string, options?: ParserOptions): RootNode {
|
||||
reset()
|
||||
currentInput = input
|
||||
currentOptions = options
|
||||
htmlMode = !!options.htmlMode
|
||||
currentOptions = extend({}, defaultParserOptions, options)
|
||||
htmlMode = !!currentOptions.htmlMode
|
||||
const root = (currentRoot = createRoot([]))
|
||||
tokenizer.parse(currentInput)
|
||||
root.children = condenseWhitespace(root.children)
|
||||
|
|
|
|||
Loading…
Reference in New Issue