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 {
return {
type: NodeTypes.ROOT,
source: '',
children: [],
helpers: new Set(),
components: [],

View File

@ -1,5 +1,4 @@
import { ParserOptions } from '../src/options'
import { TextModes } from '../src/parse'
import { ErrorCodes } from '../src/errors'
import {
CommentNode,
@ -1913,35 +1912,38 @@ describe('compiler: parse', () => {
})
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(
`
foo
is {{ bar }} but {{ baz }}`.trim()
fooSrc + barSrc + butSrc + bazSrc
).children
let offset = 0
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(bar.loc.start).toEqual({ line: 2, column: 5, offset })
const barInner = (bar as InterpolationNode).content
offset += 3
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 })
offset += 3
expect(bar.loc.end).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(baz.loc.start).toEqual({ line: 2, column: 19, offset })
const bazInner = (baz as InterpolationNode).content
offset += 3
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 })
offset += 3
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', () => {
const ast = baseParse(`<textarea>Text:\n foo</textarea>`, {
getTextMode: ({ tag }) =>
tag === 'textarea' ? TextModes.RCDATA : TextModes.DATA
parseMode: 'html'
})
const preElement = ast.children[0] as ElementNode
expect(preElement.children).toHaveLength(1)
@ -3069,24 +3070,7 @@ foo
() => {
const spy = vi.fn()
const ast = baseParse(code, {
getNamespace: (tag, parent) => {
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
},
parseMode: 'html',
...options,
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 {
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 { transformIf } from '../../src/transforms/vIf'
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 { transformIf } from '../../src/transforms/vIf'
import { transformElement } from '../../src/transforms/transformElement'

View File

@ -1,7 +1,6 @@
import { TransformContext } from '../src'
import { Position } from '../src/ast'
import {
getInnerRange,
advancePositionWithClone,
isMemberExpressionNode,
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', () => {
function commonAssertions(fn: (str: string) => boolean) {
// should work

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -6,7 +6,6 @@ import {
NodeTypes,
ExpressionNode,
createSimpleExpression,
SourceLocation,
SimpleExpressionNode,
createCallExpression,
createFunctionExpression,
@ -28,17 +27,16 @@ import {
createBlockStatement,
createCompoundExpression,
getVNodeBlockHelper,
getVNodeHelper
getVNodeHelper,
ForParseResult
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import {
getInnerRange,
findProp,
isTemplateNode,
isSlotOutlet,
injectProp,
findDir,
forAliasRE
findDir
} from '../utils'
import {
RENDER_LIST,
@ -256,12 +254,7 @@ export function processFor(
return
}
const parseResult = parseForExpression(
// can only be simple expression because vFor transform is applied
// before expression transform.
dir.exp as SimpleExpressionNode,
context
)
const parseResult = dir.forParseResult
if (!parseResult) {
context.onError(
@ -270,6 +263,8 @@ export function processFor(
return
}
finalizeForParseResult(parseResult, context)
const { addIdentifiers, removeIdentifiers, scopes } = context
const { source, value, key, index } = parseResult
@ -309,128 +304,50 @@ export function processFor(
}
}
// 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
export interface ForParseResult {
source: ExpressionNode
value: ExpressionNode | undefined
key: ExpressionNode | undefined
index: ExpressionNode | undefined
}
export function parseForExpression(
input: SimpleExpressionNode,
export function finalizeForParseResult(
result: ForParseResult,
context: TransformContext
): ForParseResult | undefined {
const loc = input.loc
const exp = input.content
const inMatch = exp.match(forAliasRE)
if (!inMatch) return
) {
if (result.finalized) 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) {
result.source = processExpression(
result.source as SimpleExpressionNode,
context
)
}
if (__DEV__ && __BROWSER__) {
validateBrowserExpression(result.source as SimpleExpressionNode, context)
}
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(
result.key as SimpleExpressionNode,
context,
true
)
}
if (result.key) {
result.key = processExpression(
result.key as SimpleExpressionNode,
context,
true
)
}
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(
result.index as SimpleExpressionNode,
context,
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,
if (result.index) {
result.index = processExpression(
result.index as SimpleExpressionNode,
context,
true
)
}
}
return result
}
function createAliasExpression(
range: SourceLocation,
content: string,
offset: number
): SimpleExpressionNode {
return createSimpleExpression(
content,
false,
getInnerRange(range, offset, content.length)
)
if (__DEV__ && __BROWSER__) {
validateBrowserExpression(result.source as SimpleExpressionNode, context)
if (result.key) {
validateBrowserExpression(
result.key as SimpleExpressionNode,
context,
true
)
}
if (result.index) {
validateBrowserExpression(
result.index as SimpleExpressionNode,
context,
true
)
}
}
result.finalized = true
}
export function createForLoopParams(

View File

@ -29,7 +29,9 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
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 =
exp.type === NodeTypes.SIMPLE_EXPRESSION ? exp.content : rawExp

View File

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

View File

@ -1,5 +1,4 @@
import {
SourceLocation,
Position,
ElementNode,
NodeTypes,
@ -176,31 +175,6 @@ export const isMemberExpression = __BROWSER__
? isMemberExpressionBrowser
: 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(
pos: Position,
source: string,

View File

@ -1,9 +1,4 @@
import {
TextModes,
ParserOptions,
ElementNode,
NodeTypes
} from '@vue/compiler-core'
import { ParserOptions, ElementNode, NodeTypes } from '@vue/compiler-core'
import { isVoidTag, isHTMLTag, isSVGTag } from '@vue/shared'
import { TRANSITION, TRANSITION_GROUP } from './runtimeHelpers'
import { decodeHtml } from './decodeHtml'
@ -16,6 +11,7 @@ export const enum DOMNamespaces {
}
export const parserOptions: ParserOptions = {
parseMode: 'html',
isVoidTag,
isNativeTag: tag => isHTMLTag(tag) || isSVGTag(tag),
isPreTag: tag => tag === 'pre',
@ -75,18 +71,5 @@ export const parserOptions: ParserOptions = {
}
}
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,
{
start: node.children[0].loc.start,
end: node.children[node.children.length - 1].loc.end,
source: ''
end: node.children[node.children.length - 1].loc.end
}
)
)
@ -43,6 +42,7 @@ export const transformTransition: NodeTransform = (node, context) => {
node.props.push({
type: NodeTypes.ATTRIBUTE,
name: 'persisted',
nameLoc: node.loc,
value: undefined,
loc: node.loc
})

View File

@ -3,7 +3,6 @@ import {
ElementNode,
SourceLocation,
CompilerError,
TextModes,
BindingMetadata
} from '@vue/compiler-core'
import * as CompilerDOM from '@vue/compiler-dom'
@ -128,31 +127,7 @@ export function parse(
const errors: (CompilerError | SyntaxError)[] = []
const ast = compiler.parse(source, {
// there are no components at SFC parsing level
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
}
},
parseMode: 'sfc',
onError: e => {
errors.push(e)
}
@ -188,7 +163,9 @@ export function parse(
`difference from stateful ones. Just use a normal <template> ` +
`instead.`
) 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)
}
} else {
@ -314,7 +291,7 @@ function createBlock(
end = node.children[node.children.length - 1].loc.end
content = source.slice(start.offset, end.offset)
} else {
const offset = node.loc.source.indexOf(`</`)
const offset = source.indexOf(`</`, start.offset)
if (offset > -1) {
start = {
line: start.line,
@ -341,18 +318,19 @@ function createBlock(
}
node.props.forEach(p => {
if (p.type === NodeTypes.ATTRIBUTE) {
attrs[p.name] = p.value ? p.value.content || true : true
if (p.name === 'lang') {
const name = p.name
attrs[name] = p.value ? p.value.content || true : true
if (name === 'lang') {
block.lang = p.value && p.value.content
} else if (p.name === 'src') {
} else if (name === 'src') {
block.src = p.value && p.value.content
} else if (type === 'style') {
if (p.name === 'scoped') {
if (name === 'scoped') {
;(block as SFCStyleBlock).scoped = true
} else if (p.name === 'module') {
;(block as SFCStyleBlock).module = attrs[p.name]
} else if (name === 'module') {
;(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
}
}

View File

@ -292,14 +292,15 @@ export const ssrTransformElement: NodeTransform = (node, context) => {
}
} else {
// 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))
} else if (!needMergeProps) {
if (prop.name === 'key' || prop.name === 'ref') {
if (name === 'key' || name === 'ref') {
continue
}
// static prop
if (prop.name === 'class' && prop.value) {
if (name === 'class' && prop.value) {
staticClassBinding = JSON.stringify(prop.value.content)
}
openTag.push(

View File

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