2023-11-21 09:05:05 +00:00
|
|
|
type HtmlEscapedCallbackOpts = { error?: Error; buffer?: [string] }
|
|
|
|
export type HtmlEscapedCallback = (opts: HtmlEscapedCallbackOpts) => Promise<string>
|
|
|
|
export type HtmlEscaped = {
|
|
|
|
isEscaped: true
|
|
|
|
callbacks?: HtmlEscapedCallback[]
|
|
|
|
}
|
2022-08-03 02:24:51 +00:00
|
|
|
export type HtmlEscapedString = string & HtmlEscaped
|
2023-11-06 22:09:04 +00:00
|
|
|
export type StringBuffer = (string | Promise<string>)[]
|
2023-11-09 05:37:47 +00:00
|
|
|
import { raw } from '../helper/html/index.ts'
|
2022-08-03 02:24:51 +00:00
|
|
|
|
2022-08-03 02:29:33 +00:00
|
|
|
// The `escapeToBuffer` implementation is based on code from the MIT licensed `react-dom` package.
|
2023-08-15 22:39:35 +00:00
|
|
|
// https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/server/escapeTextForBrowser.js
|
2022-08-03 02:24:51 +00:00
|
|
|
|
2023-07-26 14:12:25 +00:00
|
|
|
const escapeRe = /[&<>'"]/
|
2022-07-02 06:09:45 +00:00
|
|
|
|
2023-11-06 22:09:04 +00:00
|
|
|
export const stringBufferToString = async (buffer: StringBuffer): Promise<HtmlEscapedString> => {
|
|
|
|
let str = ''
|
2023-11-21 09:05:05 +00:00
|
|
|
const callbacks: HtmlEscapedCallback[] = []
|
2023-11-06 22:09:04 +00:00
|
|
|
for (let i = buffer.length - 1; i >= 0; i--) {
|
|
|
|
let r = await buffer[i]
|
|
|
|
if (typeof r === 'object') {
|
2023-11-21 09:05:05 +00:00
|
|
|
callbacks.push(...((r as HtmlEscapedString).callbacks || []))
|
2023-11-06 22:09:04 +00:00
|
|
|
}
|
|
|
|
r = await (typeof r === 'object' ? (r as HtmlEscapedString).toString() : r)
|
|
|
|
if (typeof r === 'object') {
|
2023-11-21 09:05:05 +00:00
|
|
|
callbacks.push(...((r as HtmlEscapedString).callbacks || []))
|
2023-11-06 22:09:04 +00:00
|
|
|
}
|
|
|
|
str += r
|
|
|
|
}
|
|
|
|
|
2023-11-21 09:05:05 +00:00
|
|
|
return raw(str, callbacks)
|
2023-11-06 22:09:04 +00:00
|
|
|
}
|
|
|
|
|
2022-08-03 02:24:51 +00:00
|
|
|
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
|
2023-07-26 14:12:25 +00:00
|
|
|
case 39: // '
|
|
|
|
escape = '''
|
|
|
|
break
|
2022-08-03 02:24:51 +00:00
|
|
|
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)
|
2022-07-02 06:09:45 +00:00
|
|
|
}
|
2023-11-21 09:05:05 +00:00
|
|
|
|
|
|
|
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])
|
|
|
|
)
|
|
|
|
}
|