From 8da120316531a7c91a662e1467de761ae80030e7 Mon Sep 17 00:00:00 2001 From: Steve Dodier-Lazaro Date: Sat, 26 Aug 2023 10:09:25 +0200 Subject: [PATCH] feat: Add skeleton for template modding API --- src/template/api.ts | 78 ++++++++++++++++++++++++++++- src/template/utils.ts | 11 ++++ src/transformTemplate.ts | 3 +- src/types/TemplateTransformation.ts | 4 +- 4 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/template/api.ts b/src/template/api.ts index 662b720..8aa652a 100644 --- a/src/template/api.ts +++ b/src/template/api.ts @@ -2,12 +2,14 @@ import type { AttributeNode, BaseElementNode, DirectiveNode, + Node, + RootNode, SourceLocation, TextNode, } from '@vue/compiler-core' import { NodeTypes } from '@vue/compiler-core' -import { isAttribute, isDirective, isText } from '~/template/utils' +import { isAttribute, isDirective, isNode, isText } from '~/template/utils' import { debugTemplate } from '~/utils/debug' import error from '~/utils/error' @@ -103,6 +105,41 @@ export function compareAttributeValues(a: AttributeNode['value'], b: AttributeNo return aContent === bContent } +/* -- AST EXPLORATION FUNCTIONS -- */ +export function exploreAst(ast: RootNode, matcher: (node: Node) => boolean): Node[] { + const nodeset = new Set() + const queue: Node[] = [ast] + + while (queue.length) { + const currentNode = queue.pop() + if (currentNode) { + if (matcher(currentNode)) { + nodeset.add(currentNode) + } + + if (typeof currentNode !== 'string') { + const nextNodes: unknown[] = Object.values(currentNode) + .filter(Boolean) + .reduce((acc, current) => { + if (Array.isArray(current)) { + return [...acc, ...current] + } + + return [...acc, current] + }, []) + + queue.push(...nextNodes.filter(isNode)) + } + } + } + + return Array.from(nodeset) +} + +export function findAstAttributes(ast: RootNode, matcher?: (node: Node) => boolean) { + return exploreAst(ast, (node) => isAttribute(node) && (matcher?.(node) ?? true)) +} + /* -- FIND FUNCTIONS -- */ export function findAttributes( node: BaseElementNode, @@ -156,6 +193,45 @@ export function findDirectives( }) as DirectiveNode[] } +/* -- UPDATE FUNCTIONS -- */ +export function updateAttribute( + prop: AttributeNode, + updater: (attr: AttributeNode) => Partial, 'type'>>, +) { + /* eslint-disable no-param-reassign */ + const changes = updater(prop) + + if (Object.hasOwn(changes, 'name')) { + if (!changes.name) { + throw error('updateAttribute: Invalid changes to attribute name', { prop, changes }) + } + prop.name = changes.name + } + + if (Object.hasOwn(changes, 'value')) { + if ( + changes.value !== undefined && + typeof changes.value !== 'string' && + !isText(changes.value) + ) { + throw error('updateAttribute: Invalid changes to attribute value', { prop, changes }) + } + + if (changes.value === undefined) { + prop.value = undefined + } else if (isText(changes.value)) { + prop.value = changes.value + } else { + prop.value = createText({ content: changes.value }) + } + } + + prop.loc = genFakeLoc() + + /* eslint-enable no-param-reassign */ + return null +} + /* -- REMOVE FUNCTIONS -- */ export function removeAttribute( node: BaseElementNode, diff --git a/src/template/utils.ts b/src/template/utils.ts index 0e1c5e2..b45199b 100644 --- a/src/template/utils.ts +++ b/src/template/utils.ts @@ -67,6 +67,17 @@ export function isJsProperty(node: Node): node is Property { export function isObjectExpression(node: Node): node is ObjectExpression { return node.type === NodeTypes.JS_OBJECT_EXPRESSION } +export function isNode(node: unknown): node is Node { + return ( + typeof node === 'object' && + !!node && + // Typescript in all its splendor. Object.hasOwn is unsupported. + 'type' in node && + typeof node.type === 'number' && + node.type >= 0 && + node.type <= 26 + ) +} export function isRoot(node: Node): node is RootNode { return node.type === NodeTypes.ROOT } diff --git a/src/transformTemplate.ts b/src/transformTemplate.ts index 961414c..4de79a1 100644 --- a/src/transformTemplate.ts +++ b/src/transformTemplate.ts @@ -1,6 +1,7 @@ import { compileTemplate } from '@vue/compiler-sfc' import processTransformResult from '~/processTransformResult' +import * as TemplateAPI from '~/template/api' import { stringify } from '~/template/stringify' import type { TemplateTransformation } from '~/types/TemplateTransformation' import type { TransformationBlock } from '~/types/TransformationBlock' @@ -27,7 +28,7 @@ export default function transformTemplate( ) } - const out = stringify(transformation(result.ast)) + const out = stringify(transformation(result.ast, TemplateAPI)) return processTransformResult(descriptor, out) } diff --git a/src/types/TemplateTransformation.ts b/src/types/TemplateTransformation.ts index 01d567b..0d0aff3 100644 --- a/src/types/TemplateTransformation.ts +++ b/src/types/TemplateTransformation.ts @@ -1,3 +1,5 @@ import type { RootNode } from '@vue/compiler-core' -export type TemplateTransformation = (ast: RootNode) => RootNode +import * as TemplateAPI from '~/template/api' + +export type TemplateTransformation = (ast: RootNode, api: typeof TemplateAPI) => RootNode