feat(compiler): expression prefixing + v-for scope analysis
This commit is contained in:
parent
b04be6a561
commit
e57cb51066
@ -3,13 +3,16 @@ import { compile } from '../../src'
|
|||||||
|
|
||||||
test(`should work`, async () => {
|
test(`should work`, async () => {
|
||||||
const { code, map } = compile(
|
const { code, map } = compile(
|
||||||
`<div v-if="hello">{{ ({ a }, b) => a + b + c }}</div>`,
|
`<div v-for="i in foo">
|
||||||
|
{{ ({ a }, b) => a + b + i + c }} {{ i + 'fe' }} {{ i }}
|
||||||
|
</div>
|
||||||
|
<p>{{ i }}</p>
|
||||||
|
`,
|
||||||
{
|
{
|
||||||
useWith: false
|
useWith: false
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
console.log(code)
|
console.log(code)
|
||||||
console.log(map)
|
|
||||||
const consumer = await new SourceMapConsumer(map!)
|
const consumer = await new SourceMapConsumer(map!)
|
||||||
const pos = consumer.originalPositionFor({
|
const pos = consumer.originalPositionFor({
|
||||||
line: 4,
|
line: 4,
|
||||||
|
@ -348,17 +348,14 @@ function genFor(node: ForNode, context: CodegenContext) {
|
|||||||
genExpression(source, context)
|
genExpression(source, context)
|
||||||
push(`, (`)
|
push(`, (`)
|
||||||
if (valueAlias) {
|
if (valueAlias) {
|
||||||
// not using genExpression here because these aliases can only be code
|
genExpression(valueAlias, context)
|
||||||
// that is valid in the function argument position, so the parse rule can
|
|
||||||
// be off and they don't need identifier prefixing anyway.
|
|
||||||
push(valueAlias.content, valueAlias)
|
|
||||||
}
|
}
|
||||||
if (keyAlias) {
|
if (keyAlias) {
|
||||||
if (!valueAlias) {
|
if (!valueAlias) {
|
||||||
push(`_`)
|
push(`_`)
|
||||||
}
|
}
|
||||||
push(`, `)
|
push(`, `)
|
||||||
push(keyAlias.content, keyAlias)
|
genExpression(keyAlias, context)
|
||||||
}
|
}
|
||||||
if (objectIndexAlias) {
|
if (objectIndexAlias) {
|
||||||
if (!keyAlias) {
|
if (!keyAlias) {
|
||||||
@ -369,7 +366,7 @@ function genFor(node: ForNode, context: CodegenContext) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
push(`, `)
|
push(`, `)
|
||||||
push(objectIndexAlias.content, objectIndexAlias)
|
genExpression(objectIndexAlias, context)
|
||||||
}
|
}
|
||||||
push(`) => `)
|
push(`) => `)
|
||||||
genChildren(children, context)
|
genChildren(children, context)
|
||||||
|
@ -2,7 +2,7 @@ import { SourceLocation } from './ast'
|
|||||||
|
|
||||||
export interface CompilerError extends SyntaxError {
|
export interface CompilerError extends SyntaxError {
|
||||||
code: ErrorCodes
|
code: ErrorCodes
|
||||||
loc: SourceLocation
|
loc?: SourceLocation
|
||||||
}
|
}
|
||||||
|
|
||||||
export function defaultOnError(error: CompilerError) {
|
export function defaultOnError(error: CompilerError) {
|
||||||
@ -11,13 +11,11 @@ export function defaultOnError(error: CompilerError) {
|
|||||||
|
|
||||||
export function createCompilerError(
|
export function createCompilerError(
|
||||||
code: ErrorCodes,
|
code: ErrorCodes,
|
||||||
loc: SourceLocation
|
loc?: SourceLocation
|
||||||
): CompilerError {
|
): CompilerError {
|
||||||
const error = new SyntaxError(
|
const msg = __DEV__ || !__BROWSER__ ? errorMessages[code] : code
|
||||||
`${__DEV__ || !__BROWSER__ ? errorMessages[code] : code} (${
|
const locInfo = loc ? ` (${loc.start.line}:${loc.start.column})` : ``
|
||||||
loc.start.line
|
const error = new SyntaxError(msg + locInfo) as CompilerError
|
||||||
}:${loc.start.column})`
|
|
||||||
) as CompilerError
|
|
||||||
error.code = code
|
error.code = code
|
||||||
error.loc = loc
|
error.loc = loc
|
||||||
return error
|
return error
|
||||||
@ -56,6 +54,8 @@ export const enum ErrorCodes {
|
|||||||
UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
|
UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME,
|
||||||
UNEXPECTED_SOLIDUS_IN_TAG,
|
UNEXPECTED_SOLIDUS_IN_TAG,
|
||||||
UNKNOWN_NAMED_CHARACTER_REFERENCE,
|
UNKNOWN_NAMED_CHARACTER_REFERENCE,
|
||||||
|
|
||||||
|
// Vue-specific parse errors
|
||||||
X_INVALID_END_TAG,
|
X_INVALID_END_TAG,
|
||||||
X_MISSING_END_TAG,
|
X_MISSING_END_TAG,
|
||||||
X_MISSING_INTERPOLATION_END,
|
X_MISSING_INTERPOLATION_END,
|
||||||
@ -66,7 +66,10 @@ export const enum ErrorCodes {
|
|||||||
X_ELSE_NO_ADJACENT_IF,
|
X_ELSE_NO_ADJACENT_IF,
|
||||||
X_FOR_NO_EXPRESSION,
|
X_FOR_NO_EXPRESSION,
|
||||||
X_FOR_MALFORMED_EXPRESSION,
|
X_FOR_MALFORMED_EXPRESSION,
|
||||||
X_V_BIND_NO_EXPRESSION
|
X_V_BIND_NO_EXPRESSION,
|
||||||
|
|
||||||
|
// generic errors
|
||||||
|
X_STRIP_WITH_NOT_SUPPORTED
|
||||||
}
|
}
|
||||||
|
|
||||||
export const errorMessages: { [code: number]: string } = {
|
export const errorMessages: { [code: number]: string } = {
|
||||||
@ -116,14 +119,21 @@ export const errorMessages: { [code: number]: string } = {
|
|||||||
"'<?' is allowed only in XML context.",
|
"'<?' is allowed only in XML context.",
|
||||||
[ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG]: "Illegal '/' in tags.",
|
[ErrorCodes.UNEXPECTED_SOLIDUS_IN_TAG]: "Illegal '/' in tags.",
|
||||||
[ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE]: 'Unknown entity name.',
|
[ErrorCodes.UNKNOWN_NAMED_CHARACTER_REFERENCE]: 'Unknown entity name.',
|
||||||
|
|
||||||
|
// Vue-specific parse errors
|
||||||
[ErrorCodes.X_INVALID_END_TAG]: 'Invalid end tag.',
|
[ErrorCodes.X_INVALID_END_TAG]: 'Invalid end tag.',
|
||||||
[ErrorCodes.X_MISSING_END_TAG]: 'End tag was not found.',
|
[ErrorCodes.X_MISSING_END_TAG]: 'End tag was not found.',
|
||||||
[ErrorCodes.X_MISSING_INTERPOLATION_END]:
|
[ErrorCodes.X_MISSING_INTERPOLATION_END]:
|
||||||
'Interpolation end sign was not found.',
|
'Interpolation end sign was not found.',
|
||||||
|
[ErrorCodes.X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END]:
|
||||||
|
'End bracket for dynamic directive argument was not found.',
|
||||||
|
|
||||||
// transform errors
|
// transform errors
|
||||||
[ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF]: `v-else-if has no adjacent v-if`,
|
[ErrorCodes.X_ELSE_IF_NO_ADJACENT_IF]: `v-else-if has no adjacent v-if`,
|
||||||
[ErrorCodes.X_ELSE_NO_ADJACENT_IF]: `v-else has no adjacent v-if`,
|
[ErrorCodes.X_ELSE_NO_ADJACENT_IF]: `v-else has no adjacent v-if`,
|
||||||
[ErrorCodes.X_FOR_NO_EXPRESSION]: `v-for has no expression`,
|
[ErrorCodes.X_FOR_NO_EXPRESSION]: `v-for has no expression`,
|
||||||
[ErrorCodes.X_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression`
|
[ErrorCodes.X_FOR_MALFORMED_EXPRESSION]: `v-for has invalid expression`,
|
||||||
|
|
||||||
|
// generic errors
|
||||||
|
[ErrorCodes.X_STRIP_WITH_NOT_SUPPORTED]: `useWith: false is not supported in this build of compiler because it is optimized for payload size.`
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,8 @@ import { transformFor } from './transforms/vFor'
|
|||||||
import { prepareElementForCodegen } from './transforms/element'
|
import { prepareElementForCodegen } from './transforms/element'
|
||||||
import { transformOn } from './transforms/vOn'
|
import { transformOn } from './transforms/vOn'
|
||||||
import { transformBind } from './transforms/vBind'
|
import { transformBind } from './transforms/vBind'
|
||||||
import { rewriteExpression } from './transforms/expression'
|
import { expressionTransform } from './transforms/expression'
|
||||||
|
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
|
||||||
|
|
||||||
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
|
export type CompilerOptions = ParserOptions & TransformOptions & CodegenOptions
|
||||||
|
|
||||||
@ -17,13 +18,21 @@ export function compile(
|
|||||||
options: CompilerOptions = {}
|
options: CompilerOptions = {}
|
||||||
): CodegenResult {
|
): CodegenResult {
|
||||||
const ast = isString(template) ? parse(template, options) : template
|
const ast = isString(template) ? parse(template, options) : template
|
||||||
|
const useWith = __BROWSER__ || options.useWith !== false
|
||||||
|
|
||||||
|
if (__BROWSER__ && options.useWith === false) {
|
||||||
|
;(options.onError || defaultOnError)(
|
||||||
|
createCompilerError(ErrorCodes.X_STRIP_WITH_NOT_SUPPORTED)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
transform(ast, {
|
transform(ast, {
|
||||||
...options,
|
...options,
|
||||||
|
useWith,
|
||||||
nodeTransforms: [
|
nodeTransforms: [
|
||||||
...(!__BROWSER__ && options.useWith === false ? [rewriteExpression] : []),
|
|
||||||
transformIf,
|
transformIf,
|
||||||
transformFor,
|
transformFor,
|
||||||
|
...(useWith ? [] : [expressionTransform]),
|
||||||
prepareElementForCodegen,
|
prepareElementForCodegen,
|
||||||
...(options.nodeTransforms || []) // user transforms
|
...(options.nodeTransforms || []) // user transforms
|
||||||
],
|
],
|
||||||
|
@ -5,9 +5,10 @@ import {
|
|||||||
ChildNode,
|
ChildNode,
|
||||||
ElementNode,
|
ElementNode,
|
||||||
DirectiveNode,
|
DirectiveNode,
|
||||||
Property
|
Property,
|
||||||
|
ExpressionNode
|
||||||
} from './ast'
|
} from './ast'
|
||||||
import { isString } from '@vue/shared'
|
import { isString, isArray } from '@vue/shared'
|
||||||
import { CompilerError, defaultOnError } from './errors'
|
import { CompilerError, defaultOnError } from './errors'
|
||||||
|
|
||||||
// There are two types of transforms:
|
// There are two types of transforms:
|
||||||
@ -15,7 +16,10 @@ import { CompilerError, defaultOnError } from './errors'
|
|||||||
// - NodeTransform:
|
// - NodeTransform:
|
||||||
// Transforms that operate directly on a ChildNode. NodeTransforms may mutate,
|
// Transforms that operate directly on a ChildNode. NodeTransforms may mutate,
|
||||||
// replace or remove the node being processed.
|
// replace or remove the node being processed.
|
||||||
export type NodeTransform = (node: ChildNode, context: TransformContext) => void
|
export type NodeTransform = (
|
||||||
|
node: ChildNode,
|
||||||
|
context: TransformContext
|
||||||
|
) => void | (() => void) | (() => void)[]
|
||||||
|
|
||||||
// - DirectiveTransform:
|
// - DirectiveTransform:
|
||||||
// Transforms that handles a single directive attribute on an element.
|
// Transforms that handles a single directive attribute on an element.
|
||||||
@ -34,11 +38,12 @@ export type StructuralDirectiveTransform = (
|
|||||||
node: ElementNode,
|
node: ElementNode,
|
||||||
dir: DirectiveNode,
|
dir: DirectiveNode,
|
||||||
context: TransformContext
|
context: TransformContext
|
||||||
) => void
|
) => void | (() => void)
|
||||||
|
|
||||||
export interface TransformOptions {
|
export interface TransformOptions {
|
||||||
nodeTransforms?: NodeTransform[]
|
nodeTransforms?: NodeTransform[]
|
||||||
directiveTransforms?: { [name: string]: DirectiveTransform }
|
directiveTransforms?: { [name: string]: DirectiveTransform }
|
||||||
|
useWith?: boolean
|
||||||
onError?: (error: CompilerError) => void
|
onError?: (error: CompilerError) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,19 +58,27 @@ export interface TransformContext extends Required<TransformOptions> {
|
|||||||
replaceNode(node: ChildNode): void
|
replaceNode(node: ChildNode): void
|
||||||
removeNode(node?: ChildNode): void
|
removeNode(node?: ChildNode): void
|
||||||
onNodeRemoved: () => void
|
onNodeRemoved: () => void
|
||||||
|
addIdentifier(exp: ExpressionNode): void
|
||||||
|
removeIdentifier(exp: ExpressionNode): void
|
||||||
}
|
}
|
||||||
|
|
||||||
function createTransformContext(
|
function createTransformContext(
|
||||||
root: RootNode,
|
root: RootNode,
|
||||||
options: TransformOptions
|
{
|
||||||
|
useWith = true,
|
||||||
|
nodeTransforms = [],
|
||||||
|
directiveTransforms = {},
|
||||||
|
onError = defaultOnError
|
||||||
|
}: TransformOptions
|
||||||
): TransformContext {
|
): TransformContext {
|
||||||
const context: TransformContext = {
|
const context: TransformContext = {
|
||||||
imports: new Set(),
|
imports: new Set(),
|
||||||
statements: [],
|
statements: [],
|
||||||
identifiers: {},
|
identifiers: {},
|
||||||
nodeTransforms: options.nodeTransforms || [],
|
useWith,
|
||||||
directiveTransforms: options.directiveTransforms || {},
|
nodeTransforms,
|
||||||
onError: options.onError || defaultOnError,
|
directiveTransforms,
|
||||||
|
onError,
|
||||||
parent: root,
|
parent: root,
|
||||||
ancestors: [],
|
ancestors: [],
|
||||||
childIndex: 0,
|
childIndex: 0,
|
||||||
@ -99,7 +112,13 @@ function createTransformContext(
|
|||||||
}
|
}
|
||||||
context.parent.children.splice(removalIndex, 1)
|
context.parent.children.splice(removalIndex, 1)
|
||||||
},
|
},
|
||||||
onNodeRemoved: () => {}
|
onNodeRemoved: () => {},
|
||||||
|
addIdentifier(exp) {
|
||||||
|
context.identifiers[exp.content] = true
|
||||||
|
},
|
||||||
|
removeIdentifier(exp) {
|
||||||
|
delete context.identifiers[exp.content]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return context
|
return context
|
||||||
}
|
}
|
||||||
@ -115,10 +134,7 @@ export function traverseChildren(
|
|||||||
parent: ParentNode,
|
parent: ParentNode,
|
||||||
context: TransformContext
|
context: TransformContext
|
||||||
) {
|
) {
|
||||||
// ancestors and identifiers need to be cached here since they may get
|
|
||||||
// replaced during a child's traversal
|
|
||||||
const ancestors = context.ancestors.concat(parent)
|
const ancestors = context.ancestors.concat(parent)
|
||||||
const identifiers = context.identifiers
|
|
||||||
let i = 0
|
let i = 0
|
||||||
const nodeRemoved = () => {
|
const nodeRemoved = () => {
|
||||||
i--
|
i--
|
||||||
@ -131,7 +147,6 @@ export function traverseChildren(
|
|||||||
context.ancestors = ancestors
|
context.ancestors = ancestors
|
||||||
context.childIndex = i
|
context.childIndex = i
|
||||||
context.onNodeRemoved = nodeRemoved
|
context.onNodeRemoved = nodeRemoved
|
||||||
context.identifiers = identifiers
|
|
||||||
traverseNode(child, context)
|
traverseNode(child, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,9 +154,17 @@ export function traverseChildren(
|
|||||||
export function traverseNode(node: ChildNode, context: TransformContext) {
|
export function traverseNode(node: ChildNode, context: TransformContext) {
|
||||||
// apply transform plugins
|
// apply transform plugins
|
||||||
const { nodeTransforms } = context
|
const { nodeTransforms } = context
|
||||||
|
const exitFns = []
|
||||||
for (let i = 0; i < nodeTransforms.length; i++) {
|
for (let i = 0; i < nodeTransforms.length; i++) {
|
||||||
const plugin = nodeTransforms[i]
|
const plugin = nodeTransforms[i]
|
||||||
plugin(node, context)
|
const onExit = plugin(node, context)
|
||||||
|
if (onExit) {
|
||||||
|
if (isArray(onExit)) {
|
||||||
|
exitFns.push(...onExit)
|
||||||
|
} else {
|
||||||
|
exitFns.push(onExit)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!context.currentNode) {
|
if (!context.currentNode) {
|
||||||
// node was removed
|
// node was removed
|
||||||
return
|
return
|
||||||
@ -163,6 +186,11 @@ export function traverseNode(node: ChildNode, context: TransformContext) {
|
|||||||
traverseChildren(node, context)
|
traverseChildren(node, context)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// exit transforms
|
||||||
|
for (let i = 0; i < exitFns.length; i++) {
|
||||||
|
exitFns[i]()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createStructuralDirectiveTransform(
|
export function createStructuralDirectiveTransform(
|
||||||
@ -176,6 +204,7 @@ export function createStructuralDirectiveTransform(
|
|||||||
return (node, context) => {
|
return (node, context) => {
|
||||||
if (node.type === NodeTypes.ELEMENT) {
|
if (node.type === NodeTypes.ELEMENT) {
|
||||||
const { props } = node
|
const { props } = node
|
||||||
|
const exitFns = []
|
||||||
for (let i = 0; i < props.length; i++) {
|
for (let i = 0; i < props.length; i++) {
|
||||||
const prop = props[i]
|
const prop = props[i]
|
||||||
if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
|
if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
|
||||||
@ -184,9 +213,11 @@ export function createStructuralDirectiveTransform(
|
|||||||
// traverse itself in case it moves the node around
|
// traverse itself in case it moves the node around
|
||||||
props.splice(i, 1)
|
props.splice(i, 1)
|
||||||
i--
|
i--
|
||||||
fn(node, prop, context)
|
const onExit = fn(node, prop, context)
|
||||||
|
if (onExit) exitFns.push(onExit)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return exitFns
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,30 +15,56 @@ import { NodeTypes, createExpression, ExpressionNode } from '../ast'
|
|||||||
import { Node, Function, Identifier } from 'estree'
|
import { Node, Function, Identifier } from 'estree'
|
||||||
import { advancePositionWithClone } from '../utils'
|
import { advancePositionWithClone } from '../utils'
|
||||||
|
|
||||||
export const rewriteExpression: NodeTransform = (node, context) => {
|
export const expressionTransform: NodeTransform = (node, context) => {
|
||||||
if (node.type === NodeTypes.EXPRESSION && !node.isStatic) {
|
if (node.type === NodeTypes.EXPRESSION && !node.isStatic) {
|
||||||
context.replaceNode(convertExpression(node, context))
|
processExpression(node, context)
|
||||||
} else if (node.type === NodeTypes.ELEMENT) {
|
} else if (node.type === NodeTypes.ELEMENT) {
|
||||||
// handle directives on element
|
// handle directives on element
|
||||||
for (let i = 0; i < node.props.length; i++) {
|
for (let i = 0; i < node.props.length; i++) {
|
||||||
const prop = node.props[i]
|
const prop = node.props[i]
|
||||||
if (prop.type === NodeTypes.DIRECTIVE) {
|
if (prop.type === NodeTypes.DIRECTIVE) {
|
||||||
if (prop.exp) {
|
if (prop.exp) {
|
||||||
prop.exp = convertExpression(prop.exp, context)
|
processExpression(prop.exp, context)
|
||||||
}
|
}
|
||||||
if (prop.arg && !prop.arg.isStatic) {
|
if (prop.arg && !prop.arg.isStatic) {
|
||||||
prop.arg = convertExpression(prop.arg, context)
|
processExpression(prop.arg, context)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertExpression(
|
const simpleIdRE = /^[a-zA-Z$_][\w$]*$/
|
||||||
|
|
||||||
|
// cache node requires
|
||||||
|
let _parseScript: typeof parseScript
|
||||||
|
let _walk: typeof walk
|
||||||
|
|
||||||
|
export function processExpression(
|
||||||
node: ExpressionNode,
|
node: ExpressionNode,
|
||||||
context: TransformContext
|
context: TransformContext
|
||||||
): ExpressionNode {
|
) {
|
||||||
const ast = parseScript(`(${node.content})`, { ranges: true }) as any
|
// lazy require dependencies so that they don't end up in rollup's dep graph
|
||||||
|
// and thus can be tree-shaken in browser builds.
|
||||||
|
const parseScript =
|
||||||
|
_parseScript || (_parseScript = require('meriyah').parseScript)
|
||||||
|
const walk = _walk || (_walk = require('estree-walker').walk)
|
||||||
|
|
||||||
|
// fast path if expression is a simple identifier.
|
||||||
|
if (simpleIdRE.test(node.content)) {
|
||||||
|
if (!context.identifiers[node.content]) {
|
||||||
|
node.content = `_ctx.${node.content}`
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let ast
|
||||||
|
try {
|
||||||
|
ast = parseScript(`(${node.content})`, { ranges: true }) as any
|
||||||
|
} catch (e) {
|
||||||
|
context.onError(e)
|
||||||
|
return
|
||||||
|
}
|
||||||
const ids: Node[] = []
|
const ids: Node[] = []
|
||||||
const knownIds = Object.create(context.identifiers)
|
const knownIds = Object.create(context.identifiers)
|
||||||
|
|
||||||
@ -98,10 +124,7 @@ function convertExpression(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
node.children = children
|
||||||
...node,
|
|
||||||
children
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const globals = new Set(
|
const globals = new Set(
|
||||||
|
@ -1,8 +1,17 @@
|
|||||||
import { createStructuralDirectiveTransform } from '../transform'
|
import {
|
||||||
import { NodeTypes, ExpressionNode, createExpression } from '../ast'
|
createStructuralDirectiveTransform,
|
||||||
|
TransformContext
|
||||||
|
} from '../transform'
|
||||||
|
import {
|
||||||
|
NodeTypes,
|
||||||
|
ExpressionNode,
|
||||||
|
createExpression,
|
||||||
|
SourceLocation
|
||||||
|
} from '../ast'
|
||||||
import { createCompilerError, ErrorCodes } from '../errors'
|
import { createCompilerError, ErrorCodes } from '../errors'
|
||||||
import { getInnerRange } from '../utils'
|
import { getInnerRange } from '../utils'
|
||||||
import { RENDER_LIST } from '../runtimeConstants'
|
import { RENDER_LIST } from '../runtimeConstants'
|
||||||
|
import { processExpression } from './expression'
|
||||||
|
|
||||||
const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/
|
const forAliasRE = /([\s\S]*?)(?:(?<=\))|\s+)(?:in|of)\s+([\s\S]*)/
|
||||||
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
|
const forIteratorRE = /,([^,\}\]]*)(?:,([^,\}\]]*))?$/
|
||||||
@ -13,23 +22,35 @@ export const transformFor = createStructuralDirectiveTransform(
|
|||||||
(node, dir, context) => {
|
(node, dir, context) => {
|
||||||
if (dir.exp) {
|
if (dir.exp) {
|
||||||
context.imports.add(RENDER_LIST)
|
context.imports.add(RENDER_LIST)
|
||||||
const aliases = parseAliasExpressions(dir.exp.content)
|
const parseResult = parseForExpression(dir.exp, context)
|
||||||
|
|
||||||
|
if (parseResult) {
|
||||||
|
const { source, value, key, index } = parseResult
|
||||||
|
|
||||||
if (aliases) {
|
|
||||||
// TODO inject identifiers to context
|
|
||||||
// and remove on exit
|
|
||||||
context.replaceNode({
|
context.replaceNode({
|
||||||
type: NodeTypes.FOR,
|
type: NodeTypes.FOR,
|
||||||
loc: node.loc,
|
loc: node.loc,
|
||||||
source: maybeCreateExpression(
|
source,
|
||||||
aliases.source,
|
valueAlias: value,
|
||||||
dir.exp
|
keyAlias: key,
|
||||||
) as ExpressionNode,
|
objectIndexAlias: index,
|
||||||
valueAlias: maybeCreateExpression(aliases.value, dir.exp),
|
|
||||||
keyAlias: maybeCreateExpression(aliases.key, dir.exp),
|
|
||||||
objectIndexAlias: maybeCreateExpression(aliases.index, dir.exp),
|
|
||||||
children: [node]
|
children: [node]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// scope management
|
||||||
|
const { addIdentifier, removeIdentifier } = context
|
||||||
|
|
||||||
|
// inject identifiers to context
|
||||||
|
value && addIdentifier(value)
|
||||||
|
key && addIdentifier(key)
|
||||||
|
index && addIdentifier(index)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
// remove injected identifiers on exit
|
||||||
|
value && removeIdentifier(value)
|
||||||
|
key && removeIdentifier(key)
|
||||||
|
index && removeIdentifier(index)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
context.onError(
|
context.onError(
|
||||||
createCompilerError(ErrorCodes.X_FOR_MALFORMED_EXPRESSION, dir.loc)
|
createCompilerError(ErrorCodes.X_FOR_MALFORMED_EXPRESSION, dir.loc)
|
||||||
@ -43,28 +64,31 @@ export const transformFor = createStructuralDirectiveTransform(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
interface AliasExpression {
|
interface ForParseResult {
|
||||||
offset: number
|
source: ExpressionNode
|
||||||
content: string
|
value: ExpressionNode | undefined
|
||||||
|
key: ExpressionNode | undefined
|
||||||
|
index: ExpressionNode | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AliasExpressions {
|
function parseForExpression(
|
||||||
source: AliasExpression
|
input: ExpressionNode,
|
||||||
value: AliasExpression | undefined
|
context: TransformContext
|
||||||
key: AliasExpression | undefined
|
): ForParseResult | null {
|
||||||
index: AliasExpression | undefined
|
const loc = input.loc
|
||||||
}
|
const source = input.content
|
||||||
|
|
||||||
function parseAliasExpressions(source: string): AliasExpressions | null {
|
|
||||||
const inMatch = source.match(forAliasRE)
|
const inMatch = source.match(forAliasRE)
|
||||||
if (!inMatch) return null
|
if (!inMatch) return null
|
||||||
|
|
||||||
const [, LHS, RHS] = inMatch
|
const [, LHS, RHS] = inMatch
|
||||||
const result: AliasExpressions = {
|
const result: ForParseResult = {
|
||||||
source: {
|
source: createAliasExpression(
|
||||||
offset: source.indexOf(RHS, LHS.length),
|
loc,
|
||||||
content: RHS.trim()
|
RHS.trim(),
|
||||||
},
|
source.indexOf(RHS, LHS.length),
|
||||||
|
context,
|
||||||
|
!context.useWith
|
||||||
|
),
|
||||||
value: undefined,
|
value: undefined,
|
||||||
key: undefined,
|
key: undefined,
|
||||||
index: undefined
|
index: undefined
|
||||||
@ -80,49 +104,60 @@ function parseAliasExpressions(source: string): AliasExpressions | null {
|
|||||||
valueContent = valueContent.replace(forIteratorRE, '').trim()
|
valueContent = valueContent.replace(forIteratorRE, '').trim()
|
||||||
|
|
||||||
const keyContent = iteratorMatch[1].trim()
|
const keyContent = iteratorMatch[1].trim()
|
||||||
|
let keyOffset: number | undefined
|
||||||
if (keyContent) {
|
if (keyContent) {
|
||||||
result.key = {
|
keyOffset = source.indexOf(
|
||||||
offset: source.indexOf(keyContent, trimmedOffset + valueContent.length),
|
keyContent,
|
||||||
content: keyContent
|
trimmedOffset + valueContent.length
|
||||||
}
|
)
|
||||||
|
result.key = createAliasExpression(loc, keyContent, keyOffset, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iteratorMatch[2]) {
|
if (iteratorMatch[2]) {
|
||||||
const indexContent = iteratorMatch[2].trim()
|
const indexContent = iteratorMatch[2].trim()
|
||||||
|
|
||||||
if (indexContent) {
|
if (indexContent) {
|
||||||
result.index = {
|
result.index = createAliasExpression(
|
||||||
offset: source.indexOf(
|
loc,
|
||||||
|
indexContent,
|
||||||
|
source.indexOf(
|
||||||
indexContent,
|
indexContent,
|
||||||
result.key
|
result.key
|
||||||
? result.key.offset + result.key.content.length
|
? keyOffset! + keyContent.length
|
||||||
: trimmedOffset + valueContent.length
|
: trimmedOffset + valueContent.length
|
||||||
),
|
),
|
||||||
content: indexContent
|
context
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (valueContent) {
|
if (valueContent) {
|
||||||
result.value = {
|
result.value = createAliasExpression(
|
||||||
offset: trimmedOffset,
|
loc,
|
||||||
content: valueContent
|
valueContent,
|
||||||
}
|
trimmedOffset,
|
||||||
|
context
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeCreateExpression(
|
function createAliasExpression(
|
||||||
alias: AliasExpression | undefined,
|
range: SourceLocation,
|
||||||
node: ExpressionNode
|
content: string,
|
||||||
): ExpressionNode | undefined {
|
offset: number,
|
||||||
if (alias) {
|
context: TransformContext,
|
||||||
return createExpression(
|
process: boolean = false
|
||||||
alias.content,
|
): ExpressionNode {
|
||||||
|
const exp = createExpression(
|
||||||
|
content,
|
||||||
false,
|
false,
|
||||||
getInnerRange(node.loc, alias.offset, alias.content.length)
|
getInnerRange(range, offset, content.length)
|
||||||
)
|
)
|
||||||
|
if (!__BROWSER__ && process) {
|
||||||
|
processExpression(exp, context)
|
||||||
}
|
}
|
||||||
|
return exp
|
||||||
}
|
}
|
||||||
|
@ -10,10 +10,14 @@ import {
|
|||||||
IfBranchNode
|
IfBranchNode
|
||||||
} from '../ast'
|
} from '../ast'
|
||||||
import { createCompilerError, ErrorCodes } from '../errors'
|
import { createCompilerError, ErrorCodes } from '../errors'
|
||||||
|
import { processExpression } from './expression'
|
||||||
|
|
||||||
export const transformIf = createStructuralDirectiveTransform(
|
export const transformIf = createStructuralDirectiveTransform(
|
||||||
/^(if|else|else-if)$/,
|
/^(if|else|else-if)$/,
|
||||||
(node, dir, context) => {
|
(node, dir, context) => {
|
||||||
|
if (!__BROWSER__ && !context.useWith && dir.exp) {
|
||||||
|
processExpression(dir.exp, context)
|
||||||
|
}
|
||||||
if (dir.name === 'if') {
|
if (dir.name === 'if') {
|
||||||
context.replaceNode({
|
context.replaceNode({
|
||||||
type: NodeTypes.IF,
|
type: NodeTypes.IF,
|
||||||
|
Loading…
Reference in New Issue
Block a user