2023-11-13 21:03:39 +08:00
|
|
|
import { fromCodePoint } from 'entities/lib/decode.js'
|
|
|
|
import {
|
2023-11-14 18:03:00 +08:00
|
|
|
AttributeNode,
|
2023-11-14 20:39:08 +08:00
|
|
|
ConstantTypes,
|
2023-11-14 18:03:00 +08:00
|
|
|
DirectiveNode,
|
2023-11-13 21:03:39 +08:00
|
|
|
ElementNode,
|
|
|
|
ElementTypes,
|
2023-11-14 20:39:08 +08:00
|
|
|
ExpressionNode,
|
|
|
|
Namespaces,
|
2023-11-13 21:03:39 +08:00
|
|
|
NodeTypes,
|
|
|
|
RootNode,
|
|
|
|
TemplateChildNode,
|
|
|
|
createRoot
|
|
|
|
} from '../ast'
|
2023-11-12 16:58:24 +08:00
|
|
|
import { ParserOptions } from '../options'
|
2023-11-14 21:55:16 +08:00
|
|
|
import Tokenizer, { CharCodes, isWhitespace } from './Tokenizer'
|
2023-11-14 20:39:08 +08:00
|
|
|
import { CompilerCompatOptions } from '../compat/compatConfig'
|
2023-11-14 21:55:16 +08:00
|
|
|
import { NO, extend } from '@vue/shared'
|
2023-11-14 20:39:08 +08:00
|
|
|
import { defaultOnError, defaultOnWarn } from '../errors'
|
2023-11-13 21:03:39 +08:00
|
|
|
|
2023-11-14 20:39:08 +08:00
|
|
|
type OptionalOptions =
|
|
|
|
| 'htmlMode'
|
|
|
|
| 'getTextMode' // TODO
|
|
|
|
| '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 = {
|
|
|
|
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__
|
|
|
|
}
|
|
|
|
|
|
|
|
const directiveParseRE =
|
|
|
|
/(?:^v-([a-z0-9-]+))?(?:(?::|^\.|^@|^#)(\[[^\]]+\]|[^\.]+))?(.+)?$/i
|
2023-11-13 21:03:39 +08:00
|
|
|
|
|
|
|
const foreignContextElements = new Set(['math', 'svg'])
|
|
|
|
|
|
|
|
const htmlIntegrationElements = new Set([
|
|
|
|
'mi',
|
|
|
|
'mo',
|
|
|
|
'mn',
|
|
|
|
'ms',
|
|
|
|
'mtext',
|
|
|
|
'annotation-xml',
|
|
|
|
'foreignobject',
|
|
|
|
'desc',
|
|
|
|
'title'
|
|
|
|
])
|
|
|
|
|
2023-11-14 20:39:08 +08:00
|
|
|
let currentOptions: MergedParserOptions = defaultParserOptions
|
2023-11-13 21:03:39 +08:00
|
|
|
let currentRoot: RootNode = createRoot([])
|
|
|
|
|
|
|
|
// parser state
|
|
|
|
let htmlMode = false
|
|
|
|
let currentInput = ''
|
2023-11-14 18:03:00 +08:00
|
|
|
let currentElement: ElementNode | null = null
|
|
|
|
let currentProp: AttributeNode | DirectiveNode | null = null
|
|
|
|
let currentAttrValue = ''
|
2023-11-14 21:55:16 +08:00
|
|
|
let currentAttrs: Set<string> = new Set()
|
2023-11-13 21:03:39 +08:00
|
|
|
let inPre = 0
|
2023-11-14 20:39:08 +08:00
|
|
|
let inVPre = 0
|
2023-11-14 18:03:00 +08:00
|
|
|
const stack: ElementNode[] = []
|
2023-11-13 21:03:39 +08:00
|
|
|
const foreignContext: boolean[] = [false]
|
|
|
|
|
|
|
|
const tokenizer = new Tokenizer(
|
|
|
|
// TODO handle entities
|
|
|
|
{ decodeEntities: true },
|
|
|
|
{
|
|
|
|
ontext(start, end) {
|
2023-11-14 01:14:33 +08:00
|
|
|
onText(getSlice(start, end), start, end)
|
2023-11-13 21:03:39 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
ontextentity(cp, end) {
|
2023-11-14 01:14:33 +08:00
|
|
|
onText(fromCodePoint(cp), end - 1, end)
|
2023-11-13 21:03:39 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
onopentagname(start, end) {
|
2023-11-14 18:03:00 +08:00
|
|
|
emitOpenTag(getSlice(start, end), start)
|
2023-11-13 21:03:39 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
onopentagend(end) {
|
2023-11-14 20:39:08 +08:00
|
|
|
endOpenTag(end)
|
2023-11-13 21:03:39 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
onclosetag(start, end) {
|
|
|
|
const name = getSlice(start, end)
|
|
|
|
|
|
|
|
if (
|
|
|
|
htmlMode &&
|
|
|
|
(foreignContextElements.has(name) || htmlIntegrationElements.has(name))
|
|
|
|
) {
|
|
|
|
foreignContext.shift()
|
|
|
|
}
|
|
|
|
|
2023-11-14 21:55:16 +08:00
|
|
|
if (!currentOptions.isVoidTag(name)) {
|
2023-11-14 18:03:00 +08:00
|
|
|
const pos = stack.findIndex(e => e.tag === name)
|
2023-11-13 21:03:39 +08:00
|
|
|
if (pos !== -1) {
|
|
|
|
for (let index = 0; index <= pos; index++) {
|
2023-11-14 18:03:00 +08:00
|
|
|
onCloseTag(stack.shift()!, end)
|
2023-11-13 21:03:39 +08:00
|
|
|
}
|
|
|
|
} else if (htmlMode && name === 'p') {
|
|
|
|
// Implicit open before close
|
2023-11-14 18:03:00 +08:00
|
|
|
emitOpenTag('p', start)
|
|
|
|
closeCurrentTag(end)
|
2023-11-13 21:03:39 +08:00
|
|
|
}
|
|
|
|
} else if (htmlMode && name === 'br') {
|
|
|
|
// TODO
|
|
|
|
// We can't use `emitOpenTag` for implicit open, as `br` would be implicitly closed.
|
|
|
|
// this.cbs.onopentag?.('br', {}, true)
|
|
|
|
// this.cbs.onclosetag?.('br', false)
|
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
onselfclosingtag(end) {
|
2023-11-14 18:03:00 +08:00
|
|
|
closeCurrentTag(end)
|
2023-11-13 21:03:39 +08:00
|
|
|
},
|
|
|
|
|
|
|
|
onattribname(start, end) {
|
2023-11-14 20:39:08 +08:00
|
|
|
const name = getSlice(start, end)
|
2023-11-14 21:55:16 +08:00
|
|
|
if (!inVPre && isDirective(name)) {
|
2023-11-14 20:39:08 +08:00
|
|
|
// directive
|
|
|
|
const match = directiveParseRE.exec(name)!
|
|
|
|
const firstChar = name[0]
|
|
|
|
const isPropShorthand = firstChar === '.'
|
|
|
|
const dirName =
|
|
|
|
match[1] ||
|
|
|
|
(isPropShorthand || firstChar === ':'
|
|
|
|
? 'bind'
|
|
|
|
: firstChar === '@'
|
|
|
|
? 'on'
|
|
|
|
: 'slot')
|
|
|
|
|
|
|
|
let arg: ExpressionNode | undefined
|
|
|
|
if (match[2]) {
|
|
|
|
const isSlot = dirName === 'slot'
|
|
|
|
// const startOffset = name.lastIndexOf(
|
|
|
|
// match[2],
|
|
|
|
// name.length - (match[3]?.length || 0)
|
|
|
|
// )
|
|
|
|
let content = match[2]
|
|
|
|
let isStatic = true
|
|
|
|
|
|
|
|
if (content.startsWith('[')) {
|
|
|
|
isStatic = false
|
|
|
|
|
|
|
|
if (!content.endsWith(']')) {
|
|
|
|
// TODO emitError(
|
|
|
|
// context,
|
|
|
|
// ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END
|
|
|
|
// )
|
|
|
|
content = content.slice(1)
|
|
|
|
} else {
|
|
|
|
content = content.slice(1, content.length - 1)
|
|
|
|
}
|
|
|
|
} else if (isSlot) {
|
|
|
|
// #1241 special case for v-slot: vuetify relies extensively on slot
|
|
|
|
// names containing dots. v-slot doesn't have any modifiers and Vue 2.x
|
|
|
|
// supports such usage so we are keeping it consistent with 2.x.
|
|
|
|
content += match[3] || ''
|
|
|
|
}
|
|
|
|
|
|
|
|
arg = {
|
|
|
|
type: NodeTypes.SIMPLE_EXPRESSION,
|
|
|
|
content,
|
|
|
|
isStatic,
|
|
|
|
constType: isStatic
|
|
|
|
? ConstantTypes.CAN_STRINGIFY
|
|
|
|
: ConstantTypes.NOT_CONSTANT,
|
|
|
|
// @ts-expect-error TODO
|
|
|
|
loc: {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const modifiers = match[3] ? match[3].slice(1).split('.') : []
|
|
|
|
if (isPropShorthand) modifiers.push('prop')
|
|
|
|
|
|
|
|
// 2.x compat v-bind:foo.sync -> v-model:foo
|
|
|
|
if (__COMPAT__ && dirName === 'bind' && arg) {
|
|
|
|
// TODO
|
|
|
|
// 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
|
|
|
|
// )
|
|
|
|
// }
|
|
|
|
}
|
|
|
|
|
|
|
|
currentProp = {
|
|
|
|
type: NodeTypes.DIRECTIVE,
|
|
|
|
name: dirName,
|
|
|
|
exp: undefined,
|
|
|
|
arg,
|
|
|
|
modifiers,
|
|
|
|
// @ts-expect-error TODO
|
|
|
|
loc: {}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// plain attribute
|
|
|
|
currentProp = {
|
|
|
|
type: NodeTypes.ATTRIBUTE,
|
|
|
|
name,
|
|
|
|
value: undefined,
|
|
|
|
loc: {
|
|
|
|
start: tokenizer.getPositionForIndex(start),
|
|
|
|
// @ts-expect-error to be attached on attribute end
|
|
|
|
end: undefined,
|
|
|
|
source: ''
|
|
|
|
}
|
2023-11-14 18:03:00 +08:00
|
|
|
}
|
|
|
|
}
|
2023-11-13 21:03:39 +08:00
|
|
|
},
|
|
|
|
onattribdata(start, end) {
|
2023-11-14 18:03:00 +08:00
|
|
|
currentAttrValue += getSlice(start, end)
|
2023-11-13 21:03:39 +08:00
|
|
|
},
|
|
|
|
onattribentity(codepoint) {
|
2023-11-14 18:03:00 +08:00
|
|
|
currentAttrValue += fromCodePoint(codepoint)
|
2023-11-13 21:03:39 +08:00
|
|
|
},
|
2023-11-14 01:14:33 +08:00
|
|
|
onattribend(_quote, end) {
|
2023-11-15 01:14:36 +08:00
|
|
|
// TODO check duplicate
|
|
|
|
// if (currentAttrs.has(name)) {
|
|
|
|
// // emit error DUPLICATE_ATTRIBUTE
|
|
|
|
// } else {
|
|
|
|
// currentAttrs.add(name)
|
|
|
|
// }
|
|
|
|
// if (currentElement) {
|
|
|
|
// if (currentAttrValue) {
|
|
|
|
// if (currentProp!.type === NodeTypes.ATTRIBUTE) {
|
|
|
|
// // assign value
|
|
|
|
// currentProp!.value = {
|
|
|
|
// type: NodeTypes.TEXT,
|
|
|
|
// content: currentAttrValue,
|
|
|
|
// // @ts-expect-error TODO
|
|
|
|
// loc: {}
|
|
|
|
// }
|
|
|
|
// } else {
|
|
|
|
// // directive
|
|
|
|
// currentProp!.exp = {
|
|
|
|
// type: NodeTypes.SIMPLE_EXPRESSION,
|
|
|
|
// content: currentAttrValue,
|
|
|
|
// isStatic: false,
|
|
|
|
// // Treat as non-constant by default. This can be potentially set to
|
|
|
|
// // other values by `transformExpression` to make it eligible for hoisting.
|
|
|
|
// constType: ConstantTypes.NOT_CONSTANT,
|
|
|
|
// // @ts-expect-error TODO
|
|
|
|
// loc: {}
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// currentProp!.loc.end = tokenizer.getPositionForIndex(end)
|
|
|
|
// currentElement.props.push(currentProp!)
|
|
|
|
// }
|
2023-11-14 18:03:00 +08:00
|
|
|
currentAttrValue = ''
|
2023-11-13 21:03:39 +08:00
|
|
|
},
|
|
|
|
|
2023-11-15 01:14:36 +08:00
|
|
|
ondirname(start, end) {
|
|
|
|
// console.log('name ' + getSlice(start, end))
|
|
|
|
currentProp
|
|
|
|
},
|
|
|
|
ondirarg(start, end) {
|
|
|
|
// console.log('arg ' + getSlice(start, end))
|
|
|
|
},
|
|
|
|
ondirmodifier(start, end) {
|
|
|
|
// console.log('.' + getSlice(start, end))
|
|
|
|
},
|
|
|
|
|
2023-11-13 21:03:39 +08:00
|
|
|
oncomment(start, end, offset) {
|
|
|
|
// TODO oncomment
|
|
|
|
},
|
|
|
|
|
|
|
|
onend() {
|
2023-11-15 01:14:36 +08:00
|
|
|
const end = currentInput.length - 1
|
2023-11-13 21:03:39 +08:00
|
|
|
for (let index = 0; index < stack.length; index++) {
|
2023-11-14 18:03:00 +08:00
|
|
|
onCloseTag(stack[index], end)
|
2023-11-13 21:03:39 +08:00
|
|
|
}
|
|
|
|
},
|
|
|
|
|
|
|
|
oncdata(start, end, offset) {
|
|
|
|
// TODO throw error
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
|
|
|
|
function getSlice(start: number, end: number) {
|
|
|
|
return currentInput.slice(start, end)
|
|
|
|
}
|
|
|
|
|
2023-11-14 18:03:00 +08:00
|
|
|
function emitOpenTag(name: string, start: number) {
|
|
|
|
currentElement = {
|
|
|
|
type: NodeTypes.ELEMENT,
|
|
|
|
tag: name,
|
2023-11-14 20:39:08 +08:00
|
|
|
ns: currentOptions.getNamespace(name, getParent()),
|
2023-11-14 18:03:00 +08:00
|
|
|
// TODO refine tag type
|
|
|
|
tagType: ElementTypes.ELEMENT,
|
|
|
|
props: [],
|
|
|
|
children: [],
|
|
|
|
loc: {
|
|
|
|
start: tokenizer.getPositionForIndex(start - 1),
|
|
|
|
// @ts-expect-error to be attached on tag close
|
|
|
|
end: undefined,
|
|
|
|
source: ''
|
|
|
|
},
|
|
|
|
codegenNode: undefined
|
2023-11-13 21:03:39 +08:00
|
|
|
}
|
2023-11-14 21:55:16 +08:00
|
|
|
currentAttrs.clear()
|
2023-11-14 18:03:00 +08:00
|
|
|
}
|
|
|
|
|
2023-11-14 20:39:08 +08:00
|
|
|
function endOpenTag(end: number) {
|
2023-11-14 18:03:00 +08:00
|
|
|
addNode(currentElement!)
|
|
|
|
const name = currentElement!.tag
|
2023-11-14 20:39:08 +08:00
|
|
|
if (!currentOptions.isVoidTag(name)) {
|
2023-11-14 18:03:00 +08:00
|
|
|
stack.unshift(currentElement!)
|
2023-11-13 21:03:39 +08:00
|
|
|
if (htmlMode) {
|
|
|
|
if (foreignContextElements.has(name)) {
|
|
|
|
foreignContext.unshift(true)
|
|
|
|
} else if (htmlIntegrationElements.has(name)) {
|
|
|
|
foreignContext.unshift(false)
|
|
|
|
}
|
|
|
|
}
|
2023-11-14 20:39:08 +08:00
|
|
|
} else {
|
|
|
|
onCloseTag(currentElement!, end)
|
2023-11-13 21:03:39 +08:00
|
|
|
}
|
2023-11-14 18:03:00 +08:00
|
|
|
currentElement = null
|
2023-11-13 21:03:39 +08:00
|
|
|
}
|
|
|
|
|
2023-11-14 18:03:00 +08:00
|
|
|
function closeCurrentTag(end: number) {
|
|
|
|
const name = currentElement!.tag
|
2023-11-14 20:39:08 +08:00
|
|
|
endOpenTag(end)
|
2023-11-14 18:03:00 +08:00
|
|
|
if (stack[0].tag === name) {
|
|
|
|
onCloseTag(stack.shift()!, end)
|
2023-11-13 21:03:39 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-14 01:14:33 +08:00
|
|
|
function onText(content: string, start: number, end: number) {
|
2023-11-13 21:03:39 +08:00
|
|
|
const parent = getParent()
|
|
|
|
const lastNode = parent.children[parent.children.length - 1]
|
|
|
|
if (lastNode?.type === NodeTypes.TEXT) {
|
|
|
|
// merge
|
|
|
|
lastNode.content += content
|
|
|
|
// TODO update loc
|
|
|
|
} else {
|
|
|
|
parent.children.push({
|
|
|
|
type: NodeTypes.TEXT,
|
|
|
|
content,
|
2023-11-14 01:14:33 +08:00
|
|
|
loc: {
|
2023-11-14 16:35:52 +08:00
|
|
|
start: tokenizer.getPositionForIndex(start),
|
|
|
|
end: tokenizer.getPositionForIndex(end),
|
2023-11-14 01:14:33 +08:00
|
|
|
source: content
|
|
|
|
}
|
2023-11-13 21:03:39 +08:00
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-14 18:03:00 +08:00
|
|
|
function onCloseTag(el: ElementNode, end: number) {
|
|
|
|
// attach end position
|
|
|
|
let offset = 0
|
|
|
|
while (currentInput.charCodeAt(end + offset) !== CharCodes.Gt) {
|
|
|
|
offset++
|
2023-11-13 21:03:39 +08:00
|
|
|
}
|
2023-11-14 18:03:00 +08:00
|
|
|
el.loc.end = tokenizer.getPositionForIndex(end + offset + 1)
|
2023-11-13 21:03:39 +08:00
|
|
|
// whitepsace management
|
2023-11-14 01:14:33 +08:00
|
|
|
el.children = condenseWhitespace(el.children)
|
|
|
|
}
|
|
|
|
|
|
|
|
const windowsNewlineRE = /\r\n/g
|
|
|
|
|
|
|
|
function condenseWhitespace(nodes: TemplateChildNode[]): TemplateChildNode[] {
|
2023-11-13 21:03:39 +08:00
|
|
|
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) {
|
2023-11-14 21:55:16 +08:00
|
|
|
if (isAllWhitespace(node.content)) {
|
|
|
|
const prev = nodes[i - 1]?.type
|
|
|
|
const next = nodes[i + 1]?.type
|
2023-11-13 21:03:39 +08:00
|
|
|
// Remove if:
|
|
|
|
// - the whitespace is the first or last node, or:
|
2023-11-14 21:55:16 +08:00
|
|
|
// - (condense mode) the whitespace is between two comments, or:
|
2023-11-13 21:03:39 +08:00
|
|
|
// - (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 &&
|
2023-11-14 21:55:16 +08:00
|
|
|
((prev === NodeTypes.COMMENT &&
|
|
|
|
(next === NodeTypes.COMMENT || next === NodeTypes.ELEMENT)) ||
|
|
|
|
(prev === NodeTypes.ELEMENT &&
|
|
|
|
(next === NodeTypes.COMMENT ||
|
|
|
|
(next === NodeTypes.ELEMENT &&
|
|
|
|
hasNewlineChar(node.content))))))
|
2023-11-13 21:03:39 +08:00
|
|
|
) {
|
|
|
|
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.
|
2023-11-14 21:55:16 +08:00
|
|
|
node.content = condense(node.content)
|
2023-11-13 21:03:39 +08:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// #6410 normalize windows newlines in <pre>:
|
|
|
|
// in SSR, browsers normalize server-rendered \r\n into a single \n
|
|
|
|
// in the DOM
|
2023-11-14 01:14:33 +08:00
|
|
|
node.content = node.content.replace(windowsNewlineRE, '\n')
|
2023-11-13 21:03:39 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-11-14 01:14:33 +08:00
|
|
|
return removedWhitespace ? nodes.filter(Boolean) : nodes
|
2023-11-13 21:03:39 +08:00
|
|
|
}
|
|
|
|
|
2023-11-14 21:55:16 +08:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2023-11-13 21:03:39 +08:00
|
|
|
function addNode(node: TemplateChildNode) {
|
|
|
|
getParent().children.push(node)
|
|
|
|
}
|
|
|
|
|
|
|
|
function getParent() {
|
2023-11-14 18:03:00 +08:00
|
|
|
return stack[0] || currentRoot
|
2023-11-13 21:03:39 +08:00
|
|
|
}
|
|
|
|
|
2023-11-14 21:55:16 +08:00
|
|
|
function isDirective(name: string) {
|
|
|
|
switch (name[0]) {
|
|
|
|
case ':':
|
|
|
|
case '.':
|
|
|
|
case '@':
|
|
|
|
case '#':
|
|
|
|
return true
|
|
|
|
case 'v':
|
|
|
|
return name[1] === '-'
|
|
|
|
default:
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-13 21:03:39 +08:00
|
|
|
function reset() {
|
|
|
|
tokenizer.reset()
|
2023-11-14 18:03:00 +08:00
|
|
|
currentElement = null
|
|
|
|
currentProp = null
|
2023-11-14 21:55:16 +08:00
|
|
|
currentAttrs.clear()
|
2023-11-14 18:03:00 +08:00
|
|
|
currentAttrValue = ''
|
2023-11-13 21:03:39 +08:00
|
|
|
stack.length = 0
|
|
|
|
foreignContext.length = 1
|
|
|
|
foreignContext[0] = false
|
|
|
|
}
|
2023-11-12 21:42:27 +08:00
|
|
|
|
2023-11-14 20:39:08 +08:00
|
|
|
export function baseParse(input: string, options?: ParserOptions): RootNode {
|
2023-11-13 21:03:39 +08:00
|
|
|
reset()
|
2023-11-14 01:14:33 +08:00
|
|
|
currentInput = input
|
2023-11-14 20:39:08 +08:00
|
|
|
currentOptions = extend({}, defaultParserOptions, options)
|
|
|
|
htmlMode = !!currentOptions.htmlMode
|
2023-11-13 21:03:39 +08:00
|
|
|
const root = (currentRoot = createRoot([]))
|
|
|
|
tokenizer.parse(currentInput)
|
2023-11-14 01:14:33 +08:00
|
|
|
root.children = condenseWhitespace(root.children)
|
2023-11-12 16:58:24 +08:00
|
|
|
return root
|
|
|
|
}
|