cohost/serverside/name-color.ts

105 lines
3.2 KiB
TypeScript
Raw Normal View History

2024-07-10 15:41:21 -04:00
import {
DOMParser,
} from "https://deno.land/x/deno_dom@v0.1.38/deno-dom-wasm.ts";
2024-07-11 01:12:25 -04:00
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 stylesheet.rules.length == 1 &&
stylesheet.rules[0].declarations.length == 1;
} catch {
return false;
}
}
2024-07-10 15:41:21 -04:00
// 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
2024-07-11 01:17:02 -04:00
const MAX_COLOR_LEN = 512;
2024-07-10 15:41:21 -04:00
const colors: Record<
string,
{ color: string | undefined; last_checked: Date }
> = {};
const dom_parser = new DOMParser();
async function get_name_color(name: string): Promise<string | undefined> {
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;
}
for (const p of dom.querySelectorAll("p")) {
const text = p.textContent;
if (text.startsWith("name-color: ")) {
2024-07-11 01:12:25 -04:00
const color = text.split("name-color: ")[1]
2024-07-10 16:03:16 -04:00
.slice(0, MAX_COLOR_LEN)
.replaceAll(/[;{}"']/g, "");
2024-07-11 01:12:25 -04:00
if (is_valid_color(color)) {
return color;
} else {
console.log(`invalid color!!!! ${color}`);
}
2024-07-10 15:41:21 -04:00
}
}
}
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 };
}
2024-07-10 16:03:16 -04:00
function cssify({ requester }: { requester?: string }): string {
2024-07-10 17:15:21 -04:00
const preamble = `/* customized: ${
2024-07-11 01:21:42 -04:00
Object.keys(colors)
2024-07-11 01:29:24 -04:00
.filter((name) => colors[name].color)
.sort()
2024-07-10 17:15:21 -04:00
.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`;
2024-07-10 16:03:16 -04:00
let vars = `:root {\n`;
if (requester && colors[requester]) {
vars += `--name-color: var(--name-color-${requester});\n`;
}
2024-07-10 17:15:21 -04:00
let classes = "";
2024-07-11 01:21:42 -04:00
for (const name of Object.keys(colors).sort()) {
const { color } = colors[name];
2024-07-10 16:03:16 -04:00
if (!color) continue;
vars += `--name-color-${name}: ${color};\n`;
2024-07-10 17:15:21 -04:00
classes +=
2024-07-10 19:50:07 -04:00
`a[href$='/${name}']{color: var(--name-color-${name}) !important;}\n`;
2024-07-10 16:03:16 -04:00
}
2024-07-10 17:15:21 -04:00
vars += "}\n\n";
return preamble + vars + classes;
2024-07-10 16:03:16 -04:00
}
2024-07-10 15:41:21 -04:00
Deno.serve({ port: 61266 }, async (req) => {
const url = new URL(req.url);
const names = url.searchParams.getAll("name").filter((name) =>
!name.match(/\/\?/)
);
await Promise.all(names.map(update_name_color));
2024-07-10 16:03:16 -04:00
const css = cssify({
requester: url.searchParams.get("requester") ?? undefined,
});
return new Response(css, { headers: { "content-type": "text/css" } });
2024-07-10 15:41:21 -04:00
});