| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722 |
- import { fromCodePoint } from 'entities/lib/decode.js'
- import {
- AttributeNode,
- ConstantTypes,
- DirectiveNode,
- ElementNode,
- ElementTypes,
- ForParseResult,
- Namespaces,
- NodeTypes,
- RootNode,
- SimpleExpressionNode,
- SourceLocation,
- TemplateChildNode,
- createRoot,
- createSimpleExpression
- } from '../ast'
- import { ParserOptions } from '../options'
- import Tokenizer, {
- CharCodes,
- ParseMode,
- QuoteType,
- isWhitespace,
- toCharCodes
- } from './Tokenizer'
- import { CompilerCompatOptions } from '../compat/compatConfig'
- import { NO, extend } from '@vue/shared'
- import { defaultOnError, defaultOnWarn } from '../errors'
- import { forAliasRE, isCoreComponent } from '../utils'
- type OptionalOptions =
- | 'whitespace'
- | 'isNativeTag'
- | 'isBuiltInComponent'
- | keyof CompilerCompatOptions
- type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
- Pick<ParserOptions, OptionalOptions>
- // The default decoder only provides escapes for characters reserved as part of
- // the template syntax, and is only used if the custom renderer did not provide
- // a platform-specific decoder.
- const decodeRE = /&(gt|lt|amp|apos|quot);/g
- const decodeMap: Record<string, string> = {
- gt: '>',
- lt: '<',
- amp: '&',
- apos: "'",
- quot: '"'
- }
- export const defaultParserOptions: MergedParserOptions = {
- parseMode: 'base',
- delimiters: [`{{`, `}}`],
- getNamespace: () => Namespaces.HTML,
- isVoidTag: NO,
- isPreTag: NO,
- isCustomElement: NO,
- // TODO handle entities
- decodeEntities: (rawText: string): string =>
- rawText.replace(decodeRE, (_, p1) => decodeMap[p1]),
- onError: defaultOnError,
- onWarn: defaultOnWarn,
- comments: __DEV__
- }
- let currentOptions: MergedParserOptions = defaultParserOptions
- let currentRoot: RootNode | null = null
- // parser state
- let currentInput = ''
- let currentElement: ElementNode | null = null
- let currentProp: AttributeNode | DirectiveNode | null = null
- let currentAttrValue = ''
- let currentAttrStartIndex = -1
- let currentAttrEndIndex = -1
- let currentAttrs: Set<string> = new Set()
- let inPre = 0
- let inVPre = false
- let currentVPreBoundary: ElementNode | null = null
- const stack: ElementNode[] = []
- const tokenizer = new Tokenizer(stack, {
- ontext(start, end) {
- onText(getSlice(start, end), start, end)
- },
- ontextentity(cp, end) {
- onText(fromCodePoint(cp), end - 1, end)
- },
- oninterpolation(start, end) {
- if (inVPre) {
- return onText(getSlice(start, end), start, end)
- }
- let innerStart = start + tokenizer.delimiterOpen.length
- let innerEnd = end - tokenizer.delimiterClose.length
- while (isWhitespace(currentInput.charCodeAt(innerStart))) {
- innerStart++
- }
- while (isWhitespace(currentInput.charCodeAt(innerEnd - 1))) {
- innerEnd--
- }
- addNode({
- type: NodeTypes.INTERPOLATION,
- content: createSimpleExpression(
- getSlice(innerStart, innerEnd),
- false,
- getLoc(innerStart, innerEnd)
- ),
- loc: getLoc(start, end)
- })
- },
- onopentagname(start, end) {
- const name = getSlice(start, end)
- currentElement = {
- type: NodeTypes.ELEMENT,
- tag: name,
- ns: currentOptions.getNamespace(name, getParent()),
- tagType: ElementTypes.ELEMENT, // will be refined on tag close
- props: [],
- children: [],
- loc: getLoc(start - 1),
- codegenNode: undefined
- }
- currentAttrs.clear()
- },
- onopentagend(end) {
- endOpenTag(end)
- },
- onclosetag(start, end) {
- const name = getSlice(start, end)
- if (!currentOptions.isVoidTag(name)) {
- for (let i = 0; i < stack.length; i++) {
- const e = stack[i]
- if (e.tag.toLowerCase() === name.toLowerCase()) {
- for (let j = 0; j <= i; j++) {
- onCloseTag(stack.shift()!, end)
- }
- break
- }
- }
- }
- },
- onselfclosingtag(end) {
- closeCurrentTag(end)
- },
- onattribname(start, end) {
- // plain attribute
- currentProp = {
- type: NodeTypes.ATTRIBUTE,
- name: getSlice(start, end),
- nameLoc: getLoc(start, end),
- value: undefined,
- loc: getLoc(start)
- }
- },
- ondirname(start, end) {
- const raw = getSlice(start, end)
- if (inVPre) {
- currentProp = {
- type: NodeTypes.ATTRIBUTE,
- name: raw,
- nameLoc: getLoc(start, end),
- value: undefined,
- loc: getLoc(start)
- }
- } else {
- const name =
- raw === '.' || raw === ':'
- ? 'bind'
- : raw === '@'
- ? 'on'
- : raw === '#'
- ? 'slot'
- : raw.slice(2)
- currentProp = {
- type: NodeTypes.DIRECTIVE,
- name,
- rawName: raw,
- exp: undefined,
- rawExp: undefined,
- arg: undefined,
- modifiers: raw === '.' ? ['prop'] : [],
- loc: getLoc(start)
- }
- if (name === 'pre') {
- inVPre = true
- currentVPreBoundary = currentElement
- // convert dirs before this one to attributes
- const props = currentElement!.props
- for (let i = 0; i < props.length; i++) {
- if (props[i].type === NodeTypes.DIRECTIVE) {
- props[i] = dirToAttr(props[i] as DirectiveNode)
- }
- }
- }
- }
- },
- ondirarg(start, end) {
- 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 = createSimpleExpression(
- isStatic ? arg : arg.slice(1, -1),
- isStatic,
- getLoc(start, end),
- isStatic ? ConstantTypes.CAN_STRINGIFY : ConstantTypes.NOT_CONSTANT
- )
- }
- },
- ondirmodifier(start, end) {
- const mod = getSlice(start, end)
- if (inVPre) {
- ;(currentProp as AttributeNode).name += '.' + mod
- ;(currentProp as AttributeNode).nameLoc.end = tokenizer.getPos(end)
- } else if ((currentProp as DirectiveNode).name === 'slot') {
- // slot has no modifiers, special case for edge cases like
- // https://github.com/vuejs/language-tools/issues/2710
- const arg = (currentProp as DirectiveNode).arg
- if (arg) {
- ;(arg as SimpleExpressionNode).content += '.' + mod
- arg.loc.end = tokenizer.getPos(end)
- }
- } else {
- ;(currentProp as DirectiveNode).modifiers.push(mod)
- }
- },
- onattribdata(start, end) {
- currentAttrValue += getSlice(start, end)
- if (currentAttrStartIndex < 0) currentAttrStartIndex = start
- currentAttrEndIndex = end
- },
- onattribentity(codepoint) {
- currentAttrValue += fromCodePoint(codepoint)
- },
- onattribnameend(end) {
- // check duplicate attrs
- const start = currentProp!.loc.start.offset
- const name = getSlice(start, end)
- if (currentProp!.type === NodeTypes.DIRECTIVE) {
- currentProp!.rawName = name
- }
- if (currentAttrs.has(name)) {
- currentProp = null
- // TODO emit error DUPLICATE_ATTRIBUTE
- throw new Error(`duplicate attr ${name}`)
- } else {
- currentAttrs.add(name)
- }
- },
- onattribend(quote, end) {
- if (currentElement && currentProp) {
- if (quote !== QuoteType.NoValue) {
- if (currentProp.type === NodeTypes.ATTRIBUTE) {
- // assign value
- // condense whitespaces in class
- if (currentProp!.name === 'class') {
- currentAttrValue = condense(currentAttrValue).trim()
- }
- currentProp!.value = {
- type: NodeTypes.TEXT,
- content: currentAttrValue,
- loc:
- quote === QuoteType.Unquoted
- ? getLoc(currentAttrStartIndex, currentAttrEndIndex)
- : getLoc(currentAttrStartIndex - 1, currentAttrEndIndex + 1)
- }
- } else {
- // directive
- currentProp.rawExp = currentAttrValue
- currentProp.exp = createSimpleExpression(
- currentAttrValue,
- false,
- getLoc(currentAttrStartIndex, currentAttrEndIndex)
- )
- if (currentProp.name === 'for') {
- currentProp.forParseResult = parseForExpression(currentProp.exp)
- }
- }
- }
- currentProp.loc.end = tokenizer.getPos(end)
- if (
- currentProp.type !== NodeTypes.DIRECTIVE ||
- currentProp.name !== 'pre'
- ) {
- currentElement.props.push(currentProp)
- }
- }
- currentAttrValue = ''
- currentAttrStartIndex = currentAttrEndIndex = -1
- },
- oncomment(start, end) {
- if (currentOptions.comments) {
- addNode({
- type: NodeTypes.COMMENT,
- content: getSlice(start, end),
- loc: getLoc(start - 4, end + 3)
- })
- }
- },
- onend() {
- const end = currentInput.length - 1
- for (let index = 0; index < stack.length; index++) {
- onCloseTag(stack[index], end)
- }
- },
- oncdata(start, end) {
- // TODO throw error
- }
- })
- // 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)
- }
- function endOpenTag(end: number) {
- addNode(currentElement!)
- const name = currentElement!.tag
- if (currentOptions.isPreTag(name)) {
- inPre++
- }
- if (currentOptions.isVoidTag(name)) {
- onCloseTag(currentElement!, end)
- } else {
- stack.unshift(currentElement!)
- }
- currentElement = null
- }
- function closeCurrentTag(end: number) {
- const name = currentElement!.tag
- endOpenTag(end)
- if (stack[0].tag === name) {
- onCloseTag(stack.shift()!, end)
- }
- }
- function onText(content: string, start: number, end: number) {
- const parent = getParent()
- const lastNode = parent.children[parent.children.length - 1]
- if (lastNode?.type === NodeTypes.TEXT) {
- // merge
- lastNode.content += content
- lastNode.loc.end = tokenizer.getPos(end)
- } else {
- parent.children.push({
- type: NodeTypes.TEXT,
- content,
- loc: getLoc(start, end)
- })
- }
- }
- function onCloseTag(el: ElementNode, end: number) {
- // attach end position
- let offset = 0
- while (currentInput.charCodeAt(end + offset) !== CharCodes.Gt) {
- offset++
- }
- el.loc.end = tokenizer.getPos(end + offset + 1)
- // refine element type
- const tag = el.tag
- if (!inVPre) {
- if (tag === 'slot') {
- el.tagType = ElementTypes.SLOT
- } else if (isFragmentTemplate(el)) {
- el.tagType = ElementTypes.TEMPLATE
- } else if (isComponent(el)) {
- el.tagType = ElementTypes.COMPONENT
- }
- }
- // whitepsace management
- if (!tokenizer.inRCDATA) {
- el.children = condenseWhitespace(el.children, el.tag)
- }
- if (currentOptions.isPreTag(tag)) {
- inPre--
- }
- if (currentVPreBoundary === el) {
- inVPre = false
- currentVPreBoundary = null
- }
- }
- const specialTemplateDir = new Set(['if', 'else', 'else-if', 'for', 'slot'])
- function isFragmentTemplate({ tag, props }: ElementNode): boolean {
- if (tag === 'template') {
- for (let i = 0; i < props.length; i++) {
- if (
- props[i].type === NodeTypes.DIRECTIVE &&
- specialTemplateDir.has((props[i] as DirectiveNode).name)
- ) {
- return true
- }
- }
- }
- return false
- }
- function isComponent({ tag, props }: ElementNode): boolean {
- if (currentOptions.isCustomElement(tag)) {
- return false
- }
- if (
- tag === 'component' ||
- isUpperCase(tag.charCodeAt(0)) ||
- isCoreComponent(tag) ||
- currentOptions.isBuiltInComponent?.(tag) ||
- (currentOptions.isNativeTag && !currentOptions.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
- }
- // TODO else if (
- // __COMPAT__ &&
- // checkCompatEnabled(
- // CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
- // context,
- // p.loc
- // )
- // ) {
- // return true
- // }
- }
- }
- // TODO else if (
- // __COMPAT__ &&
- // // :is on plain element - only treat as component in compat mode
- // p.name === 'bind' &&
- // isStaticArgOf(p.arg, 'is') &&
- // checkCompatEnabled(
- // CompilerDeprecationTypes.COMPILER_IS_ON_ELEMENT,
- // context,
- // p.loc
- // )
- // ) {
- // return true
- // }
- }
- return false
- }
- function isUpperCase(c: number) {
- return c > 64 && c < 91
- }
- const windowsNewlineRE = /\r\n/g
- function condenseWhitespace(
- nodes: TemplateChildNode[],
- tag?: string
- ): TemplateChildNode[] {
- const shouldCondense = currentOptions.whitespace !== 'preserve'
- let removedWhitespace = false
- for (let i = 0; i < nodes.length; i++) {
- const node = nodes[i]
- if (node.type === NodeTypes.TEXT) {
- if (!inPre) {
- if (isAllWhitespace(node.content)) {
- const prev = nodes[i - 1]?.type
- const next = nodes[i + 1]?.type
- // Remove if:
- // - the whitespace is the first or last node, or:
- // - (condense mode) the whitespace is between two 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 === NodeTypes.COMMENT &&
- (next === NodeTypes.COMMENT || next === NodeTypes.ELEMENT)) ||
- (prev === NodeTypes.ELEMENT &&
- (next === NodeTypes.COMMENT ||
- (next === NodeTypes.ELEMENT &&
- hasNewlineChar(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 = condense(node.content)
- }
- } 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(windowsNewlineRE, '\n')
- }
- }
- }
- if (inPre && tag && currentOptions.isPreTag(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 isAllWhitespace(str: string) {
- for (let i = 0; i < str.length; i++) {
- if (!isWhitespace(str.charCodeAt(i))) {
- return false
- }
- }
- return true
- }
- function hasNewlineChar(str: string) {
- for (let i = 0; i < str.length; i++) {
- const c = str.charCodeAt(i)
- if (c === CharCodes.NewLine || c === CharCodes.CarriageReturn) {
- return true
- }
- }
- return false
- }
- function condense(str: string) {
- let ret = ''
- let prevCharIsWhitespace = false
- for (let i = 0; i < str.length; i++) {
- if (isWhitespace(str.charCodeAt(i))) {
- if (!prevCharIsWhitespace) {
- ret += ' '
- prevCharIsWhitespace = true
- }
- } else {
- ret += str[i]
- prevCharIsWhitespace = false
- }
- }
- return ret
- }
- function addNode(node: TemplateChildNode) {
- getParent().children.push(node)
- }
- function getParent() {
- return stack[0] || currentRoot
- }
- function getLoc(start: number, end?: number): SourceLocation {
- return {
- start: tokenizer.getPos(start),
- // @ts-expect-error allow late attachment
- end: end && tokenizer.getPos(end)
- }
- }
- function dirToAttr(dir: DirectiveNode): AttributeNode {
- const attr: AttributeNode = {
- type: NodeTypes.ATTRIBUTE,
- name: dir.rawName!,
- nameLoc: getLoc(
- dir.loc.start.offset,
- dir.loc.start.offset + dir.rawName!.length
- ),
- value: undefined,
- loc: dir.loc
- }
- if (dir.exp) {
- // account for quotes
- const loc = dir.exp.loc
- if (loc.end.offset < dir.loc.end.offset) {
- loc.start.offset--
- loc.start.column--
- loc.end.offset++
- loc.end.column++
- }
- attr.value = {
- type: NodeTypes.TEXT,
- content: (dir.exp as SimpleExpressionNode).content,
- loc
- }
- }
- return attr
- }
- function reset() {
- tokenizer.reset()
- currentElement = null
- currentProp = null
- currentAttrs.clear()
- currentAttrValue = ''
- currentAttrStartIndex = -1
- currentAttrEndIndex = -1
- stack.length = 0
- }
- export function baseParse(input: string, options?: ParserOptions): RootNode {
- reset()
- currentInput = input
- currentOptions = extend({}, defaultParserOptions, options)
- tokenizer.mode =
- currentOptions.parseMode === 'html'
- ? ParseMode.HTML
- : currentOptions.parseMode === 'sfc'
- ? ParseMode.SFC
- : ParseMode.BASE
- const delimiters = options?.delimiters
- if (delimiters) {
- tokenizer.delimiterOpen = toCharCodes(delimiters[0])
- tokenizer.delimiterClose = toCharCodes(delimiters[1])
- }
- const root = (currentRoot = createRoot([], input))
- tokenizer.parse(currentInput)
- root.loc = getLoc(0, input.length)
- root.children = condenseWhitespace(root.children)
- currentRoot = null
- return root
- }
|