675 lines
16 KiB
JavaScript
675 lines
16 KiB
JavaScript
|
import {
|
||
|
encodeEntities,
|
||
|
styleObjToCss,
|
||
|
UNSAFE_NAME,
|
||
|
NAMESPACE_REPLACE_REGEX,
|
||
|
HTML_LOWER_CASE,
|
||
|
SVG_CAMEL_CASE
|
||
|
} from './util.js';
|
||
|
import { options, h, Fragment } from 'preact';
|
||
|
import {
|
||
|
CHILDREN,
|
||
|
COMMIT,
|
||
|
COMPONENT,
|
||
|
DIFF,
|
||
|
DIFFED,
|
||
|
DIRTY,
|
||
|
NEXT_STATE,
|
||
|
PARENT,
|
||
|
RENDER,
|
||
|
SKIP_EFFECTS,
|
||
|
VNODE
|
||
|
} from './constants.js';
|
||
|
|
||
|
/** @typedef {import('preact').VNode} VNode */
|
||
|
|
||
|
const EMPTY_ARR = [];
|
||
|
const isArray = Array.isArray;
|
||
|
const assign = Object.assign;
|
||
|
|
||
|
// 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
|
||
|
* @returns {string} serialized HTML
|
||
|
*/
|
||
|
export function renderToString(vnode, context) {
|
||
|
// 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 {
|
||
|
return _renderToString(
|
||
|
vnode,
|
||
|
context || EMPTY_OBJ,
|
||
|
false,
|
||
|
undefined,
|
||
|
parent,
|
||
|
false
|
||
|
);
|
||
|
} 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;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Render Preact JSX + Components to an HTML string.
|
||
|
* @param {VNode} vnode JSX Element / VNode to render
|
||
|
* @param {Object} [context={}] Initial root context object
|
||
|
* @returns {string} serialized HTML
|
||
|
*/
|
||
|
export async function renderToStringAsync(vnode, context) {
|
||
|
// 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,
|
||
|
true
|
||
|
);
|
||
|
|
||
|
if (Array.isArray(rendered)) {
|
||
|
let count = 0;
|
||
|
let resolved = rendered;
|
||
|
|
||
|
// Resolving nested Promises with a maximum depth of 25
|
||
|
while (
|
||
|
resolved.some((element) => typeof element.then === 'function') &&
|
||
|
count++ < 25
|
||
|
) {
|
||
|
resolved = (await Promise.all(resolved)).flat();
|
||
|
}
|
||
|
|
||
|
return resolved.join('');
|
||
|
}
|
||
|
|
||
|
return rendered;
|
||
|
} 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;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Installed as setState/forceUpdate for function components
|
||
|
function markAsDirty() {
|
||
|
this.__d = true;
|
||
|
}
|
||
|
|
||
|
const EMPTY_OBJ = {};
|
||
|
|
||
|
/**
|
||
|
* @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
|
||
|
* @returns {string | Promise<string> | (string | Promise<string>)[]}
|
||
|
*/
|
||
|
function _renderToString(
|
||
|
vnode,
|
||
|
context,
|
||
|
isSvgMode,
|
||
|
selectValue,
|
||
|
parent,
|
||
|
asyncMode
|
||
|
) {
|
||
|
// Ignore non-rendered VNodes/values
|
||
|
if (vnode == null || vnode === true || vnode === false || vnode === '') {
|
||
|
return '';
|
||
|
}
|
||
|
|
||
|
// Text VNodes: escape as HTML
|
||
|
if (typeof vnode !== 'object') {
|
||
|
if (typeof vnode === 'function') return '';
|
||
|
return encodeEntities(vnode + '');
|
||
|
}
|
||
|
|
||
|
// Recurse into children / Arrays
|
||
|
if (isArray(vnode)) {
|
||
|
let rendered = '',
|
||
|
renderArray;
|
||
|
parent[CHILDREN] = vnode;
|
||
|
for (let i = 0; i < vnode.length; i++) {
|
||
|
let child = vnode[i];
|
||
|
if (child == null || typeof child === 'boolean') continue;
|
||
|
|
||
|
const childRender = _renderToString(
|
||
|
child,
|
||
|
context,
|
||
|
isSvgMode,
|
||
|
selectValue,
|
||
|
parent,
|
||
|
asyncMode
|
||
|
);
|
||
|
|
||
|
if (typeof childRender === 'string') {
|
||
|
rendered += childRender;
|
||
|
} else {
|
||
|
renderArray = renderArray || [];
|
||
|
|
||
|
if (rendered) renderArray.push(rendered);
|
||
|
|
||
|
rendered = '';
|
||
|
|
||
|
if (Array.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 '';
|
||
|
|
||
|
vnode[PARENT] = parent;
|
||
|
if (beforeDiff) beforeDiff(vnode);
|
||
|
|
||
|
let type = vnode.type,
|
||
|
props = vnode.props,
|
||
|
cctx = context,
|
||
|
contextType,
|
||
|
rendered,
|
||
|
component;
|
||
|
|
||
|
// Invoke rendering on Components
|
||
|
if (typeof type === 'function') {
|
||
|
if (type === Fragment) {
|
||
|
// Serialized precompiled JSX.
|
||
|
if (props.tpl) {
|
||
|
let out = '';
|
||
|
for (let i = 0; i < props.tpl.length; i++) {
|
||
|
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 += _renderToString(
|
||
|
value,
|
||
|
context,
|
||
|
isSvgMode,
|
||
|
selectValue,
|
||
|
vnode,
|
||
|
asyncMode
|
||
|
);
|
||
|
} else {
|
||
|
// Values are pre-escaped by the JSX transform
|
||
|
out += value;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return out;
|
||
|
} else if (props.UNSTABLE_comment) {
|
||
|
// 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.__;
|
||
|
}
|
||
|
|
||
|
if (type.prototype && typeof type.prototype.render === 'function') {
|
||
|
rendered = /**#__NOINLINE__**/ renderClassComponent(vnode, cctx);
|
||
|
component = vnode[COMPONENT];
|
||
|
} else {
|
||
|
component = {
|
||
|
__v: vnode,
|
||
|
props,
|
||
|
context: cctx,
|
||
|
// silently drop state updates
|
||
|
setState: markAsDirty,
|
||
|
forceUpdate: markAsDirty,
|
||
|
__d: true,
|
||
|
// hooks
|
||
|
__h: []
|
||
|
};
|
||
|
vnode[COMPONENT] = component;
|
||
|
|
||
|
// 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 (
|
||
|
(type.getDerivedStateFromError || component.componentDidCatch) &&
|
||
|
options.errorBoundaries
|
||
|
) {
|
||
|
let str = '';
|
||
|
// 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 = isTopLevelFragment ? rendered.props.children : rendered;
|
||
|
|
||
|
try {
|
||
|
str = _renderToString(
|
||
|
rendered,
|
||
|
context,
|
||
|
isSvgMode,
|
||
|
selectValue,
|
||
|
vnode,
|
||
|
asyncMode
|
||
|
);
|
||
|
return str;
|
||
|
} catch (err) {
|
||
|
if (type.getDerivedStateFromError) {
|
||
|
component[NEXT_STATE] = type.getDerivedStateFromError(err);
|
||
|
}
|
||
|
|
||
|
if (component.componentDidCatch) {
|
||
|
component.componentDidCatch(err, {});
|
||
|
}
|
||
|
|
||
|
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 = isTopLevelFragment ? rendered.props.children : rendered;
|
||
|
|
||
|
str = _renderToString(
|
||
|
rendered,
|
||
|
context,
|
||
|
isSvgMode,
|
||
|
selectValue,
|
||
|
vnode,
|
||
|
asyncMode
|
||
|
);
|
||
|
}
|
||
|
|
||
|
return str;
|
||
|
} finally {
|
||
|
if (afterDiff) afterDiff(vnode);
|
||
|
vnode[PARENT] = null;
|
||
|
|
||
|
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
|
||
|
);
|
||
|
|
||
|
if (afterDiff) afterDiff(vnode);
|
||
|
vnode[PARENT] = null;
|
||
|
|
||
|
if (ummountHook) ummountHook(vnode);
|
||
|
|
||
|
return str;
|
||
|
} catch (error) {
|
||
|
if (!asyncMode) throw error;
|
||
|
|
||
|
if (!error || typeof error.then !== 'function') throw error;
|
||
|
|
||
|
const renderNestedChildren = () => {
|
||
|
try {
|
||
|
return _renderToString(
|
||
|
rendered,
|
||
|
context,
|
||
|
isSvgMode,
|
||
|
selectValue,
|
||
|
vnode,
|
||
|
asyncMode
|
||
|
);
|
||
|
} catch (e) {
|
||
|
if (!e || typeof e.then !== 'function') throw e;
|
||
|
|
||
|
return e.then(
|
||
|
() =>
|
||
|
_renderToString(
|
||
|
rendered,
|
||
|
context,
|
||
|
isSvgMode,
|
||
|
selectValue,
|
||
|
vnode,
|
||
|
asyncMode
|
||
|
),
|
||
|
() => renderNestedChildren()
|
||
|
);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return error.then(() => renderNestedChildren());
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Serialize Element VNodes to HTML
|
||
|
let s = '<' + type,
|
||
|
html = '',
|
||
|
children;
|
||
|
|
||
|
for (let name in props) {
|
||
|
let v = props[name];
|
||
|
|
||
|
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&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] === '-' || name === 'draggable') && v != null) {
|
||
|
// serialize boolean aria-xyz or draggable attribute values as strings
|
||
|
// `draggable` is an enumerated attribute and not Boolean. A value of `true` or `false` is mandatory
|
||
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/draggable
|
||
|
v += '';
|
||
|
} 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 && typeof v !== 'function') {
|
||
|
if (v === true || v === '') {
|
||
|
s = s + ' ' + name;
|
||
|
} else {
|
||
|
s = s + ' ' + name + '="' + encodeEntities(v + '') + '"';
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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) {
|
||
|
// dangerouslySetInnerHTML defined this node's contents
|
||
|
} 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
|
||
|
);
|
||
|
}
|
||
|
|
||
|
if (afterDiff) afterDiff(vnode);
|
||
|
vnode[PARENT] = null;
|
||
|
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 (Array.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'
|
||
|
]);
|
||
|
|
||
|
export default renderToString;
|
||
|
export const render = renderToString;
|
||
|
export const renderToStaticMarkup = renderToString;
|