UNPKG

hono

Version:

Web framework built on Web Standards

315 lines (314 loc) 8.62 kB
// src/jsx/base.ts import { raw } from "../helper/html/index.js"; import { escapeToBuffer, resolveCallbackSync, stringBufferToString } from "../utils/html.js"; import { createContext, globalContexts, useContext } from "./context.js"; import { DOM_RENDERER } from "./constants.js"; import { normalizeIntrinsicElementKey, styleObjectForEach } from "./utils.js"; import * as intrinsicElementTags from "./intrinsic-element/components.js"; import { domRenderers } from "./intrinsic-element/common.js"; var nameSpaceContext = void 0; var getNameSpaceContext = () => nameSpaceContext; var toSVGAttributeName = (key) => /[A-Z]/.test(key) && key.match( /^(?:al|basel|clip(?:Path|Rule)$|co|do|fill|fl|fo|gl|let|lig|i|marker[EMS]|o|pai|pointe|sh|st[or]|text[^L]|tr|u|ve|w)/ ) ? key.replace(/([A-Z])/g, "-$1").toLowerCase() : key; var emptyTags = [ "area", "base", "br", "col", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr" ]; var booleanAttributes = [ "allowfullscreen", "async", "autofocus", "autoplay", "checked", "controls", "default", "defer", "disabled", "download", "formnovalidate", "hidden", "inert", "ismap", "itemscope", "loop", "multiple", "muted", "nomodule", "novalidate", "open", "playsinline", "readonly", "required", "reversed", "selected" ]; var childrenToStringToBuffer = (children, buffer) => { for (let i = 0, len = children.length; i < len; i++) { const child = children[i]; if (typeof child === "string") { escapeToBuffer(child, buffer); } else if (typeof child === "boolean" || child === null || child === void 0) { continue; } else if (child instanceof JSXNode) { child.toStringToBuffer(buffer); } else if (typeof child === "number" || child.isEscaped) { ; buffer[0] += child; } else if (child instanceof Promise) { buffer.unshift("", child); } else { childrenToStringToBuffer(child, buffer); } } }; var JSXNode = class { tag; props; key; children; isEscaped = true; localContexts; constructor(tag, props, children) { this.tag = tag; this.props = props; this.children = children; } get type() { return this.tag; } get ref() { return this.props.ref || null; } toString() { const buffer = [""]; this.localContexts?.forEach(([context, value]) => { context.values.push(value); }); try { this.toStringToBuffer(buffer); } finally { this.localContexts?.forEach(([context]) => { context.values.pop(); }); } return buffer.length === 1 ? "callbacks" in buffer ? resolveCallbackSync(raw(buffer[0], buffer.callbacks)).toString() : buffer[0] : stringBufferToString(buffer, buffer.callbacks); } toStringToBuffer(buffer) { const tag = this.tag; const props = this.props; let { children } = this; buffer[0] += `<${tag}`; const normalizeKey = nameSpaceContext && useContext(nameSpaceContext) === "svg" ? (key) => toSVGAttributeName(normalizeIntrinsicElementKey(key)) : (key) => normalizeIntrinsicElementKey(key); for (let [key, v] of Object.entries(props)) { key = normalizeKey(key); if (key === "children") { } else if (key === "style" && typeof v === "object") { let styleStr = ""; styleObjectForEach(v, (property, value) => { if (value != null) { styleStr += `${styleStr ? ";" : ""}${property}:${value}`; } }); buffer[0] += ' style="'; escapeToBuffer(styleStr, buffer); buffer[0] += '"'; } else if (typeof v === "string") { buffer[0] += ` ${key}="`; escapeToBuffer(v, buffer); buffer[0] += '"'; } else if (v === null || v === void 0) { } else if (typeof v === "number" || v.isEscaped) { buffer[0] += ` ${key}="${v}"`; } else if (typeof v === "boolean" && booleanAttributes.includes(key)) { if (v) { buffer[0] += ` ${key}=""`; } } else if (key === "dangerouslySetInnerHTML") { if (children.length > 0) { throw "Can only set one of `children` or `props.dangerouslySetInnerHTML`."; } children = [raw(v.__html)]; } else if (v instanceof Promise) { buffer[0] += ` ${key}="`; buffer.unshift('"', v); } else if (typeof v === "function") { if (!key.startsWith("on")) { throw `Invalid prop '${key}' of type 'function' supplied to '${tag}'.`; } } else { buffer[0] += ` ${key}="`; escapeToBuffer(v.toString(), buffer); buffer[0] += '"'; } } if (emptyTags.includes(tag) && children.length === 0) { buffer[0] += "/>"; return; } buffer[0] += ">"; childrenToStringToBuffer(children, buffer); buffer[0] += `</${tag}>`; } }; var JSXFunctionNode = class extends JSXNode { toStringToBuffer(buffer) { const { children } = this; const res = this.tag.call(null, { ...this.props, children: children.length <= 1 ? children[0] : children }); if (typeof res === "boolean" || res == null) { return; } else if (res instanceof Promise) { if (globalContexts.length === 0) { buffer.unshift("", res); } else { const currentContexts = globalContexts.map((c) => [c, c.values.at(-1)]); buffer.unshift( "", res.then((childRes) => { if (childRes instanceof JSXNode) { childRes.localContexts = currentContexts; } return childRes; }) ); } } else if (res instanceof JSXNode) { res.toStringToBuffer(buffer); } else if (typeof res === "number" || res.isEscaped) { buffer[0] += res; if (res.callbacks) { buffer.callbacks ||= []; buffer.callbacks.push(...res.callbacks); } } else { escapeToBuffer(res, buffer); } } }; var JSXFragmentNode = class extends JSXNode { toStringToBuffer(buffer) { childrenToStringToBuffer(this.children, buffer); } }; var jsx = (tag, props, ...children) => { props ??= {}; if (children.length) { props.children = children.length === 1 ? children[0] : children; } const key = props.key; delete props["key"]; const node = jsxFn(tag, props, children); node.key = key; return node; }; var initDomRenderer = false; var jsxFn = (tag, props, children) => { if (!initDomRenderer) { for (const k in domRenderers) { ; intrinsicElementTags[k][DOM_RENDERER] = domRenderers[k]; } initDomRenderer = true; } if (typeof tag === "function") { return new JSXFunctionNode(tag, props, children); } else if (intrinsicElementTags[tag]) { return new JSXFunctionNode( intrinsicElementTags[tag], props, children ); } else if (tag === "svg") { nameSpaceContext ||= createContext(""); return new JSXNode(tag, props, [ new JSXFunctionNode( nameSpaceContext, { value: tag }, children ) ]); } else { return new JSXNode(tag, props, children); } }; var shallowEqual = (a, b) => { if (a === b) { return true; } const aKeys = Object.keys(a).sort(); const bKeys = Object.keys(b).sort(); if (aKeys.length !== bKeys.length) { return false; } for (let i = 0, len = aKeys.length; i < len; i++) { if (aKeys[i] === "children" && bKeys[i] === "children" && !a.children?.length && !b.children?.length) { continue; } else if (a[aKeys[i]] !== b[aKeys[i]]) { return false; } } return true; }; var memo = (component, propsAreEqual = shallowEqual) => { let computed = null; let prevProps = void 0; return (props) => { if (prevProps && !propsAreEqual(prevProps, props)) { computed = null; } prevProps = props; return computed ||= component(props); }; }; var Fragment = ({ children }) => { return new JSXFragmentNode( "", { children }, Array.isArray(children) ? children : children ? [children] : [] ); }; var isValidElement = (element) => { return !!(element && typeof element === "object" && "tag" in element && "props" in element); }; var cloneElement = (element, props, ...children) => { return jsx( element.tag, { ...element.props, ...props }, ...children ); }; var reactAPICompatVersion = "19.0.0-hono-jsx"; export { Fragment, JSXFragmentNode, JSXNode, cloneElement, getNameSpaceContext, isValidElement, jsx, jsxFn, memo, reactAPICompatVersion };