vue3-yuanma/packages/compiler-core/src/transform.ts

147 lines
3.6 KiB
TypeScript
Raw Normal View History

import {
RootNode,
NodeTypes,
ParentNode,
ChildNode,
ElementNode,
DirectiveNode
} from './ast'
import { isString } from '@vue/shared'
import { CompilerError } from './errors'
export type Transform = (node: ChildNode, context: TransformContext) => void
export type DirectiveTransform = (
node: ElementNode,
dir: DirectiveNode,
context: TransformContext
) => false | void
export interface TransformOptions {
transforms: Transform[]
onError?: (error: CompilerError) => void
}
export interface TransformContext extends Required<TransformOptions> {
parent: ParentNode
ancestors: ParentNode[]
childIndex: number
replaceNode(node: ChildNode): void
removeNode(): void
nodeRemoved: boolean
}
export function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
traverseChildren(root, context, context.ancestors)
}
function createTransformContext(
root: RootNode,
options: TransformOptions
): TransformContext {
const context: TransformContext = {
onError(error: CompilerError) {
throw error
},
...options,
parent: root,
ancestors: [],
childIndex: 0,
replaceNode(node) {
if (__DEV__ && context.nodeRemoved) {
throw new Error(`node being replaced is already removed`)
}
context.parent.children[context.childIndex] = node
},
removeNode() {
context.parent.children.splice(context.childIndex, 1)
context.nodeRemoved = true
},
nodeRemoved: false
}
return context
}
function traverseChildren(
parent: ParentNode,
context: TransformContext,
ancestors: ParentNode[]
) {
ancestors = ancestors.concat(parent)
for (let i = 0; i < parent.children.length; i++) {
context.parent = parent
context.ancestors = ancestors
context.childIndex = i
traverseNode(parent.children[i], context, ancestors)
if (context.nodeRemoved) {
i--
}
}
}
function traverseNode(
node: ChildNode,
context: TransformContext,
ancestors: ParentNode[]
) {
// apply transform plugins
const transforms = context.transforms
for (let i = 0; i < transforms.length; i++) {
const plugin = transforms[i]
context.nodeRemoved = false
plugin(node, context)
if (context.nodeRemoved) {
return
} else {
// node may have been replaced
node = context.parent.children[context.childIndex]
}
}
// further traverse downwards
switch (node.type) {
case NodeTypes.IF:
for (let i = 0; i < node.branches.length; i++) {
traverseChildren(node.branches[i], context, ancestors)
}
break
case NodeTypes.FOR:
case NodeTypes.ELEMENT:
traverseChildren(node, context, ancestors)
break
}
}
const identity = <T>(_: T): T => _
export function createDirectiveTransform(
name: string | RegExp,
fn: DirectiveTransform
): Transform {
const matches = isString(name)
? (n: string) => n === name
: (n: string) => name.test(n)
return (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
const dirs = node.directives
let didRemove = false
for (let i = 0; i < dirs.length; i++) {
if (matches(dirs[i].name)) {
const res = fn(node, dirs[i], context)
// Directives are removed after transformation by default. A transform
// returning false means the directive should not be removed.
if (res !== false) {
;(dirs as any)[i] = undefined
didRemove = true
}
}
}
if (didRemove) {
node.directives = dirs.filter(identity)
}
}
}
}