/**
* 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);
}