| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060 |
- import { ParserOptions } from './options'
- import { NO, isArray } 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
- } from './ast'
- import { extend } from '@vue/shared'
- type OptionalOptions = 'isNativeTag' | 'isBuiltInComponent'
- type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
- Pick<ParserOptions, OptionalOptions>
- export const defaultParserOptions: MergedParserOptions = {
- delimiters: [`{{`, `}}`],
- getNamespace: () => Namespaces.HTML,
- getTextMode: () => TextModes.DATA,
- isVoidTag: NO,
- isPreTag: NO,
- isCustomElement: NO,
- namedCharacterReferences: {
- 'gt;': '>',
- 'lt;': '<',
- 'amp;': '&',
- 'apos;': "'",
- 'quot;': '"'
- },
- maxCRNameLength: 5,
- 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
- }
- interface ParserContext {
- options: MergedParserOptions
- readonly originalSource: string
- source: string
- offset: number
- line: number
- column: number
- inPre: boolean
- }
- export function baseParse(
- content: string,
- options: ParserOptions = {}
- ): RootNode {
- const context = createParserContext(content, options)
- const start = getCursor(context)
- return {
- type: NodeTypes.ROOT,
- children: parseChildren(context, TextModes.DATA, []),
- helpers: [],
- components: [],
- directives: [],
- hoists: [],
- imports: [],
- cached: 0,
- codegenNode: undefined,
- loc: getSelection(context, start)
- }
- }
- function createParserContext(
- content: string,
- options: ParserOptions
- ): ParserContext {
- return {
- options: {
- ...defaultParserOptions,
- ...options
- },
- column: 1,
- line: 1,
- offset: 0,
- originalSource: content,
- source: content,
- inPre: 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) {
- if (!context.inPre && startsWith(s, context.options.delimiters[0])) {
- // '{{'
- node = parseInterpolation(context, mode)
- } else if (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 &&
- (!parent || !context.options.isPreTag(parent.tag))
- ) {
- for (let i = 0; i < nodes.length; i++) {
- const node = nodes[i]
- if (node.type === NodeTypes.TEXT) {
- if (!node.content.trim()) {
- 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(/\s+/g, ' ')
- }
- }
- }
- }
- return removedWhitespace ? nodes.filter(node => node !== null) : nodes
- }
- function pushNode(nodes: TemplateChildNode[], node: TemplateChildNode): void {
- // ignore comments in production
- /* istanbul ignore next */
- if (!__DEV__ && node.type === NodeTypes.COMMENT) {
- return
- }
- 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 parent = last(ancestors)
- const element = parseTag(context, TagType.Start, parent)
- const isPreBoundary = context.inPre && !wasInPre
- if (element.isSelfClosing || context.options.isVoidTag(element.tag)) {
- return element
- }
- // Children.
- ancestors.push(element)
- const mode = context.options.getTextMode(element.tag, element.ns, 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
- }
- return element
- }
- const enum TagType {
- Start,
- End
- }
- /**
- * 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 v-pre
- if (
- !context.inPre &&
- props.some(p => p.type === NodeTypes.DIRECTIVE && p.name === 'pre')
- ) {
- context.inPre = 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.inPre && !options.isCustomElement(tag)) {
- if (options.isNativeTag) {
- if (!options.isNativeTag(tag)) tagType = ElementTypes.COMPONENT
- } else if (
- isCoreComponent(tag) ||
- (options.isBuiltInComponent && options.isBuiltInComponent(tag)) ||
- /^[A-Z]/.test(tag)
- ) {
- tagType = ElementTypes.COMPONENT
- }
- if (tag === 'slot') {
- tagType = ElementTypes.SLOT
- } else if (tag === 'template') {
- 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)) !== null) {
- 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.inPre && /^(v-|:|@|#)/.test(name)) {
- const match = /(?:^v-([a-z0-9-]+))?(?:(?::|^@|^#)([^\.]+))?(.+)?$/i.exec(
- name
- )!
- let arg: ExpressionNode | undefined
- if (match[2]) {
- const startOffset = name.indexOf(match[2])
- const loc = getSelection(
- context,
- getNewPosition(context, start, startOffset),
- getNewPosition(context, start, startOffset + match[2].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)
- }
- 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:
- match[1] ||
- (startsWith(name, ':')
- ? 'bind'
- : startsWith(name, '@')
- ? 'on'
- : 'slot'),
- 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
- }
- let unexpectedChars = /["'<=`]/g
- let m: RegExpExecArray | null
- while ((m = unexpectedChars.exec(match[0])) !== null) {
- 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 {
- let rawText = context.source.slice(0, length)
- if (
- mode === TextModes.RAWTEXT ||
- mode === TextModes.CDATA ||
- rawText.indexOf('&') === -1
- ) {
- advanceBy(context, length)
- return rawText
- }
- // DATA or RCDATA containing "&"". Entity decoding required.
- const end = context.offset + length
- let decodedText = ''
- function advance(length: number) {
- advanceBy(context, length)
- rawText = rawText.slice(length)
- }
- while (context.offset < end) {
- const head = /&(?:#x?)?/i.exec(rawText)
- if (!head || context.offset + head.index >= end) {
- const remaining = end - context.offset
- decodedText += rawText.slice(0, remaining)
- advance(remaining)
- break
- }
- // Advance to the "&".
- decodedText += rawText.slice(0, head.index)
- advance(head.index)
- if (head[0] === '&') {
- // Named character reference.
- let name = '',
- value: string | undefined = undefined
- if (/[0-9a-z]/i.test(rawText[1])) {
- for (
- let length = context.options.maxCRNameLength;
- !value && length > 0;
- --length
- ) {
- name = rawText.substr(1, length)
- value = context.options.namedCharacterReferences[name]
- }
- if (value) {
- const semi = name.endsWith(';')
- if (
- mode === TextModes.ATTRIBUTE_VALUE &&
- !semi &&
- /[=a-z0-9]/i.test(rawText[1 + name.length] || '')
- ) {
- decodedText += '&' + name
- advance(1 + name.length)
- } else {
- decodedText += value
- advance(1 + name.length)
- if (!semi) {
- emitError(
- context,
- ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE
- )
- }
- }
- } else {
- emitError(context, ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE)
- decodedText += '&' + name
- advance(1 + name.length)
- }
- } else {
- decodedText += '&'
- advance(1)
- }
- } else {
- // Numeric character reference.
- const hex = head[0] === '&#x'
- const pattern = hex ? /^&#x([0-9a-f]+);?/i : /^&#([0-9]+);?/
- const body = pattern.exec(rawText)
- if (!body) {
- decodedText += head[0]
- emitError(
- context,
- ErrorCodes.ABSENCE_OF_DIGITS_IN_NUMERIC_CHARACTER_REFERENCE
- )
- advance(head[0].length)
- } else {
- // https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
- let cp = Number.parseInt(body[1], hex ? 16 : 10)
- if (cp === 0) {
- emitError(context, ErrorCodes.NULL_CHARACTER_REFERENCE)
- cp = 0xfffd
- } else if (cp > 0x10ffff) {
- emitError(
- context,
- ErrorCodes.CHARACTER_REFERENCE_OUTSIDE_UNICODE_RANGE
- )
- cp = 0xfffd
- } else if (cp >= 0xd800 && cp <= 0xdfff) {
- emitError(context, ErrorCodes.SURROGATE_CHARACTER_REFERENCE)
- cp = 0xfffd
- } else if ((cp >= 0xfdd0 && cp <= 0xfdef) || (cp & 0xfffe) === 0xfffe) {
- emitError(context, ErrorCodes.NONCHARACTER_CHARACTER_REFERENCE)
- } else if (
- (cp >= 0x01 && cp <= 0x08) ||
- cp === 0x0b ||
- (cp >= 0x0d && cp <= 0x1f) ||
- (cp >= 0x7f && cp <= 0x9f)
- ) {
- emitError(context, ErrorCodes.CONTROL_CHARACTER_REFERENCE)
- cp = CCR_REPLACEMENTS[cp] || cp
- }
- decodedText += String.fromCodePoint(cp)
- advance(body[0].length)
- if (!body![0].endsWith(';')) {
- emitError(
- context,
- ErrorCodes.MISSING_SEMICOLON_AFTER_CHARACTER_REFERENCE
- )
- }
- }
- }
- }
- return decodedText
- }
- 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\n\f />]/.test(source[2 + tag.length] || '>')
- )
- }
- // https://html.spec.whatwg.org/multipage/parsing.html#numeric-character-reference-end-state
- const CCR_REPLACEMENTS: { [key: number]: number | undefined } = {
- 0x80: 0x20ac,
- 0x82: 0x201a,
- 0x83: 0x0192,
- 0x84: 0x201e,
- 0x85: 0x2026,
- 0x86: 0x2020,
- 0x87: 0x2021,
- 0x88: 0x02c6,
- 0x89: 0x2030,
- 0x8a: 0x0160,
- 0x8b: 0x2039,
- 0x8c: 0x0152,
- 0x8e: 0x017d,
- 0x91: 0x2018,
- 0x92: 0x2019,
- 0x93: 0x201c,
- 0x94: 0x201d,
- 0x95: 0x2022,
- 0x96: 0x2013,
- 0x97: 0x2014,
- 0x98: 0x02dc,
- 0x99: 0x2122,
- 0x9a: 0x0161,
- 0x9b: 0x203a,
- 0x9c: 0x0153,
- 0x9e: 0x017e,
- 0x9f: 0x0178
- }
|