refactor: swap to new template parser

- get rid of SourceLocation.source for memory efficiency
- move source location generation logic transform phase into the parser
  itself so that SourceLocation.source is no longer needed
  - move v-for expression parsing into the parser itself
  - added nameLoc on AttributeNode for use in transformElement

Tests are not passing yet.
This commit is contained in:
Evan You 2023-11-17 14:17:30 +08:00
parent 65b44045ef
commit a60ad9180d
25 changed files with 222 additions and 1541 deletions

View File

@ -40,6 +40,7 @@ import { PatchFlags } from '@vue/shared'
function createRoot(options: Partial<RootNode> = {}): RootNode { function createRoot(options: Partial<RootNode> = {}): RootNode {
return { return {
type: NodeTypes.ROOT, type: NodeTypes.ROOT,
source: '',
children: [], children: [],
helpers: new Set(), helpers: new Set(),
components: [], components: [],

View File

@ -1,5 +1,4 @@
import { ParserOptions } from '../src/options' import { ParserOptions } from '../src/options'
import { TextModes } from '../src/parse'
import { ErrorCodes } from '../src/errors' import { ErrorCodes } from '../src/errors'
import { import {
CommentNode, CommentNode,
@ -1913,35 +1912,38 @@ describe('compiler: parse', () => {
}) })
test.skip('parse with correct location info', () => { test.skip('parse with correct location info', () => {
const fooSrc = `foo
is `
const barSrc = `{{ bar }}`
const butSrc = ` but `
const bazSrc = `{{ baz }}`
const [foo, bar, but, baz] = baseParse( const [foo, bar, but, baz] = baseParse(
` fooSrc + barSrc + butSrc + bazSrc
foo
is {{ bar }} but {{ baz }}`.trim()
).children ).children
let offset = 0 let offset = 0
expect(foo.loc.start).toEqual({ line: 1, column: 1, offset }) expect(foo.loc.start).toEqual({ line: 1, column: 1, offset })
offset += foo.loc.source.length offset += fooSrc.length
expect(foo.loc.end).toEqual({ line: 2, column: 5, offset }) expect(foo.loc.end).toEqual({ line: 2, column: 5, offset })
expect(bar.loc.start).toEqual({ line: 2, column: 5, offset }) expect(bar.loc.start).toEqual({ line: 2, column: 5, offset })
const barInner = (bar as InterpolationNode).content const barInner = (bar as InterpolationNode).content
offset += 3 offset += 3
expect(barInner.loc.start).toEqual({ line: 2, column: 8, offset }) expect(barInner.loc.start).toEqual({ line: 2, column: 8, offset })
offset += barInner.loc.source.length offset += 3
expect(barInner.loc.end).toEqual({ line: 2, column: 11, offset }) expect(barInner.loc.end).toEqual({ line: 2, column: 11, offset })
offset += 3 offset += 3
expect(bar.loc.end).toEqual({ line: 2, column: 14, offset }) expect(bar.loc.end).toEqual({ line: 2, column: 14, offset })
expect(but.loc.start).toEqual({ line: 2, column: 14, offset }) expect(but.loc.start).toEqual({ line: 2, column: 14, offset })
offset += but.loc.source.length offset += butSrc.length
expect(but.loc.end).toEqual({ line: 2, column: 19, offset }) expect(but.loc.end).toEqual({ line: 2, column: 19, offset })
expect(baz.loc.start).toEqual({ line: 2, column: 19, offset }) expect(baz.loc.start).toEqual({ line: 2, column: 19, offset })
const bazInner = (baz as InterpolationNode).content const bazInner = (baz as InterpolationNode).content
offset += 3 offset += 3
expect(bazInner.loc.start).toEqual({ line: 2, column: 22, offset }) expect(bazInner.loc.start).toEqual({ line: 2, column: 22, offset })
offset += bazInner.loc.source.length offset += 3
expect(bazInner.loc.end).toEqual({ line: 2, column: 25, offset }) expect(bazInner.loc.end).toEqual({ line: 2, column: 25, offset })
offset += 3 offset += 3
expect(baz.loc.end).toEqual({ line: 2, column: 28, offset }) expect(baz.loc.end).toEqual({ line: 2, column: 28, offset })
@ -2073,8 +2075,7 @@ foo
test.skip('should NOT condense whitespaces in RCDATA text mode', () => { test.skip('should NOT condense whitespaces in RCDATA text mode', () => {
const ast = baseParse(`<textarea>Text:\n foo</textarea>`, { const ast = baseParse(`<textarea>Text:\n foo</textarea>`, {
getTextMode: ({ tag }) => parseMode: 'html'
tag === 'textarea' ? TextModes.RCDATA : TextModes.DATA
}) })
const preElement = ast.children[0] as ElementNode const preElement = ast.children[0] as ElementNode
expect(preElement.children).toHaveLength(1) expect(preElement.children).toHaveLength(1)
@ -3069,24 +3070,7 @@ foo
() => { () => {
const spy = vi.fn() const spy = vi.fn()
const ast = baseParse(code, { const ast = baseParse(code, {
getNamespace: (tag, parent) => { parseMode: 'html',
const ns = parent ? parent.ns : Namespaces.HTML
if (ns === Namespaces.HTML) {
if (tag === 'svg') {
return (Namespaces.HTML + 1) as any
}
}
return ns
},
getTextMode: ({ tag }) => {
if (tag === 'textarea') {
return TextModes.RCDATA
}
if (tag === 'script') {
return TextModes.RAWTEXT
}
return TextModes.DATA
},
...options, ...options,
onError: spy onError: spy
}) })

View File

@ -1,4 +1,4 @@
import { baseParse } from '../src/parse' import { baseParse } from '../src/parser'
import { transform, NodeTransform } from '../src/transform' import { transform, NodeTransform } from '../src/transform'
import { import {
ElementNode, ElementNode,

View File

@ -1,4 +1,4 @@
import { baseParse as parse } from '../../src/parse' import { baseParse as parse } from '../../src/parser'
import { transform } from '../../src/transform' import { transform } from '../../src/transform'
import { transformIf } from '../../src/transforms/vIf' import { transformIf } from '../../src/transforms/vIf'
import { transformFor } from '../../src/transforms/vFor' import { transformFor } from '../../src/transforms/vFor'

View File

@ -1,4 +1,4 @@
import { baseParse as parse } from '../../src/parse' import { baseParse as parse } from '../../src/parser'
import { transform } from '../../src/transform' import { transform } from '../../src/transform'
import { transformIf } from '../../src/transforms/vIf' import { transformIf } from '../../src/transforms/vIf'
import { transformElement } from '../../src/transforms/transformElement' import { transformElement } from '../../src/transforms/transformElement'

View File

@ -1,7 +1,6 @@
import { TransformContext } from '../src' import { TransformContext } from '../src'
import { Position } from '../src/ast' import { Position } from '../src/ast'
import { import {
getInnerRange,
advancePositionWithClone, advancePositionWithClone,
isMemberExpressionNode, isMemberExpressionNode,
isMemberExpressionBrowser, isMemberExpressionBrowser,
@ -41,32 +40,6 @@ describe('advancePositionWithClone', () => {
}) })
}) })
describe('getInnerRange', () => {
const loc1 = {
source: 'foo\nbar\nbaz',
start: p(1, 1, 0),
end: p(3, 3, 11)
}
test('at start', () => {
const loc2 = getInnerRange(loc1, 0, 4)
expect(loc2.start).toEqual(loc1.start)
expect(loc2.end.column).toBe(1)
expect(loc2.end.line).toBe(2)
expect(loc2.end.offset).toBe(4)
})
test('in between', () => {
const loc2 = getInnerRange(loc1, 4, 3)
expect(loc2.start.column).toBe(1)
expect(loc2.start.line).toBe(2)
expect(loc2.start.offset).toBe(4)
expect(loc2.end.column).toBe(4)
expect(loc2.end.line).toBe(2)
expect(loc2.end.offset).toBe(7)
})
})
describe('isMemberExpression', () => { describe('isMemberExpression', () => {
function commonAssertions(fn: (str: string) => boolean) { function commonAssertions(fn: (str: string) => boolean) {
// should work // should work

View File

@ -1,5 +1,4 @@
import { isString } from '@vue/shared' import { isString } from '@vue/shared'
import { ForParseResult } from './transforms/vFor'
import { import {
RENDER_SLOT, RENDER_SLOT,
CREATE_SLOTS, CREATE_SLOTS,
@ -76,7 +75,6 @@ export interface Node {
export interface SourceLocation { export interface SourceLocation {
start: Position start: Position
end: Position end: Position
source: string
} }
export interface Position { export interface Position {
@ -102,6 +100,7 @@ export type TemplateChildNode =
export interface RootNode extends Node { export interface RootNode extends Node {
type: NodeTypes.ROOT type: NodeTypes.ROOT
source: string
children: TemplateChildNode[] children: TemplateChildNode[]
helpers: Set<symbol> helpers: Set<symbol>
components: string[] components: string[]
@ -182,20 +181,33 @@ export interface CommentNode extends Node {
export interface AttributeNode extends Node { export interface AttributeNode extends Node {
type: NodeTypes.ATTRIBUTE type: NodeTypes.ATTRIBUTE
name: string name: string
nameLoc: SourceLocation
value: TextNode | undefined value: TextNode | undefined
} }
export interface DirectiveNode extends Node { export interface DirectiveNode extends Node {
type: NodeTypes.DIRECTIVE type: NodeTypes.DIRECTIVE
/**
* the normalized name without prefix or shorthands, e.g. "bind", "on"
*/
name: string name: string
/**
* the raw attribute name, preserving shorthand, and including arg & modifiers
* this is only used during parse.
*/
rawName?: string
exp: ExpressionNode | undefined exp: ExpressionNode | undefined
/**
* the raw expression as a string
* only required on directives parsed from templates
*/
rawExp?: string
arg: ExpressionNode | undefined arg: ExpressionNode | undefined
modifiers: string[] modifiers: string[]
raw?: string
/** /**
* optional property to cache the expression parse result for v-for * optional property to cache the expression parse result for v-for
*/ */
parseResult?: ForParseResult forParseResult?: ForParseResult
} }
/** /**
@ -277,6 +289,14 @@ export interface ForNode extends Node {
codegenNode?: ForCodegenNode codegenNode?: ForCodegenNode
} }
export interface ForParseResult {
source: ExpressionNode
value: ExpressionNode | undefined
key: ExpressionNode | undefined
index: ExpressionNode | undefined
finalized: boolean
}
export interface TextCallNode extends Node { export interface TextCallNode extends Node {
type: NodeTypes.TEXT_CALL type: NodeTypes.TEXT_CALL
content: TextNode | InterpolationNode | CompoundExpressionNode content: TextNode | InterpolationNode | CompoundExpressionNode
@ -548,17 +568,17 @@ export interface ForIteratorExpression extends FunctionExpression {
// associated with template nodes, so their source locations are just a stub. // associated with template nodes, so their source locations are just a stub.
// Container types like CompoundExpression also don't need a real location. // Container types like CompoundExpression also don't need a real location.
export const locStub: SourceLocation = { export const locStub: SourceLocation = {
source: '',
start: { line: 1, column: 1, offset: 0 }, start: { line: 1, column: 1, offset: 0 },
end: { line: 1, column: 1, offset: 0 } end: { line: 1, column: 1, offset: 0 }
} }
export function createRoot( export function createRoot(
children: TemplateChildNode[], children: TemplateChildNode[],
loc = locStub source = ''
): RootNode { ): RootNode {
return { return {
type: NodeTypes.ROOT, type: NodeTypes.ROOT,
source,
children, children,
helpers: new Set(), helpers: new Set(),
components: [], components: [],
@ -568,7 +588,7 @@ export function createRoot(
cached: 0, cached: 0,
temps: 0, temps: 0,
codegenNode: undefined, codegenNode: undefined,
loc loc: locStub
} }
} }

View File

@ -116,7 +116,7 @@ function createCodegenContext(
ssr, ssr,
isTS, isTS,
inSSR, inSSR,
source: ast.loc.source, source: ast.source,
code: ``, code: ``,
column: 1, column: 1,
line: 1, line: 1,

View File

@ -1,5 +1,6 @@
import { SourceLocation } from '../ast' import { SourceLocation } from '../ast'
import { CompilerError } from '../errors' import { CompilerError } from '../errors'
// @ts-expect-error TODO
import { ParserContext } from '../parse' import { ParserContext } from '../parse'
import { TransformContext } from '../transform' import { TransformContext } from '../transform'

View File

@ -1,5 +1,5 @@
import { CompilerOptions } from './options' import { CompilerOptions } from './options'
import { baseParse } from './parse' import { baseParse } from './parser/index'
import { transform, NodeTransform, DirectiveTransform } from './transform' import { transform, NodeTransform, DirectiveTransform } from './transform'
import { generate, CodegenResult } from './codegen' import { generate, CodegenResult } from './codegen'
import { RootNode } from './ast' import { RootNode } from './ast'

View File

@ -10,7 +10,7 @@ export {
type BindingMetadata, type BindingMetadata,
BindingTypes BindingTypes
} from './options' } from './options'
export { baseParse, TextModes } from './parse' export { baseParse } from './parser'
export { export {
transform, transform,
type TransformContext, type TransformContext,
@ -70,5 +70,3 @@ export {
warnDeprecation, warnDeprecation,
CompilerDeprecationTypes CompilerDeprecationTypes
} from './compat/compatConfig' } from './compat/compatConfig'
export { baseParse as newParse } from './parser/index'

View File

@ -1,5 +1,4 @@
import { ElementNode, Namespace, TemplateChildNode, ParentNode } from './ast' import { ElementNode, Namespace, TemplateChildNode, ParentNode } from './ast'
import { TextModes } from './parse'
import { CompilerError } from './errors' import { CompilerError } from './errors'
import { import {
NodeTransform, NodeTransform,
@ -42,13 +41,6 @@ export interface ParserOptions
* Get tag namespace * Get tag namespace
*/ */
getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace getNamespace?: (tag: string, parent: ElementNode | undefined) => Namespace
/**
* Get text parsing mode for this element
*/
getTextMode?: (
node: ElementNode,
parent: ElementNode | undefined
) => TextModes
/** /**
* @default ['{{', '}}'] * @default ['{{', '}}']
*/ */

File diff suppressed because it is too large Load Diff

View File

@ -5,13 +5,15 @@ import {
DirectiveNode, DirectiveNode,
ElementNode, ElementNode,
ElementTypes, ElementTypes,
ForParseResult,
Namespaces, Namespaces,
NodeTypes, NodeTypes,
RootNode, RootNode,
SimpleExpressionNode, SimpleExpressionNode,
SourceLocation, SourceLocation,
TemplateChildNode, TemplateChildNode,
createRoot createRoot,
createSimpleExpression
} from '../ast' } from '../ast'
import { ParserOptions } from '../options' import { ParserOptions } from '../options'
import Tokenizer, { import Tokenizer, {
@ -24,13 +26,12 @@ import Tokenizer, {
import { CompilerCompatOptions } from '../compat/compatConfig' import { CompilerCompatOptions } from '../compat/compatConfig'
import { NO, extend } from '@vue/shared' import { NO, extend } from '@vue/shared'
import { defaultOnError, defaultOnWarn } from '../errors' import { defaultOnError, defaultOnWarn } from '../errors'
import { isCoreComponent } from '../utils' import { forAliasRE, isCoreComponent } from '../utils'
type OptionalOptions = type OptionalOptions =
| 'whitespace' | 'whitespace'
| 'isNativeTag' | 'isNativeTag'
| 'isBuiltInComponent' | 'isBuiltInComponent'
| 'getTextMode'
| keyof CompilerCompatOptions | keyof CompilerCompatOptions
type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> & type MergedParserOptions = Omit<Required<ParserOptions>, OptionalOptions> &
@ -64,7 +65,7 @@ export const defaultParserOptions: MergedParserOptions = {
} }
let currentOptions: MergedParserOptions = defaultParserOptions let currentOptions: MergedParserOptions = defaultParserOptions
let currentRoot: RootNode = createRoot([]) let currentRoot: RootNode | null = null
// parser state // parser state
let currentInput = '' let currentInput = ''
@ -102,14 +103,11 @@ const tokenizer = new Tokenizer(stack, {
} }
addNode({ addNode({
type: NodeTypes.INTERPOLATION, type: NodeTypes.INTERPOLATION,
content: { content: createSimpleExpression(
type: NodeTypes.SIMPLE_EXPRESSION, getSlice(innerStart, innerEnd),
isStatic: false, false,
// Set `isConstant` to false by default and will decide in transformExpression getLoc(innerStart, innerEnd)
constType: ConstantTypes.NOT_CONSTANT, ),
content: getSlice(innerStart, innerEnd),
loc: getLoc(innerStart, innerEnd)
},
loc: getLoc(start, end) loc: getLoc(start, end)
}) })
}, },
@ -123,12 +121,7 @@ const tokenizer = new Tokenizer(stack, {
tagType: ElementTypes.ELEMENT, // will be refined on tag close tagType: ElementTypes.ELEMENT, // will be refined on tag close
props: [], props: [],
children: [], children: [],
loc: { loc: getLoc(start - 1),
start: tokenizer.getPos(start - 1),
// @ts-expect-error to be attached on tag close
end: undefined,
source: ''
},
codegenNode: undefined codegenNode: undefined
} }
currentAttrs.clear() currentAttrs.clear()
@ -159,6 +152,7 @@ const tokenizer = new Tokenizer(stack, {
currentProp = { currentProp = {
type: NodeTypes.ATTRIBUTE, type: NodeTypes.ATTRIBUTE,
name: getSlice(start, end), name: getSlice(start, end),
nameLoc: getLoc(start, end),
value: undefined, value: undefined,
loc: getLoc(start) loc: getLoc(start)
} }
@ -170,6 +164,7 @@ const tokenizer = new Tokenizer(stack, {
currentProp = { currentProp = {
type: NodeTypes.ATTRIBUTE, type: NodeTypes.ATTRIBUTE,
name: raw, name: raw,
nameLoc: getLoc(start, end),
value: undefined, value: undefined,
loc: getLoc(start) loc: getLoc(start)
} }
@ -185,7 +180,7 @@ const tokenizer = new Tokenizer(stack, {
currentProp = { currentProp = {
type: NodeTypes.DIRECTIVE, type: NodeTypes.DIRECTIVE,
name, name,
raw, rawName: raw,
exp: undefined, exp: undefined,
arg: undefined, arg: undefined,
modifiers: [], modifiers: [],
@ -209,17 +204,15 @@ const tokenizer = new Tokenizer(stack, {
const arg = getSlice(start, end) const arg = getSlice(start, end)
if (inVPre) { if (inVPre) {
;(currentProp as AttributeNode).name += arg ;(currentProp as AttributeNode).name += arg
;(currentProp as AttributeNode).nameLoc.end = tokenizer.getPos(end)
} else { } else {
const isStatic = arg[0] !== `[` const isStatic = arg[0] !== `[`
;(currentProp as DirectiveNode).arg = { ;(currentProp as DirectiveNode).arg = createSimpleExpression(
type: NodeTypes.SIMPLE_EXPRESSION, arg,
content: arg,
isStatic, isStatic,
constType: isStatic getLoc(start, end),
? ConstantTypes.CAN_STRINGIFY isStatic ? ConstantTypes.CAN_STRINGIFY : ConstantTypes.NOT_CONSTANT
: ConstantTypes.NOT_CONSTANT, )
loc: getLoc(start, end)
}
} }
}, },
@ -227,6 +220,7 @@ const tokenizer = new Tokenizer(stack, {
const mod = getSlice(start, end) const mod = getSlice(start, end)
if (inVPre) { if (inVPre) {
;(currentProp as AttributeNode).name += '.' + mod ;(currentProp as AttributeNode).name += '.' + mod
;(currentProp as AttributeNode).nameLoc.end = tokenizer.getPos(end)
} else { } else {
;(currentProp as DirectiveNode).modifiers.push(mod) ;(currentProp as DirectiveNode).modifiers.push(mod)
} }
@ -247,7 +241,7 @@ const tokenizer = new Tokenizer(stack, {
const start = currentProp!.loc.start.offset const start = currentProp!.loc.start.offset
const name = getSlice(start, end) const name = getSlice(start, end)
if (currentProp!.type === NodeTypes.DIRECTIVE) { if (currentProp!.type === NodeTypes.DIRECTIVE) {
currentProp!.raw = name currentProp!.rawName = name
} }
if (currentAttrs.has(name)) { if (currentAttrs.has(name)) {
currentProp = null currentProp = null
@ -273,15 +267,14 @@ const tokenizer = new Tokenizer(stack, {
} }
} else { } else {
// directive // directive
currentProp.exp = { currentProp.rawExp = currentAttrValue
type: NodeTypes.SIMPLE_EXPRESSION, currentProp.exp = createSimpleExpression(
content: currentAttrValue, currentAttrValue,
isStatic: false, false,
// Treat as non-constant by default. This can be potentially set getLoc(currentAttrStartIndex, currentAttrEndIndex)
// to other values by `transformExpression` to make it eligible )
// for hoisting. if (currentProp.name === 'for') {
constType: ConstantTypes.NOT_CONSTANT, currentProp.forParseResult = parseForExpression(currentProp.exp)
loc: getLoc(currentAttrStartIndex, currentAttrEndIndex)
} }
} }
} }
@ -319,6 +312,73 @@ const tokenizer = new Tokenizer(stack, {
} }
}) })
// 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) { function getSlice(start: number, end: number) {
return currentInput.slice(start, end) return currentInput.slice(start, end)
} }
@ -356,11 +416,7 @@ function onText(content: string, start: number, end: number) {
parent.children.push({ parent.children.push({
type: NodeTypes.TEXT, type: NodeTypes.TEXT,
content, content,
loc: { loc: getLoc(start, end)
start: tokenizer.getPos(start),
end: tokenizer.getPos(end),
source: ''
}
}) })
} }
} }
@ -403,7 +459,7 @@ function isFragmentTemplate({ tag, props }: ElementNode): boolean {
for (let i = 0; i < props.length; i++) { for (let i = 0; i < props.length; i++) {
if ( if (
props[i].type === NodeTypes.DIRECTIVE && props[i].type === NodeTypes.DIRECTIVE &&
specialTemplateDir.has(props[i].name) specialTemplateDir.has((props[i] as DirectiveNode).name)
) { ) {
return true return true
} }
@ -571,7 +627,11 @@ function getLoc(start: number, end?: number): SourceLocation {
function dirToAttr(dir: DirectiveNode): AttributeNode { function dirToAttr(dir: DirectiveNode): AttributeNode {
const attr: AttributeNode = { const attr: AttributeNode = {
type: NodeTypes.ATTRIBUTE, type: NodeTypes.ATTRIBUTE,
name: dir.raw!, name: dir.rawName!,
nameLoc: getLoc(
dir.loc.start.offset,
dir.loc.start.offset + dir.rawName!.length
),
value: undefined, value: undefined,
loc: dir.loc loc: dir.loc
} }
@ -622,9 +682,9 @@ export function baseParse(input: string, options?: ParserOptions): RootNode {
tokenizer.delimiterClose = toCharCodes(delimiters[1]) tokenizer.delimiterClose = toCharCodes(delimiters[1])
} }
const root = (currentRoot = createRoot([])) const root = (currentRoot = createRoot([], input))
tokenizer.parse(currentInput) tokenizer.parse(currentInput)
root.loc.end = tokenizer.getPos(input.length)
root.children = condenseWhitespace(root.children) root.children = condenseWhitespace(root.children)
currentRoot = null
return root return root
} }

View File

@ -49,7 +49,6 @@ import {
GUARD_REACTIVE_PROPS GUARD_REACTIVE_PROPS
} from '../runtimeHelpers' } from '../runtimeHelpers'
import { import {
getInnerRange,
toValidAssetId, toValidAssetId,
findProp, findProp,
isCoreComponent, isCoreComponent,
@ -160,8 +159,7 @@ export const transformElement: NodeTransform = (node, context) => {
context.onError( context.onError(
createCompilerError(ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN, { createCompilerError(ErrorCodes.X_KEEP_ALIVE_INVALID_CHILDREN, {
start: node.children[0].loc.start, start: node.children[0].loc.start,
end: node.children[node.children.length - 1].loc.end, end: node.children[node.children.length - 1].loc.end
source: ''
}) })
) )
} }
@ -489,7 +487,7 @@ export function buildProps(
// static attribute // static attribute
const prop = props[i] const prop = props[i]
if (prop.type === NodeTypes.ATTRIBUTE) { if (prop.type === NodeTypes.ATTRIBUTE) {
const { loc, name, value } = prop const { loc, name, nameLoc, value } = prop
let isStatic = true let isStatic = true
if (name === 'ref') { if (name === 'ref') {
hasRef = true hasRef = true
@ -536,11 +534,7 @@ export function buildProps(
} }
properties.push( properties.push(
createObjectProperty( createObjectProperty(
createSimpleExpression( createSimpleExpression(name, true, nameLoc),
name,
true,
getInnerRange(loc, 0, name.length)
),
createSimpleExpression( createSimpleExpression(
value ? value.content : '', value ? value.content : '',
isStatic, isStatic,

View File

@ -336,7 +336,6 @@ export function processExpression(
id.name, id.name,
false, false,
{ {
source,
start: advancePositionWithClone(node.loc.start, source, start), start: advancePositionWithClone(node.loc.start, source, start),
end: advancePositionWithClone(node.loc.start, source, end) end: advancePositionWithClone(node.loc.start, source, end)
}, },

View File

@ -6,7 +6,6 @@ import {
NodeTypes, NodeTypes,
ExpressionNode, ExpressionNode,
createSimpleExpression, createSimpleExpression,
SourceLocation,
SimpleExpressionNode, SimpleExpressionNode,
createCallExpression, createCallExpression,
createFunctionExpression, createFunctionExpression,
@ -28,17 +27,16 @@ import {
createBlockStatement, createBlockStatement,
createCompoundExpression, createCompoundExpression,
getVNodeBlockHelper, getVNodeBlockHelper,
getVNodeHelper getVNodeHelper,
ForParseResult
} from '../ast' } from '../ast'
import { createCompilerError, ErrorCodes } from '../errors' import { createCompilerError, ErrorCodes } from '../errors'
import { import {
getInnerRange,
findProp, findProp,
isTemplateNode, isTemplateNode,
isSlotOutlet, isSlotOutlet,
injectProp, injectProp,
findDir, findDir
forAliasRE
} from '../utils' } from '../utils'
import { import {
RENDER_LIST, RENDER_LIST,
@ -256,12 +254,7 @@ export function processFor(
return return
} }
const parseResult = parseForExpression( const parseResult = dir.forParseResult
// can only be simple expression because vFor transform is applied
// before expression transform.
dir.exp as SimpleExpressionNode,
context
)
if (!parseResult) { if (!parseResult) {
context.onError( context.onError(
@ -270,6 +263,8 @@ export function processFor(
return return
} }
finalizeForParseResult(parseResult, context)
const { addIdentifiers, removeIdentifiers, scopes } = context const { addIdentifiers, removeIdentifiers, scopes } = context
const { source, value, key, index } = parseResult const { source, value, key, index } = parseResult
@ -309,91 +304,42 @@ export function processFor(
} }
} }
// This regex doesn't cover the case if key or index aliases have destructuring, export function finalizeForParseResult(
// but those do not make sense in the first place, so this works in practice. result: ForParseResult,
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
const stripParensRE = /^\(|\)$/g
export interface ForParseResult {
source: ExpressionNode
value: ExpressionNode | undefined
key: ExpressionNode | undefined
index: ExpressionNode | undefined
}
export function parseForExpression(
input: SimpleExpressionNode,
context: TransformContext context: TransformContext
): ForParseResult | undefined { ) {
const loc = input.loc if (result.finalized) return
const exp = input.content
const inMatch = exp.match(forAliasRE)
if (!inMatch) return
const [, LHS, RHS] = inMatch
const result: ForParseResult = {
source: createAliasExpression(
loc,
RHS.trim(),
exp.indexOf(RHS, LHS.length)
),
value: undefined,
key: undefined,
index: undefined
}
if (!__BROWSER__ && context.prefixIdentifiers) { if (!__BROWSER__ && context.prefixIdentifiers) {
result.source = processExpression( result.source = processExpression(
result.source as SimpleExpressionNode, result.source as SimpleExpressionNode,
context context
) )
if (result.key) {
result.key = processExpression(
result.key as SimpleExpressionNode,
context,
true
)
}
if (result.index) {
result.index = processExpression(
result.index as SimpleExpressionNode,
context,
true
)
}
} }
if (__DEV__ && __BROWSER__) { if (__DEV__ && __BROWSER__) {
validateBrowserExpression(result.source as SimpleExpressionNode, context) validateBrowserExpression(result.source as SimpleExpressionNode, context)
} if (result.key) {
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(loc, keyContent, keyOffset)
if (!__BROWSER__ && context.prefixIdentifiers) {
result.key = processExpression(result.key, context, true)
}
if (__DEV__ && __BROWSER__) {
validateBrowserExpression( validateBrowserExpression(
result.key as SimpleExpressionNode, result.key as SimpleExpressionNode,
context, context,
true true
) )
} }
} if (result.index) {
if (iteratorMatch[2]) {
const indexContent = iteratorMatch[2].trim()
if (indexContent) {
result.index = createAliasExpression(
loc,
indexContent,
exp.indexOf(
indexContent,
result.key
? keyOffset! + keyContent.length
: trimmedOffset + valueContent.length
)
)
if (!__BROWSER__ && context.prefixIdentifiers) {
result.index = processExpression(result.index, context, true)
}
if (__DEV__ && __BROWSER__) {
validateBrowserExpression( validateBrowserExpression(
result.index as SimpleExpressionNode, result.index as SimpleExpressionNode,
context, context,
@ -401,36 +347,7 @@ export function parseForExpression(
) )
} }
} }
} result.finalized = true
}
if (valueContent) {
result.value = createAliasExpression(loc, valueContent, trimmedOffset)
if (!__BROWSER__ && context.prefixIdentifiers) {
result.value = processExpression(result.value, context, true)
}
if (__DEV__ && __BROWSER__) {
validateBrowserExpression(
result.value as SimpleExpressionNode,
context,
true
)
}
}
return result
}
function createAliasExpression(
range: SourceLocation,
content: string,
offset: number
): SimpleExpressionNode {
return createSimpleExpression(
content,
false,
getInnerRange(range, offset, content.length)
)
} }
export function createForLoopParams( export function createForLoopParams(

View File

@ -29,7 +29,9 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
return createTransformProps() return createTransformProps()
} }
const rawExp = exp.loc.source // we assume v-model directives are always parsed
// (not artificially created by a transform)
const rawExp = dir.rawExp!
const expString = const expString =
exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : rawExp exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : rawExp

View File

@ -14,7 +14,6 @@ import {
SourceLocation, SourceLocation,
createConditionalExpression, createConditionalExpression,
ConditionalExpression, ConditionalExpression,
SimpleExpressionNode,
FunctionExpression, FunctionExpression,
CallExpression, CallExpression,
createCallExpression, createCallExpression,
@ -32,7 +31,7 @@ import {
isStaticExp isStaticExp
} from '../utils' } from '../utils'
import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers' import { CREATE_SLOTS, RENDER_LIST, WITH_CTX } from '../runtimeHelpers'
import { parseForExpression, createForLoopParams } from './vFor' import { createForLoopParams, finalizeForParseResult } from './vFor'
import { SlotFlags, slotFlagsText } from '@vue/shared' import { SlotFlags, slotFlagsText } from '@vue/shared'
const defaultFallback = createSimpleExpression(`undefined`, false) const defaultFallback = createSimpleExpression(`undefined`, false)
@ -78,11 +77,9 @@ export const trackVForSlotScopes: NodeTransform = (node, context) => {
node.props.some(isVSlot) && node.props.some(isVSlot) &&
(vFor = findDir(node, 'for')) (vFor = findDir(node, 'for'))
) { ) {
const result = (vFor.parseResult = parseForExpression( const result = vFor.forParseResult
vFor.exp as SimpleExpressionNode,
context
))
if (result) { if (result) {
finalizeForParseResult(result, context)
const { value, key, index } = result const { value, key, index } = result
const { addIdentifiers, removeIdentifiers } = context const { addIdentifiers, removeIdentifiers } = context
value && addIdentifiers(value) value && addIdentifiers(value)
@ -266,10 +263,9 @@ export function buildSlots(
} }
} else if (vFor) { } else if (vFor) {
hasDynamicSlots = true hasDynamicSlots = true
const parseResult = const parseResult = vFor.forParseResult
vFor.parseResult ||
parseForExpression(vFor.exp as SimpleExpressionNode, context)
if (parseResult) { if (parseResult) {
finalizeForParseResult(parseResult, context)
// Render the dynamic slots as an array and add it to the createSlot() // Render the dynamic slots as an array and add it to the createSlot()
// args. The runtime knows how to handle it appropriately. // args. The runtime knows how to handle it appropriately.
dynamicSlots.push( dynamicSlots.push(

View File

@ -1,5 +1,4 @@
import { import {
SourceLocation,
Position, Position,
ElementNode, ElementNode,
NodeTypes, NodeTypes,
@ -176,31 +175,6 @@ export const isMemberExpression = __BROWSER__
? isMemberExpressionBrowser ? isMemberExpressionBrowser
: isMemberExpressionNode : isMemberExpressionNode
export function getInnerRange(
loc: SourceLocation,
offset: number,
length: number
): SourceLocation {
__TEST__ && assert(offset <= loc.source.length)
const source = loc.source.slice(offset, offset + length)
const newLoc: SourceLocation = {
source,
start: advancePositionWithClone(loc.start, loc.source, offset),
end: loc.end
}
if (length != null) {
__TEST__ && assert(offset + length <= loc.source.length)
newLoc.end = advancePositionWithClone(
loc.start,
loc.source,
offset + length
)
}
return newLoc
}
export function advancePositionWithClone( export function advancePositionWithClone(
pos: Position, pos: Position,
source: string, source: string,

View File

@ -1,9 +1,4 @@
import { import { ParserOptions, ElementNode, NodeTypes } from '@vue/compiler-core'
TextModes,
ParserOptions,
ElementNode,
NodeTypes
} from '@vue/compiler-core'
import { isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared' import { isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers' import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
import { decodeHtml } from './decodeHtml' import { decodeHtml } from './decodeHtml'
@ -16,6 +11,7 @@ export const enum DOMNamespaces {
} }
export const parserOptions: ParserOptions = { export const parserOptions: ParserOptions = {
parseMode: 'html',
isVoidTag, isVoidTag,
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag), isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
isPreTag: tag => tag === 'pre', isPreTag: tag => tag === 'pre',
@ -75,18 +71,5 @@ export const parserOptions: ParserOptions = {
} }
} }
return ns return ns
},
// https://html.spec.whatwg.org/multipage/parsing.html#parsing-html-fragments
getTextMode({ tag, ns }: ElementNode): TextModes {
if (ns === DOMNamespaces.HTML) {
if (tag === 'textarea' || tag === 'title') {
return TextModes.RCDATA
}
if (tag === 'style' || tag === 'script') {
return TextModes.RAWTEXT
}
}
return TextModes.DATA
} }
} }

View File

@ -27,8 +27,7 @@ export const transformTransition: NodeTransform = (node, context) => {
DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN, DOMErrorCodes.X_TRANSITION_INVALID_CHILDREN,
{ {
start: node.children[0].loc.start, start: node.children[0].loc.start,
end: node.children[node.children.length - 1].loc.end, end: node.children[node.children.length - 1].loc.end
source: ''
} }
) )
) )
@ -43,6 +42,7 @@ export const transformTransition: NodeTransform = (node, context) => {
node.props.push({ node.props.push({
type: NodeTypes.ATTRIBUTE, type: NodeTypes.ATTRIBUTE,
name: 'persisted', name: 'persisted',
nameLoc: node.loc,
value: undefined, value: undefined,
loc: node.loc loc: node.loc
}) })

View File

@ -3,7 +3,6 @@ import {
ElementNode, ElementNode,
SourceLocation, SourceLocation,
CompilerError, CompilerError,
TextModes,
BindingMetadata BindingMetadata
} from '@vue/compiler-core' } from '@vue/compiler-core'
import * as CompilerDOM from '@vue/compiler-dom' import * as CompilerDOM from '@vue/compiler-dom'
@ -128,31 +127,7 @@ export function parse(
const errors: (CompilerError | SyntaxError)[] = [] const errors: (CompilerError | SyntaxError)[] = []
const ast = compiler.parse(source, { const ast = compiler.parse(source, {
// there are no components at SFC parsing level parseMode: 'sfc',
isNativeTag: () => true,
// preserve all whitespaces
isPreTag: () => true,
getTextMode: ({ tag, props }, parent) => {
// all top level elements except <template> are parsed as raw text
// containers
if (
(!parent && tag !== 'template') ||
// <template lang="xxx"> should also be treated as raw text
(tag === 'template' &&
props.some(
p =>
p.type === NodeTypes.ATTRIBUTE &&
p.name === 'lang' &&
p.value &&
p.value.content &&
p.value.content !== 'html'
))
) {
return TextModes.RAWTEXT
} else {
return TextModes.DATA
}
},
onError: e => { onError: e => {
errors.push(e) errors.push(e)
} }
@ -188,7 +163,9 @@ export function parse(
`difference from stateful ones. Just use a normal <template> ` + `difference from stateful ones. Just use a normal <template> ` +
`instead.` `instead.`
) as CompilerError ) as CompilerError
err.loc = node.props.find(p => p.name === 'functional')!.loc err.loc = node.props.find(
p => p.type === NodeTypes.ATTRIBUTE && p.name === 'functional'
)!.loc
errors.push(err) errors.push(err)
} }
} else { } else {
@ -314,7 +291,7 @@ function createBlock(
end = node.children[node.children.length - 1].loc.end end = node.children[node.children.length - 1].loc.end
content = source.slice(start.offset, end.offset) content = source.slice(start.offset, end.offset)
} else { } else {
const offset = node.loc.source.indexOf(`</`) const offset = source.indexOf(`</`, start.offset)
if (offset > -1) { if (offset > -1) {
start = { start = {
line: start.line, line: start.line,
@ -341,18 +318,19 @@ function createBlock(
} }
node.props.forEach(p => { node.props.forEach(p => {
if (p.type === NodeTypes.ATTRIBUTE) { if (p.type === NodeTypes.ATTRIBUTE) {
attrs[p.name] = p.value ? p.value.content || true : true const name = p.name
if (p.name === 'lang') { attrs[name] = p.value ? p.value.content || true : true
if (name === 'lang') {
block.lang = p.value && p.value.content block.lang = p.value && p.value.content
} else if (p.name === 'src') { } else if (name === 'src') {
block.src = p.value && p.value.content block.src = p.value && p.value.content
} else if (type === 'style') { } else if (type === 'style') {
if (p.name === 'scoped') { if (name === 'scoped') {
;(block as SFCStyleBlock).scoped = true ;(block as SFCStyleBlock).scoped = true
} else if (p.name === 'module') { } else if (name === 'module') {
;(block as SFCStyleBlock).module = attrs[p.name] ;(block as SFCStyleBlock).module = attrs[name]
} }
} else if (type === 'script' && p.name === 'setup') { } else if (type === 'script' && name === 'setup') {
;(block as SFCScriptBlock).setup = attrs.setup ;(block as SFCScriptBlock).setup = attrs.setup
} }
} }

View File

@ -292,14 +292,15 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
} }
} else { } else {
// special case: value on <textarea> // special case: value on <textarea>
if (node.tag === 'textarea' && prop.name === 'value' && prop.value) { const name = prop.name
if (node.tag === 'textarea' && name === 'value' && prop.value) {
rawChildrenMap.set(node, escapeHtml(prop.value.content)) rawChildrenMap.set(node, escapeHtml(prop.value.content))
} else if (!needMergeProps) { } else if (!needMergeProps) {
if (prop.name === 'key' || prop.name === 'ref') { if (name === 'key' || name === 'ref') {
continue continue
} }
// static prop // static prop
if (prop.name === 'class' && prop.value) { if (name === 'class' && prop.value) {
staticClassBinding = JSON.stringify(prop.value.content) staticClassBinding = JSON.stringify(prop.value.content)
} }
openTag.push( openTag.push(

View File

@ -91,5 +91,3 @@ registerRuntimeCompiler(compileToFunction)
export { compileToFunction as compile } export { compileToFunction as compile }
export * from '@vue/runtime-dom' export * from '@vue/runtime-dom'
export { newParse } from '@vue/compiler-dom'