"
return function render() {
with (this) {
- return _toString(_ctx.foo)
+ return _ctx.foo + _toString(bar)
}
}"
`;
const { code } = generate(
createRoot({
children: [
- createInterpolation(
- createCompoundExpression(
- [`_ctx.`, createSimpleExpression(`foo`, false, mockLoc)],
- mockLoc
- ),
+ createCompoundExpression(
+ [
+ `_ctx.`,
+ createSimpleExpression(`foo`, false, mockLoc),
+ ` + `,
+ {
+ type: NodeTypes.INTERPOLATION,
+ loc: mockLoc,
+ content: createSimpleExpression(`bar`, false, mockLoc)
+ }
+ ],
mockLoc
)
]
})
)
- expect(code).toMatch(`return _${TO_STRING}(_ctx.foo)`)
+ expect(code).toMatch(`return _ctx.foo + _${TO_STRING}(bar)`)
expect(code).toMatchSnapshot()
})
})
const div = ast.children[0] as ElementNode
- expect(calls.length).toBe(3)
+ expect(calls.length).toBe(4)
expect(calls[0]).toMatchObject([
+ ast,
+ {
+ parent: null,
+ currentNode: ast
+ }
+ ])
+ expect(calls[1]).toMatchObject([
div,
{
parent: ast,
currentNode: div
}
])
- expect(calls[1]).toMatchObject([
+ expect(calls[2]).toMatchObject([
div.children[0],
{
parent: div,
currentNode: div.children[0]
}
])
- expect(calls[2]).toMatchObject([
+ expect(calls[3]).toMatchObject([
div.children[1],
{
parent: div,
expect(ast.children.length).toBe(2)
const newElement = ast.children[0] as ElementNode
expect(newElement.tag).toBe('p')
- expect(spy).toHaveBeenCalledTimes(3)
+ expect(spy).toHaveBeenCalledTimes(4)
// should traverse the children of replaced node
- expect(spy.mock.calls[1][0]).toBe(newElement.children[0])
+ expect(spy.mock.calls[2][0]).toBe(newElement.children[0])
// should traverse the node after the replaced node
- expect(spy.mock.calls[2][0]).toBe(ast.children[1])
+ expect(spy.mock.calls[3][0]).toBe(ast.children[1])
})
test('context.removeNode', () => {
expect(ast.children[1]).toBe(c2)
// should not traverse children of remove node
- expect(spy).toHaveBeenCalledTimes(3)
+ expect(spy).toHaveBeenCalledTimes(4)
// should traverse nodes around removed
- expect(spy.mock.calls[0][0]).toBe(c1)
- expect(spy.mock.calls[2][0]).toBe(c2)
+ expect(spy.mock.calls[1][0]).toBe(c1)
+ expect(spy.mock.calls[3][0]).toBe(c2)
})
test('context.removeNode (prev sibling)', () => {
if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
context.removeNode()
// remove previous sibling
- context.removeNode(context.parent.children[0])
+ context.removeNode(context.parent!.children[0])
}
}
const spy = jest.fn(plugin)
expect(ast.children.length).toBe(1)
expect(ast.children[0]).toBe(c2)
- expect(spy).toHaveBeenCalledTimes(3)
+ expect(spy).toHaveBeenCalledTimes(4)
// should still traverse first span before removal
- expect(spy.mock.calls[0][0]).toBe(c1)
+ expect(spy.mock.calls[1][0]).toBe(c1)
// should still traverse last span
- expect(spy.mock.calls[2][0]).toBe(c2)
+ expect(spy.mock.calls[3][0]).toBe(c2)
})
test('context.removeNode (next sibling)', () => {
if (node.type === NodeTypes.ELEMENT && node.tag === 'div') {
context.removeNode()
// remove next sibling
- context.removeNode(context.parent.children[1])
+ context.removeNode(context.parent!.children[1])
}
}
const spy = jest.fn(plugin)
expect(ast.children.length).toBe(1)
expect(ast.children[0]).toBe(c1)
- expect(spy).toHaveBeenCalledTimes(2)
+ expect(spy).toHaveBeenCalledTimes(3)
// should still traverse first span before removal
- expect(spy.mock.calls[0][0]).toBe(c1)
+ expect(spy.mock.calls[1][0]).toBe(c1)
// should not traverse last span
- expect(spy.mock.calls[1][0]).toBe(d1)
+ expect(spy.mock.calls[2][0]).toBe(d1)
})
test('context.hoist', () => {
const ast = parse(`<div :id="foo"/><div :id="bar"/>`)
const hoisted: ExpressionNode[] = []
const mock: NodeTransform = (node, context) => {
- const dir = (node as ElementNode).props[0] as DirectiveNode
- hoisted.push(dir.exp!)
- dir.exp = context.hoist(dir.exp!)
+ if (node.type === NodeTypes.ELEMENT) {
+ const dir = node.props[0] as DirectiveNode
+ hoisted.push(dir.exp!)
+ dir.exp = context.hoist(dir.exp!)
+ }
}
transform(ast, {
nodeTransforms: [mock]
--- /dev/null
+import { CompilerOptions, parse, transform, NodeTypes } from '../../src'
+import { optimizeText } from '../../src/transforms/optimizeText'
+import { transformExpression } from '../../src/transforms/transformExpression'
+
+function transformWithTextOpt(template: string, options: CompilerOptions = {}) {
+ const ast = parse(template)
+ transform(ast, {
+ nodeTransforms: [
+ ...(options.prefixIdentifiers ? [transformExpression] : []),
+ optimizeText
+ ],
+ ...options
+ })
+ return ast
+}
+
+describe('compiler: optimize interpolation', () => {
+ test('no consecutive text', () => {
+ const root = transformWithTextOpt(`{{ foo }}`)
+ expect(root.children[0]).toMatchObject({
+ type: NodeTypes.INTERPOLATION,
+ content: {
+ content: `foo`
+ }
+ })
+ })
+
+ test('consecutive text', () => {
+ const root = transformWithTextOpt(`{{ foo }} bar {{ baz }}`)
+ expect(root.children.length).toBe(1)
+ expect(root.children[0]).toMatchObject({
+ type: NodeTypes.COMPOUND_EXPRESSION,
+ children: [
+ { type: NodeTypes.INTERPOLATION, content: { content: `foo` } },
+ ` + `,
+ { type: NodeTypes.TEXT, content: ` bar ` },
+ ` + `,
+ { type: NodeTypes.INTERPOLATION, content: { content: `baz` } }
+ ]
+ })
+ })
+
+ test('consecutive text between elements', () => {
+ const root = transformWithTextOpt(`<div/>{{ foo }} bar {{ baz }}<div/>`)
+ expect(root.children.length).toBe(3)
+ expect(root.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect(root.children[1]).toMatchObject({
+ type: NodeTypes.COMPOUND_EXPRESSION,
+ children: [
+ { type: NodeTypes.INTERPOLATION, content: { content: `foo` } },
+ ` + `,
+ { type: NodeTypes.TEXT, content: ` bar ` },
+ ` + `,
+ { type: NodeTypes.INTERPOLATION, content: { content: `baz` } }
+ ]
+ })
+ expect(root.children[2].type).toBe(NodeTypes.ELEMENT)
+ })
+
+ test('consecutive text mixed with elements', () => {
+ const root = transformWithTextOpt(
+ `<div/>{{ foo }} bar {{ baz }}<div/>{{ foo }} bar {{ baz }}<div/>`
+ )
+ expect(root.children.length).toBe(5)
+ expect(root.children[0].type).toBe(NodeTypes.ELEMENT)
+ expect(root.children[1]).toMatchObject({
+ type: NodeTypes.COMPOUND_EXPRESSION,
+ children: [
+ { type: NodeTypes.INTERPOLATION, content: { content: `foo` } },
+ ` + `,
+ { type: NodeTypes.TEXT, content: ` bar ` },
+ ` + `,
+ { type: NodeTypes.INTERPOLATION, content: { content: `baz` } }
+ ]
+ })
+ expect(root.children[2].type).toBe(NodeTypes.ELEMENT)
+ expect(root.children[3]).toMatchObject({
+ type: NodeTypes.COMPOUND_EXPRESSION,
+ children: [
+ { type: NodeTypes.INTERPOLATION, content: { content: `foo` } },
+ ` + `,
+ { type: NodeTypes.TEXT, content: ` bar ` },
+ ` + `,
+ { type: NodeTypes.INTERPOLATION, content: { content: `baz` } }
+ ]
+ })
+ expect(root.children[4].type).toBe(NodeTypes.ELEMENT)
+ })
+
+ test('with prefixIdentifiers: true', () => {
+ const root = transformWithTextOpt(`{{ foo }} bar {{ baz + qux }}`, {
+ prefixIdentifiers: true
+ })
+ expect(root.children.length).toBe(1)
+ expect(root.children[0]).toMatchObject({
+ type: NodeTypes.COMPOUND_EXPRESSION,
+ children: [
+ { type: NodeTypes.INTERPOLATION, content: { content: `_ctx.foo` } },
+ ` + `,
+ { type: NodeTypes.TEXT, content: ` bar ` },
+ ` + `,
+ {
+ type: NodeTypes.INTERPOLATION,
+ content: {
+ type: NodeTypes.COMPOUND_EXPRESSION,
+ children: [{ content: `_ctx.baz` }, ` + `, { content: `_ctx.qux` }]
+ }
+ }
+ ]
+ })
+ })
+})
export type ChildNode =
| ElementNode
| InterpolationNode
+ | CompoundExpressionNode
| TextNode
| CommentNode
| IfNode
// always dynamic
export interface CompoundExpressionNode extends Node {
type: NodeTypes.COMPOUND_EXPRESSION
- children: (SimpleExpressionNode | string)[]
+ children: (SimpleExpressionNode | InterpolationNode | TextNode | string)[]
// an expression parsed as the params of a function will track
// the identifiers declared inside the function body.
identifiers?: string[]
(allowSingle ||
type === NodeTypes.TEXT ||
type === NodeTypes.INTERPOLATION ||
+ type === NodeTypes.COMPOUND_EXPRESSION ||
(type === NodeTypes.ELEMENT &&
(child as ElementNode).tagType === ElementTypes.SLOT))
) {
if (isString(child)) {
context.push(child)
} else {
- genExpression(child, context)
+ genNode(child, context)
}
}
}
import { transformBind } from './transforms/vBind'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
import { trackSlotScopes } from './transforms/vSlot'
+import { optimizeText } from './transforms/optimizeText'
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
transformIf,
transformFor,
...(prefixIdentifiers ? [transformExpression, trackSlotScopes] : []),
+ optimizeText,
transformStyle,
transformSlotOutlet,
transformElement,
// Transforms that operate directly on a ChildNode. NodeTransforms may mutate,
// replace or remove the node being processed.
export type NodeTransform = (
- node: ChildNode,
+ node: RootNode | ChildNode,
context: TransformContext
) => void | (() => void) | (() => void)[]
statements: Set<string>
hoists: JSChildNode[]
identifiers: { [name: string]: number | undefined }
- parent: ParentNode
+ parent: ParentNode | null
childIndex: number
- currentNode: ChildNode | null
+ currentNode: RootNode | ChildNode | null
helper(name: string): string
replaceNode(node: ChildNode): void
removeNode(node?: ChildNode): void
nodeTransforms,
directiveTransforms,
onError,
- parent: root,
+ parent: null,
+ currentNode: root,
childIndex: 0,
- currentNode: null,
helper(name) {
context.imports.add(name)
return prefixIdentifiers ? name : `_${name}`
},
replaceNode(node) {
/* istanbul ignore if */
- if (__DEV__ && !context.currentNode) {
- throw new Error(`node being replaced is already removed.`)
+ if (__DEV__) {
+ if (!context.currentNode) {
+ throw new Error(`Node being replaced is already removed.`)
+ }
+ if (!context.parent) {
+ throw new Error(`Cannot replace root node.`)
+ }
}
- context.parent.children[context.childIndex] = context.currentNode = node
+ context.parent!.children[context.childIndex] = context.currentNode = node
},
removeNode(node) {
- const list = context.parent.children
+ if (__DEV__ && !context.parent) {
+ throw new Error(`Cannot remove root node.`)
+ }
+ const list = context.parent!.children
const removalIndex = node
? list.indexOf(node as any)
: context.currentNode
context.onNodeRemoved()
}
}
- context.parent.children.splice(removalIndex, 1)
+ context.parent!.children.splice(removalIndex, 1)
},
onNodeRemoved: () => {},
addIdentifiers(exp) {
export function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
- traverseChildren(root, context)
+ traverseNode(root, context)
root.imports = [...context.imports]
root.statements = [...context.statements]
root.hoists = context.hoists
}
}
-export function traverseNode(node: ChildNode, context: TransformContext) {
+export function traverseNode(
+ node: RootNode | ChildNode,
+ context: TransformContext
+) {
// apply transform plugins
const { nodeTransforms } = context
const exitFns = []
break
case NodeTypes.FOR:
case NodeTypes.ELEMENT:
+ case NodeTypes.ROOT:
traverseChildren(node, context)
break
}
+++ /dev/null
-// TODO merge adjacent text nodes and expressions into a single expression
-// e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child.
--- /dev/null
+import { NodeTransform } from '../transform'
+import {
+ NodeTypes,
+ ChildNode,
+ TextNode,
+ InterpolationNode,
+ CompoundExpressionNode
+} from '../ast'
+
+const isText = (node: ChildNode): node is TextNode | InterpolationNode =>
+ node.type === NodeTypes.INTERPOLATION || node.type === NodeTypes.TEXT
+
+// Merge adjacent text nodes and expressions into a single expression
+// e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child.
+export const optimizeText: NodeTransform = node => {
+ if (node.type === NodeTypes.ROOT || node.type === NodeTypes.ELEMENT) {
+ // perform the transform on node exit so that all expressions have already
+ // been processed.
+ return () => {
+ const children = node.children
+ let currentContainer: CompoundExpressionNode | undefined = undefined
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i]
+ if (isText(child)) {
+ for (let j = i + 1; j < children.length; j++) {
+ const next = children[j]
+ if (isText(next)) {
+ if (!currentContainer) {
+ currentContainer = children[i] = {
+ type: NodeTypes.COMPOUND_EXPRESSION,
+ loc: child.loc,
+ children: [child]
+ }
+ }
+ // merge adjacent text node into current
+ currentContainer.children.push(` + `, next)
+ children.splice(j, 1)
+ j--
+ } else {
+ currentContainer = undefined
+ break
+ }
+ }
+ }
+ }
+ }
+ }
+}
})
} else {
// locate the adjacent v-if
- const siblings = context.parent.children
+ const siblings = context.parent!.children
const comments = []
let i = siblings.indexOf(node as any)
while (i-- >= -1) {