mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-08-28 00:46:33 +01:00
281 lines
7.3 KiB
JavaScript
281 lines
7.3 KiB
JavaScript
// 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 += 'export type _AxoSymbolName =\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 += 'type SymbolDef = string | { ltr: string; rtl: string };\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 += '};\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 += '};\n';
|
|
res += '\n';
|
|
res += '/** @testexport */\n';
|
|
res +=
|
|
'export function _getAllAxoSymbolNames(): ReadonlyArray<_AxoSymbolName> {\n';
|
|
res += ' return Object.keys(IconDefs) as Array<_AxoSymbolName>;\n';
|
|
res += '}\n';
|
|
res += '\n';
|
|
res += 'export function _getAxoSymbolIcon(\n';
|
|
res += ' name: _AxoSymbolName,\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: _AxoSymbolName,\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()]);
|