linktree/node_modules/preact-render-to-string/dist/stream/node/index.mjs
2025-06-16 13:37:14 +02:00

819 lines
25 KiB
JavaScript

import { PassThrough } from 'node:stream';
import { options, h, Fragment } from 'preact';
const UNSAFE_NAME = /[\s\n\\/='"\0<>]/;
const NAMESPACE_REPLACE_REGEX = /^(xlink|xmlns|xml)([A-Z])/;
const HTML_LOWER_CASE = /^(?:accessK|auto[A-Z]|cell|ch|col|cont|cross|dateT|encT|form[A-Z]|frame|hrefL|inputM|maxL|minL|noV|playsI|popoverT|readO|rowS|src[A-Z]|tabI|useM|item[A-Z])/;
const SVG_CAMEL_CASE = /^ac|^ali|arabic|basel|cap|clipPath$|clipRule$|color|dominant|enable|fill|flood|font|glyph[^R]|horiz|image|letter|lighting|marker[^WUH]|overline|panose|pointe|paint|rendering|shape|stop|strikethrough|stroke|text[^L]|transform|underline|unicode|units|^v[^i]|^w|^xH/; // Boolean DOM properties that translate to enumerated ('true'/'false') attributes
const HTML_ENUMERATED = new Set(['draggable', 'spellcheck']); // DOM properties that should NOT have "px" added when numeric
const ENCODED_ENTITIES = /["&<]/;
/** @param {string} str */
function encodeEntities(str) {
// Skip all work for strings with no entities needing encoding:
if (str.length === 0 || ENCODED_ENTITIES.test(str) === false) return str;
let last = 0,
i = 0,
out = '',
ch = ''; // Seek forward in str until the next entity char:
for (; i < str.length; i++) {
switch (str.charCodeAt(i)) {
case 34:
ch = '&quot;';
break;
case 38:
ch = '&amp;';
break;
case 60:
ch = '&lt;';
break;
default:
continue;
} // Append skipped/buffered characters and the encoded entity:
if (i !== last) out = out + str.slice(last, i);
out = out + ch; // Start the next seek/buffer after the entity's offset:
last = i + 1;
}
if (i !== last) out = out + str.slice(last, i);
return out;
}
const JS_TO_CSS = {};
const IS_NON_DIMENSIONAL = new Set(['animation-iteration-count', 'border-image-outset', 'border-image-slice', 'border-image-width', 'box-flex', 'box-flex-group', 'box-ordinal-group', 'column-count', 'fill-opacity', 'flex', 'flex-grow', 'flex-negative', 'flex-order', 'flex-positive', 'flex-shrink', 'flood-opacity', 'font-weight', 'grid-column', 'grid-row', 'line-clamp', 'line-height', 'opacity', 'order', 'orphans', 'stop-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-miterlimit', 'stroke-opacity', 'stroke-width', 'tab-size', 'widows', 'z-index', 'zoom']);
const CSS_REGEX = /[A-Z]/g; // Convert an Object style to a CSSText string
function styleObjToCss(s) {
let str = '';
for (let prop in s) {
let val = s[prop];
if (val != null && val !== '') {
const name = prop[0] == '-' ? prop : JS_TO_CSS[prop] || (JS_TO_CSS[prop] = prop.replace(CSS_REGEX, '-$&').toLowerCase());
let suffix = ';';
if (typeof val === 'number' && // Exclude custom-attributes
!name.startsWith('--') && !IS_NON_DIMENSIONAL.has(name)) {
suffix = 'px;';
}
str = str + name + ':' + val + suffix;
}
}
return str || undefined;
}
function markAsDirty() {
this.__d = true;
}
function createComponent(vnode, context) {
return {
__v: vnode,
context,
props: vnode.props,
// silently drop state updates
setState: markAsDirty,
forceUpdate: markAsDirty,
__d: true,
// hooks
__h: new Array(0)
};
} // Necessary for createContext api. Setting this property will pass
/**
* @template T
*/
class Deferred {
constructor() {
// eslint-disable-next-line lines-around-comment
/** @type {Promise<T>} */
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
}
}
// Options hooks
const DIFF = '__b';
const RENDER = '__r';
const DIFFED = 'diffed';
const COMMIT = '__c';
const SKIP_EFFECTS = '__s';
const CATCH_ERROR = '__e'; // VNode properties
const COMPONENT = '__c';
const CHILDREN = '__k';
const PARENT = '__';
const VNODE = '__v';
const DIRTY = '__d';
const NEXT_STATE = '__s';
const CHILD_DID_SUSPEND = '__c';
const EMPTY_OBJ = {};
const EMPTY_ARR = [];
const isArray = Array.isArray;
const assign = Object.assign;
const EMPTY_STR = ''; // Global state for the current render pass
let beforeDiff, afterDiff, renderHook, ummountHook;
/**
* Render Preact JSX + Components to an HTML string.
* @param {VNode} vnode JSX Element / VNode to render
* @param {Object} [context={}] Initial root context object
* @param {RendererState} [_rendererState] for internal use
* @returns {string} serialized HTML
*/
function renderToString(vnode, context, _rendererState) {
// Performance optimization: `renderToString` is synchronous and we
// therefore don't execute any effects. To do that we pass an empty
// array to `options._commit` (`__c`). But we can go one step further
// and avoid a lot of dirty checks and allocations by setting
// `options._skipEffects` (`__s`) too.
const previousSkipEffects = options[SKIP_EFFECTS];
options[SKIP_EFFECTS] = true; // store options hooks once before each synchronous render call
beforeDiff = options[DIFF];
afterDiff = options[DIFFED];
renderHook = options[RENDER];
ummountHook = options.unmount;
const parent = h(Fragment, null);
parent[CHILDREN] = [vnode];
try {
const rendered = _renderToString(vnode, context || EMPTY_OBJ, false, undefined, parent, false, _rendererState);
if (isArray(rendered)) {
return rendered.join(EMPTY_STR);
}
return rendered;
} catch (e) {
if (e.then) {
throw new Error('Use "renderToStringAsync" for suspenseful rendering.');
}
throw e;
} finally {
// options._commit, we don't schedule any effects in this library right now,
// so we can pass an empty queue to this hook.
if (options[COMMIT]) options[COMMIT](vnode, EMPTY_ARR);
options[SKIP_EFFECTS] = previousSkipEffects;
EMPTY_ARR.length = 0;
}
}
/**
* @param {VNode} vnode
* @param {Record<string, unknown>} context
*/
function renderClassComponent(vnode, context) {
let type =
/** @type {import("preact").ComponentClass<typeof vnode.props>} */
vnode.type;
let isMounting = true;
let c;
if (vnode[COMPONENT]) {
isMounting = false;
c = vnode[COMPONENT];
c.state = c[NEXT_STATE];
} else {
c = new type(vnode.props, context);
}
vnode[COMPONENT] = c;
c[VNODE] = vnode;
c.props = vnode.props;
c.context = context; // turn off stateful re-rendering:
c[DIRTY] = true;
if (c.state == null) c.state = EMPTY_OBJ;
if (c[NEXT_STATE] == null) {
c[NEXT_STATE] = c.state;
}
if (type.getDerivedStateFromProps) {
c.state = assign({}, c.state, type.getDerivedStateFromProps(c.props, c.state));
} else if (isMounting && c.componentWillMount) {
c.componentWillMount(); // If the user called setState in cWM we need to flush pending,
// state updates. This is the same behaviour in React.
c.state = c[NEXT_STATE] !== c.state ? c[NEXT_STATE] : c.state;
} else if (!isMounting && c.componentWillUpdate) {
c.componentWillUpdate();
}
if (renderHook) renderHook(vnode);
return c.render(c.props, c.state, context);
}
/**
* Recursively render VNodes to HTML.
* @param {VNode|any} vnode
* @param {any} context
* @param {boolean} isSvgMode
* @param {any} selectValue
* @param {VNode} parent
* @param {boolean} asyncMode
* @param {RendererState | undefined} [renderer]
* @returns {string | Promise<string> | (string | Promise<string>)[]}
*/
function _renderToString(vnode, context, isSvgMode, selectValue, parent, asyncMode, renderer) {
// Ignore non-rendered VNodes/values
if (vnode == null || vnode === true || vnode === false || vnode === EMPTY_STR) {
return EMPTY_STR;
}
let vnodeType = typeof vnode; // Text VNodes: escape as HTML
if (vnodeType != 'object') {
if (vnodeType == 'function') return EMPTY_STR;
return vnodeType == 'string' ? encodeEntities(vnode) : vnode + EMPTY_STR;
} // Recurse into children / Arrays
if (isArray(vnode)) {
let rendered = EMPTY_STR,
renderArray;
parent[CHILDREN] = vnode;
const vnodeLength = vnode.length;
for (let i = 0; i < vnodeLength; i++) {
let child = vnode[i];
if (child == null || typeof child == 'boolean') continue;
const childRender = _renderToString(child, context, isSvgMode, selectValue, parent, asyncMode, renderer);
if (typeof childRender == 'string') {
rendered = rendered + childRender;
} else {
if (!renderArray) {
renderArray = new Array(vnodeLength);
}
if (rendered) renderArray.push(rendered);
rendered = EMPTY_STR;
if (isArray(childRender)) {
renderArray.push(...childRender);
} else {
renderArray.push(childRender);
}
}
}
if (renderArray) {
if (rendered) renderArray.push(rendered);
return renderArray;
}
return rendered;
} // VNodes have {constructor:undefined} to prevent JSON injection:
if (vnode.constructor !== undefined) return EMPTY_STR;
vnode[PARENT] = parent;
if (beforeDiff) beforeDiff(vnode);
let type = vnode.type,
props = vnode.props; // Invoke rendering on Components
if (typeof type == 'function') {
let cctx = context,
contextType,
rendered,
component;
if (type === Fragment) {
// Serialized precompiled JSX.
if ('tpl' in props) {
let out = EMPTY_STR;
for (let i = 0; i < props.tpl.length; i++) {
out = out + props.tpl[i];
if (props.exprs && i < props.exprs.length) {
const value = props.exprs[i];
if (value == null) continue; // Check if we're dealing with a vnode or an array of nodes
if (typeof value == 'object' && (value.constructor === undefined || isArray(value))) {
out = out + _renderToString(value, context, isSvgMode, selectValue, vnode, asyncMode, renderer);
} else {
// Values are pre-escaped by the JSX transform
out = out + value;
}
}
}
return out;
} else if ('UNSTABLE_comment' in props) {
// Fragments are the least used components of core that's why
// branching here for comments has the least effect on perf.
return '<!--' + encodeEntities(props.UNSTABLE_comment) + '-->';
}
rendered = props.children;
} else {
contextType = type.contextType;
if (contextType != null) {
let provider = context[contextType.__c];
cctx = provider ? provider.props.value : contextType.__;
}
let isClassComponent = type.prototype && typeof type.prototype.render == 'function';
if (isClassComponent) {
rendered =
/**#__NOINLINE__**/
renderClassComponent(vnode, cctx);
component = vnode[COMPONENT];
} else {
vnode[COMPONENT] = component =
/**#__NOINLINE__**/
createComponent(vnode, cctx); // If a hook invokes setState() to invalidate the component during rendering,
// re-render it up to 25 times to allow "settling" of memoized states.
// Note:
// This will need to be updated for Preact 11 to use internal.flags rather than component._dirty:
// https://github.com/preactjs/preact/blob/d4ca6fdb19bc715e49fd144e69f7296b2f4daa40/src/diff/component.js#L35-L44
let count = 0;
while (component[DIRTY] && count++ < 25) {
component[DIRTY] = false;
if (renderHook) renderHook(vnode);
rendered = type.call(component, props, cctx);
}
component[DIRTY] = true;
}
if (component.getChildContext != null) {
context = assign({}, context, component.getChildContext());
}
if (isClassComponent && options.errorBoundaries && (type.getDerivedStateFromError || component.componentDidCatch)) {
// When a component returns a Fragment node we flatten it in core, so we
// need to mirror that logic here too
let isTopLevelFragment = rendered != null && rendered.type === Fragment && rendered.key == null && rendered.props.tpl == null;
rendered = isTopLevelFragment ? rendered.props.children : rendered;
try {
return _renderToString(rendered, context, isSvgMode, selectValue, vnode, asyncMode, renderer);
} catch (err) {
if (type.getDerivedStateFromError) {
component[NEXT_STATE] = type.getDerivedStateFromError(err);
}
if (component.componentDidCatch) {
component.componentDidCatch(err, EMPTY_OBJ);
}
if (component[DIRTY]) {
rendered = renderClassComponent(vnode, context);
component = vnode[COMPONENT];
if (component.getChildContext != null) {
context = assign({}, context, component.getChildContext());
}
let isTopLevelFragment = rendered != null && rendered.type === Fragment && rendered.key == null && rendered.props.tpl == null;
rendered = isTopLevelFragment ? rendered.props.children : rendered;
return _renderToString(rendered, context, isSvgMode, selectValue, vnode, asyncMode, renderer);
}
return EMPTY_STR;
} finally {
if (afterDiff) afterDiff(vnode);
if (ummountHook) ummountHook(vnode);
}
}
} // When a component returns a Fragment node we flatten it in core, so we
// need to mirror that logic here too
let isTopLevelFragment = rendered != null && rendered.type === Fragment && rendered.key == null && rendered.props.tpl == null;
rendered = isTopLevelFragment ? rendered.props.children : rendered;
try {
// Recurse into children before invoking the after-diff hook
const str = _renderToString(rendered, context, isSvgMode, selectValue, vnode, asyncMode, renderer);
if (afterDiff) afterDiff(vnode); // when we are dealing with suspense we can't do this...
if (options.unmount) options.unmount(vnode);
return str;
} catch (error) {
if (!asyncMode && renderer && renderer.onError) {
let res = renderer.onError(error, vnode, (child, parent) => _renderToString(child, context, isSvgMode, selectValue, parent, asyncMode, renderer));
if (res !== undefined) return res;
let errorHook = options[CATCH_ERROR];
if (errorHook) errorHook(error, vnode);
return EMPTY_STR;
}
if (!asyncMode) throw error;
if (!error || typeof error.then != 'function') throw error;
const renderNestedChildren = () => {
try {
return _renderToString(rendered, context, isSvgMode, selectValue, vnode, asyncMode, renderer);
} catch (e) {
if (!e || typeof e.then != 'function') throw e;
return e.then(() => _renderToString(rendered, context, isSvgMode, selectValue, vnode, asyncMode, renderer), renderNestedChildren);
}
};
return error.then(renderNestedChildren);
}
} // Serialize Element VNodes to HTML
let s = '<' + type,
html = EMPTY_STR,
children;
for (let name in props) {
let v = props[name];
if (typeof v == 'function' && name !== 'class' && name !== 'className') {
continue;
}
switch (name) {
case 'children':
children = v;
continue;
// VDOM-specific props
case 'key':
case 'ref':
case '__self':
case '__source':
continue;
// prefer for/class over htmlFor/className
case 'htmlFor':
if ('for' in props) continue;
name = 'for';
break;
case 'className':
if ('class' in props) continue;
name = 'class';
break;
// Form element reflected properties
case 'defaultChecked':
name = 'checked';
break;
case 'defaultSelected':
name = 'selected';
break;
// Special value attribute handling
case 'defaultValue':
case 'value':
name = 'value';
switch (type) {
// <textarea value="a&b"> --> <textarea>a&amp;b</textarea>
case 'textarea':
children = v;
continue;
// <select value> is serialized as a selected attribute on the matching option child
case 'select':
selectValue = v;
continue;
// Add a selected attribute to <option> if its value matches the parent <select> value
case 'option':
if (selectValue == v && !('selected' in props)) {
s = s + ' selected';
}
break;
}
break;
case 'dangerouslySetInnerHTML':
html = v && v.__html;
continue;
// serialize object styles to a CSS string
case 'style':
if (typeof v === 'object') {
v = styleObjToCss(v);
}
break;
case 'acceptCharset':
name = 'accept-charset';
break;
case 'httpEquiv':
name = 'http-equiv';
break;
default:
{
if (NAMESPACE_REPLACE_REGEX.test(name)) {
name = name.replace(NAMESPACE_REPLACE_REGEX, '$1:$2').toLowerCase();
} else if (UNSAFE_NAME.test(name)) {
continue;
} else if ((name[4] === '-' || HTML_ENUMERATED.has(name)) && v != null) {
// serialize boolean aria-xyz or enumerated attribute values as strings
v = v + EMPTY_STR;
} else if (isSvgMode) {
if (SVG_CAMEL_CASE.test(name)) {
name = name === 'panose1' ? 'panose-1' : name.replace(/([A-Z])/g, '-$1').toLowerCase();
}
} else if (HTML_LOWER_CASE.test(name)) {
name = name.toLowerCase();
}
}
} // write this attribute to the buffer
if (v != null && v !== false) {
if (v === true || v === EMPTY_STR) {
s = s + ' ' + name;
} else {
s = s + ' ' + name + '="' + (typeof v == 'string' ? encodeEntities(v) : v + EMPTY_STR) + '"';
}
}
}
if (UNSAFE_NAME.test(type)) {
// this seems to performs a lot better than throwing
// return '<!-- -->';
throw new Error(`${type} is not a valid HTML tag name in ${s}>`);
}
if (html) ; else if (typeof children === 'string') {
// single text child
html = encodeEntities(children);
} else if (children != null && children !== false && children !== true) {
// recurse into this element VNode's children
let childSvgMode = type === 'svg' || type !== 'foreignObject' && isSvgMode;
html = _renderToString(children, context, childSvgMode, selectValue, vnode, asyncMode, renderer);
}
if (afterDiff) afterDiff(vnode);
if (ummountHook) ummountHook(vnode); // Emit self-closing tag for empty void elements:
if (!html && SELF_CLOSING.has(type)) {
return s + '/>';
}
const endTag = '</' + type + '>';
const startTag = s + '>';
if (isArray(html)) return [startTag, ...html, endTag];else if (typeof html != 'string') return [startTag, html, endTag];
return startTag + html + endTag;
}
const SELF_CLOSING = new Set(['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr']);
/* eslint-disable no-var, key-spacing, object-curly-spacing, prefer-arrow-callback, semi, keyword-spacing */
// function initPreactIslandElement() {
// class PreactIslandElement extends HTMLElement {
// connectedCallback() {
// var d = this;
// if (!d.isConnected) return;
// let i = this.getAttribute('data-target');
// if (!i) return;
// var s,
// e,
// c = document.createNodeIterator(document, 128);
// while (c.nextNode()) {
// let n = c.referenceNode;
// if (n.data == 'preact-island:' + i) s = n;
// else if (n.data == '/preact-island:' + i) e = n;
// if (s && e) break;
// }
// if (s && e) {
// requestAnimationFrame(() => {
// var p = e.previousSibling;
// while (p != s) {
// if (!p || p == s) break;
// e.parentNode.removeChild(p);
// p = e.previousSibling;
// }
// c = s;
// while (d.firstChild) {
// s = d.firstChild;
// d.removeChild(s);
// c.after(s);
// c = s;
// }
// d.parentNode.removeChild(d);
// });
// }
// }
// }
// customElements.define('preact-island', PreactIslandElement);
// }
// To modify the INIT_SCRIPT, uncomment the above code, modify it, and paste it into https://try.terser.org/.
const INIT_SCRIPT = `class e extends HTMLElement{connectedCallback(){var e=this;if(!e.isConnected)return;let t=this.getAttribute("data-target");if(t){for(var r,a,i=document.createNodeIterator(document,128);i.nextNode();){let e=i.referenceNode;if(e.data=="preact-island:"+t?r=e:e.data=="/preact-island:"+t&&(a=e),r&&a)break}r&&a&&requestAnimationFrame((()=>{for(var t=a.previousSibling;t!=r&&t&&t!=r;)a.parentNode.removeChild(t),t=a.previousSibling;for(i=r;e.firstChild;)r=e.firstChild,e.removeChild(r),i.after(r),i=r;e.parentNode.removeChild(e)}))}}}customElements.define("preact-island",e);`;
function createInitScript() {
return `<script>(function(){${INIT_SCRIPT}}())</script>`;
}
/**
* @param {string} id
* @param {string} content
* @returns {string}
*/
function createSubtree(id, content) {
return `<preact-island hidden data-target="${id}">${content}</preact-island>`;
}
/**
* @param {VNode} vnode
* @param {RenderToChunksOptions} options
* @returns {Promise<void>}
*/
async function renderToChunks(vnode, {
context,
onWrite,
abortSignal
}) {
context = context || {};
/** @type {RendererState} */
const renderer = {
start: Date.now(),
abortSignal,
onWrite,
onError: handleError,
suspended: []
}; // Synchronously render the shell
// @ts-ignore - using third internal RendererState argument
const shell = renderToString(vnode, context, renderer);
onWrite(shell); // Wait for any suspended sub-trees if there are any
const len = renderer.suspended.length;
if (len > 0) {
onWrite('<div hidden>');
onWrite(createInitScript()); // We should keep checking all promises
await forkPromises(renderer);
onWrite('</div>');
}
}
async function forkPromises(renderer) {
if (renderer.suspended.length > 0) {
const suspensions = [...renderer.suspended];
await Promise.all(renderer.suspended.map(s => s.promise));
renderer.suspended = renderer.suspended.filter(s => !suspensions.includes(s));
await forkPromises(renderer);
}
}
/** @type {RendererErrorHandler} */
function handleError(error, vnode, renderChild) {
if (!error || !error.then) return; // walk up to the Suspense boundary
while (vnode = vnode[PARENT]) {
let component = vnode[COMPONENT];
if (component && component[CHILD_DID_SUSPEND]) {
break;
}
}
if (!vnode) return;
const id = vnode.__v;
const found = this.suspended.find(x => x.id === id);
const race = new Deferred();
const abortSignal = this.abortSignal;
if (abortSignal) {
// @ts-ignore 2554 - implicit undefined arg
if (abortSignal.aborted) race.resolve();else abortSignal.addEventListener('abort', race.resolve);
}
const promise = error.then(() => {
if (abortSignal && abortSignal.aborted) return;
const child = renderChild(vnode.props.children, vnode);
if (child) this.onWrite(createSubtree(id, child));
}, // TODO: Abort and send hydration code snippet to client
// to attempt to recover during hydration
this.onError);
this.suspended.push({
id,
vnode,
promise: Promise.race([promise, race.promise])
});
const fallback = renderChild(vnode.props.fallback);
return found ? '' : `<!--preact-island:${id}-->${fallback}<!--/preact-island:${id}-->`;
}
/**
* @typedef {object} RenderToPipeableStreamOptions
* @property {() => void} [onShellReady]
* @property {() => void} [onAllReady]
* @property {(error) => void} [onError]
*/
/**
* @typedef {object} PipeableStream
* @property {() => void} abort
* @property {(writable: import('stream').Writable) => void} pipe
*/
/**
* @param {import('preact').VNode} vnode
* @param {RenderToPipeableStreamOptions} options
* @param {any} [context]
* @returns {PipeableStream}
*/
function renderToPipeableStream(vnode, options, context) {
const encoder = new TextEncoder('utf-8');
const controller = new AbortController();
const stream = new PassThrough();
renderToChunks(vnode, {
context,
abortSignal: controller.signal,
onError: error => {
if (options.onError) {
options.onError(error);
}
controller.abort(error);
},
onWrite(s) {
stream.write(encoder.encode(s));
}
}).then(() => {
options.onAllReady && options.onAllReady();
stream.end();
}).catch(error => {
stream.destroy();
if (options.onError) {
options.onError(error);
} else {
throw error;
}
});
Promise.resolve().then(() => {
options.onShellReady && options.onShellReady();
});
return {
/**
* @param {unknown} [reason]
*/
abort(reason = new Error('The render was aborted by the server without a reason.')) {
controller.abort();
stream.destroy();
if (options.onError) {
options.onError(reason);
}
},
/**
* @param {import("stream").Writable} writable
*/
pipe(writable) {
stream.pipe(writable, {
end: true
});
}
};
}
export { renderToPipeableStream };
//# sourceMappingURL=index.module.js.map