/* IMPORT */ import {SYMBOL_TEMPLATE_ACCESSOR} from '~/constants'; import wrapElement from '~/methods/wrap_element'; import {assign, indexOf, isFunction, isString} from '~/utils/lang'; import {setAttribute, setChildReplacement, setClasses, setEvent, setHTML, setProperty, setRef, setStyles} from '~/utils/setters'; import type {Child, TemplateActionPath, TemplateActionWithNodes, TemplateActionWithPaths, TemplateVariableProperties, TemplateVariableData, TemplateVariablesMap} from '~/types'; /* MAIN */ //TODO: Avoid using "Function" and "eval", while still keeping similar performance, if possible //TODO: Support complex children in the template function //TODO: Support argumentless calls on props, like props.foo.bar() const template =
( fn: (( props: P ) => Child) ): (( props: P ) => () => Child) => {
const safePropertyRe = /^[a-z0-9-_]+$/i;
const checkValidProperty = ( property: unknown ): property is string => {
if ( isString ( property ) && safePropertyRe.test ( property ) ) return true;
throw new Error ( `Invalid property, only alphanumeric properties are allowed inside templates, received: "${property}"` );
};
const makeAccessor = ( actionsWithNodes: TemplateActionWithNodes[] ): any => {
return new Proxy ( {}, {
get ( target: unknown, prop: string ) {
checkValidProperty ( prop );
const accessor = ( node: Node, method: string, key?: string, targetNode?: Node ): void => {
if ( key ) checkValidProperty ( key );
actionsWithNodes.push ([ node, method, prop, key, targetNode ]);
};
const metadata = { [SYMBOL_TEMPLATE_ACCESSOR]: true };
return assign ( accessor, metadata );
}
});
};
const makeActionsWithNodesAndTemplate = (): { actionsWithNodes: TemplateActionWithNodes[], root: Element } => {
const actionsWithNodes: TemplateActionWithNodes[] = [];
const accessor = makeAccessor ( actionsWithNodes );
const component = fn ( accessor );
if ( isFunction ( component ) ) {
const root = component ();
if ( root instanceof Element ) {
return { actionsWithNodes, root };
}
}
throw new Error ( 'Invalid template, it must return a function that returns an Element' );
};
const makeActionsWithPaths = ( actionsWithNodes: TemplateActionWithNodes[] ): TemplateActionWithPaths[] => {
const actionsWithPaths: TemplateActionWithPaths[] = [];
for ( let i = 0, l = actionsWithNodes.length; i < l; i++ ) {
const [node, method, prop, key, targetNode] = actionsWithNodes[i];
const nodePath = makeNodePath ( node );
const targetNodePath = targetNode ? makeNodePath ( targetNode ) : undefined;
actionsWithPaths.push ([ nodePath, method, prop, key, targetNodePath ]);
}
return actionsWithPaths;
};
const makeNodePath = (() => {
let prevNode: Node | null = null;
let prevPath: TemplateActionPath;
return ( node: Node ): TemplateActionPath => {
if ( node === prevNode ) return prevPath; // Cache hit
const path: TemplateActionPath = [];
let child = node;
let parent = child.parentNode;
while ( parent ) {
const index = !child.previousSibling ? 0 : !child.nextSibling ? -0 : indexOf ( parent.childNodes, child );
path.push ( index );
child = parent;
parent = parent.parentNode;
}
prevNode = node;
prevPath = path;
return path;
};
})();
const makeNodePathProperties = ( path: TemplateActionPath ): TemplateVariableProperties => {
const properties: TemplateVariableProperties = ['root'];
const parts = path.slice ().reverse ();
for ( let i = 0, l = parts.length; i < l; i++ ) {
const part = parts[i];
if ( Object.is ( 0, part ) ) {
properties.push ( 'firstChild' );
} else if ( Object.is ( -0, part ) ) {
properties.push ( 'lastChild' );
} else {
properties.push ( 'firstChild' );
for ( let nsi = 0; nsi < part; nsi++ ) {
properties.push ( 'nextSibling' );
}
}
}
return properties;
};
const makeReviverPaths = ( actionsWithPaths: TemplateActionWithPaths[] ): TemplateActionPath[] => {
const paths: TemplateActionPath[] = [];
for ( let i = 0, l = actionsWithPaths.length; i < l; i++ ) {
const action = actionsWithPaths[i];
const nodePath = action[0];
const targetNodePath = action[4];
paths.push ( nodePath );
if ( targetNodePath ) {
paths.push ( targetNodePath );
}
}
return paths;
};
const makeReviverVariablesData = ( paths: TemplateActionPath[], properties: TemplateVariableProperties[] ): TemplateVariableData[] => {
const data: TemplateVariableData[] = new Array ( paths.length );
for ( let i = 0, l = paths.length; i < l; i++ ) {
data[i] = {
path: paths[i],
properties: properties[i]
};
}
return data;
};
const makeReviverVariables = ( actionsWithPaths: TemplateActionWithPaths[] ): { assignments: string[], map: Map