| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955 |
- import { ParserOptions } from './options'
- import { NO, isArray, makeMap, extend } from '@vue/shared'
- import { ErrorCodes, createCompilerError, defaultOnError } from './errors'
- import {
- assert,
- advancePositionWithMutation,
- advancePositionWithClone,
- isCoreComponent
- } from './utils'
- import {
- Namespaces,
- AttributeNode,
- CommentNode,
- DirectiveNode,
- ElementNode,
- ElementTypes,
- ExpressionNode,
- NodeTypes,
- Position,
- RootNode,
- SourceLocation,
- TextNode,
- TemplateChildNode,
- InterpolationNode,
- createRoot
- } from './ast'
- type OptionalOptions = 'isNativeTag' | 'isBuiltInComponent'
- 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
- }
- 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
- }
- 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,
- options: ParserOptions
- ): ParserContext {
- return {
- options: extend({}, defaultParserOptions, options),
- column: 1,
- line: 1,
- offset: 0,
- originalSource: content,
- source: content,
- inPre: false,
- inVPre: false
- }
- }
- 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)
- } 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 management for more efficient output
- // (same as v2 whitespace: 'condense')
- let removedWhitespace = false
- if (mode !== TextModes.RAWTEXT) {
- if (!context.inPre) {
- for (let i = 0; i < nodes.length; i++) {
- const node = nodes[i]
- if (node.type === NodeTypes.TEXT) {
- if (!/[^\t\r\n\f ]/.test(node.content)) {
- const prev = nodes[i - 1]
- const next = nodes[i + 1]
- // If:
- // - the whitespace is the first or last node, or:
- // - the whitespace is adjacent to a comment, or:
- // - the whitespace is between two elements AND contains newline
- // Then the whitespace is ignored.
- if (
- !prev ||
- !next ||
- 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, condensed consecutive whitespace inside the text
- // down to a single space
- node.content = ' '
- }
- } else {
- node.content = node.content.replace(/[\t\r\n\f ]+/g, ' ')
- }
- } else if (!__DEV__ && node.type === NodeTypes.COMMENT) {
- // remove comment nodes in prod
- removedWhitespace = true
- nodes[i] = null as any
- }
- }
- } else if (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)) {
- return element
- }
- // Children.
- ancestors.push(element)
- const mode = context.options.getTextMode(element, parent)
- const children = parseChildren(context, mode, ancestors)
- ancestors.pop()
- 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,
- parent: ElementNode | undefined
- ): ElementNode {
- __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
- // Attributes.
- let props = parseAttributes(context, type)
- // check <pre> tag
- if (context.options.isPreTag(tag)) {
- context.inPre = true
- }
- // check v-pre
- if (
- !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)
- }
- let tagType = ElementTypes.ELEMENT
- const options = context.options
- if (!context.inVPre && !options.isCustomElement(tag)) {
- const hasVIs = props.some(
- p => p.type === NodeTypes.DIRECTIVE && p.name === 'is'
- )
- if (options.isNativeTag && !hasVIs) {
- if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
- } else if (
- hasVIs ||
- isCoreComponent(tag) ||
- (options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
- /^[A-Z]/.test(tag) ||
- tag === 'component'
- ) {
- tagType = ElementTypes.COMPONENT
- }
- if (tag === 'slot') {
- tagType = ElementTypes.SLOT
- } else if (
- tag === 'template' &&
- props.some(p => {
- return (
- p.type === NodeTypes.DIRECTIVE && isSpecialTemplateDirective(p.name)
- )
- })
- ) {
- tagType = ElementTypes.TEMPLATE
- }
- }
- return {
- type: NodeTypes.ELEMENT,
- ns,
- tag,
- tagType,
- props,
- isSelfClosing,
- children: [],
- loc: getSelection(context, start),
- codegenNode: undefined // to be created during transform phase
- }
- }
- 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:
- | {
- content: string
- isQuoted: boolean
- loc: SourceLocation
- }
- | undefined = 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
- )!
- const dirName =
- match[1] ||
- (startsWith(name, ':') ? 'bind' : startsWith(name, '@') ? 'on' : 'slot')
- let arg: ExpressionNode | undefined
- if (match[2]) {
- const isSlot = dirName === 'slot'
- const startOffset = name.indexOf(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,
- isConstant: isStatic,
- 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)
- }
- 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
- // true by `transformExpression` to make it eligible for hoisting.
- isConstant: false,
- loc: value.loc
- },
- arg,
- modifiers: match[3] ? match[3].substr(1).split('.') : [],
- loc
- }
- }
- return {
- type: NodeTypes.ATTRIBUTE,
- name,
- value: value && {
- type: NodeTypes.TEXT,
- content: value.content,
- loc: value.loc
- },
- loc
- }
- }
- function parseAttributeValue(
- context: ParserContext
- ):
- | {
- content: string
- isQuoted: boolean
- loc: SourceLocation
- }
- | undefined {
- 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
- isConstant: false,
- 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] || '>')
- )
- }
|