Merge remote-tracking branch 'upstream/master' into pr/jerch/5178

This commit is contained in:
Daniel Imms
2026-01-03 15:21:50 -08:00
237 changed files with 24997 additions and 8371 deletions
+457
View File
@@ -0,0 +1,457 @@
/**
* Converts an SVG file as exported by fontforge into the SVG-like format as expected by the custom
* glyph rasterizer.
*
* Usage: node convert_svg_to_custom_glyph.js <svg-file-or-folder>
*/
const fs = require('fs');
const path = require('path');
const input = process.argv[2];
if (!input) {
console.error('Usage: node convert_svg_to_custom_glyph.js <svg-file-or-folder>');
process.exit(1);
}
const inputPath = path.resolve(process.cwd(), input);
const stat = fs.statSync(inputPath);
const files = stat.isDirectory()
? fs.readdirSync(inputPath).filter(f => f.endsWith('.svg')).map(f => path.join(inputPath, f))
: [inputPath];
if (files.length === 0) {
console.error('No SVG files found');
process.exit(1);
}
for (const file of files) {
console.log(`\n${'='.repeat(60)}\nProcessing: ${path.basename(file)}\n${'='.repeat(60)}`);
processFile(file);
}
function processFile(filePath) {
// Get file content
const content = fs.readFileSync(filePath, 'utf8');
// Get viewBox
const viewBoxMatch = content.match(/viewBox="([^"]+)"/);
if (!viewBoxMatch) {
console.error('No viewBox found in SVG');
return;
}
const [minX, minY, width, height] = viewBoxMatch[1].split(/\s+/).map(Number);
console.log(`ViewBox: ${minX} ${minY} ${width} ${height}`);
// Get path `d` property
const pathMatch = content.match(/<path[^>]*\sd="([^"]+)"/);
if (!pathMatch) {
console.error('No path d attribute found in SVG');
return;
}
const originalPath = pathMatch[1].replace(/\s+/g, ' ').trim();
console.log(`\nOriginal path length: ${originalPath.length} chars`);
// Parse path into commands
function parsePath(d) {
const commands = [];
const regex = /([MmLlHhVvCcSsQqTtAaZz])([^MmLlHhVvCcSsQqTtAaZz]*)/g;
let match;
while ((match = regex.exec(d)) !== null) {
const cmd = match[1];
const argsStr = match[2].trim();
const args = argsStr ? argsStr.split(/[\s,]+/).map(Number) : [];
commands.push({ cmd, args });
}
return commands;
}
// Convert relative commands to absolute and expand T/S to Q/C
function toAbsolute(commands) {
const result = [];
let x = 0, y = 0; // Current position
let startX = 0, startY = 0; // Start of current subpath
let lastControlX = 0, lastControlY = 0; // Last control point for T/S
let lastCmd = '';
for (const { cmd, args } of commands) {
const isRelative = cmd === cmd.toLowerCase();
const absCmd = cmd.toUpperCase();
switch (absCmd) {
case 'M': {
// MoveTo: M x y (or m dx dy)
const absArgs = [];
for (let i = 0; i < args.length; i += 2) {
const newX = isRelative ? x + args[i] : args[i];
const newY = isRelative ? y + args[i + 1] : args[i + 1];
absArgs.push(newX, newY);
x = newX;
y = newY;
if (i === 0) {
startX = x;
startY = y;
}
}
lastControlX = x;
lastControlY = y;
result.push({ cmd: 'M', args: absArgs });
break;
}
case 'L': {
// LineTo: L x y (or l dx dy)
const absArgs = [];
for (let i = 0; i < args.length; i += 2) {
const newX = isRelative ? x + args[i] : args[i];
const newY = isRelative ? y + args[i + 1] : args[i + 1];
absArgs.push(newX, newY);
x = newX;
y = newY;
}
lastControlX = x;
lastControlY = y;
result.push({ cmd: 'L', args: absArgs });
break;
}
case 'H': {
// Horizontal LineTo - convert to L
for (let i = 0; i < args.length; i++) {
const newX = isRelative ? x + args[i] : args[i];
result.push({ cmd: 'L', args: [newX, y] });
x = newX;
}
lastControlX = x;
lastControlY = y;
break;
}
case 'V': {
// Vertical LineTo - convert to L
for (let i = 0; i < args.length; i++) {
const newY = isRelative ? y + args[i] : args[i];
result.push({ cmd: 'L', args: [x, newY] });
y = newY;
}
lastControlX = x;
lastControlY = y;
break;
}
case 'C': {
// CurveTo: C x1 y1 x2 y2 x y (or c dx1 dy1 dx2 dy2 dx dy)
const absArgs = [];
for (let i = 0; i < args.length; i += 6) {
const x1 = isRelative ? x + args[i] : args[i];
const y1 = isRelative ? y + args[i + 1] : args[i + 1];
const x2 = isRelative ? x + args[i + 2] : args[i + 2];
const y2 = isRelative ? y + args[i + 3] : args[i + 3];
const newX = isRelative ? x + args[i + 4] : args[i + 4];
const newY = isRelative ? y + args[i + 5] : args[i + 5];
absArgs.push(x1, y1, x2, y2, newX, newY);
lastControlX = x2;
lastControlY = y2;
x = newX;
y = newY;
}
result.push({ cmd: 'C', args: absArgs });
break;
}
case 'S': {
// Smooth CurveTo - expand to C
for (let i = 0; i < args.length; i += 4) {
// Reflect last control point
let x1, y1;
if (lastCmd === 'C' || lastCmd === 'S') {
x1 = 2 * x - lastControlX;
y1 = 2 * y - lastControlY;
} else {
x1 = x;
y1 = y;
}
const x2 = isRelative ? x + args[i] : args[i];
const y2 = isRelative ? y + args[i + 1] : args[i + 1];
const newX = isRelative ? x + args[i + 2] : args[i + 2];
const newY = isRelative ? y + args[i + 3] : args[i + 3];
result.push({ cmd: 'C', args: [x1, y1, x2, y2, newX, newY] });
lastControlX = x2;
lastControlY = y2;
x = newX;
y = newY;
}
break;
}
case 'Q': {
// Quadratic CurveTo: Q x1 y1 x y (or q dx1 dy1 dx dy)
const absArgs = [];
for (let i = 0; i < args.length; i += 4) {
const x1 = isRelative ? x + args[i] : args[i];
const y1 = isRelative ? y + args[i + 1] : args[i + 1];
const newX = isRelative ? x + args[i + 2] : args[i + 2];
const newY = isRelative ? y + args[i + 3] : args[i + 3];
absArgs.push(x1, y1, newX, newY);
lastControlX = x1;
lastControlY = y1;
x = newX;
y = newY;
}
result.push({ cmd: 'Q', args: absArgs });
break;
}
case 'T': {
// Smooth Quadratic CurveTo - keep as T
const absArgs = [];
for (let i = 0; i < args.length; i += 2) {
// Reflect last control point for tracking
let cpX, cpY;
if (lastCmd === 'Q' || lastCmd === 'T') {
cpX = 2 * x - lastControlX;
cpY = 2 * y - lastControlY;
} else {
cpX = x;
cpY = y;
}
const newX = isRelative ? x + args[i] : args[i];
const newY = isRelative ? y + args[i + 1] : args[i + 1];
absArgs.push(newX, newY);
lastControlX = cpX;
lastControlY = cpY;
x = newX;
y = newY;
lastCmd = 'T'; // For chained T commands
}
result.push({ cmd: 'T', args: absArgs });
break;
}
case 'A': {
// Arc: A rx ry x-axis-rotation large-arc-flag sweep-flag x y
const absArgs = [];
for (let i = 0; i < args.length; i += 7) {
const rx = args[i];
const ry = args[i + 1];
const rotation = args[i + 2];
const largeArc = args[i + 3];
const sweep = args[i + 4];
const newX = isRelative ? x + args[i + 5] : args[i + 5];
const newY = isRelative ? y + args[i + 6] : args[i + 6];
absArgs.push(rx, ry, rotation, largeArc, sweep, newX, newY);
x = newX;
y = newY;
}
lastControlX = x;
lastControlY = y;
result.push({ cmd: 'A', args: absArgs });
break;
}
case 'Z': {
// ClosePath
x = startX;
y = startY;
lastControlX = x;
lastControlY = y;
result.push({ cmd: 'Z', args: [] });
break;
}
}
if (absCmd !== 'T') {
lastCmd = absCmd;
}
}
return result;
}
// Scale coordinates to 0-1 range
function scaleToNormalized(commands, minX, minY, width, height) {
function scaleX(val) {
return (val - minX) / width;
}
function scaleY(val) {
return (val - minY) / height;
}
function scaleRx(val) {
return val / width;
}
function scaleRy(val) {
return val / height;
}
const result = [];
for (const { cmd, args } of commands) {
const scaledArgs = [];
switch (cmd) {
case 'M':
case 'L':
case 'T': {
for (let i = 0; i < args.length; i += 2) {
scaledArgs.push(scaleX(args[i]), scaleY(args[i + 1]));
}
break;
}
case 'H': {
for (let i = 0; i < args.length; i++) {
scaledArgs.push(scaleX(args[i]));
}
break;
}
case 'V': {
for (let i = 0; i < args.length; i++) {
scaledArgs.push(scaleY(args[i]));
}
break;
}
case 'C': {
for (let i = 0; i < args.length; i += 6) {
scaledArgs.push(
scaleX(args[i]), scaleY(args[i + 1]),
scaleX(args[i + 2]), scaleY(args[i + 3]),
scaleX(args[i + 4]), scaleY(args[i + 5])
);
}
break;
}
case 'S':
case 'Q': {
for (let i = 0; i < args.length; i += 4) {
scaledArgs.push(
scaleX(args[i]), scaleY(args[i + 1]),
scaleX(args[i + 2]), scaleY(args[i + 3])
);
}
break;
}
case 'A': {
for (let i = 0; i < args.length; i += 7) {
// rx, ry need to be scaled; rotation and flags stay the same
scaledArgs.push(
scaleRx(args[i]), // rx
scaleRy(args[i + 1]), // ry
args[i + 2], // rotation
args[i + 3], // large-arc
args[i + 4], // sweep
scaleX(args[i + 5]), // x
scaleY(args[i + 6]) // y
);
}
break;
}
case 'Z': {
// No args
break;
}
}
result.push({ cmd, args: scaledArgs });
}
return result;
}
// Format number to reasonable precision
function formatNum(n, precision = 4) {
const rounded = Number(n.toFixed(precision));
return String(rounded);
}
// Convert commands back to path string
function commandsToPath(commands) {
return commands.map(({ cmd, args }, i) => {
const prefix = i === 0 ? '' : ' ';
if (args.length === 0) return prefix + cmd;
return prefix + cmd + args.map(a => formatNum(a)).join(',');
}).join('');
}
// Main conversion
const parsed = parsePath(originalPath);
const absolute = toAbsolute(parsed);
// Calculate actual bounding box from path data
function getBoundingBox(commands) {
let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
for (const { cmd, args } of commands) {
switch (cmd) {
case 'M':
case 'L':
case 'T': {
for (let i = 0; i < args.length; i += 2) {
minX = Math.min(minX, args[i]);
maxX = Math.max(maxX, args[i]);
minY = Math.min(minY, args[i + 1]);
maxY = Math.max(maxY, args[i + 1]);
}
break;
}
case 'H': {
for (let i = 0; i < args.length; i++) {
minX = Math.min(minX, args[i]);
maxX = Math.max(maxX, args[i]);
}
break;
}
case 'V': {
for (let i = 0; i < args.length; i++) {
minY = Math.min(minY, args[i]);
maxY = Math.max(maxY, args[i]);
}
break;
}
case 'C': {
for (let i = 0; i < args.length; i += 6) {
// Include control points and endpoint
minX = Math.min(minX, args[i], args[i + 2], args[i + 4]);
maxX = Math.max(maxX, args[i], args[i + 2], args[i + 4]);
minY = Math.min(minY, args[i + 1], args[i + 3], args[i + 5]);
maxY = Math.max(maxY, args[i + 1], args[i + 3], args[i + 5]);
}
break;
}
case 'S':
case 'Q': {
for (let i = 0; i < args.length; i += 4) {
minX = Math.min(minX, args[i], args[i + 2]);
maxX = Math.max(maxX, args[i], args[i + 2]);
minY = Math.min(minY, args[i + 1], args[i + 3]);
maxY = Math.max(maxY, args[i + 1], args[i + 3]);
}
break;
}
case 'A': {
for (let i = 0; i < args.length; i += 7) {
minX = Math.min(minX, args[i + 5]);
maxX = Math.max(maxX, args[i + 5]);
minY = Math.min(minY, args[i + 6]);
maxY = Math.max(maxY, args[i + 6]);
}
break;
}
}
}
return { minX, minY, width: maxX - minX, height: maxY - minY };
}
const bbox = getBoundingBox(absolute);
console.log(`Path bounding box: x=${bbox.minX}, y=${bbox.minY}, w=${bbox.width}, h=${bbox.height}`);
// Use viewBox for normalization (not path bounding box)
const normalized = scaleToNormalized(absolute, minX, minY, width, height);
const result = commandsToPath(normalized);
console.log(`\nConverted path (${result.length} chars):\n`);
console.log(result);
console.log(`\n\nFor CustomGlyphDefinitions.ts:\n`);
console.log(`'\\u{E0C0}': { type: CustomGlyphDefinitionType.VECTOR_SHAPE, data: { d: '${result}', type: CustomGlyphVectorType.FILL } },`);
// Write output file
const ext = path.extname(filePath);
const outputPath = filePath.replace(ext, `_output${ext}`);
const svgOutput = `<?xml version="1.0" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 1 1">
<path fill="currentColor" d="${result}" />
</svg>
`;
fs.writeFileSync(outputPath, svgOutput, 'utf8');
console.log(`\nOutput written to: ${outputPath}`);
}
+17 -2
View File
@@ -12,6 +12,7 @@ const config = {
isProd: argv.includes('--prod'),
isWatch: argv.includes('--watch'),
isDemoClient: argv.includes('--demo-client'),
isDemoServer: argv.includes('--demo-server'),
isHeadless: argv.includes('--headless'),
addon: argv.find(e => e.startsWith('--addon='))?.replace(/^--addon=/, ''),
};
@@ -25,7 +26,7 @@ const commonOptions = {
target: 'es2021',
sourcemap: true,
treeShaking: true,
logLevel: 'debug',
logLevel: 'warning',
};
/** @type {esbuild.BuildOptions} */
@@ -126,7 +127,7 @@ if (config.addon) {
} else if (config.isDemoClient) {
bundleConfig = {
...bundleConfig,
entryPoints: [`demo/client.ts`],
entryPoints: [`demo/client/client.ts`],
outfile: 'demo/dist/client-bundle.js',
external: ['util', 'os', 'fs', 'path', 'stream', 'Terminal'],
alias: {
@@ -136,6 +137,7 @@ if (config.addon) {
"@xterm/addon-clipboard": "./addons/addon-clipboard/lib/addon-clipboard.mjs",
"@xterm/addon-fit": "./addons/addon-fit/lib/addon-fit.mjs",
"@xterm/addon-image": "./addons/addon-image/lib/addon-image.mjs",
"@xterm/addon-progress": "./addons/addon-progress/lib/addon-progress.mjs",
"@xterm/addon-search": "./addons/addon-search/lib/addon-search.mjs",
"@xterm/addon-serialize": "./addons/addon-serialize/lib/addon-serialize.mjs",
"@xterm/addon-web-fonts": "./addons/addon-web-fonts/lib/addon-web-fonts.mjs",
@@ -152,6 +154,19 @@ if (config.addon) {
"@xterm/addon-ligatures": "./addons/addon-ligatures/out-esbuild/LigaturesAddon",
}
}
skipOut = true;
skipOutTest = true;
} else if (config.isDemoServer) {
bundleConfig = {
...bundleConfig,
entryPoints: [`demo/server/server.ts`],
outfile: 'demo/dist/server-bundle.js',
format: 'cjs',
platform: 'node',
external: ['node-pty'],
}
skipOut = true;
skipOutTest = true;
} else if (config.isHeadless) {
bundleConfig = {
...bundleConfig,
+2 -2
View File
@@ -4,14 +4,14 @@ import { spawn } from "child_process";
import { readdir } from "fs/promises";
import { argv } from "process";
/** @type {{cp: import("child_process").ChildProcessByStdio, name: string}[]} */
/** @type {{cp: import("child_process").ChildProcessByStdio<any, any, any>, name: string}[]} */
const jobs = [];
// Core job
jobs.push(createJob('xterm', []));
// Addon jobs
const addons = (await readdir('addons')).map(e => e.replace('addon-', ''));
const addons = (await readdir('addons')).filter(e => e.startsWith('addon-')).map(e => e.replace('addon-', ''));
for (const addon of addons) {
jobs.push(createJob(`xterm-addon-${addon}`, [`--addon=${addon}`]));
}
-56
View File
@@ -1,56 +0,0 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*
* Script to initialize addon packages under "addons/" with outer deps.
*/
const path = require('path');
const cp = require('child_process');
const fs = require('fs');
const PACKAGE_ROOT = path.join(__dirname, '..');
// install addon deps
const addonsPath = path.join(PACKAGE_ROOT, 'addons');
if (fs.existsSync(addonsPath)) {
console.log('pulling addon dependencies...');
// whether to use yarn or npm
let hasYarn = false;
try {
cp.execSync('yarn --version').toString();
hasYarn = true;
} catch(e) {}
// walk all addon folders
fs.readdir(addonsPath, (err, files) => {
for (const folder of files) {
const addonPath = path.join(addonsPath, folder);
// install only if there are dependencies listed
let packageJson;
try {
packageJson = require(path.join(addonPath, 'package.json'));
} catch (e) {
// swallow as changing branches can leave folders around
}
if (packageJson
&& (
(packageJson.devDependencies && Object.keys(packageJson.devDependencies).length)
|| (packageJson.dependencies && Object.keys(packageJson.dependencies).length)
)
)
{
console.log('Preparing', folder);
if (hasYarn) {
cp.execSync('yarn', {cwd: addonPath});
} else {
cp.execSync('npm install', {cwd: addonPath});
}
} else {
console.log('Skipped', folder);
}
}
});
}
+17 -5
View File
@@ -30,7 +30,7 @@ const changedFiles = getChangedFilesInCommit('HEAD');
// Always publish xterm, technically this isn't needed if
// `changedFiles.some(e => e.search(/^addons\//)`, but it's here for convenience to get the right
// peer dependencies for addons.
const result = checkAndPublishPackage(path.resolve(__dirname, '..'), repoCommit);
const result = checkAndPublishPackage(path.resolve(__dirname, '..'), repoCommit, undefined, true);
const isStableRelease = result.isStableRelease;
const peerDependencies = result.nextVersion.includes('beta') ? {
'@xterm/xterm': `^${result.nextVersion}`,
@@ -46,10 +46,11 @@ const addonPackageDirs = [
path.resolve(__dirname, '../addons/addon-fit'),
path.resolve(__dirname, '../addons/addon-image'),
path.resolve(__dirname, '../addons/addon-ligatures'),
path.resolve(__dirname, '../addons/addon-progress'),
path.resolve(__dirname, '../addons/addon-search'),
path.resolve(__dirname, '../addons/addon-serialize'),
path.resolve(__dirname, '../addons/addon-unicode11'),
// path.resolve(__dirname, '../addons/addon-unicode-graphemes'),
path.resolve(__dirname, '../addons/addon-unicode-graphemes'),
path.resolve(__dirname, '../addons/addon-web-links'),
path.resolve(__dirname, '../addons/addon-webgl')
];
@@ -66,7 +67,7 @@ if (isStableRelease) {
updateWebsite();
}
function checkAndPublishPackage(packageDir, repoCommit, peerDependencies) {
function checkAndPublishPackage(packageDir, repoCommit, peerDependencies, updateVersionTs = false) {
const packageJson = require(path.join(packageDir, 'package.json'));
// Determine if this is a stable or beta release
@@ -77,6 +78,18 @@ function checkAndPublishPackage(packageDir, repoCommit, peerDependencies) {
let nextVersion = isStableRelease ? packageJson.version : getNextBetaVersion(packageJson);
log(`Publishing version: ${nextVersion}`, 'green');
// Update Version.ts with the new version
if (updateVersionTs) {
const versionTsPath = path.join(packageDir, 'src/common/Version.ts');
const versionTsContent = fs.readFileSync(versionTsPath, 'utf8');
const updatedVersionTs = versionTsContent.replace(
/export const XTERM_VERSION = '[^']+';/,
`export const XTERM_VERSION = '${nextVersion}';`
);
fs.writeFileSync(versionTsPath, updatedVersionTs);
log(`Updated ${versionTsPath} to version ${nextVersion}`);
}
// Set the version in package.json
const packageJsonFile = path.join(packageDir, 'package.json');
packageJson.version = nextVersion;
@@ -114,8 +127,7 @@ function checkAndPublishPackage(packageDir, repoCommit, peerDependencies) {
stdio: 'inherit'
});
if (result.status) {
error(`Spawn exited with code ${result.status}`);
process.exit(result.status);
throw new Error(`Spawn exited with code ${result.status}`);
}
return { isStableRelease, nextVersion };
+1
View File
@@ -25,6 +25,7 @@ const addons = [
'clipboard',
'fit',
'image',
'progress',
'search',
'serialize',
'unicode-graphemes',
+1 -1
View File
@@ -20,7 +20,7 @@ if (process.argv.length > 2) {
const args = process.argv.slice(2);
flagArgs = args.filter(e => e.startsWith('--'));
// ability to inject particular test files via
// yarn test [testFileA testFileB ...]
// npm run test [testFileA testFileB ...]
files = args.filter(e => !e.startsWith('--'));
if (files.length) {
testFiles = files;
+1 -1
View File
@@ -6,7 +6,7 @@ VERSION=$1
# Clone docs repo and update the documentation
git clone https://github.com/xtermjs/xtermjs.org
cd xtermjs.org
yarn
npm ci
./bin/update-docs
# Add changes to index and only proceed if there are changes to commit