commit 23abbc259fdf513d352778984305a8a72d637c14
parent 62067cc679295cac9a6753a25e754bd733aa1de0
Author: typable <contact@typable.dev>
Date: Fri, 26 May 2023 01:36:55 +0200
Switched from TypeScript to JSDoc
Diffstat:
M | .gitignore | | | 1 | - |
A | lib.js | | | 197 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
D | lib.ts | | | 175 | ------------------------------------------------------------------------------- |
D | types.ts | | | 13 | ------------- |
4 files changed, 197 insertions(+), 189 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1 +0,0 @@
-*.js
diff --git a/lib.js b/lib.js
@@ -0,0 +1,197 @@
+// @ts-check
+
+/**
+ * @typedef {Object} ReactElement
+ * @typedef {function(Props=): ReactElement} ReactFunction
+ * @typedef {function(ReactFunction | string, Props?=, ...Object): ReactElement} ReactCreateFunction
+ *
+ * @typedef {TemplateStringsArray} Slices
+ * @typedef {any} Value
+ * @typedef {{[key: string]: any}} Refs
+ * @typedef {any} Props
+ * @typedef {{[key: string]: ReactFunction | Dict}} Dict
+ * @typedef {{ dict: DictFunction, dyn: DynFunction }} Figure
+ *
+ * @typedef {function(Dict=): HTMLFunction} DictFunction
+ * @typedef {function(Slices, ...Value): ReactElement[]} HTMLFunction
+ * @typedef {ReactCreateFunction} DynFunction
+ */
+
+/**
+ * Initializes the figure framework.
+ *
+ * @param {ReactCreateFunction} create The React createElement function
+ * @returns {Figure} The util functions collected in an object
+ */
+export default function figure(create) {
+
+ // the parser for interpreting HTML
+ const parser = new DOMParser();
+ // the counter for creating unique references
+ let count = 0;
+
+ /**
+ * Returns the a function for rendering HTML.
+ *
+ * @param {Dict} [dict] The dictionary for resolving React components
+ * @returns {HTMLFunction} The function for rendering HTML
+ */
+ function dict(dict) {
+
+ /**
+ * Converts the template literal HTML syntax into React elements.
+ *
+ * @param {Slices} slices The template literal slices
+ * @param {...Value} values The template literal values
+ * @returns {ReactElement[]} The converted HTML as React elements
+ */
+ function html(slices, ...values) {
+ const [html, refs] = compose(slices, values);
+ try {
+ const dom = parser.parseFromString(html, 'text/html');
+ // collect all nodes from head and body
+ const nodes = [...dom.head.childNodes, ...dom.body.childNodes];
+ return nodes.map((node) => render(node, refs, dict ?? {}));
+ }
+ catch (error) {
+ console.error(error);
+ throw 'Invalid DOM structure!';
+ }
+ }
+
+ return html;
+ }
+
+ /**
+ * Joins the template literal slices together and replaces the values with references.
+ * The values are being mapped to there corresponding references and with the populated
+ * HTML string returned.
+ *
+ * @param {Slices} slices The template literal slices
+ * @param {Value[]} values The template literal values
+ * @returns {[string, Refs]} The joined HTML string and the values mapped to there references
+ */
+ function compose(slices, values) {
+ if (slices == null) {
+ // handle dyn function without body
+ return ['', {}];
+ }
+ const refs = /** @type {Refs} */ ({});
+ let slice = '';
+ for (let i = 0; i < slices.length; i++) {
+ slice += slices[i];
+ if (values[i] != null) {
+ // create unique reference
+ const uid = `$fig-${count++}`;
+ refs[uid] = values[i];
+ slice += uid ?? '';
+ }
+ }
+ return [slice.trim(), refs];
+ }
+
+ /**
+ * Injects the values into the corresponding reference locations of the string.
+ *
+ * @param {string} slice The string containing references
+ * @param {Refs} refs The values mapped to there references
+ * @returns {ReactElement[]} The string populated with the passed values
+ */
+ function feed(slice, refs) {
+ const expr = /\$fig-\d+/g;
+ const elements = /** @type {ReactElement[]} */ ([]);
+ let match = null;
+ let last = 0;
+ while ((match = expr.exec(slice)) !== null) {
+ const index = match.index;
+ const uid = match[0];
+ const before = slice.substring(last, index);
+ // ignore empty strings
+ if (before.length > 0) {
+ elements.push(before);
+ }
+ const value = refs[uid];
+ // ignore empty values
+ if (value) {
+ elements.push(value);
+ }
+ last = index + uid.length;
+ }
+ const after = slice.substring(last);
+ // ignore empty strings
+ if (after.length > 0) {
+ elements.push(after);
+ }
+ return elements;
+ }
+
+ /**
+ * Converts a HTML node into a React element.
+ *
+ * @param {Node} node The HTML node
+ * @param {Refs} refs The values mapped to there references
+ * @param {Dict} dict The dictionary for resolving React components
+ * @returns {ReactElement[]} The converted HTML node as React element
+ */
+ function render(node, refs, dict) {
+ if (node.nodeType === Node.TEXT_NODE) {
+ const text = /** @type {Text} */ (node);
+ if (text.textContent == null) {
+ // ignore empty text nodes
+ return [];
+ }
+ return feed(text.textContent, refs);
+ }
+ if (node.nodeType === Node.COMMENT_NODE) {
+ // ignore comments
+ return [];
+ }
+ const element = /** @type {HTMLElement} */ (node);
+ const tag = element.tagName.toLowerCase();
+ const props = /** @type {Props} */ ({});
+ // iterate over each attribute and add it to the props
+ for (const attribute of element.attributes) {
+ const key = attribute.name;
+ const slice = attribute.textContent;
+ if (slice == null) {
+ // ignore empty attribute values
+ continue;
+ }
+ const values = feed(slice, refs);
+ const value = values.length == 1 ? values[0] : values;
+ let attr = key;
+ const match = /^(\w+):(\w+)$/.exec(attr);
+ if (match) {
+ // camel case attribute name
+ const [, pre, name] = match;
+ attr = `${pre}${name.substring(0, 1).toUpperCase()}${name.substring(1)}`;
+ }
+ props[attr] = value instanceof Array ? value.join('') : value;
+ }
+ const children = /** @type {ReactElement[]} */ ([]);
+ // recursively render all child nodes
+ (node.childNodes ?? []).forEach((child) => children.push(...render(child, refs, dict)));
+ const domains = tag.split(':');
+ let component = /** @type {ReactFunction | null} */ (null);
+ let layer = /** @type {Dict | ReactFunction | null} */ (dict);
+ // look up tag name in dictionary
+ for (const domain of domains) {
+ if (layer && typeof layer === 'object' && layer[domain]) {
+ // traverse sublayer
+ layer = /** @type {Dict} */ (layer[domain]);
+ continue;
+ }
+ // domain is not in layer
+ layer = null;
+ break;
+ }
+ if (layer) {
+ // found component for tag
+ component = /** @type {ReactFunction} */ (layer);
+ }
+ // use React component or tag name
+ return [create(component ?? tag, props, ...children)];
+ }
+
+ return { dict, dyn: create };
+}
diff --git a/lib.ts b/lib.ts
@@ -1,175 +0,0 @@
-import {
- Dict,
- Figure,
- Props,
- ReactElement,
- HtmlFunction,
- ReactFunction,
- CreateFunction,
- Refs,
- Slices,
- Values,
-} from './types.ts';
-
-/**
- * Initializes the figure utility.
- * @param {CreateFunction} create - The React createElement function.
- * @return {Figure} The util functions collected in an object.
- */
-export default function figure(create: CreateFunction): Figure {
-
- // the parser for interpreting HTML
- const parser = new DOMParser();
- // the counter for creating unique references
- let count = 0;
-
- /**
- * Returns the a function for rendering HTML.
- * @param {Dict} dict - The dictionary for resolving React components.
- * @return {Function} The function for rendering HTML.
- */
- function dict(dict?: Dict): HtmlFunction {
-
- /**
- * Converts the template literal HTML syntax into React elements.
- * @param {Slices} slices - The template literal slices
- * @param {Values} values - The template literal values
- * @return {ReactElement[]} The converted HTML as React elements.
- */
- function html(slices: Slices, ...values: Values): ReactElement[] {
- const [html, refs] = compose(slices, values);
- try {
- const dom = parser.parseFromString(html, 'text/html');
- // collect all nodes from head and body
- const nodes = [...dom.head.childNodes, ...dom.body.childNodes];
- return nodes.map((node) => render(node, refs, dict ?? {}));
- }
- catch (error) {
- console.error(error);
- throw 'Invalid DOM structure!';
- }
- }
-
- return html;
- }
-
- /**
- * Joins the template literal slices together and replaces the values with references.
- * The values are being mapped to there corresponding references and with the populated
- * HTML string returned.
- *
- * @param {Slices} slices - The template literal slices
- * @param {Values} values - The template literal values
- * @return {[string, Refs]} The joined HTML string and the values mapped to there references.
- */
- function compose(slices: Slices, values: Values): [string, Refs] {
- if (slices == null) {
- // handle dyn function without body
- return ['', {}];
- }
- const refs: Refs = {};
- let slice = '';
- for (let i = 0; i < slices.length; i++) {
- slice += slices[i];
- if (values[i] != null) {
- // create unique reference
- const uid = `$fig-${count++}`;
- refs[uid] = values[i];
- slice += uid ?? '';
- }
- }
- return [slice.trim(), refs];
- }
-
- /**
- * Injects the values into the corresponding reference locations of the string.
- *
- * @param {string} slice - The string containing references
- * @param {Refs} refs - The values mapped to there references
- * @return {ReactElement[]} The string populated with the passed values
- */
- function feed(slice: string, refs: Refs): ReactElement[] {
- const expr = /\$fig-\d+/g;
- const elements: ReactElement[] = [];
- let match: RegExpExecArray | null = null;
- let last = 0;
- while ((match = expr.exec(slice)) !== null) {
- const index = match.index;
- const uid = match[0];
- const before = slice.substring(last, index);
- // ignore empty strings
- if (before.length > 0) {
- elements.push(before);
- }
- const value = refs[uid];
- // ignore empty values
- if (value) {
- elements.push(value);
- }
- last = index + uid.length;
- }
- const after = slice.substring(last);
- // ignore empty strings
- if (after.length > 0) {
- elements.push(after);
- }
- return elements;
- }
-
- /**
- * Converts a HTML node into a React element.
- *
- * @param {Node} node - The HTML node
- * @param {Refs} refs - The values mapped to there references
- * @return {ReactElement[]} The converted HTML node as React element
- */
- function render(node: Node, refs: Refs, dict: Dict): ReactElement[] {
- if (node.nodeType === Node.TEXT_NODE) {
- const text = node as Text;
- if (text.textContent == null) {
- // ignore empty text nodes
- return [];
- }
- return feed(text.textContent, refs);
- }
- if (node.nodeType === Node.COMMENT_NODE) {
- // ignore comments
- return [];
- }
- const element = node as HTMLElement;
- const tag = element.tagName.toLowerCase();
- const props: Props = {};
- // iterate over each attribute and add it to the props
- for (const attribute of element.attributes) {
- const key = attribute.name;
- const slice = attribute.textContent;
- if (slice == null) {
- // ignore empty attribute values
- continue;
- }
- const values = feed(slice, refs);
- const value = values.length == 1 ? values[0] : values;
- let attr = key;
- const match = /^(\w+):(\w+)$/.exec(attr);
- if (match) {
- // camel case attribute name
- const [, pre, name] = match;
- attr = `${pre}${name.substring(0, 1).toUpperCase()}${name.substring(1)}`;
- }
- props[attr] = value instanceof Array ? value.join('') : value;
- }
- const children: ReactElement[] = [];
- // recursively render all child nodes
- (node.childNodes ?? []).forEach((child) => children.push(...render(child, refs, dict)));
- const domain = tag.split(':');
- // look up tag name in dictionary
- // deno-lint-ignore no-explicit-any
- const component: ReactFunction | null = domain.reduce((dict: any, level) => {
- return dict && dict[level] ? dict[level] : null;
- }, dict);
- // use React component or tag name
- return [create(component ?? tag, props, ...children)];
- }
-
- return { dict, dyn: create };
-}
diff --git a/types.ts b/types.ts
@@ -1,13 +0,0 @@
-export type ReactElement = unknown;
-export type ReactFunction = (props: unknown) => ReactElement;
-export type CreateFunction = (element: ReactFunction | string, props?: Props, ...children: ReactElement[]) => ReactElement;
-export type HtmlFunction = (slices: Slices, ...values: Values) => ReactElement[];
-export type Figure = { dict: (dict?: Dict) => HtmlFunction, dyn: CreateFunction };
-export type Refs = Record<string, unknown>;
-export type Props = Record<string, unknown>;
-export type Slices = TemplateStringsArray;
-export type Values = unknown[];
-
-export interface Dict {
- [key: string]: ReactFunction | Dict;
-}