feat(compiler-vapor): `v-if` (#96)

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
This commit is contained in:
Rizumu Ayaka 2024-01-28 01:31:20 +08:00 committed by GitHub
parent 2b5e8e4df6
commit ede6c29434
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 448 additions and 82 deletions

View File

@ -44,6 +44,10 @@ PR are welcome!
- #17 - #17
- needs #19 first - needs #19 first
- [ ] `v-if` / `v-else` / `v-else-if` - [ ] `v-if` / `v-else` / `v-else-if`
- [x] `v-if`
- [ ] `v-else`
- [ ] `v-else-if`
- [ ] with `<template>`
- #9 - #9
- [ ] `v-for` - [ ] `v-for`
- #21 - #21

View File

@ -175,7 +175,6 @@ exports[`compile > dynamic root 1`] = `
export function render(_ctx) { export function render(_ctx) {
const t0 = _fragment() const t0 = _fragment()
const n0 = t0() const n0 = t0()
const n1 = _createTextNode(1) const n1 = _createTextNode(1)
const n2 = _createTextNode(2) const n2 = _createTextNode(2)
@ -217,7 +216,6 @@ export function render(_ctx) {
exports[`compile > expression parsing > interpolation 1`] = ` exports[`compile > expression parsing > interpolation 1`] = `
"(() => { "(() => {
const t0 = _fragment() const t0 = _fragment()
const n0 = t0() const n0 = t0()
const n1 = _createTextNode(a + b.value) const n1 = _createTextNode(a + b.value)
_append(n0, n1) _append(n0, n1)

View File

@ -0,0 +1,61 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: v-if > basic v-if 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, children as _children, renderEffect as _renderEffect, setText as _setText, append as _append } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>")
const t1 = _fragment()
const n0 = t1()
const n1 = _createIf(() => (_ctx.ok), () => {
const n2 = t0()
const { 0: [n3],} = _children(n2)
_renderEffect(() => {
_setText(n3, _ctx.msg)
})
return n2
})
_append(n0, n1)
return n0
}"
`;
exports[`compiler: v-if > dedupe same template 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, append as _append } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div>hello</div>")
const t1 = _fragment()
const n0 = t1()
const n1 = _createIf(() => (_ctx.ok), () => {
const n2 = t0()
return n2
})
const n3 = _createIf(() => (_ctx.ok), () => {
const n4 = t0()
return n4
})
_append(n0, n1, n3)
return n0
}"
`;
exports[`compiler: v-if > template v-if 1`] = `
"import { template as _template, fragment as _fragment, createIf as _createIf, children as _children, renderEffect as _renderEffect, setText as _setText, append as _append } from 'vue/vapor';
export function render(_ctx) {
const t0 = _template("<div></div>hello<p></p>")
const t1 = _fragment()
const n0 = t1()
const n1 = _createIf(() => (_ctx.ok), () => {
const n2 = t0()
const { 2: [n3],} = _children(n2)
_renderEffect(() => {
_setText(n3, _ctx.msg)
})
return n2
})
_append(n0, n1)
return n0
}"
`;

View File

@ -0,0 +1,52 @@
import { makeCompile } from './_utils'
import {
transformElement,
transformInterpolation,
transformOnce,
transformVIf,
transformVText,
} from '../../src'
const compileWithVIf = makeCompile({
nodeTransforms: [
transformOnce,
transformInterpolation,
transformVIf,
transformElement,
],
directiveTransforms: {
text: transformVText,
},
})
describe('compiler: v-if', () => {
test('basic v-if', () => {
const { code } = compileWithVIf(`<div v-if="ok">{{msg}}</div>`)
expect(code).matchSnapshot()
})
test('template v-if', () => {
const { code } = compileWithVIf(
`<template v-if="ok"><div/>hello<p v-text="msg"/></template>`,
)
expect(code).matchSnapshot()
})
test('dedupe same template', () => {
const { code, ir } = compileWithVIf(
`<div v-if="ok">hello</div><div v-if="ok">hello</div>`,
)
expect(code).matchSnapshot()
expect(ir.template).lengthOf(2)
})
test.todo('v-if with v-once')
test.todo('component v-if')
test.todo('v-if + v-else')
test.todo('v-if + v-else-if')
test.todo('v-if + v-else-if + v-else')
test.todo('comment between branches')
describe.todo('errors')
describe.todo('codegen')
test.todo('v-on with v-if')
})

View File

@ -24,6 +24,7 @@ import { transformRef } from './transforms/transformRef'
import { transformInterpolation } from './transforms/transformInterpolation' import { transformInterpolation } from './transforms/transformInterpolation'
import type { HackOptions } from './ir' import type { HackOptions } from './ir'
import { transformVModel } from './transforms/vModel' import { transformVModel } from './transforms/vModel'
import { transformVIf } from './transforms/vIf'
export type CompilerOptions = HackOptions<BaseCompilerOptions> export type CompilerOptions = HackOptions<BaseCompilerOptions>
@ -96,7 +97,13 @@ export function getBaseTransformPreset(
prefixIdentifiers?: boolean, prefixIdentifiers?: boolean,
): TransformPreset { ): TransformPreset {
return [ return [
[transformOnce, transformRef, transformInterpolation, transformElement], [
transformOnce,
transformRef,
transformInterpolation,
transformVIf,
transformElement,
],
{ {
bind: transformVBind, bind: transformVBind,
on: transformVOn, on: transformVOn,

View File

@ -8,6 +8,7 @@ import {
locStub, locStub,
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { import {
type BlockFunctionIRNode,
type IRDynamicChildren, type IRDynamicChildren,
IRNodeTypes, IRNodeTypes,
type OperationNode, type OperationNode,
@ -26,6 +27,7 @@ import { genSetRef } from './generators/ref'
import { genSetModelValue } from './generators/modelValue' import { genSetModelValue } from './generators/modelValue'
import { genAppendNode, genInsertNode, genPrependNode } from './generators/dom' import { genAppendNode, genInsertNode, genPrependNode } from './generators/dom'
import { genWithDirective } from './generators/directive' import { genWithDirective } from './generators/directive'
import { genIf } from './generators/if'
interface CodegenOptions extends BaseCodegenOptions { interface CodegenOptions extends BaseCodegenOptions {
expressionPlugins?: ParserPlugin[] expressionPlugins?: ParserPlugin[]
@ -271,49 +273,11 @@ export function generate(
) )
} else { } else {
// fragment // fragment
pushNewline( pushNewline(`const t${i} = ${vaporHelper('fragment')}()`)
`const t0 = ${vaporHelper('fragment')}()\n`,
NewlineType.End,
)
} }
}) })
{ genBlockFunctionContent(ir, ctx)
pushNewline(`const n${ir.dynamic.id} = t0()`)
const children = genChildren(ir.dynamic.children)
if (children) {
pushNewline(
`const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`,
)
}
const directiveOps = ir.operation.filter(
(oper): oper is WithDirectiveIRNode =>
oper.type === IRNodeTypes.WITH_DIRECTIVE,
)
for (const directives of groupDirective(directiveOps)) {
genWithDirective(directives, ctx)
}
for (const operation of ir.operation) {
genOperation(operation, ctx)
}
for (const { operations } of ir.effect) {
pushNewline(`${vaporHelper('renderEffect')}(() => {`)
withIndent(() => {
for (const operation of operations) {
genOperation(operation, ctx)
}
})
pushNewline('})')
}
// TODO multiple-template
// TODO return statement in IR
pushNewline(`return n${ir.dynamic.id}`)
}
}) })
newline() newline()
@ -396,6 +360,8 @@ function genOperation(oper: OperationNode, context: CodegenContext) {
return genPrependNode(oper, context) return genPrependNode(oper, context)
case IRNodeTypes.APPEND_NODE: case IRNodeTypes.APPEND_NODE:
return genAppendNode(oper, context) return genAppendNode(oper, context)
case IRNodeTypes.IF:
return genIf(oper, context)
case IRNodeTypes.WITH_DIRECTIVE: case IRNodeTypes.WITH_DIRECTIVE:
// generated, skip // generated, skip
return return
@ -404,6 +370,45 @@ function genOperation(oper: OperationNode, context: CodegenContext) {
} }
} }
export function genBlockFunctionContent(
ir: BlockFunctionIRNode | RootIRNode,
ctx: CodegenContext,
) {
const { pushNewline, withIndent, vaporHelper } = ctx
pushNewline(`const n${ir.dynamic.id} = t${ir.templateIndex}()`)
const children = genChildren(ir.dynamic.children)
if (children) {
pushNewline(
`const ${children} = ${vaporHelper('children')}(n${ir.dynamic.id})`,
)
}
const directiveOps = ir.operation.filter(
(oper): oper is WithDirectiveIRNode =>
oper.type === IRNodeTypes.WITH_DIRECTIVE,
)
for (const directives of groupDirective(directiveOps)) {
genWithDirective(directives, ctx)
}
for (const operation of ir.operation) {
genOperation(operation, ctx)
}
for (const { operations } of ir.effect) {
pushNewline(`${vaporHelper('renderEffect')}(() => {`)
withIndent(() => {
for (const operation of operations) {
genOperation(operation, ctx)
}
})
pushNewline('})')
}
pushNewline(`return n${ir.dynamic.id}`)
}
function groupDirective(ops: WithDirectiveIRNode[]): WithDirectiveIRNode[][] { function groupDirective(ops: WithDirectiveIRNode[]): WithDirectiveIRNode[][] {
const directiveMap: Record<number, WithDirectiveIRNode[]> = {} const directiveMap: Record<number, WithDirectiveIRNode[]> = {}
for (const oper of ops) { for (const oper of ops) {

View File

@ -0,0 +1,28 @@
import { type CodegenContext, genBlockFunctionContent } from '../generate'
import type { BlockFunctionIRNode, IfIRNode } from '../ir'
import { genExpression } from './expression'
export function genIf(oper: IfIRNode, context: CodegenContext) {
const { pushFnCall, vaporHelper, pushNewline, push, withIndent } = context
const { condition, positive, negative } = oper
pushNewline(`const n${oper.id} = `)
pushFnCall(
vaporHelper('createIf'),
() => {
push('() => (')
genExpression(condition, context)
push(')')
},
() => genBlockFunction(positive),
!!negative && (() => genBlockFunction(negative!)),
)
function genBlockFunction(oper: BlockFunctionIRNode) {
push('() => {')
withIndent(() => {
genBlockFunctionContent(oper, context)
})
pushNewline('}')
}
}

View File

@ -12,3 +12,4 @@ export { transformVOn } from './transforms/vOn'
export { transformOnce } from './transforms/vOnce' export { transformOnce } from './transforms/vOnce'
export { transformVShow } from './transforms/vShow' export { transformVShow } from './transforms/vShow'
export { transformVText } from './transforms/vText' export { transformVText } from './transforms/vText'
export { transformVIf } from './transforms/vIf'

View File

@ -5,6 +5,7 @@ import type {
RootNode, RootNode,
SimpleExpressionNode, SimpleExpressionNode,
SourceLocation, SourceLocation,
TemplateChildNode,
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import type { Prettify } from '@vue/shared' import type { Prettify } from '@vue/shared'
import type { DirectiveTransform, NodeTransform } from './transform' import type { DirectiveTransform, NodeTransform } from './transform'
@ -27,6 +28,9 @@ export enum IRNodeTypes {
CREATE_TEXT_NODE, CREATE_TEXT_NODE,
WITH_DIRECTIVE, WITH_DIRECTIVE,
IF,
BLOCK_FUNCTION,
} }
export interface BaseIRNode { export interface BaseIRNode {
@ -37,16 +41,30 @@ export interface BaseIRNode {
// TODO refactor // TODO refactor
export type VaporHelper = keyof typeof import('../../runtime-vapor/src') export type VaporHelper = keyof typeof import('../../runtime-vapor/src')
export interface RootIRNode extends BaseIRNode { export interface BlockFunctionIRNode extends BaseIRNode {
type: IRNodeTypes.ROOT type: IRNodeTypes.BLOCK_FUNCTION
source: string node: RootNode | TemplateChildNode
node: RootNode templateIndex: number
template: Array<TemplateFactoryIRNode | FragmentFactoryIRNode>
dynamic: IRDynamicInfo dynamic: IRDynamicInfo
effect: IREffect[] effect: IREffect[]
operation: OperationNode[] operation: OperationNode[]
} }
export interface RootIRNode extends Omit<BlockFunctionIRNode, 'type'> {
type: IRNodeTypes.ROOT
node: RootNode
source: string
template: Array<TemplateFactoryIRNode | FragmentFactoryIRNode>
}
export interface IfIRNode extends BaseIRNode {
type: IRNodeTypes.IF
id: number
condition: IRExpression
positive: BlockFunctionIRNode
negative?: BlockFunctionIRNode
}
export interface TemplateFactoryIRNode extends BaseIRNode { export interface TemplateFactoryIRNode extends BaseIRNode {
type: IRNodeTypes.TEMPLATE_FACTORY type: IRNodeTypes.TEMPLATE_FACTORY
template: string template: string
@ -158,6 +176,9 @@ export type OperationNode =
| PrependNodeIRNode | PrependNodeIRNode
| AppendNodeIRNode | AppendNodeIRNode
| WithDirectiveIRNode | WithDirectiveIRNode
| IfIRNode
export type BlockIRNode = RootIRNode | BlockFunctionIRNode
export interface IRDynamicInfo { export interface IRDynamicInfo {
id: number | null id: number | null

View File

@ -3,14 +3,16 @@ import {
type TransformOptions as BaseTransformOptions, type TransformOptions as BaseTransformOptions,
type CompilerCompatOptions, type CompilerCompatOptions,
type ElementNode, type ElementNode,
ElementTypes,
NodeTypes, NodeTypes,
type ParentNode, type ParentNode,
type RootNode, type RootNode,
type TemplateChildNode, type TemplateChildNode,
defaultOnError, defaultOnError,
defaultOnWarn, defaultOnWarn,
isVSlot,
} from '@vue/compiler-dom' } from '@vue/compiler-dom'
import { EMPTY_OBJ, NOOP, extend, isArray } from '@vue/shared' import { EMPTY_OBJ, NOOP, extend, isArray, isString } from '@vue/shared'
import { import {
type IRDynamicInfo, type IRDynamicInfo,
type IRExpression, type IRExpression,
@ -18,7 +20,13 @@ import {
type OperationNode, type OperationNode,
type RootIRNode, type RootIRNode,
} from './ir' } from './ir'
import type { HackOptions, VaporDirectiveNode } from './ir' import type {
BlockIRNode,
FragmentFactoryIRNode,
HackOptions,
TemplateFactoryIRNode,
VaporDirectiveNode,
} from './ir'
export type NodeTransform = ( export type NodeTransform = (
node: RootNode | TemplateChildNode, node: RootNode | TemplateChildNode,
@ -31,6 +39,14 @@ export type DirectiveTransform = (
context: TransformContext<ElementNode>, context: TransformContext<ElementNode>,
) => void ) => void
// A structural directive transform is technically also a NodeTransform;
// Only v-if and v-for fall into this category.
export type StructuralDirectiveTransform = (
node: RootNode | TemplateChildNode,
dir: VaporDirectiveNode,
context: TransformContext<RootNode | TemplateChildNode>,
) => void | (() => void)
export type TransformOptions = HackOptions<BaseTransformOptions> export type TransformOptions = HackOptions<BaseTransformOptions>
export interface TransformContext<T extends AllNode = AllNode> { export interface TransformContext<T extends AllNode = AllNode> {
@ -38,6 +54,7 @@ export interface TransformContext<T extends AllNode = AllNode> {
parent: TransformContext<ParentNode> | null parent: TransformContext<ParentNode> | null
root: TransformContext<RootNode> root: TransformContext<RootNode>
index: number index: number
block: BlockIRNode
options: Required< options: Required<
Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions> Omit<TransformOptions, 'filename' | keyof CompilerCompatOptions>
> >
@ -48,6 +65,7 @@ export interface TransformContext<T extends AllNode = AllNode> {
inVOnce: boolean inVOnce: boolean
enterBlock(ir: TransformContext['block']): () => void
reference(): number reference(): number
increaseId(): number increaseId(): number
registerTemplate(): number registerTemplate(): number
@ -84,20 +102,34 @@ const defaultOptions = {
// TODO use class for better perf // TODO use class for better perf
function createRootContext( function createRootContext(
ir: RootIRNode, root: RootIRNode,
node: RootNode, node: RootNode,
options: TransformOptions = {}, options: TransformOptions = {},
): TransformContext<RootNode> { ): TransformContext<RootNode> {
let globalId = 0 let globalId = 0
const { effect, operation: operation } = ir
const ctx: TransformContext<RootNode> = { const ctx: TransformContext<RootNode> = {
node, node,
parent: null, parent: null,
index: 0, index: 0,
root: null!, // set later root: null!, // set later
block: root,
enterBlock(ir) {
const { block, template, dynamic, childrenTemplate } = this
this.block = ir
this.dynamic = ir.dynamic
this.template = ''
this.childrenTemplate = []
return () => {
// exit
this.block = block
this.template = template
this.dynamic = dynamic
this.childrenTemplate = childrenTemplate
}
},
options: extend({}, defaultOptions, options), options: extend({}, defaultOptions, options),
dynamic: ir.dynamic, dynamic: root.dynamic,
inVOnce: false, inVOnce: false,
increaseId: () => globalId++, increaseId: () => globalId++,
@ -113,13 +145,13 @@ function createRootContext(
) { ) {
return this.registerOperation(...operations) return this.registerOperation(...operations)
} }
const existing = effect.find((e) => const existing = this.block.effect.find((e) =>
isSameExpression(e.expressions, expressions as IRExpression[]), isSameExpression(e.expressions, expressions as IRExpression[]),
) )
if (existing) { if (existing) {
existing.operations.push(...operations) existing.operations.push(...operations)
} else { } else {
effect.push({ this.block.effect.push({
expressions: expressions as IRExpression[], expressions: expressions as IRExpression[],
operations, operations,
}) })
@ -140,24 +172,34 @@ function createRootContext(
template: '', template: '',
childrenTemplate: [], childrenTemplate: [],
registerTemplate() { registerTemplate() {
if (!ctx.template) return -1 let templateNode: TemplateFactoryIRNode | FragmentFactoryIRNode
const idx = ir.template.findIndex( if (this.template) {
(t) => const idx = root.template.findIndex(
t.type === IRNodeTypes.TEMPLATE_FACTORY && (t) =>
t.template === ctx.template, t.type === IRNodeTypes.TEMPLATE_FACTORY &&
) t.template === this.template,
if (idx !== -1) return idx )
if (idx !== -1) {
return (this.block.templateIndex = idx)
}
ir.template.push({ templateNode = {
type: IRNodeTypes.TEMPLATE_FACTORY, type: IRNodeTypes.TEMPLATE_FACTORY,
template: ctx.template, template: this.template,
loc: node.loc, loc: node.loc,
}) }
return ir.template.length - 1 } else {
templateNode = {
type: IRNodeTypes.FRAGMENT_FACTORY,
loc: node.loc,
}
}
root.template.push(templateNode)
return (this.block.templateIndex = root.template.length - 1)
}, },
registerOperation(...node) { registerOperation(...node) {
operation.push(...node) this.block.operation.push(...node)
}, },
} }
ctx.root = ctx ctx.root = ctx
@ -199,6 +241,7 @@ export function transform(
source: root.source, source: root.source,
loc: root.loc, loc: root.loc,
template: [], template: [],
templateIndex: -1,
dynamic: { dynamic: {
id: null, id: null,
referenced: true, referenced: true,
@ -211,17 +254,9 @@ export function transform(
} }
const ctx = createRootContext(ir, root, options) const ctx = createRootContext(ir, root, options)
transformNode(ctx)
if (ctx.node.type === NodeTypes.ROOT) { transformNode(ctx)
ctx.registerTemplate() ctx.registerTemplate()
}
if (ir.template.length === 0) {
ir.template.push({
type: IRNodeTypes.FRAGMENT_FACTORY,
loc: root.loc,
})
}
return ir return ir
} }
@ -251,7 +286,6 @@ function transformNode(
node = context.node node = context.node
} }
} }
switch (node.type) { switch (node.type) {
case NodeTypes.ROOT: case NodeTypes.ROOT:
case NodeTypes.ELEMENT: { case NodeTypes.ELEMENT: {
@ -351,3 +385,37 @@ function processDynamicChildren(ctx: TransformContext<RootNode | ElementNode>) {
} }
} }
} }
export function createStructuralDirectiveTransform(
name: string | RegExp,
fn: StructuralDirectiveTransform,
): NodeTransform {
const matches = isString(name)
? (n: string) => n === name
: (n: string) => name.test(n)
return (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
const { props } = node
// structural directive transforms are not concerned with slots
// as they are handled separately in vSlot.ts
if (node.tagType === ElementTypes.TEMPLATE && props.some(isVSlot)) {
return
}
const exitFns = []
for (let i = 0; i < props.length; i++) {
const prop = props[i]
if (prop.type === NodeTypes.DIRECTIVE && matches(prop.name)) {
// structural directives are removed to avoid infinite recursion
// also we remove them *before* applying so that it can further
// traverse itself in case it moves the node around
props.splice(i, 1)
i--
const onExit = fn(node, prop as VaporDirectiveNode, context)
if (onExit) exitFns.push(onExit)
}
}
return exitFns
}
}
}

View File

@ -0,0 +1,91 @@
import {
ElementTypes,
NodeTypes,
type RootNode,
type TemplateChildNode,
type TemplateNode,
} from '@vue/compiler-dom'
import {
type TransformContext,
createStructuralDirectiveTransform,
} from '../transform'
import {
type BlockFunctionIRNode,
IRNodeTypes,
type IfIRNode,
type VaporDirectiveNode,
} from '../ir'
import { extend } from '@vue/shared'
export const transformVIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
processIf,
)
export function processIf(
node: RootNode | TemplateChildNode,
dir: VaporDirectiveNode,
context: TransformContext<RootNode | TemplateChildNode>,
) {
// TODO refactor this
const parentContext = extend({}, context, {
currentScopeIR: context.block,
})
if (dir.name === 'if') {
const id = context.reference()
context.dynamic.ghost = true
const [branch, onExit] = createIfBranch(node, dir, context)
const operation: IfIRNode = {
type: IRNodeTypes.IF,
id,
loc: dir.loc,
condition: dir.exp!,
positive: branch,
}
parentContext.registerOperation(operation)
return onExit
}
}
export function createIfBranch(
node: RootNode | TemplateChildNode,
dir: VaporDirectiveNode,
context: TransformContext<RootNode | TemplateChildNode>,
): [BlockFunctionIRNode, () => void] {
if (
node.type === NodeTypes.ELEMENT &&
node.tagType !== ElementTypes.TEMPLATE
) {
node = extend({}, node, {
tagType: ElementTypes.TEMPLATE,
children: [node],
} as TemplateNode)
context.node = node
}
const branch: BlockFunctionIRNode = {
type: IRNodeTypes.BLOCK_FUNCTION,
loc: dir.loc,
node: node,
templateIndex: -1,
dynamic: {
id: null,
referenced: true,
ghost: true,
placeholder: null,
children: {},
},
effect: [],
operation: [],
}
const exitBlock = context.enterBlock(branch)
context.reference()
const onExit = () => {
context.template += context.childrenTemplate.join('')
context.registerTemplate()
exitBlock()
}
return [branch, onExit]
}

View File

@ -18,7 +18,22 @@ export function insert(block: Block, parent: Node, anchor: Node | null = null) {
// } // }
} }
export function prepend(parent: ParentBlock, ...nodes: Node[]) { export function prepend(parent: ParentBlock, ...blocks: Block[]) {
const nodes: Node[] = []
for (const block of blocks) {
if (block instanceof Node) {
nodes.push(block)
} else if (isArray(block)) {
prepend(parent, ...block)
} else {
prepend(parent, block.nodes)
block.anchor && prepend(parent, block.anchor)
}
}
if (!nodes.length) return
if (parent instanceof Node) { if (parent instanceof Node) {
// TODO use insertBefore for better performance https://jsbench.me/rolpg250hh/1 // TODO use insertBefore for better performance https://jsbench.me/rolpg250hh/1
parent.prepend(...nodes) parent.prepend(...nodes)
@ -27,7 +42,22 @@ export function prepend(parent: ParentBlock, ...nodes: Node[]) {
} }
} }
export function append(parent: ParentBlock, ...nodes: Node[]) { export function append(parent: ParentBlock, ...blocks: Block[]) {
const nodes: Node[] = []
for (const block of blocks) {
if (block instanceof Node) {
nodes.push(block)
} else if (isArray(block)) {
append(parent, ...block)
} else {
append(parent, block.nodes)
block.anchor && append(parent, block.anchor)
}
}
if (!nodes.length) return
if (parent instanceof Node) { if (parent instanceof Node) {
// TODO use insertBefore for better performance // TODO use insertBefore for better performance
parent.append(...nodes) parent.append(...nodes)