Init update-signal-symbols script
This commit is contained in:
parent
79004c7727
commit
92012b3546
@ -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"
|
||||
|
||||
323
scripts/update-signal-symbols.mjs
Normal file
323
scripts/update-signal-symbols.mjs
Normal 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()]);
|
||||
Loading…
Reference in New Issue
Block a user