UNPKG

@trifrost/core

Version:

Blazingly fast, runtime-agnostic server framework for modern edge and node environments

244 lines (243 loc) 7.79 kB
"use strict"; /* eslint-disable no-use-before-define */ Object.defineProperty(exports, "__esModule", { value: true }); exports.escape = escape; exports.fromLruCookie = fromLruCookie; exports.toLruCookie = toLruCookie; exports.render = render; exports.rootRender = rootRender; const runtime_1 = require("./runtime"); const Engine_1 = require("./style/Engine"); const use_1 = require("./style/use"); const util_1 = require("./style/util"); const Engine_2 = require("./script/Engine"); const Script_1 = require("./script/Script"); const use_2 = require("./script/use"); const use_3 = require("./ctx/use"); const SCRIPT_LRU = 'tfscriptlru'; const MODULES_LRU = 'tfmoduleslru'; const VOID_TAGS = { /* HTML */ area: true, base: true, br: true, col: true, embed: true, hr: true, img: true, input: true, link: true, meta: true, source: true, track: true, wbr: true, /* SVG */ path: true, circle: true, ellipse: true, line: true, polygon: true, polyline: true, rect: true, stop: true, use: true, }; const BOOL_PROPS = { allowfullscreen: true, async: true, autofocus: true, autoplay: true, checked: true, controls: true, default: true, defer: true, disabled: true, formnovalidate: true, hidden: true, ismap: true, loop: true, multiple: true, muted: true, nomodule: true, novalidate: true, open: true, readonly: true, required: true, reversed: true, selected: true, }; const ESCAPE_LOOKUP = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;', }; const RGX_ESCAPE = /(?:&(?![a-z#0-9]+;))|[<>"']/gi; /** * Escape HTML entities in strings to prevent XSS attacks. * * @param {string} str - Input string to escape. */ function escape(str) { return str.replace(RGX_ESCAPE, (ch) => ESCAPE_LOOKUP[ch]); } /** * Takes an lru cookie and converts it to a set * @param {string|null} val - Value to convert */ function fromLruCookie(val) { if (!val) return new Set(); const acc = new Set(); const parts = val.split('|'); for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (part) acc.add(part); } return acc; } /** * Convert a set back to an LRU cookie * @param {Set<string>} val - Set to convert */ function toLruCookie(val) { if (!val.size) return null; /* We cap at 64 latest (lru) entries */ return [...val].slice(-64).join('|'); } /** * Renders properties such as style/attributes * * @param {Record<string, unknown>} props - Props to render */ function renderProps(props) { let acc = ''; for (const key in props) { const val = props[key]; switch (key) { case 'style': if (Object.prototype.toString.call(val) === '[object Object]') { const style = (0, util_1.styleToString)(val); if (style) acc += ' style="' + escape(style) + '"'; } break; case 'children': case 'dangerouslySetInnerHTML': break; case 'className': { if (val !== undefined && val !== null) acc += ' class="' + escape(val + '') + '"'; break; } default: { if (val !== undefined && val !== null) { acc += ' ' + key + (val !== true || !BOOL_PROPS[key] ? '="' + escape(val + '') + '"' : ''); } break; } } } return acc; } /** * Renders child elements */ function renderChildren(children, parentProps) { if (!Array.isArray(children)) return children ? render(children, parentProps) : ''; let output = ''; for (let i = 0; i < children.length; i++) output += render(children[i], parentProps); return output; } /** * Renders a JSXElement or primitive to a string. * @param node - JSX tree or primitive. */ function render(node, parentProps = {}) { switch (typeof node) { case 'string': return node ? escape(node) : ''; case 'number': return node + ''; case 'boolean': return ''; default: { switch (typeof node?.type) { case 'string': { const tag = node.type; if (tag === Script_1.SCRIPT_MARKER) { if (node.props.fn_id) { parentProps['data-tfhf'] = node.props.fn_id; if (node.props.data_id) parentProps['data-tfhd'] = node.props.data_id; } return ''; /* Dont render the marker */ } /* Render children */ const innerHTML = typeof node.props.dangerouslySetInnerHTML?.__html === 'string' ? node.props.dangerouslySetInnerHTML.__html : renderChildren(node.props.children, node.props); return VOID_TAGS[tag] ? '<' + tag + renderProps(node.props) + ' />' : '<' + tag + renderProps(node.props) + '>' + innerHTML + '</' + tag + '>'; } case 'function': return node.type === runtime_1.Fragment ? renderChildren(node.props.children, parentProps) : render(node.type(node.props), parentProps); default: { if (!node) { return ''; } else if (Array.isArray(node)) { let output = ''; for (let i = 0; i < node.length; i++) output += render(node[i], parentProps); return output; } else { return ''; } } } } } } /** * Starts the render process for a JSX element */ function rootRender(ctx, tree, options = {}) { /* Instantiate globals */ const style_engine = (0, use_1.getActiveStyleEngine)() || (0, use_1.setActiveStyleEngine)(new Engine_1.StyleEngine()); const script_engine = (0, use_2.getActiveScriptEngine)() || (0, use_2.setActiveScriptEngine)(new Engine_2.ScriptEngine()); (0, use_3.setActiveCtx)(ctx); /* Known scripts on frontend */ const script_lru_cookie = ctx.cookies.get(SCRIPT_LRU); const script_lru = fromLruCookie(script_lru_cookie); /* Known modules on frontend */ const module_lru_cookie = ctx.cookies.get(MODULES_LRU); const module_lru = fromLruCookie(module_lru_cookie); /* Auto-call root() if script or css provided in ctx config */ options?.script?.root?.(); options?.css?.root?.(); /* Render jsx to html */ const html = script_engine.inject(style_engine.inject(render(tree)), { scripts: script_lru, modules: module_lru }); /* Set script cookie */ const script_cookie = toLruCookie(script_lru); if (script_cookie && script_cookie !== script_lru_cookie) ctx.cookies.set(SCRIPT_LRU, script_cookie, { httponly: true, secure: true }); /* Set modules cookie */ const module_cookie = toLruCookie(module_lru); if (module_cookie && module_cookie !== module_lru_cookie) ctx.cookies.set(MODULES_LRU, module_cookie, { httponly: true, secure: true }); /* Cleanup globals */ (0, use_3.setActiveCtx)(null); (0, use_1.setActiveStyleEngine)(null); (0, use_2.setActiveScriptEngine)(null); return html; }