brf/server/lib/html.ts
2025-12-13 21:12:08 +01:00

81 lines
2.4 KiB
TypeScript

import { Readable } from 'node:stream'
type ExpressionPrimitives = string | number | Date | boolean | undefined | null | ExpressionPrimitives[]
type Expression = ExpressionPrimitives | (() => ExpressionPrimitives) | Promise<Expression>
type ParseGenerator = Generator<ExpressionPrimitives | Promise<ExpressionPrimitives>, void>
export interface Template {
generator: (minify?: boolean) => ParseGenerator
stream: (minify?: boolean) => Readable
text: (minify?: boolean) => string | Promise<string>
}
const cache = new WeakMap()
function apply(expression: Expression): ExpressionPrimitives | Promise<ExpressionPrimitives> {
if (expression == null) {
return ''
} else if (expression instanceof Promise) {
return expression.then((resolved) => apply(resolved))
} else if (typeof expression === 'function') {
return apply(expression())
} else if (Array.isArray(expression)) {
return expression.join('')
}
return expression
}
function* parse(strings: TemplateStringsArray, expressions: Expression[], minify?: boolean): ParseGenerator {
if (minify) {
let minified = cache.get(strings)
if (!minified) {
minified = strings.map(removeWhitespace)
cache.set(strings, minified)
}
strings = minified
}
for (let i = 0; i < strings.length; i++) {
yield strings[i]
const expression = expressions[i]
yield apply(expression)
}
}
export default function html(strings: TemplateStringsArray, ...expressions: Expression[]): Template {
return {
generator: (minify?: boolean) => parse(strings, expressions, minify),
stream: (minify?: boolean) => stream(parse(strings, expressions, minify)),
text: (minify?: boolean) => text(parse(strings, expressions, minify)),
}
}
html.generator = (strings: TemplateStringsArray, ...expressions: Expression[]) => parse(strings, expressions)
html.stream = (strings: TemplateStringsArray, ...expressions: Expression[]) => stream(parse(strings, expressions))
html.text = (strings: TemplateStringsArray, ...expressions: Expression[]) => text(parse(strings, expressions))
function stream(generator: ParseGenerator) {
return Readable.from(generator)
}
async function text(generator: ParseGenerator) {
let result = ''
for (const chunk of generator) {
result += chunk instanceof Promise ? await chunk : chunk
}
return result
}
function removeWhitespace(str: string) {
return str.split(/\n\s*/).join('')
}