import { fromCodePoint } from 'entities/lib/decode.js'
import {
AttributeNode,
+ ConstantTypes,
DirectiveNode,
ElementNode,
ElementTypes,
+ ExpressionNode,
+ Namespaces,
NodeTypes,
RootNode,
TemplateChildNode,
} 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'])
'title'
])
-let currentOptions: ParserOptions = {}
+let currentOptions: MergedParserOptions = defaultParserOptions
let currentRoot: RootNode = createRoot([])
// parser state
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]
},
onopentagend(end) {
- endOpenTag()
+ endOpenTag(end)
},
onclosetag(start, end) {
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++) {
},
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: ''
+ }
}
}
},
},
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!)
currentElement = {
type: NodeTypes.ELEMENT,
tag: name,
- // TODO refine namespace
- ns: 0,
+ ns: currentOptions.getNamespace(name, getParent()),
// TODO refine tag type
tagType: ElementTypes.ELEMENT,
props: [],
},
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)) {
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)
}
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)