Skip to main content
Module

x/htm/mod.tsx

Create a html response with JSX.
Go to Latest
File
/** @jsx h */
import { h, type VNode } from "https://esm.sh/preact@10.7.2";import { renderToString,} from "https://esm.sh/preact-render-to-string@5.2.0?deps=preact@10.7.2";import { type Preset, UnoGenerator, type UserConfig,} from "https://esm.sh/@unocss/core@0.38.1";import presetWind from "https://esm.sh/@unocss/preset-wind@0.38.1?bundle&no-check";
const resetCSS = `/* reset */*,::after,::before{box-sizing:border-box;border-width:0;border-style:solid;border-color:currentColor}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";scrollbar-gutter:stable}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}menu,ol,ul{margin:0;padding:0}textarea{resize:vertical}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}.dark{color-scheme:dark}.dark ::-moz-selection{background:#444}.dark ::selection{background:#444}div{display:flex;flex-direction:column}div[row=""]{display:flex;flex-direction:row}div[block=""]{display:block}`;
let uno = new UnoGenerator({ presets: [presetWind() as unknown as Preset] });
export function configureUnoCSS(config: UserConfig) { uno = new UnoGenerator(config);}
export interface HtmlOptions { lang?: string; dark?: boolean; title?: string; meta?: Record<string, string | null | undefined>; links?: { [key: string]: string; href: string; rel: string }[]; styles?: (string | { href: string; id?: string })[]; scripts?: (string | { src: string; type?: string; id?: string })[]; reset?: boolean | string;}
export interface Options extends HtmlOptions { body: VNode; status?: number; headers?: HeadersInit;}
export async function html(options: Options): Promise<Response> { const { body, status, headers: headersInit, ...rest } = options; const headers = new Headers(headersInit); headers.append("Content-Type", "text/html; charset=utf-8"); const bodyHtml = renderToString(body); const { css } = await uno.generate(bodyHtml, { minify: true }); return new Response( `<!DOCTYPE html>` + renderToString( <Html body={bodyHtml} unocss={{ css, version: uno.version }} {...rest} />, ), { status, headers }, );}
interface HtmlProps extends HtmlOptions { body: string; unocss: { css: string; version: string };}
function Html({ lang, dark, title, meta, links, styles, scripts, body, unocss, reset,}: HtmlProps) { const parsedResetCSS = typeof reset === "string" ? reset : reset === false ? undefined : resetCSS; return ( <html lang={lang ?? "en"} class={dark ? "dark" : undefined}> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> {title && <title>{title}</title>} {meta && Object.entries(meta).filter(([name, content]) => !!name && !!content) .map(([name, content]) => ( name.startsWith("og:") ? <meta property={name} content={String(content)} /> : <meta name={name} content={String(content)} /> ))} {parsedResetCSS && <style dangerouslySetInnerHTML={{ __html: parsedResetCSS }} />} {unocss.css && ( <style data-unocss={unocss.version} dangerouslySetInnerHTML={{ __html: unocss.css }} /> )} {links && links.map(({ rel, href, ...rest }) => ( <link rel={rel} href={href} {...rest} /> ))} {styles && styles.map((style) => ( typeof style === "string" ? <style dangerouslySetInnerHTML={{ __html: style }} /> : <link rel="stylesheet" href={style.href} id={style.id} /> ))} {scripts && scripts.map((script) => ( typeof script === "string" ? <script dangerouslySetInnerHTML={{ __html: script }} /> : ( <script src={script.src} type={script.type} id={script.id}> </script> ) ))} <script dangerouslySetInnerHTML={{ __html: `(()=>{let v=localStorage.getItem("color-scheme"),a=window.matchMedia("(prefers-color-scheme: dark)").matches,cl=document.documentElement.classList,setColorScheme=v=>(!v||v==="auto"?a:v==="dark")?cl.add("dark"):cl.remove("dark");setColorScheme(v);window.setColorScheme=v=>{setColorScheme(v);localStorage.setItem("color-scheme",v)};})();`, }} /> </head> <body dangerouslySetInnerHTML={{ __html: body }} /> </html> );}
export * from "https://esm.sh/preact@10.7.2";