diff --git a/build/files.js b/build/files.js index fd26bb4..dbc25ea 100644 --- a/build/files.js +++ b/build/files.js @@ -15,6 +15,14 @@ export default (target, plugins) => [ file: `./dist/${target}/cdn.js`, } }, + { + plugins, + input: './src/jsx/index.js', + output: { + esModule: true, + file: `./dist/${target}/jsx.js`, + } + }, { plugins, input: './src/json/index.js', diff --git a/package.json b/package.json index 381149e..27f8aa3 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,11 @@ "types": "./types/dom/index.d.ts", "description": "explicit uhtml browsers' production export" }, + "./jsx": { + "import": "./dist/prod/jsx.js", + "types": "./types/jsx/index.d.ts", + "description": "experimental - transform templates into JSX compatible syntax" + }, "./dom": { "import": "./src/dom/index.js", "types": "./types/dom/index.d.ts", diff --git a/src/jsx/index.js b/src/jsx/index.js new file mode 100644 index 0000000..5fb2c44 --- /dev/null +++ b/src/jsx/index.js @@ -0,0 +1,100 @@ +//@ts-nocheck + +import { + ELEMENT, + Node, + Comment, + DocumentType, + Text, + Fragment, + Element, + Component, + fromJSON, + props, +} from '../dom/ish.js'; + +import { + COMPONENT, + DOTS, + KEY, + HOLE, + update, +} from './update.js'; + +import parser from '../parser/index.js'; +import resolve from '../json/resolve.js'; +import { assign } from '../utils.js'; + +const textParser = parser({ + Comment, + DocumentType, + Text, + Fragment, + Element, + Component, + update, +}); + +const { stringify, parse } = JSON; +const isNode = node => node instanceof Node; +const get = node => node.props === props ? (node.props = props) : node.props; + +export default (jsx, jsxs = jsx) => { + const twm = new WeakMap; + const cache = (template, values) => { + const parsed = textParser(template, values, true); + parsed[0] = parse(stringify(parsed[0])); + twm.set(template, parsed); + return parsed; + }; + + const getProps = (node) => { + const { children } = node; + if (children.length) get(node).children = children.map(getValue); + return get(node); + }; + + const getValue = node => { + if (isNode(node)) { + return node.type === ELEMENT ? + getInvoke(node)(node.name, getProps(node)) : + node.toString() + ; + } + return node; + }; + + const getInvoke = ({ children }) => (jsx === jsxs || children.every(isNode)) ? jsx : jsxs; + + return (template, ...values) => { + const [json, updates] = twm.get(template) || cache(template, values); + // TODO: this could be mapped once so that every consecutive call + // will simply loop over values and updates[length](values[length]) + // before returning the list or arguments to pass to jsx or jsxs + // this way it'd be way faster on repeated invokes, if needed/desired + const root = fromJSON(json); + let length = values.length, args, prev, node; + while (length--) { + const [path, type] = updates[length]; + const value = values[length]; + if (prev !== path) { + node = resolve(root, path); + prev = path; + args = [node.name, getProps(node)]; + } + if (type === COMPONENT) { + args[0] = value; + const { children } = node.parent; + children[children.indexOf(node)] = getInvoke(node)(...args); + } + else if (type === KEY) args.push(value); + else if (type === DOTS) assign(get(node), value); + else if (type === HOLE) { + const { children } = node.parent; + children[children.indexOf(node)] = value; + } + else get(node)[type] = value; + } + return getValue(root.children[0]); + }; +}; diff --git a/src/jsx/update.js b/src/jsx/update.js new file mode 100644 index 0000000..659fb82 --- /dev/null +++ b/src/jsx/update.js @@ -0,0 +1,24 @@ +import { + ATTRIBUTE as TEMPLATE_ATTRIBUTE, + COMMENT as TEMPLATE_COMMENT, + COMPONENT as TEMPLATE_COMPONENT, +} from '../dom/ish.js'; + +export const COMPONENT = 1; +export const DOTS = 3; +export const KEY = 4; +export const HOLE = 5; + +export const update = (_, type, path, name) => { + switch (type) { + case TEMPLATE_COMMENT: return [path, HOLE]; + case TEMPLATE_COMPONENT: return [path, COMPONENT]; + case TEMPLATE_ATTRIBUTE: { + switch (name) { + case 'key': return [path, KEY]; + case '...': return [path, DOTS]; + default: return [path, name]; + } + } + } +}; diff --git a/test/jsx.html b/test/jsx.html new file mode 100644 index 0000000..5171b17 --- /dev/null +++ b/test/jsx.html @@ -0,0 +1,13 @@ + + diff --git a/types/jsx/index.d.ts b/types/jsx/index.d.ts new file mode 100644 index 0000000..9d1b18c --- /dev/null +++ b/types/jsx/index.d.ts @@ -0,0 +1,2 @@ +declare function _default(jsx: any, jsxs?: any): (template: any, ...values: any[]) => any; +export default _default; diff --git a/types/jsx/update.d.ts b/types/jsx/update.d.ts new file mode 100644 index 0000000..36df0d1 --- /dev/null +++ b/types/jsx/update.d.ts @@ -0,0 +1,5 @@ +export const COMPONENT: 1; +export const DOTS: 3; +export const KEY: 4; +export const HOLE: 5; +export function update(_: any, type: any, path: any, name: any): any[];