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, isBindKey } 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 = | '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 preserve = context.options.whitespace === 'preserve' for (let i = 0; i < nodes.length; i++) { const node = nodes[i] if (!context.inPre && node.type === NodeTypes.TEXT) { 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 adjacent to a comment, or: // - (condense mode) the whitespace is between two elements AND contains newline if ( !prev || !next || (!preserve && (prev.type === NodeTypes.COMMENT || 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 (!preserve) { // in condense mode, consecutive whitespaces in text are condensed // down to a single space. node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ') } } // 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 (context.options.isPreTag(element.tag)) { context.inPre = 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 const isPreTag = context.options.isPreTag(tag) if (isPreTag) { 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) ) } } } 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 Deprecate) if (p.name === 'is') { return true } else if ( // :is on plain element - only treat as component in compat mode p.name === 'bind' && isBindKey(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) 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-|:|\.|@|#)/.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]) 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.substr(1, content.length - 2) } 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].substr(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 } } 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 = ['<', context.options.delimiters[0]] if (mode === TextModes.CDATA) { endTokens.push(']]>') } 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.indexOf('&') === -1 ) { return rawText } else { // DATA or RCDATA containing "&"". Entity decoding 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.substr(2, tag.length).toLowerCase() === tag.toLowerCase() && /[\t\r\n\f />]/.test(source[2 + tag.length] || '>') ) }