Init update-signal-symbols script

This commit is contained in:
Jamie 2026-06-24 14:58:51 -07:00 committed by GitHub
parent 79004c7727
commit 92012b3546
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 325 additions and 1 deletions

View File

@ -112,7 +112,8 @@
"electron:install-app-deps": "electron-builder install-app-deps",
"check-upgradeable-deps": "node scripts/check-upgradeable-deps.mjs",
"react-devtools": "react-devtools",
"run-with-devtools": "cross-env REACT_DEVTOOLS=1 run-p --print-label react-devtools start"
"run-with-devtools": "cross-env REACT_DEVTOOLS=1 run-p --print-label react-devtools start",
"update-signal-symbols": "node scripts/update-signal-symbols.mjs"
},
"optionalDependencies": {
"fs-xattr": "0.4.0"

View File

@ -0,0 +1,323 @@
// Copyright 2026 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { readFile, copyFile, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { assert } from './utils/assert.mjs';
const DESIGN_ASSETS_PATH = process.env.DESIGN_ASSETS_PATH;
assert(DESIGN_ASSETS_PATH != null, 'Missing DESIGN_ASSETS_PATH');
const DESKTOP_ROOT_DIR = join(import.meta.dirname, '..');
const DESIGN_ROOT_DIR = join(DESKTOP_ROOT_DIR, DESIGN_ASSETS_PATH);
const DESIGN_SYMBOLS_FONT_PATH = join(
DESIGN_ROOT_DIR,
'Signal Symbols',
'Font',
'SignalSymbolsVariable.woff2'
);
const DESKTOP_SYMBOLS_FONT_PATH = join(
DESKTOP_ROOT_DIR,
'fonts',
'signal-symbols',
'SignalSymbolsVariable.woff2'
);
const DESIGN_SYMBOLS_JSON_PATH = join(
DESIGN_ROOT_DIR,
'Signal Symbols',
'Font',
'symbols.json'
);
const DESKTOP_SYMBOLS_DEFS_PATH = join(
DESKTOP_ROOT_DIR,
'ts',
'axo',
'_internal',
'AxoSymbolDefs.generated.std.ts'
);
/**
* @typedef {Readonly<{ name: string; unicode: string }>} Glyph
* @typedef {ReadonlyArray<Glyph>} Glyphs
* @typedef {string | { ltr: string; rtl: string }} AxoSymbolBidi
* @typedef {{ icon: AxoSymbolBidi | null; inline: AxoSymbolBidi | null }} AxoSymbolValue
* @typedef {Readonly<{ name: string; value: AxoSymbolValue }>} AxoSymbol
*/
async function copyVariableFontFile() {
console.log('Copying variable font file...');
await copyFile(DESIGN_SYMBOLS_FONT_PATH, DESKTOP_SYMBOLS_FONT_PATH);
}
async function generateAxoSymbolDefs() {
console.log('Generating axo symbol defs...');
const text = await readFile(DESIGN_SYMBOLS_JSON_PATH, 'utf-8');
/** @type {Glyphs} */
const glyphs = JSON.parse(text);
/** @type {Map<string, Glyph>} */
const glyphsByName = new Map();
for (const glyph of glyphs) {
glyphsByName.set(glyph.name, glyph);
}
/**
* @param {string} name
* @returns {boolean}
*/
function isLeftOnly(name) {
return name.includes('left') && !name.includes('right');
}
/**
* @param {string} name
* @returns {boolean}
*/
function isRightOnly(name) {
return name.includes('right') && !name.includes('left');
}
/** @type {Array<AxoSymbol>} */
const symbols = [];
/**
* @param {string} name
* @returns {string}
*/
function stripInline(name) {
return name.replace(/-inline$/, '');
}
/**
* @param {Glyph} glyph
* @returns {string}
*/
function getSymbolName(glyph) {
const base = stripInline(glyph.name);
if (isLeftOnly(glyph.name)) {
return base.replace('left', '[start]');
}
if (isRightOnly(glyph.name)) {
return base.replace('right', '[end]');
}
return base;
}
/**
* @param {Glyph} glyph
* @returns {AxoSymbolBidi}
*/
function getSymbolBidi(glyph) {
/** @type {string | null} */
let otherName = null;
if (isLeftOnly(glyph.name)) {
otherName = glyph.name.replace('left', 'right');
}
if (isRightOnly(glyph.name)) {
otherName = glyph.name.replace('right', 'left');
}
if (otherName != null) {
const other = glyphsByName.get(otherName);
if (other == null) {
throw new Error(`Missing glyph ${otherName} for ${glyph.name}`);
}
return { ltr: glyph.unicode, rtl: other.unicode };
}
return glyph.unicode;
}
/** @type {Set<string>} */
const seen = new Set();
/**
* @param {Glyph} glyph
* @returns {AxoSymbolValue}
*/
function getSymbolValue(glyph) {
/** @type {Glyph | null} */
let icon;
/** @type {Glyph | null} */
let inline;
if (glyph.name.endsWith('-inline')) {
icon = glyphsByName.get(stripInline(glyph.name)) ?? null;
inline = glyph;
} else {
icon = glyph;
inline = glyphsByName.get(`${glyph.name}-inline`) ?? null;
}
if (icon != null) {
seen.add(icon.name);
}
if (inline != null) {
seen.add(inline.name);
}
return {
icon: icon != null ? getSymbolBidi(icon) : null,
inline: inline != null ? getSymbolBidi(inline) : null,
};
}
for (const glyph of glyphs) {
if (seen.has(glyph.name)) {
continue;
}
symbols.push({
name: getSymbolName(glyph),
value: getSymbolValue(glyph),
});
}
let res = '';
res += '// Copyright 2025 Signal Messenger, LLC\n';
res += '// SPDX-License-Identifier: AGPL-3.0-only\n';
res += '\n';
res +=
'// WARNING: This file is automatically generated, do not edit directly\n';
{
res += '\n';
res += '// Can be used in <AxoSymbol.Icon>\n';
res += 'export type AxoSymbolIconName =\n';
const lines = [];
for (const symbol of symbols) {
if (symbol.value.icon != null) {
lines.push(` | '${symbol.name}'`);
}
}
res += lines.join('\n');
res += ';\n';
}
{
res += '\n';
res += '// Symbols that can only be used in <AxoSymbol.InlineGlyph>\n';
res += 'type AxoSymbolInlineGlyphOnlyName =\n';
const lines = [];
for (const symbol of symbols) {
if (symbol.value.icon == null && symbol.value.inline != null) {
lines.push(` | '${symbol.name}'`);
}
}
res += lines.join('\n');
res += ';\n';
}
{
res += '\n';
res +=
'// Symbols with an inline-specific glyph that override the icon glyph\n';
res += 'type AxoSymbolInlineGlyphOverrideName =\n';
const lines = [];
for (const symbol of symbols) {
if (symbol.value.icon != null && symbol.value.inline != null) {
lines.push(` | '${symbol.name}'`);
}
}
res += lines.join('\n');
res += ';\n';
}
res += '\n';
res += '// Symbols that can be used in <AxoSymbol.InlineGlyph>\n';
res += 'export type AxoSymbolInlineGlyphName =\n';
res += ' | AxoSymbolIconName\n';
res += ' | AxoSymbolInlineGlyphOnlyName;\n';
res += '\n';
res += 'type SymbolDef = string | { ltr: string; rtl: string };\n';
res += 'type IconDefsName = AxoSymbolIconName;\n';
res += 'type InlineDefsName =\n';
res += ' | AxoSymbolInlineGlyphOnlyName\n';
res += ' | AxoSymbolInlineGlyphOverrideName;\n';
res += '\n';
res += 'const IconDefs: Record<string, SymbolDef> = {\n';
for (const symbol of symbols) {
if (symbol.value.icon != null) {
res += ' ';
if (/^[a-z_]+$/.test(symbol.name)) {
res += symbol.name;
} else {
res += `'${symbol.name}'`;
}
res += ': ';
if (typeof symbol.value.icon === 'string') {
res += `'\\u{${symbol.value.icon}}'`;
} else {
res += '{ ';
res += `ltr: '\\u{${symbol.value.icon.ltr}}', `;
res += `rtl: '\\u{${symbol.value.icon.rtl}}'`;
res += ' }';
}
res += ',\n';
}
}
res += '} satisfies Record<IconDefsName, SymbolDef>;\n';
res += '\n';
res += 'const InlineDefs: Record<string, SymbolDef> = {\n';
for (const symbol of symbols) {
if (symbol.value.inline != null) {
res += ' ';
if (/^[a-z_]+$/.test(symbol.name)) {
res += symbol.name;
} else {
res += `'${symbol.name}'`;
}
res += ': ';
if (typeof symbol.value.inline === 'string') {
res += `'\\u{${symbol.value.inline}}'`;
} else {
res += '{ ';
res += `ltr: '\\u{${symbol.value.inline.ltr}}', `;
res += `rtl: '\\u{${symbol.value.inline.rtl}}'`;
res += ' }';
}
res += ',\n';
}
}
res += '} satisfies Record<InlineDefsName, SymbolDef>;\n';
res += '\n';
res += '/** @testexport */\n';
res +=
'export function _getAllAxoSymbolIconNames(): ReadonlyArray<AxoSymbolIconName> {\n';
res += ' return Object.keys(IconDefs) as Array<AxoSymbolIconName>;\n';
res += '}\n';
res += '\n';
res += '/** @testexport */\n';
res +=
'export function _getAllAxoSymbolInlineGlyphNames(): ReadonlyArray<AxoSymbolInlineGlyphName> {\n';
res += ' return Object.keys(IconDefs) as Array<AxoSymbolIconName>;\n';
res += '}\n';
res += '\n';
res += 'export function getAxoSymbolIcon(\n';
res += ' name: AxoSymbolIconName,\n';
res += " dir: 'ltr' | 'rtl'\n";
res += '): string {\n';
res += ' const value = IconDefs[name];\n';
res += ' if (value == null) {\n';
res +=
// eslint-disable-next-line no-template-curly-in-string
' throw new TypeError(`Invalid symbol name for icon: ${name}`);\n';
res += ' }\n';
res += " return typeof value === 'string' ? value : value[dir];\n";
res += '}\n';
res += '\n';
res += 'export function getAxoSymbolInlineGlyph(\n';
res += ' name: AxoSymbolInlineGlyphName,\n';
res += " dir: 'ltr' | 'rtl'\n";
res += '): string {\n';
res += ' const value = InlineDefs[name] ?? IconDefs[name];\n';
res += ' if (value == null) {\n';
res +=
// eslint-disable-next-line no-template-curly-in-string
' throw new TypeError(`Invalid symbol name for inline glyph: ${name}`);\n';
res += ' }\n';
res += " return typeof value === 'string' ? value : value[dir];\n";
res += '}\n';
await writeFile(DESKTOP_SYMBOLS_DEFS_PATH, res, 'utf-8');
}
await Promise.all([copyVariableFontFile(), generateAxoSymbolDefs()]);