Created
June 21, 2026 04:55
-
-
Save dominikwilkowski/ff227f0f18d82e6450090f6957d2b875 to your computer and use it in GitHub Desktop.
cfonts_font_json_to_rust.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env node | |
| "use strict"; | |
| const fs = require("node:fs"); | |
| const path = require("node:path"); | |
| const DEFAULT_ORDER = Array.from("ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789" + "!?.+-_=@#$%&()/:;,'\" "); | |
| function fail(message) { | |
| console.error(`json-to-rust-font: ${message}`); | |
| process.exit(1); | |
| } | |
| function usage() { | |
| console.log(`Usage: | |
| node json-to-rust-font.js <font.json> [font.rs] | |
| Examples: | |
| node json-to-rust-font.js block.json > block.rs | |
| node json-to-rust-font.js block.json src/fonts/block.rs`); | |
| } | |
| function readJson(file) { | |
| try { | |
| return JSON.parse(fs.readFileSync(file, "utf8")); | |
| } catch (error) { | |
| fail(`could not read/parse ${file}: ${error.message}`); | |
| } | |
| } | |
| function requiredString(value, name) { | |
| if (typeof value !== "string") fail(`${name} must be a string`); | |
| return value; | |
| } | |
| function requiredInt(value, name) { | |
| if (!Number.isInteger(value) || value < 0) fail(`${name} must be a non-negative integer`); | |
| return value; | |
| } | |
| function requiredPositiveInt(value, name) { | |
| if (!Number.isInteger(value) || value <= 0) fail(`${name} must be a positive integer`); | |
| return value; | |
| } | |
| function requiredStringArray(value, name, length) { | |
| if (!Array.isArray(value)) fail(`${name} must be an array`); | |
| if (length !== undefined && value.length !== length) { | |
| fail(`${name} must contain exactly ${length} lines, found ${value.length}`); | |
| } | |
| for (let index = 0; index < value.length; index += 1) { | |
| if (typeof value[index] !== "string") fail(`${name}[${index}] must be a string`); | |
| } | |
| return value; | |
| } | |
| function getGlyphs(font) { | |
| const glyphs = font.chars ?? font.glyphs; | |
| if (!glyphs || typeof glyphs !== "object" || Array.isArray(glyphs)) { | |
| fail("font must contain a chars or glyphs object"); | |
| } | |
| return glyphs; | |
| } | |
| function validateFont(font) { | |
| const lines = font.lines ?? font.buffer?.length; | |
| const validated = { | |
| name: requiredString(font.name, "name"), | |
| version: requiredString(font.version, "version"), | |
| homepage: requiredString(font.homepage, "homepage"), | |
| colors: requiredInt(font.colors, "colors"), | |
| lines: requiredPositiveInt(lines, "lines"), | |
| letterspaceSize: requiredInt(font.letterspace_size, "letterspace_size"), | |
| buffer: null, | |
| letterspace: null, | |
| glyphs: getGlyphs(font), | |
| }; | |
| validated.buffer = requiredStringArray(font.buffer, "buffer", validated.lines); | |
| validated.letterspace = requiredStringArray(font.letterspace, "letterspace", validated.lines); | |
| for (const [char, glyph] of Object.entries(validated.glyphs)) { | |
| if (Array.from(char).length !== 1) fail(`glyph key ${JSON.stringify(char)} must be one character`); | |
| if (char.codePointAt(0) > 127) fail(`glyph key ${JSON.stringify(char)} is not ASCII; table has length 128`); | |
| requiredStringArray(glyph, `chars[${JSON.stringify(char)}]`, validated.lines); | |
| } | |
| return validated; | |
| } | |
| function rustRawString(value) { | |
| let hashes = ""; | |
| while (value.includes(`"${hashes}`)) hashes += "#"; | |
| return `r${hashes}"${value}"${hashes}`; | |
| } | |
| function rustCharLiteral(char) { | |
| switch (char) { | |
| case "\0": | |
| return `'\\0'`; | |
| case "\t": | |
| return `'\\t'`; | |
| case "\n": | |
| return `'\\n'`; | |
| case "\r": | |
| return `'\\r'`; | |
| case "'": | |
| return `'\\''`; | |
| case "\\": | |
| return `'\\\\'`; | |
| default: { | |
| const code = char.codePointAt(0); | |
| if (code < 0x20 || code === 0x7f) return `'\\u{${code.toString(16)}}'`; | |
| return `'${char}'`; | |
| } | |
| } | |
| } | |
| function constName(name) { | |
| const cleaned = name | |
| .trim() | |
| .replace(/[^A-Za-z0-9]+/g, "_") | |
| .replace(/^_+|_+$/g, "") | |
| .toUpperCase(); | |
| return `FONT_${cleaned || "UNKNOWN"}`; | |
| } | |
| function fnPrefix(name) { | |
| let cleaned = name | |
| .trim() | |
| .replace(/[^A-Za-z0-9]+/g, "_") | |
| .replace(/^_+|_+$/g, "") | |
| .toLowerCase(); | |
| if (!cleaned) cleaned = "font"; | |
| if (!/^[a-z_]/.test(cleaned)) cleaned = `font_${cleaned}`; | |
| return cleaned; | |
| } | |
| function orderedGlyphEntries(glyphs) { | |
| const preferred = new Map(DEFAULT_ORDER.map((char, index) => [char, index])); | |
| return Object.entries(glyphs).sort(([left], [right]) => { | |
| const leftRank = preferred.has(left) ? preferred.get(left) : Number.POSITIVE_INFINITY; | |
| const rightRank = preferred.has(right) ? preferred.get(right) : Number.POSITIVE_INFINITY; | |
| if (leftRank !== rightRank) return leftRank - rightRank; | |
| return left.codePointAt(0) - right.codePointAt(0); | |
| }); | |
| } | |
| function emitStringArray(items, indent) { | |
| return items.map((item) => `${indent}${rustRawString(item)},`).join("\n"); | |
| } | |
| function emitGlyph(char, lines) { | |
| return [`\t\ttable[${rustCharLiteral(char)} as usize] = Some(&[`, emitStringArray(lines, "\t\t\t"), "\t\t]);"].join("\n"); | |
| } | |
| function emitRust(font) { | |
| const constant = constName(font.name); | |
| const testPrefix = fnPrefix(font.name); | |
| const glyphs = orderedGlyphEntries(font.glyphs) | |
| .map(([char, lines]) => emitGlyph(char, lines)) | |
| .join("\n"); | |
| return `use crate::fonts::Font; | |
| pub const ${constant}: Font<${font.lines}> = Font { | |
| \tname: "${font.name}", | |
| \tversion: "2.0.0", | |
| \t#[rustfmt::skip] | |
| \tbuffer: [ | |
| ${emitStringArray(font.buffer, "\t\t")} | |
| \t], | |
| \t#[rustfmt::skip] | |
| \tletterspace: [ | |
| ${emitStringArray(font.letterspace, "\t\t")} | |
| \t], | |
| \tletterspace_size: ${font.letterspaceSize}, | |
| \tcolors: ${font.colors}, | |
| \thomepage: "${font.homepage}", | |
| \t#[rustfmt::skip] | |
| \tglyphs: { | |
| \t\tlet mut table = [None; 128]; | |
| ${glyphs} | |
| \t\ttable | |
| \t}, | |
| }; | |
| #[cfg(test)] | |
| mod tests { | |
| \tuse crate::fonts::assert_supported; | |
| \t#[test] | |
| \tfn ${testPrefix}_test_all_supported_glyphs_defined() { | |
| \t\tassert_supported(&super::${constant}); | |
| \t} | |
| } | |
| `; | |
| } | |
| function main() { | |
| const args = process.argv.slice(2); | |
| if (args.includes("-h") || args.includes("--help")) { | |
| usage(); | |
| return; | |
| } | |
| if (args.length < 1 || args.length > 2) { | |
| usage(); | |
| process.exit(args.length === 0 ? 0 : 1); | |
| } | |
| const input = args[0]; | |
| const output = args[1]; | |
| const font = validateFont(readJson(input)); | |
| const rust = emitRust(font); | |
| if (output) { | |
| fs.mkdirSync(path.dirname(output), { recursive: true }); | |
| fs.writeFileSync(output, rust); | |
| } else { | |
| process.stdout.write(rust); | |
| } | |
| } | |
| main(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment