0
0
mirror of https://github.com/honojs/hono.git synced 2024-12-01 11:51:01 +01:00
hono/deno_dist/utils/html.ts
Taku Amano cd6c488b76
feat(jsx): Introduce ErrorBoundary component (#1714)
* feat(jsx/streaming): Support Suspense in non-streaming mode.

* feat(jsx): Introduce ErrorBoundary component.

* chore: denoify

* feat: Support ErrorBoundary[fallbackRender].

* chore: denoify

* Rename utils.ts to components.ts

* refactor: export the ErrorBoundary component from the top level.

* fix: tweaks `resolveStream` to work with nested components

* refactor: Import `childrenToString` from `components.ts`

* fix: return immediately if the element is not found

* test: add test for jsx/components

* fix: run `npm run format:fix`

* chore: denoify
2023-11-21 18:05:05 +09:00

90 lines
2.5 KiB
TypeScript

type HtmlEscapedCallbackOpts = { error?: Error; buffer?: [string] }
export type HtmlEscapedCallback = (opts: HtmlEscapedCallbackOpts) => Promise<string>
export type HtmlEscaped = {
isEscaped: true
callbacks?: HtmlEscapedCallback[]
}
export type HtmlEscapedString = string & HtmlEscaped
export type StringBuffer = (string | Promise<string>)[]
import { raw } from '../helper/html/index.ts'
// The `escapeToBuffer` implementation is based on code from the MIT licensed `react-dom` package.
// https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/server/escapeTextForBrowser.js
const escapeRe = /[&<>'"]/
export const stringBufferToString = async (buffer: StringBuffer): Promise<HtmlEscapedString> => {
let str = ''
const callbacks: HtmlEscapedCallback[] = []
for (let i = buffer.length - 1; i >= 0; i--) {
let r = await buffer[i]
if (typeof r === 'object') {
callbacks.push(...((r as HtmlEscapedString).callbacks || []))
}
r = await (typeof r === 'object' ? (r as HtmlEscapedString).toString() : r)
if (typeof r === 'object') {
callbacks.push(...((r as HtmlEscapedString).callbacks || []))
}
str += r
}
return raw(str, callbacks)
}
export const escapeToBuffer = (str: string, buffer: StringBuffer): void => {
const match = str.search(escapeRe)
if (match === -1) {
buffer[0] += str
return
}
let escape
let index
let lastIndex = 0
for (index = match; index < str.length; index++) {
switch (str.charCodeAt(index)) {
case 34: // "
escape = '&quot;'
break
case 39: // '
escape = '&#39;'
break
case 38: // &
escape = '&amp;'
break
case 60: // <
escape = '&lt;'
break
case 62: // >
escape = '&gt;'
break
default:
continue
}
buffer[0] += str.substring(lastIndex, index) + escape
lastIndex = index + 1
}
buffer[0] += str.substring(lastIndex, index)
}
export const resolveStream = (
str: string | HtmlEscapedString,
buffer?: [string]
): Promise<string> => {
if (!(str as HtmlEscapedString).callbacks?.length) {
return Promise.resolve(str)
}
const callbacks = (str as HtmlEscapedString).callbacks as HtmlEscapedCallback[]
if (buffer) {
buffer[0] += str
} else {
buffer = [str]
}
return Promise.all(callbacks.map((c) => c({ buffer }))).then((res) =>
Promise.all(res.map((str) => resolveStream(str, buffer))).then(() => (buffer as [string])[0])
)
}