Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save dominikwilkowski/ff227f0f18d82e6450090f6957d2b875 to your computer and use it in GitHub Desktop.

Select an option

Save dominikwilkowski/ff227f0f18d82e6450090f6957d2b875 to your computer and use it in GitHub Desktop.
cfonts_font_json_to_rust.js
#!/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