import { DOMParser, } from "https://deno.land/x/deno_dom@v0.1.46/deno-dom-wasm.ts"; import * as css from "https://deno.land/x/css@0.3.0/mod.ts"; // so, so brittle function is_valid_color(color: string): boolean { try { const { stylesheet } = css.parse(`*{color:${color};}`); return color.toLowerCase() != "example" && stylesheet.rules.length == 1 && stylesheet.rules[0].declarations.length == 1; } catch { return false; } } // maybe a bit high, maybe a bit low const MAX_AGE_MS = 10 * 60 * 1000; // > "light-dark(oklab(000, 000, 000), oklab(255, 255, 255))".length // 54 const MAX_COLOR_LEN = 512; const colors: Record< string, { color: string | undefined; last_checked: Date } > = {}; const dom_parser = new DOMParser(); function parse_color(str: string): string | undefined { const text = str.replace(/^#/, ""); if (text.startsWith("name-color:")) { const line = text.split("\n", 1)[0]; const color = line.split("name-color:")[1] .trim() .slice(0, MAX_COLOR_LEN) .replaceAll(/[;{}"']|\/\*|\*\//g, ""); if (is_valid_color(color)) { return color; } else { console.log(`invalid color!!!! ${color}`); } } } async function get_name_color(name: string): Promise { console.log(`fetching ${name}...`); const resp = await fetch(`https://cohost.org/${name}`); if (!resp.ok) { console.log(`fetching https://cohost.org/${name} failed!`); return; } const body = await resp.text(); const dom = dom_parser.parseFromString(body, "text/html"); if (!dom) { console.log(`parsing https://cohost.org/${name} to the dom failed!`); return; } const a = dom.querySelector("a[href*='name-color:']"); if (a) { const href = a.getAttribute("href"); if (href) { const color = parse_color(href); if (color) return color; } } for (const p of dom.querySelectorAll("p, a")) { if (!p.textContent.includes("name-color:")) continue; for (const node of p.childNodes) { const color = parse_color(node.textContent); if (color) return color; } } } async function update_name_color(name: string) { const now = new Date(); if ( colors[name] && now.getTime() - colors[name].last_checked.getTime() <= MAX_AGE_MS ) { return; } const color = await get_name_color(name); colors[name] = { color, last_checked: now }; } function cssify({ requester }: { requester?: string }): string { const preamble = `/* customized: ${ Object.keys(colors) .filter((name) => colors[name].color) .sort() .join(", ") } */\n/* look, i don't like !important either, but i think it's a better choice than some lame .spec.ific.ity.hack.ing */\n\n`; let vars = `:root {\n`; if (requester && colors[requester]) { vars += `--name-color: var(--name-color-${requester.toLowerCase()});\n`; } let classes = ""; for (const name of Object.keys(colors).sort()) { const { color } = colors[name]; if (!color) continue; vars += `--name-color-${name.toLowerCase()}: ${color};\n`; classes += `a[href$='/${name}' i]{color: var(--name-color-${name.toLowerCase()}) !important;}\n`; } vars += "}\n\n"; return preamble + vars + classes; } Deno.serve({ port: 61266 }, async (req) => { const url = new URL(req.url); const names = url.searchParams.getAll("name").filter((name) => !name.match(/[\/\\?"';{}]/) ).map((name) => name.toLowerCase()); await Promise.all(names.map(update_name_color)); const css = cssify({ requester: url.searchParams.get("requester") ?? undefined, }); return new Response(css, { headers: { "content-type": "text/css" } }); });