+++ /dev/null
-import { ErrorHandlingOptions, ParserOptions } from './options'
-import { NO, isArray, makeMap, extend } from '@vue/shared'
-import {
- ErrorCodes,
- createCompilerError,
- defaultOnError,
- defaultOnWarn
-} from './errors'
-import {
- assert,
- advancePositionWithMutation,
- advancePositionWithClone,
- isCoreComponent,
- isStaticArgOf
-} from './utils'
-import {
- Namespaces,
- AttributeNode,
- CommentNode,
- DirectiveNode,
- ElementNode,
- ElementTypes,
- ExpressionNode,
- NodeTypes,
- Position,
- RootNode,
- SourceLocation,
- TextNode,
- TemplateChildNode,
- InterpolationNode,
- createRoot,
- ConstantTypes
-} from './ast'
-import {
- checkCompatEnabled,
- CompilerCompatOptions,
- CompilerDeprecationTypes,
- isCompatEnabled,
- warnDeprecation
-} from './compat/compatConfig'
-
-type OptionalOptions =
- | 'parseMode'
- | 'whitespace'
- | 'isNativeTag'
- | 'isBuiltInComponent'
- | keyof CompilerCompatOptions
-type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
- Pick<ParserOptions, OptionalOptions>
-type AttributeValue =
- | {
- content: string
- isQuoted: boolean
- loc: SourceLocation
- }
- | undefined
-
-// 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__
-}
-
-export const enum TextModes {
- // | Elements | Entities | End sign | Inside of
- DATA, // | ✔ | ✔ | End tags of ancestors |
- RCDATA, // | ✘ | ✔ | End tag of the parent | <textarea>
- RAWTEXT, // | ✘ | ✘ | End tag of the parent | <style>,<script>
- CDATA,
- ATTRIBUTE_VALUE
-}
-
-export interface ParserContext {
- options: MergedParserOptions
- readonly originalSource: string
- source: string
- offset: number
- line: number
- column: number
- inPre: boolean // HTML <pre> tag, preserve whitespaces
- inVPre: boolean // v-pre, do not process directives and interpolations
- onWarn: NonNullable<ErrorHandlingOptions['onWarn']>
-}
-
-export function baseParse(
- content: string,
- options: ParserOptions = {}
-): RootNode {
- const context = createParserContext(content, options)
- const start = getCursor(context)
- return createRoot(
- parseChildren(context, TextModes.DATA, []),
- getSelection(context, start)
- )
-}
-
-function createParserContext(
- content: string,
- rawOptions: ParserOptions
-): ParserContext {
- const options = extend({}, defaultParserOptions)
-
- let key: keyof ParserOptions
- for (key in rawOptions) {
- // @ts-ignore
- options[key] =
- rawOptions[key] === undefined
- ? defaultParserOptions[key]
- : rawOptions[key]
- }
- return {
- options,
- column: 1,
- line: 1,
- offset: 0,
- originalSource: content,
- source: content,
- inPre: false,
- inVPre: false,
- onWarn: options.onWarn
- }
-}
-
-function parseChildren(
- context: ParserContext,
- mode: TextModes,
- ancestors: ElementNode[]
-): TemplateChildNode[] {
- const parent = last(ancestors)
- const ns = parent ? parent.ns : Namespaces.HTML
- const nodes: TemplateChildNode[] = []
-
- while (!isEnd(context, mode, ancestors)) {
- __TEST__ && assert(context.source.length > 0)
- const s = context.source
- let node: TemplateChildNode | TemplateChildNode[] | undefined = undefined
-
- if (mode === TextModes.DATA || mode === TextModes.RCDATA) {
- if (!context.inVPre && startsWith(s, context.options.delimiters[0])) {
- // '{{'
- node = parseInterpolation(context, mode)
- } else if (mode === TextModes.DATA && s[0] === '<') {
- // https://html.spec.whatwg.org/multipage/parsing.html#tag-open-state
- if (s.length === 1) {
- emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 1)
- } else if (s[1] === '!') {
- // https://html.spec.whatwg.org/multipage/parsing.html#markup-declaration-open-state
- if (startsWith(s, '<!--')) {
- node = parseComment(context)
- } else if (startsWith(s, '<!DOCTYPE')) {
- // Ignore DOCTYPE by a limitation.
- node = parseBogusComment(context)
- } else if (startsWith(s, '<![CDATA[')) {
- if (ns !== Namespaces.HTML) {
- node = parseCDATA(context, ancestors)
- } else {
- emitError(context, ErrorCodes.CDATA_IN_HTML_CONTENT)
- node = parseBogusComment(context)
- }
- } else {
- emitError(context, ErrorCodes.INCORRECTLY_OPENED_COMMENT)
- node = parseBogusComment(context)
- }
- } else if (s[1] === '/') {
- // https://html.spec.whatwg.org/multipage/parsing.html#end-tag-open-state
- if (s.length === 2) {
- emitError(context, ErrorCodes.EOF_BEFORE_TAG_NAME, 2)
- } else if (s[2] === '>') {
- emitError(context, ErrorCodes.MISSING_END_TAG_NAME, 2)
- advanceBy(context, 3)
- continue
- } else if (/[a-z]/i.test(s[2])) {
- emitError(context, ErrorCodes.X_INVALID_END_TAG)
- parseTag(context, TagType.End, parent)
- continue
- } else {
- emitError(
- context,
- ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME,
- 2
- )
- node = parseBogusComment(context)
- }
- } else if (/[a-z]/i.test(s[1])) {
- node = parseElement(context, ancestors)
-
- // 2.x <template> with no directive compat
- if (
- __COMPAT__ &&
- isCompatEnabled(
- CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE,
- context
- ) &&
- node &&
- node.tag === 'template' &&
- !node.props.some(
- p =>
- p.type === NodeTypes.DIRECTIVE &&
- isSpecialTemplateDirective(p.name)
- )
- ) {
- __DEV__ &&
- warnDeprecation(
- CompilerDeprecationTypes.COMPILER_NATIVE_TEMPLATE,
- context,
- node.loc
- )
- node = node.children
- }
- } else if (s[1] === '?') {
- emitError(
- context,
- ErrorCodes.UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
- 1
- )
- node = parseBogusComment(context)
- } else {
- emitError(context, ErrorCodes.INVALID_FIRST_CHARACTER_OF_TAG_NAME, 1)
- }
- }
- }
- if (!node) {
- node = parseText(context, mode)
- }
-
- if (isArray(node)) {
- for (let i = 0; i < node.length; i++) {
- pushNode(nodes, node[i])
- }
- } else {
- pushNode(nodes, node)
- }
- }
-
- // Whitespace handling strategy like v2
- let removedWhitespace = false
- if (mode !== TextModes.RAWTEXT && mode !== TextModes.RCDATA) {
- const shouldCondense = context.options.whitespace !== 'preserve'
- for (let i = 0; i < nodes.length; i++) {
- const node = nodes[i]
- if (node.type === NodeTypes.TEXT) {
- if (!context.inPre) {
- if (!/[^\t\r\n\f ]/.test(node.content)) {
- const prev = nodes[i - 1]
- const next = nodes[i + 1]
- // Remove if:
- // - the whitespace is the first or last node, or:
- // - (condense mode) the whitespace is between twos comments, or:
- // - (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 &&
- ((prev.type === NodeTypes.COMMENT &&
- next.type === NodeTypes.COMMENT) ||
- (prev.type === NodeTypes.COMMENT &&
- next.type === NodeTypes.ELEMENT) ||
- (prev.type === NodeTypes.ELEMENT &&
- next.type === NodeTypes.COMMENT) ||
- (prev.type === NodeTypes.ELEMENT &&
- next.type === NodeTypes.ELEMENT &&
- /[\r\n]/.test(node.content))))
- ) {
- 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.
- node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
- }
- } else {
- // #6410 normalize windows newlines in <pre>:
- // in SSR, browsers normalize server-rendered \r\n into a single \n
- // in the DOM
- node.content = node.content.replace(/\r\n/g, '\n')
- }
- }
- // Remove comment nodes if desired by configuration.
- else if (node.type === NodeTypes.COMMENT && !context.options.comments) {
- removedWhitespace = true
- nodes[i] = null as any
- }
- }
- if (context.inPre && parent && context.options.isPreTag(parent.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/, '')
- }
- }
- }
-
- return removedWhitespace ? nodes.filter(Boolean) : nodes
-}
-
-function pushNode(nodes: TemplateChildNode[], node: TemplateChildNode): void {
- if (node.type === NodeTypes.TEXT) {
- const prev = last(nodes)
- // Merge if both this and the previous node are text and those are
- // consecutive. This happens for cases like "a < b".
- if (
- prev &&
- prev.type === NodeTypes.TEXT &&
- prev.loc.end.offset === node.loc.start.offset
- ) {
- prev.content += node.content
- prev.loc.end = node.loc.end
- prev.loc.source += node.loc.source
- return
- }
- }
-
- nodes.push(node)
-}
-
-function parseCDATA(
- context: ParserContext,
- ancestors: ElementNode[]
-): TemplateChildNode[] {
- __TEST__ &&
- assert(last(ancestors) == null || last(ancestors)!.ns !== Namespaces.HTML)
- __TEST__ && assert(startsWith(context.source, '<![CDATA['))
-
- advanceBy(context, 9)
- const nodes = parseChildren(context, TextModes.CDATA, ancestors)
- if (context.source.length === 0) {
- emitError(context, ErrorCodes.EOF_IN_CDATA)
- } else {
- __TEST__ && assert(startsWith(context.source, ']]>'))
- advanceBy(context, 3)
- }
-
- return nodes
-}
-
-function parseComment(context: ParserContext): CommentNode {
- __TEST__ && assert(startsWith(context.source, '<!--'))
-
- const start = getCursor(context)
- let content: string
-
- // Regular comment.
- const match = /--(\!)?>/.exec(context.source)
- if (!match) {
- content = context.source.slice(4)
- advanceBy(context, context.source.length)
- emitError(context, ErrorCodes.EOF_IN_COMMENT)
- } else {
- if (match.index <= 3) {
- emitError(context, ErrorCodes.ABRUPT_CLOSING_OF_EMPTY_COMMENT)
- }
- if (match[1]) {
- emitError(context, ErrorCodes.INCORRECTLY_CLOSED_COMMENT)
- }
- content = context.source.slice(4, match.index)
-
- // Advancing with reporting nested comments.
- const s = context.source.slice(0, match.index)
- let prevIndex = 1,
- nestedIndex = 0
- while ((nestedIndex = s.indexOf('<!--', prevIndex)) !== -1) {
- advanceBy(context, nestedIndex - prevIndex + 1)
- if (nestedIndex + 4 < s.length) {
- emitError(context, ErrorCodes.NESTED_COMMENT)
- }
- prevIndex = nestedIndex + 1
- }
- advanceBy(context, match.index + match[0].length - prevIndex + 1)
- }
-
- return {
- type: NodeTypes.COMMENT,
- content,
- loc: getSelection(context, start)
- }
-}
-
-function parseBogusComment(context: ParserContext): CommentNode | undefined {
- __TEST__ && assert(/^<(?:[\!\?]|\/[^a-z>])/i.test(context.source))
-
- const start = getCursor(context)
- const contentStart = context.source[1] === '?' ? 1 : 2
- let content: string
-
- const closeIndex = context.source.indexOf('>')
- if (closeIndex === -1) {
- content = context.source.slice(contentStart)
- advanceBy(context, context.source.length)
- } else {
- content = context.source.slice(contentStart, closeIndex)
- advanceBy(context, closeIndex + 1)
- }
-
- return {
- type: NodeTypes.COMMENT,
- content,
- loc: getSelection(context, start)
- }
-}
-
-function parseElement(
- context: ParserContext,
- ancestors: ElementNode[]
-): ElementNode | undefined {
- __TEST__ && assert(/^<[a-z]/i.test(context.source))
-
- // Start tag.
- const wasInPre = context.inPre
- const wasInVPre = context.inVPre
- const parent = last(ancestors)
- const element = parseTag(context, TagType.Start, parent)
- const isPreBoundary = context.inPre && !wasInPre
- const isVPreBoundary = context.inVPre && !wasInVPre
-
- if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
- // #4030 self-closing <pre> tag
- if (isPreBoundary) {
- context.inPre = false
- }
- if (isVPreBoundary) {
- context.inVPre = false
- }
- return element
- }
-
- // Children.
- ancestors.push(element)
- const mode = context.options.getTextMode(element, parent)
- const children = parseChildren(context, mode, ancestors)
- ancestors.pop()
-
- // 2.x inline-template compat
- if (__COMPAT__) {
- const inlineTemplateProp = element.props.find(
- p => p.type === NodeTypes.ATTRIBUTE && p.name === 'inline-template'
- ) as AttributeNode
- if (
- inlineTemplateProp &&
- checkCompatEnabled(
- CompilerDeprecationTypes.COMPILER_INLINE_TEMPLATE,
- context,
- inlineTemplateProp.loc
- )
- ) {
- const loc = getSelection(context, element.loc.end)
- inlineTemplateProp.value = {
- type: NodeTypes.TEXT,
- content: loc.source,
- loc
- }
- }
- }
-
- element.children = children
-
- // End tag.
- if (startsWithEndTagOpen(context.source, element.tag)) {
- parseTag(context, TagType.End, parent)
- } else {
- emitError(context, ErrorCodes.X_MISSING_END_TAG, 0, element.loc.start)
- if (context.source.length === 0 && element.tag.toLowerCase() === 'script') {
- const first = children[0]
- if (first && startsWith(first.loc.source, '<!--')) {
- emitError(context, ErrorCodes.EOF_IN_SCRIPT_HTML_COMMENT_LIKE_TEXT)
- }
- }
- }
-
- element.loc = getSelection(context, element.loc.start)
-
- if (isPreBoundary) {
- context.inPre = false
- }
- if (isVPreBoundary) {
- context.inVPre = false
- }
- return element
-}
-
-const enum TagType {
- Start,
- End
-}
-
-const isSpecialTemplateDirective = /*#__PURE__*/ makeMap(
- `if,else,else-if,for,slot`
-)
-
-/**
- * Parse a tag (E.g. `<div id=a>`) with that type (start tag or end tag).
- */
-function parseTag(
- context: ParserContext,
- type: TagType.Start,
- parent: ElementNode | undefined
-): ElementNode
-function parseTag(
- context: ParserContext,
- type: TagType.End,
- parent: ElementNode | undefined
-): void
-function parseTag(
- context: ParserContext,
- type: TagType,
- parent: ElementNode | undefined
-): ElementNode | undefined {
- __TEST__ && assert(/^<\/?[a-z]/i.test(context.source))
- __TEST__ &&
- assert(
- type === (startsWith(context.source, '</') ? TagType.End : TagType.Start)
- )
-
- // Tag open.
- const start = getCursor(context)
- const match = /^<\/?([a-z][^\t\r\n\f />]*)/i.exec(context.source)!
- const tag = match[1]
- const ns = context.options.getNamespace(tag, parent)
-
- advanceBy(context, match[0].length)
- advanceSpaces(context)
-
- // save current state in case we need to re-parse attributes with v-pre
- const cursor = getCursor(context)
- const currentSource = context.source
-
- // check <pre> tag
- if (context.options.isPreTag(tag)) {
- context.inPre = true
- }
-
- // Attributes.
- let props = parseAttributes(context, type)
-
- // check v-pre
- if (
- type === TagType.Start &&
- !context.inVPre &&
- props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
- ) {
- context.inVPre = true
- // reset context
- extend(context, cursor)
- context.source = currentSource
- // re-parse attrs and filter out v-pre itself
- props = parseAttributes(context, type).filter(p => p.name !== 'v-pre')
- }
-
- // Tag close.
- let isSelfClosing = false
- if (context.source.length === 0) {
- emitError(context, ErrorCodes.EOF_IN_TAG)
- } else {
- isSelfClosing = startsWith(context.source, '/>')
- if (type === TagType.End && isSelfClosing) {
- emitError(context, ErrorCodes.END_TAG_WITH_TRAILING_SOLIDUS)
- }
- advanceBy(context, isSelfClosing ? 2 : 1)
- }
-
- if (type === TagType.End) {
- return
- }
-
- // 2.x deprecation checks
- if (
- __COMPAT__ &&
- __DEV__ &&
- isCompatEnabled(
- CompilerDeprecationTypes.COMPILER_V_IF_V_FOR_PRECEDENCE,
- context
- )
- ) {
- 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,
- context,
- getSelection(context, start)
- )
- break
- }
- }
- }
-
- let tagType = ElementTypes.ELEMENT
- if (!context.inVPre) {
- if (tag === 'slot') {
- tagType = ElementTypes.SLOT
- } else if (tag === 'template') {
- if (
- props.some(
- p =>
- p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
- )
- ) {
- tagType = ElementTypes.TEMPLATE
- }
- } else if (isComponent(tag, props, context)) {
- tagType = ElementTypes.COMPONENT
- }
- }
-
- return {
- type: NodeTypes.ELEMENT,
- ns,
- tag,
- tagType,
- props,
- isSelfClosing,
- children: [],
- loc: getSelection(context, start),
- codegenNode: undefined // to be created during transform phase
- }
-}
-
-function isComponent(
- tag: string,
- props: (AttributeNode | DirectiveNode)[],
- context: ParserContext
-) {
- const options = context.options
- if (options.isCustomElement(tag)) {
- return false
- }
- if (
- tag === 'component' ||
- /^[A-Z]/.test(tag) ||
- isCoreComponent(tag) ||
- (options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
- (options.isNativeTag && !options.isNativeTag(tag))
- ) {
- 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
- } else if (
- __COMPAT__ &&
- checkCompatEnabled(
- CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
- context,
- p.loc
- )
- ) {
- return true
- }
- }
- } else {
- // directive
- // v-is (TODO: remove in 3.4)
- if (p.name === 'is') {
- return true
- } else if (
- // :is on plain element - only treat as component in compat mode
- p.name === 'bind' &&
- isStaticArgOf(p.arg, 'is') &&
- __COMPAT__ &&
- checkCompatEnabled(
- CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
- context,
- p.loc
- )
- ) {
- return true
- }
- }
- }
-}
-
-function parseAttributes(
- context: ParserContext,
- type: TagType
-): (AttributeNode | DirectiveNode)[] {
- const props = []
- const attributeNames = new Set<string>()
- while (
- context.source.length > 0 &&
- !startsWith(context.source, '>') &&
- !startsWith(context.source, '/>')
- ) {
- if (startsWith(context.source, '/')) {
- emitError(context, ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG)
- advanceBy(context, 1)
- advanceSpaces(context)
- continue
- }
- if (type === TagType.End) {
- emitError(context, ErrorCodes.END_TAG_WITH_ATTRIBUTES)
- }
-
- const attr = parseAttribute(context, attributeNames)
-
- // Trim whitespace between class
- // https://github.com/vuejs/core/issues/4251
- if (
- attr.type === NodeTypes.ATTRIBUTE &&
- attr.value &&
- attr.name === 'class'
- ) {
- attr.value.content = attr.value.content.replace(/\s+/g, ' ').trim()
- }
-
- if (type === TagType.Start) {
- props.push(attr)
- }
-
- if (/^[^\t\r\n\f />]/.test(context.source)) {
- emitError(context, ErrorCodes.MISSING_WHITESPACE_BETWEEN_ATTRIBUTES)
- }
- advanceSpaces(context)
- }
- return props
-}
-
-function parseAttribute(
- context: ParserContext,
- nameSet: Set<string>
-): AttributeNode | DirectiveNode {
- __TEST__ && assert(/^[^\t\r\n\f />]/.test(context.source))
-
- // Name.
- const start = getCursor(context)
- const match = /^[^\t\r\n\f />][^\t\r\n\f />=]*/.exec(context.source)!
- const name = match[0]
-
- if (nameSet.has(name)) {
- emitError(context, ErrorCodes.DUPLICATE_ATTRIBUTE)
- }
- nameSet.add(name)
-
- if (name[0] === '=') {
- emitError(context, ErrorCodes.UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME)
- }
- {
- const pattern = /["'<]/g
- let m: RegExpExecArray | null
- while ((m = pattern.exec(name))) {
- emitError(
- context,
- ErrorCodes.UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME,
- m.index
- )
- }
- }
-
- advanceBy(context, name.length)
-
- // Value
- let value: AttributeValue = undefined
-
- if (/^[\t\r\n\f ]*=/.test(context.source)) {
- advanceSpaces(context)
- advanceBy(context, 1)
- advanceSpaces(context)
- value = parseAttributeValue(context)
- if (!value) {
- emitError(context, ErrorCodes.MISSING_ATTRIBUTE_VALUE)
- }
- }
- const loc = getSelection(context, start)
-
- if (!context.inVPre && /^(v-[A-Za-z0-9-]|:|\.|@|#)/.test(name)) {
- const match =
- /(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i.exec(
- name
- )!
-
- let isPropShorthand = startsWith(name, '.')
- let dirName =
- match[1] ||
- (isPropShorthand || startsWith(name, ':')
- ? 'bind'
- : startsWith(name, '@')
- ? '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)
- )
- const loc = getSelection(
- context,
- getNewPosition(context, start, startOffset),
- getNewPosition(
- context,
- start,
- startOffset + match[2].length + ((isSlot && match[3]) || '').length
- )
- )
- let content = match[2]
- let isStatic = true
-
- if (content.startsWith('[')) {
- isStatic = false
-
- if (!content.endsWith(']')) {
- 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,
- loc
- }
- }
-
- if (value && value.isQuoted) {
- const valueLoc = value.loc
- valueLoc.start.offset++
- valueLoc.start.column++
- valueLoc.end = advancePositionWithClone(valueLoc.start, value.content)
- valueLoc.source = valueLoc.source.slice(1, -1)
- }
-
- 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) {
- 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
- )
- }
- }
-
- return {
- type: NodeTypes.DIRECTIVE,
- name: dirName,
- exp: value && {
- type: NodeTypes.SIMPLE_EXPRESSION,
- content: value.content,
- 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: value.loc
- },
- arg,
- modifiers,
- loc
- }
- }
-
- // missing directive name or illegal directive name
- if (!context.inVPre && startsWith(name, 'v-')) {
- emitError(context, ErrorCodes.X_MISSING_DIRECTIVE_NAME)
- }
-
- return {
- type: NodeTypes.ATTRIBUTE,
- name,
- value: value && {
- type: NodeTypes.TEXT,
- content: value.content,
- loc: value.loc
- },
- loc
- }
-}
-
-function parseAttributeValue(context: ParserContext): AttributeValue {
- const start = getCursor(context)
- let content: string
-
- const quote = context.source[0]
- const isQuoted = quote === `"` || quote === `'`
- if (isQuoted) {
- // Quoted value.
- advanceBy(context, 1)
-
- const endIndex = context.source.indexOf(quote)
- if (endIndex === -1) {
- content = parseTextData(
- context,
- context.source.length,
- TextModes.ATTRIBUTE_VALUE
- )
- } else {
- content = parseTextData(context, endIndex, TextModes.ATTRIBUTE_VALUE)
- advanceBy(context, 1)
- }
- } else {
- // Unquoted
- const match = /^[^\t\r\n\f >]+/.exec(context.source)
- if (!match) {
- return undefined
- }
- const unexpectedChars = /["'<=`]/g
- let m: RegExpExecArray | null
- while ((m = unexpectedChars.exec(match[0]))) {
- emitError(
- context,
- ErrorCodes.UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE,
- m.index
- )
- }
- content = parseTextData(context, match[0].length, TextModes.ATTRIBUTE_VALUE)
- }
-
- return { content, isQuoted, loc: getSelection(context, start) }
-}
-
-function parseInterpolation(
- context: ParserContext,
- mode: TextModes
-): InterpolationNode | undefined {
- const [open, close] = context.options.delimiters
- __TEST__ && assert(startsWith(context.source, open))
-
- const closeIndex = context.source.indexOf(close, open.length)
- if (closeIndex === -1) {
- emitError(context, ErrorCodes.X_MISSING_INTERPOLATION_END)
- return undefined
- }
-
- const start = getCursor(context)
- advanceBy(context, open.length)
- const innerStart = getCursor(context)
- const innerEnd = getCursor(context)
- const rawContentLength = closeIndex - open.length
- const rawContent = context.source.slice(0, rawContentLength)
- const preTrimContent = parseTextData(context, rawContentLength, mode)
- const content = preTrimContent.trim()
- const startOffset = preTrimContent.indexOf(content)
- if (startOffset > 0) {
- advancePositionWithMutation(innerStart, rawContent, startOffset)
- }
- const endOffset =
- rawContentLength - (preTrimContent.length - content.length - startOffset)
- advancePositionWithMutation(innerEnd, rawContent, endOffset)
- advanceBy(context, close.length)
-
- return {
- 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,
- loc: getSelection(context, innerStart, innerEnd)
- },
- loc: getSelection(context, start)
- }
-}
-
-function parseText(context: ParserContext, mode: TextModes): TextNode {
- __TEST__ && assert(context.source.length > 0)
-
- const endTokens =
- mode === TextModes.CDATA ? [']]>'] : ['<', context.options.delimiters[0]]
-
- let endIndex = context.source.length
- for (let i = 0; i < endTokens.length; i++) {
- const index = context.source.indexOf(endTokens[i], 1)
- if (index !== -1 && endIndex > index) {
- endIndex = index
- }
- }
-
- __TEST__ && assert(endIndex > 0)
-
- const start = getCursor(context)
- const content = parseTextData(context, endIndex, mode)
-
- return {
- type: NodeTypes.TEXT,
- content,
- loc: getSelection(context, start)
- }
-}
-
-/**
- * Get text data with a given length from the current location.
- * This translates HTML entities in the text data.
- */
-function parseTextData(
- context: ParserContext,
- length: number,
- mode: TextModes
-): string {
- const rawText = context.source.slice(0, length)
- advanceBy(context, length)
- if (
- mode === TextModes.RAWTEXT ||
- mode === TextModes.CDATA ||
- !rawText.includes('&')
- ) {
- return rawText
- } else {
- // DATA or RCDATA containing "&". Entity decoding is required.
- return context.options.decodeEntities(
- rawText,
- mode === TextModes.ATTRIBUTE_VALUE
- )
- }
-}
-
-function getCursor(context: ParserContext): Position {
- const { column, line, offset } = context
- return { column, line, offset }
-}
-
-function getSelection(
- context: ParserContext,
- start: Position,
- end?: Position
-): SourceLocation {
- end = end || getCursor(context)
- return {
- start,
- end,
- source: context.originalSource.slice(start.offset, end.offset)
- }
-}
-
-function last<T>(xs: T[]): T | undefined {
- return xs[xs.length - 1]
-}
-
-function startsWith(source: string, searchString: string): boolean {
- return source.startsWith(searchString)
-}
-
-function advanceBy(context: ParserContext, numberOfCharacters: number): void {
- const { source } = context
- __TEST__ && assert(numberOfCharacters <= source.length)
- advancePositionWithMutation(context, source, numberOfCharacters)
- context.source = source.slice(numberOfCharacters)
-}
-
-function advanceSpaces(context: ParserContext): void {
- const match = /^[\t\r\n\f ]+/.exec(context.source)
- if (match) {
- advanceBy(context, match[0].length)
- }
-}
-
-function getNewPosition(
- context: ParserContext,
- start: Position,
- numberOfCharacters: number
-): Position {
- return advancePositionWithClone(
- start,
- context.originalSource.slice(start.offset, numberOfCharacters),
- numberOfCharacters
- )
-}
-
-function emitError(
- context: ParserContext,
- code: ErrorCodes,
- offset?: number,
- loc: Position = getCursor(context)
-): void {
- if (offset) {
- loc.offset += offset
- loc.column += offset
- }
- context.options.onError(
- createCompilerError(code, {
- start: loc,
- end: loc,
- source: ''
- })
- )
-}
-
-function isEnd(
- context: ParserContext,
- mode: TextModes,
- ancestors: ElementNode[]
-): boolean {
- const s = context.source
-
- switch (mode) {
- case TextModes.DATA:
- if (startsWith(s, '</')) {
- // TODO: probably bad performance
- for (let i = ancestors.length - 1; i >= 0; --i) {
- if (startsWithEndTagOpen(s, ancestors[i].tag)) {
- return true
- }
- }
- }
- break
-
- case TextModes.RCDATA:
- case TextModes.RAWTEXT: {
- const parent = last(ancestors)
- if (parent && startsWithEndTagOpen(s, parent.tag)) {
- return true
- }
- break
- }
-
- case TextModes.CDATA:
- if (startsWith(s, ']]>')) {
- return true
- }
- break
- }
-
- return !s
-}
-
-function startsWithEndTagOpen(source: string, tag: string): boolean {
- return (
- startsWith(source, '</') &&
- source.slice(2, 2 + tag.length).toLowerCase() === tag.toLowerCase() &&
- /[\t\r\n\f />]/.test(source[2 + tag.length] || '>')
- )
-}
DirectiveNode,
ElementNode,
ElementTypes,
+ ForParseResult,
Namespaces,
NodeTypes,
RootNode,
SimpleExpressionNode,
SourceLocation,
TemplateChildNode,
- createRoot
+ createRoot,
+ createSimpleExpression
} from '../ast'
import { ParserOptions } from '../options'
import Tokenizer, {
import { CompilerCompatOptions } from '../compat/compatConfig'
import { NO, extend } from '@vue/shared'
import { defaultOnError, defaultOnWarn } from '../errors'
-import { isCoreComponent } from '../utils'
+import { forAliasRE, isCoreComponent } from '../utils'
type OptionalOptions =
| 'whitespace'
| 'isNativeTag'
| 'isBuiltInComponent'
- | 'getTextMode'
| keyof CompilerCompatOptions
type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
}
let currentOptions: MergedParserOptions = defaultParserOptions
-let currentRoot: RootNode = createRoot([])
+let currentRoot: RootNode | null = null
// parser state
let currentInput = ''
}
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)
- },
+ content: createSimpleExpression(
+ getSlice(innerStart, innerEnd),
+ false,
+ getLoc(innerStart, innerEnd)
+ ),
loc: getLoc(start, end)
})
},
tagType: ElementTypes.ELEMENT, // will be refined on tag close
props: [],
children: [],
- loc: {
- start: tokenizer.getPos(start - 1),
- // @ts-expect-error to be attached on tag close
- end: undefined,
- source: ''
- },
+ loc: getLoc(start - 1),
codegenNode: undefined
}
currentAttrs.clear()
currentProp = {
type: NodeTypes.ATTRIBUTE,
name: getSlice(start, end),
+ nameLoc: getLoc(start, end),
value: undefined,
loc: getLoc(start)
}
currentProp = {
type: NodeTypes.ATTRIBUTE,
name: raw,
+ nameLoc: getLoc(start, end),
value: undefined,
loc: getLoc(start)
}
currentProp = {
type: NodeTypes.DIRECTIVE,
name,
- raw,
+ rawName: raw,
exp: undefined,
arg: undefined,
modifiers: [],
const arg = getSlice(start, end)
if (inVPre) {
;(currentProp as AttributeNode).name += arg
+ ;(currentProp as AttributeNode).nameLoc.end = tokenizer.getPos(end)
} else {
const isStatic = arg[0] !== `[`
- ;(currentProp as DirectiveNode).arg = {
- type: NodeTypes.SIMPLE_EXPRESSION,
- content: arg,
+ ;(currentProp as DirectiveNode).arg = createSimpleExpression(
+ arg,
isStatic,
- constType: isStatic
- ? ConstantTypes.CAN_STRINGIFY
- : ConstantTypes.NOT_CONSTANT,
- loc: getLoc(start, end)
- }
+ getLoc(start, end),
+ isStatic ? ConstantTypes.CAN_STRINGIFY : ConstantTypes.NOT_CONSTANT
+ )
}
},
const mod = getSlice(start, end)
if (inVPre) {
;(currentProp as AttributeNode).name += '.' + mod
+ ;(currentProp as AttributeNode).nameLoc.end = tokenizer.getPos(end)
} else {
;(currentProp as DirectiveNode).modifiers.push(mod)
}
const start = currentProp!.loc.start.offset
const name = getSlice(start, end)
if (currentProp!.type === NodeTypes.DIRECTIVE) {
- currentProp!.raw = name
+ currentProp!.rawName = name
}
if (currentAttrs.has(name)) {
currentProp = null
}
} 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.rawExp = currentAttrValue
+ currentProp.exp = createSimpleExpression(
+ currentAttrValue,
+ false,
+ getLoc(currentAttrStartIndex, currentAttrEndIndex)
+ )
+ if (currentProp.name === 'for') {
+ currentProp.forParseResult = parseForExpression(currentProp.exp)
}
}
}
}
})
+// 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
+}
+
function getSlice(start: number, end: number) {
return currentInput.slice(start, end)
}
parent.children.push({
type: NodeTypes.TEXT,
content,
- loc: {
- start: tokenizer.getPos(start),
- end: tokenizer.getPos(end),
- source: ''
- }
+ loc: getLoc(start, end)
})
}
}
for (let i = 0; i < props.length; i++) {
if (
props[i].type === NodeTypes.DIRECTIVE &&
- specialTemplateDir.has(props[i].name)
+ specialTemplateDir.has((props[i] as DirectiveNode).name)
) {
return true
}
function dirToAttr(dir: DirectiveNode): AttributeNode {
const attr: AttributeNode = {
type: NodeTypes.ATTRIBUTE,
- name: dir.raw!,
+ name: dir.rawName!,
+ nameLoc: getLoc(
+ dir.loc.start.offset,
+ dir.loc.start.offset + dir.rawName!.length
+ ),
value: undefined,
loc: dir.loc
}
tokenizer.delimiterClose = toCharCodes(delimiters[1])
}
- const root = (currentRoot = createRoot([]))
+ const root = (currentRoot = createRoot([], input))
tokenizer.parse(currentInput)
- root.loc.end = tokenizer.getPos(input.length)
root.children = condenseWhitespace(root.children)
+ currentRoot = null
return root
}