Luigit
repositories / dotfiles

dotfiles

bugabingas dorkfiles

owned by admin

pi/agent/skills/nerdfont-icons/scripts/get-nerdfont-codepoint.js

Raw
#!/usr/bin/env node

import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import zlib from "node:zlib";

const __dirname = path.dirname(fileURLToPath(import.meta.url));

const URL = "https://www.nerdfonts.com/cheat-sheet?q=nf";
const ROOT = path.resolve(__dirname, "..");
const DB = path.join(ROOT, "references", "nerdfont-icons.txt.br");
const FORMATS = new Set([
	"hex",
	"codepoint",
	"glyph",
	"json",
	"all",
	"js",
	"html",
	"java",
	"json-escape",
	"css",
	"css-content",
	"python",
	"rust",
	"go",
	"c",
	"cpp",
	"swift",
	"lua",
	"shell",
	"printf",
	"toml",
	"yaml",
	"vim",
	"elisp",
	"wezterm",
]);

function usage(exitCode = 2) {
	const msg = `usage: node scripts/get-nerdfont-codepoint.js <query> [format]\n\nformat enum:\n  hex          eb99\n  codepoint    U+EB99\n  glyph        actual glyph char\n  js           \\u{EB99}\n  html         &#xEB99;\n  java         \\uEB99 or surrogate pair\n  json-escape  \\uEB99 or surrogate pair\n  css          \\EB99\n  css-content  content: "\\EB99";\n  python       \\U0000EB99\n  rust         \\u{EB99}\n  go           \\U0000EB99\n  c|cpp        \\U0000EB99\n  swift        \\u{EB99}\n  lua          utf8.char(0xEB99)\n  shell        $'\\U0000EB99'\n  printf       printf '\\U0000EB99\\n'\n  toml|yaml    \\U0000EB99\n  vim          nr2char(0xEB99)\n  elisp        (string #xEB99)\n  wezterm      wezterm.nerdfonts.cod_account\n  json         { name, hex, ... }\n  all          name<TAB>hex<TAB>codepoint<TAB>glyph\n\nflags:\n  --refresh    update references/nerdfont-icons.txt\n`;
	(exitCode ? console.error : console.log)(msg);
	process.exit(exitCode);
}

function readDb() {
	if (!fs.existsSync(DB)) return [];
	const raw = fs.readFileSync(DB);
	const text = zlib.brotliDecompressSync(raw).toString("utf8");
	return text
		.split(/\r?\n/)
		.filter(Boolean)
		.map((line) => {
			const [name, hex] = line.split(/\s+/);
			return name && hex ? { name, hex: hex.toLowerCase() } : null;
		})
		.filter(Boolean);
}

async function fetchText(url) {
	const res = await fetch(url, {
		headers: { "user-agent": "pi-nerdfont-icons-skill" },
	});
	if (!res.ok) throw new Error(`fetch failed: ${res.status} ${res.statusText}`);
	return await res.text();
}

async function refreshDb() {
	const html = await fetchText(URL);
	const match = html.match(/const glyphs = \{([\s\S]*?)\n\}\n<\/script>/);
	if (!match) throw new Error("glyph table not found");

	const entries = [
		...match[1].matchAll(/"([^"]+)"\s*:\s*"([0-9a-fA-F]+)"/g),
	].map((m) => [m[1], m[2].toLowerCase()]);
	const lines = entries
		.filter(([name]) => name.startsWith("nf-"))
		.sort(([a], [b]) => a.localeCompare(b))
		.map(([name, hex]) => `${name}\t${hex}`);

	fs.mkdirSync(path.dirname(DB), { recursive: true });
	const next = `${lines.join("\n")}\n`;
	const nextBytes = zlib.brotliCompressSync(next, {
		params: {
			[zlib.constants.BROTLI_PARAM_QUALITY]: 11,
			[zlib.constants.BROTLI_PARAM_MODE]: zlib.constants.BROTLI_MODE_TEXT,
		},
	});
	let prev = null;
	if (fs.existsSync(DB)) {
		const raw = fs.readFileSync(DB);
		prev = zlib.brotliDecompressSync(raw).toString("utf8");
	}
	if (next !== prev) fs.writeFileSync(DB, nextBytes);
	return lines.length;
}

function normalizeName(s) {
	return s.trim().toLowerCase().replace(/\s+/g, "_");
}

function findMatches(db, query) {
	const q = normalizeName(query);
	const variants = new Set([q]);
	if (!q.startsWith("nf-")) {
		variants.add(`nf-${q}`);
		variants.add(`nf-md-${q}`);
		variants.add(`nf-fa-${q}`);
		variants.add(`nf-fae-${q}`);
		variants.add(`nf-cod-${q}`);
		variants.add(`nf-oct-${q}`);
	}

	const exact = db.filter((x) => variants.has(x.name.toLowerCase()));
	if (exact.length) return exact;

	const needle = q.replace(/^nf-[a-z]+-/, "");
	return db.filter((x) => x.name.toLowerCase().includes(needle));
}

function javaEscape(cp) {
	if (cp <= 0xffff)
		return `\\u${cp.toString(16).toUpperCase().padStart(4, "0")}`;
	const n = cp - 0x10000;
	const hi = 0xd800 + (n >> 10);
	const lo = 0xdc00 + (n & 0x3ff);
	return `\\u${hi.toString(16).toUpperCase()}\\u${lo.toString(16).toUpperCase()}`;
}

function eightHex(cp) {
	return cp.toString(16).toUpperCase().padStart(8, "0");
}

function render(item, format) {
	const cp = Number.parseInt(item.hex, 16);
	const hexUpper = item.hex.toUpperCase();
	const glyph = String.fromCodePoint(cp);
	const row = {
		name: item.name,
		hex: item.hex,
		codepoint: `U+${hexUpper}`,
		glyph,
		js: `\\u{${hexUpper}}`,
		html: `&#x${hexUpper};`,
		java: javaEscape(cp),
		jsonEscape: javaEscape(cp),
		css: `\\${hexUpper}`,
		cssContent: `content: "\\${hexUpper}";`,
		python: `\\U${eightHex(cp)}`,
		rust: `\\u{${hexUpper}}`,
		go: `\\U${eightHex(cp)}`,
		c: `\\U${eightHex(cp)}`,
		cpp: `\\U${eightHex(cp)}`,
		swift: `\\u{${hexUpper}}`,
		lua: `utf8.char(0x${hexUpper})`,
		shell: `$'\\U${eightHex(cp)}'`,
		printf: `printf '\\U${eightHex(cp)}\\n'`,
		toml: `\\U${eightHex(cp)}`,
		yaml: `\\U${eightHex(cp)}`,
		vim: `nr2char(0x${hexUpper})`,
		elisp: `(string #x${hexUpper})`,
		wezterm: `wezterm.nerdfonts.${item.name.replace(/^nf-/, "").replace(/-/g, "_")}`,
	};

	switch (format) {
		case "hex":
			return row.hex;
		case "codepoint":
			return row.codepoint;
		case "glyph":
			return row.glyph;
		case "js":
			return row.js;
		case "html":
			return row.html;
		case "java":
			return row.java;
		case "json-escape":
			return row.jsonEscape;
		case "css":
			return row.css;
		case "css-content":
			return row.cssContent;
		case "python":
			return row.python;
		case "rust":
			return row.rust;
		case "go":
			return row.go;
		case "c":
			return row.c;
		case "cpp":
			return row.cpp;
		case "swift":
			return row.swift;
		case "lua":
			return row.lua;
		case "shell":
			return row.shell;
		case "printf":
			return row.printf;
		case "toml":
			return row.toml;
		case "yaml":
			return row.yaml;
		case "vim":
			return row.vim;
		case "elisp":
			return row.elisp;
		case "wezterm":
			return row.wezterm;
		case "json":
			return JSON.stringify(row);
		case "all":
			return `${row.name}\t${row.hex}\t${row.codepoint}\t${row.glyph}`;
		default:
			throw new Error(`bad format: ${format}`);
	}
}

async function main() {
	const args = process.argv.slice(2);
	if (args.includes("-h") || args.includes("--help")) usage(0);

	const refresh = args.includes("--refresh");
	const rest = args.filter((x) => x !== "--refresh");
	const query = rest[0];
	const format = rest[1] ?? "codepoint";

	if (refresh) {
		const count = await refreshDb();
		if (!query) {
			console.error(`updated ${count} icons: ${DB}`);
			return;
		}
	}

	if (!query || !FORMATS.has(format)) usage(2);

	let db = readDb();
	let matches = findMatches(db, query);

	if (matches.length === 0) {
		await refreshDb();
		db = readDb();
		matches = findMatches(db, query);
	}

	if (matches.length === 0) process.exit(1);

	const exact = matches.find(
		(x) => x.name.toLowerCase() === normalizeName(query),
	);
	const out = exact ? [exact] : matches;
	console.log(out.map((x) => render(x, format)).join("\n"));
}

main().catch((err) => {
	console.error(err.message);
	process.exit(1);
});