#!/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 [format]\n\nformat enum:\n hex eb99\n codepoint U+EB99\n glyph actual glyph char\n js \\u{EB99}\n html \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 namehexcodepointglyph\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); });