type HtmlEscapedCallbackOpts = { error?: Error; buffer?: [string] } export type HtmlEscapedCallback = (opts: HtmlEscapedCallbackOpts) => Promise export type HtmlEscaped = { isEscaped: true callbacks?: HtmlEscapedCallback[] } export type HtmlEscapedString = string & HtmlEscaped /** * StringBuffer contains string and Promise alternately * The length of the array will be odd, the odd numbered element will be a string, * and the even numbered element will be a Promise. * When concatenating into a single string, it must be processed from the tail. * @example * [ * 'framework.', * Promise.resolve('ultra fast'), * 'a ', * Promise.resolve('is '), * 'Hono', * ] */ export type StringBuffer = (string | Promise)[] 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 => { let str = '' const callbacks: HtmlEscapedCallback[] = [] for (let i = buffer.length - 1; ; i--) { str += buffer[i] i-- if (i < 0) { break } let r = await buffer[i] if (typeof r === 'object') { callbacks.push(...((r as HtmlEscapedString).callbacks || [])) } const isEscaped = (r as HtmlEscapedString).isEscaped r = await (typeof r === 'object' ? (r as HtmlEscapedString).toString() : r) if (typeof r === 'object') { callbacks.push(...((r as HtmlEscapedString).callbacks || [])) } if ((r as HtmlEscapedString).isEscaped ?? isEscaped) { str += r } else { const buf = [str] escapeToBuffer(r, buf) str = buf[0] } } 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 = '"' break case 39: // ' escape = ''' break case 38: // & escape = '&' break case 60: // < escape = '<' break case 62: // > escape = '>' 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 => { 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]) ) }