diff --git a/src/index.js b/src/index.js index 8c83b59..58ac9d4 100644 --- a/src/index.js +++ b/src/index.js @@ -2,9 +2,7 @@ import { encodeEntities, styleObjToCss, UNSAFE_NAME, - NAMESPACE_REPLACE_REGEX, HTML_LOWER_CASE, - HTML_ENUMERATED, SVG_CAMEL_CASE, createComponent, setDirty, @@ -139,7 +137,7 @@ export async function renderToStringAsync(vnode, context) { if (options[ROOT]) options[ROOT](vnode, { [CHILDREN]: parent, nodeType: 1 }); try { - const rendered = await _renderToString( + let rendered = _renderToString( vnode, context || EMPTY_OBJ, false, @@ -149,6 +147,10 @@ export async function renderToStringAsync(vnode, context) { undefined ); + if (typeof rendered !== 'string' && !isArray(rendered)) { + rendered = await rendered; + } + if (isArray(rendered)) { let count = 0; let resolved = rendered; @@ -381,8 +383,8 @@ function _renderToString( cctx = provider ? provider.props.value : contextType.__; } - let isClassComponent = - type.prototype && typeof type.prototype.render == 'function'; + let prototype = type.prototype; + let isClassComponent = prototype && typeof prototype.render == 'function'; if (isClassComponent) { rendered = /**#__NOINLINE__**/ renderClassComponent(vnode, cctx); component = vnode[COMPONENT]; @@ -661,12 +663,19 @@ function _renderToString( break; default: { + let namespaceLength; if (UNSAFE_NAME.test(name)) { continue; - } else if (NAMESPACE_REPLACE_REGEX.test(name)) { - name = name.replace(NAMESPACE_REPLACE_REGEX, '$1:$2').toLowerCase(); } else if ( - (name[4] === '-' || HTML_ENUMERATED.has(name)) && + name[0] === 'x' && + (namespaceLength = getNamespaceLength(name)) !== 0 + ) { + name = + name.slice(0, namespaceLength) + + ':' + + name.slice(namespaceLength).toLowerCase(); + } else if ( + (name[4] === '-' || name === 'draggable' || name === 'spellcheck') && v != null ) { // serialize boolean aria-xyz or enumerated attribute values as strings @@ -711,6 +720,8 @@ function _renderToString( } else if (typeof children === 'string') { // single text child html = encodeEntities(children); + } else if (typeof children === 'number') { + html = children + EMPTY_STR; } else if (children != null && children !== false && children !== true) { // recurse into this element VNode's children let childSvgMode = @@ -774,3 +785,17 @@ function isSignal(x) { 'value' in x ); } + +function getNamespaceLength(name) { + let length = name[1] === 'l' || name[3] === 'n' ? 5 : 3; + let char = name.charCodeAt(length); + return char >= 65 && + char <= 90 && + (length === 3 + ? name.slice(0, 3) === 'xml' + : name[1] === 'l' + ? name.slice(0, 5) === 'xlink' + : name.slice(0, 5) === 'xmlns') + ? length + : 0; +} diff --git a/src/lib/chunked.js b/src/lib/chunked.js index b5ec6c6..9e3e1d3 100644 --- a/src/lib/chunked.js +++ b/src/lib/chunked.js @@ -59,13 +59,10 @@ function getDocumentClosingTagsIndex(html) { } async function forkPromises(renderer) { - if (renderer.suspended.length > 0) { - const suspensions = [...renderer.suspended]; + while (renderer.suspended.length > 0) { + const length = renderer.suspended.length; await Promise.all(renderer.suspended.map((s) => s.promise)); - renderer.suspended = renderer.suspended.filter( - (s) => !suspensions.includes(s) - ); - await forkPromises(renderer); + renderer.suspended.splice(0, length); } } @@ -85,16 +82,8 @@ function handleError(error, vnode, renderChild) { 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( + let promise = error.then( () => { if (abortSignal && abortSignal.aborted) return; const child = renderChild(vnode.props.children, vnode); @@ -104,11 +93,18 @@ function handleError(error, vnode, renderChild) { // to attempt to recover during hydration this.onError ); + if (abortSignal) { + const race = new Deferred(); + // @ts-ignore 2554 - implicit undefined arg + if (abortSignal.aborted) race.resolve(); + else abortSignal.addEventListener('abort', race.resolve); + promise = Promise.race([promise, race.promise]); + } this.suspended.push({ id, vnode, - promise: Promise.race([promise, race.promise]) + promise }); const fallback = renderChild(vnode.props.fallback); diff --git a/src/lib/util.js b/src/lib/util.js index 933c392..d2bee87 100644 --- a/src/lib/util.js +++ b/src/lib/util.js @@ -37,16 +37,25 @@ export function isDirty(component) { return component[DIRTY] === true; } -// DOM properties that should NOT have "px" added when numeric -const ENCODED_ENTITIES = /["&<]/; - /** @param {string} str */ export 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 i = 0; + if (str.length < 8) { + for (; i < str.length; i++) { + const char = str.charCodeAt(i); + if (char === 34 || char === 38 || char === 60) break; + } + if (i === str.length) return str; + } else if ( + str.indexOf('"') === -1 && + str.indexOf('&') === -1 && + str.indexOf('<') === -1 + ) { + return str; + } let last = 0, - i = 0, out = '', ch = ''; diff --git a/src/pretty.js b/src/pretty.js index dc2aa2a..19fcf17 100644 --- a/src/pretty.js +++ b/src/pretty.js @@ -7,7 +7,6 @@ import { createComponent, UNSAFE_NAME, VOID_ELEMENTS, - NAMESPACE_REPLACE_REGEX, SVG_CAMEL_CASE, HTML_ENUMERATED, HTML_LOWER_CASE, @@ -154,7 +153,6 @@ function _renderToStringPretty( let rendered; let c = (vnode.__c = createComponent(vnode, context)); - let renderHook = options[RENDER]; if ( @@ -255,7 +253,8 @@ function _renderToStringPretty( for (let i = 0; i < attrs.length; i++) { let name = attrs[i], - v = props[name]; + v = props[name], + namespaceLength; if (name === 'children') { propChildren = v; continue; @@ -285,8 +284,14 @@ function _renderToStringPretty( name = 'accept-charset'; } else if (name === 'httpEquiv') { name = 'http-equiv'; - } else if (NAMESPACE_REPLACE_REGEX.test(name)) { - name = name.replace(NAMESPACE_REPLACE_REGEX, '$1:$2').toLowerCase(); + } else if ( + name[0] === 'x' && + (namespaceLength = getNamespaceLength(name)) !== 0 + ) { + name = + name.slice(0, namespaceLength) + + ':' + + name.slice(namespaceLength).toLowerCase(); } else if ( (name.at(4) === '-' || HTML_ENUMERATED.has(name)) && v != null @@ -461,6 +466,20 @@ function _renderToStringPretty( return s; } +function getNamespaceLength(name) { + let length = name[1] === 'l' || name[3] === 'n' ? 5 : 3; + let char = name.charCodeAt(length); + return char >= 65 && + char <= 90 && + (length === 3 + ? name.slice(0, 3) === 'xml' + : name[1] === 'l' + ? name.slice(0, 5) === 'xlink' + : name.slice(0, 5) === 'xmlns') + ? length + : 0; +} + function getComponentName(component) { return ( component.displayName ||