/** * Based on the https://github.com/HCESrl/html-to-vue */ import type { ComponentInternalInstance, VNode } from "vue"; import { isNode } from "./ast"; import type { NodeObject } from "./getOptionsFromNode"; import { getOptionsFromNode } from "./getOptionsFromNode"; type ASTNode = NodeObject | NodeObject[]; type RawChildren = string | number | boolean | VNode; type ExtraComponentRenderer = ( node: NodeObject, children: RawChildren[], createElement: typeof import("vue").h, context: ComponentInternalInstance | null, ) => VNode; export type ExtraComponentConfig = { conditions: (node: NodeObject) => boolean; renderer: ExtraComponentRenderer; }; export type RendererConfig = { container: { type: string; class?: string; }; extraComponentsMap: Record; renderAnyway: boolean; textTransformer: (text: string) => string; }; function flattenChildren( children: RawChildren | RawChildren[] | undefined, ): RawChildren[] { if (children === undefined) { return []; } if (Array.isArray(children)) { const result: RawChildren[] = []; for (const child of children) { if (child !== null && child !== undefined) { if (Array.isArray(child)) { result.push(...flattenChildren(child)); } else { result.push(child); } } } return result; } return [children]; } /** * rendering the ast into vue render functions * @param ast AST generated by html-parse-stringify * @param config our configuration * @param createElement vue's createElement * @param context vue functional component context * @param resolveUrl function to resolve URLs */ export function renderer( ast: ASTNode, config: RendererConfig, createElement: typeof import("vue").h, context: ComponentInternalInstance | null, resolveUrl: (url: string) => string, ): VNode { function _render( h: typeof createElement, node: unknown, ): RawChildren | RawChildren[] | undefined { if (Array.isArray(node)) { const nodes: RawChildren[] = []; // node is an array for (const subnode of node) { const rendered = _render(h, subnode); const flattened = flattenChildren(rendered); nodes.push(...flattened); } return nodes; } if (isNode(node)) { // node is either a node with children or a node or a text node if (node.type === "text") { return config.textTransformer(node.content); // return text } if (node.type === "tag") { const children: RawChildren[] = []; for (const child of node.children) { const rendered = _render(h, child); const flattened = flattenChildren(rendered); children.push(...flattened); } // if it's an extra component use custom renderer const componentConfig = config.extraComponentsMap[node.name]; if (componentConfig !== undefined) { return componentConfig.renderer(node, children, h, context); } // else, create normal html element; Vue 3 takes attributes as flat props const { attrs, ...options } = getOptionsFromNode(node, resolveUrl); return h(node.name, { ...attrs, ...options }, [...children]); } } return undefined; } const rendered = _render(createElement, ast); const children = flattenChildren(rendered); const containerProps: Record = { ...(context?.data || {}) }; if (config.container.class) { containerProps.class = containerProps.class ? [containerProps.class, config.container.class] : config.container.class; } return createElement(config.container.type, containerProps, children); }