@emasoft/svg-matrix 1.2.1 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -0
- package/bin/svgfonts.js +1412 -0
- package/bin/svgm.js +1 -1
- package/dist/svg-matrix.global.min.js +8 -0
- package/dist/svg-matrix.min.js +2 -2
- package/dist/svg-toolbox.global.min.js +493 -0
- package/dist/svg-toolbox.min.js +16 -16
- package/dist/svgm.min.js +60 -60
- package/dist/version.json +44 -16
- package/package.json +11 -3
- package/src/bezier-intersections.js +1 -1
- package/src/browser-verify.js +0 -1
- package/src/clip-path-resolver.js +3 -1
- package/src/font-manager.js +1013 -0
- package/src/index.js +2 -2
- package/src/inkscape-support.js +2 -2
- package/src/mask-resolver.js +14 -6
- package/src/mesh-gradient.js +0 -2
- package/src/off-canvas-detection.js +14 -22
- package/src/svg-boolean-ops.js +0 -5
- package/src/svg-collections.js +11 -0
- package/src/svg-matrix-lib.js +2 -2
- package/src/svg-parser.js +0 -24
- package/src/svg-rendering-context.js +2 -4
- package/src/svg-toolbox-lib.js +2 -2
- package/src/svgm-lib.js +2 -2
- package/src/transform-optimization.js +93 -142
- package/src/verification.js +0 -2
- package/templates/svgm_replacement_map.yml +53 -0
package/bin/svgfonts.js
ADDED
|
@@ -0,0 +1,1412 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview SVG Font Management CLI
|
|
4
|
+
*
|
|
5
|
+
* Dedicated CLI tool for SVG font operations:
|
|
6
|
+
* - Embed external fonts as base64 data URIs
|
|
7
|
+
* - Extract embedded fonts to files
|
|
8
|
+
* - Apply font replacement maps
|
|
9
|
+
* - Interactive font management mode
|
|
10
|
+
*
|
|
11
|
+
* @module bin/svgfonts
|
|
12
|
+
* @license MIT
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
readFileSync,
|
|
17
|
+
writeFileSync,
|
|
18
|
+
existsSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
readdirSync,
|
|
21
|
+
statSync,
|
|
22
|
+
realpathSync,
|
|
23
|
+
} from "fs";
|
|
24
|
+
import { join, dirname, basename, extname, resolve, isAbsolute } from "path";
|
|
25
|
+
import { createInterface } from "readline";
|
|
26
|
+
|
|
27
|
+
// Import library modules
|
|
28
|
+
import { VERSION } from "../src/index.js";
|
|
29
|
+
import * as SVGToolbox from "../src/svg-toolbox.js";
|
|
30
|
+
import { parseSVG, serializeSVG } from "../src/svg-parser.js";
|
|
31
|
+
import * as FontManager from "../src/font-manager.js";
|
|
32
|
+
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// CONSTANTS
|
|
35
|
+
// ============================================================================
|
|
36
|
+
const CONSTANTS = {
|
|
37
|
+
MAX_FILE_SIZE_BYTES: 50 * 1024 * 1024, // 50MB
|
|
38
|
+
EXIT_SUCCESS: 0,
|
|
39
|
+
EXIT_ERROR: 1,
|
|
40
|
+
SVG_EXTENSIONS: [".svg", ".svgz"],
|
|
41
|
+
DEFAULT_TIMEOUT: 30000,
|
|
42
|
+
DEFAULT_FONTS_DIR: "./fonts",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ============================================================================
|
|
46
|
+
// COLORS (respects NO_COLOR env)
|
|
47
|
+
// ============================================================================
|
|
48
|
+
const colors =
|
|
49
|
+
process.env.NO_COLOR !== undefined || process.argv.includes("--no-color")
|
|
50
|
+
? {
|
|
51
|
+
reset: "",
|
|
52
|
+
red: "",
|
|
53
|
+
yellow: "",
|
|
54
|
+
green: "",
|
|
55
|
+
cyan: "",
|
|
56
|
+
dim: "",
|
|
57
|
+
bright: "",
|
|
58
|
+
magenta: "",
|
|
59
|
+
}
|
|
60
|
+
: {
|
|
61
|
+
reset: "\x1b[0m",
|
|
62
|
+
red: "\x1b[31m",
|
|
63
|
+
yellow: "\x1b[33m",
|
|
64
|
+
green: "\x1b[32m",
|
|
65
|
+
cyan: "\x1b[36m",
|
|
66
|
+
dim: "\x1b[2m",
|
|
67
|
+
bright: "\x1b[1m",
|
|
68
|
+
magenta: "\x1b[35m",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// ============================================================================
|
|
72
|
+
// CONFIGURATION
|
|
73
|
+
// ============================================================================
|
|
74
|
+
const DEFAULT_CONFIG = {
|
|
75
|
+
command: null,
|
|
76
|
+
inputs: [],
|
|
77
|
+
output: null,
|
|
78
|
+
recursive: false,
|
|
79
|
+
quiet: false,
|
|
80
|
+
verbose: false,
|
|
81
|
+
noBackup: false,
|
|
82
|
+
validate: false,
|
|
83
|
+
dryRun: false,
|
|
84
|
+
// Embed options
|
|
85
|
+
subset: true,
|
|
86
|
+
full: false,
|
|
87
|
+
source: null, // 'google' | 'local' | 'fontget' | 'fnt'
|
|
88
|
+
timeout: CONSTANTS.DEFAULT_TIMEOUT,
|
|
89
|
+
// Replace options
|
|
90
|
+
mapFile: null,
|
|
91
|
+
autoDownload: true,
|
|
92
|
+
// Extract options
|
|
93
|
+
extractDir: CONSTANTS.DEFAULT_FONTS_DIR,
|
|
94
|
+
restoreLinks: false,
|
|
95
|
+
// Template options
|
|
96
|
+
templateOutput: null,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
let config = { ...DEFAULT_CONFIG };
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// LOGGING
|
|
103
|
+
// ============================================================================
|
|
104
|
+
function log(msg) {
|
|
105
|
+
if (!config.quiet) console.log(msg);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function logError(msg) {
|
|
109
|
+
console.error(`${colors.red}error:${colors.reset} ${msg}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function logWarn(msg) {
|
|
113
|
+
if (!config.quiet) console.warn(`${colors.yellow}warn:${colors.reset} ${msg}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function logSuccess(msg) {
|
|
117
|
+
if (!config.quiet) console.log(`${colors.green}${msg}${colors.reset}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function logVerbose(msg) {
|
|
121
|
+
if (config.verbose && !config.quiet) {
|
|
122
|
+
console.log(`${colors.dim}${msg}${colors.reset}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ============================================================================
|
|
127
|
+
// HELP
|
|
128
|
+
// ============================================================================
|
|
129
|
+
const HELP = `
|
|
130
|
+
${colors.bright}svgfonts${colors.reset} - SVG Font Management Tool v${VERSION}
|
|
131
|
+
|
|
132
|
+
${colors.cyan}USAGE:${colors.reset}
|
|
133
|
+
svgfonts [command] [options] <input.svg...>
|
|
134
|
+
|
|
135
|
+
${colors.cyan}COMMANDS:${colors.reset}
|
|
136
|
+
embed Embed external fonts into SVG (default)
|
|
137
|
+
extract Extract embedded fonts to files
|
|
138
|
+
replace Apply font replacement map
|
|
139
|
+
list List fonts in SVG
|
|
140
|
+
interactive Interactive font management mode
|
|
141
|
+
template Generate replacement map template
|
|
142
|
+
|
|
143
|
+
${colors.cyan}GLOBAL OPTIONS:${colors.reset}
|
|
144
|
+
-i, --input <file> Input SVG file(s)
|
|
145
|
+
-o, --output <file> Output file (default: overwrite input)
|
|
146
|
+
-r, --recursive Process directories recursively
|
|
147
|
+
-q, --quiet Suppress output
|
|
148
|
+
-v, --verbose Verbose output
|
|
149
|
+
--no-backup Skip backup creation (default: create .bak)
|
|
150
|
+
--validate Validate SVG after each operation
|
|
151
|
+
--dry-run Show what would be done without changes
|
|
152
|
+
--no-color Disable colored output
|
|
153
|
+
-h, --help Show this help
|
|
154
|
+
--version Show version
|
|
155
|
+
|
|
156
|
+
${colors.cyan}EMBED OPTIONS:${colors.reset}
|
|
157
|
+
--subset Only embed glyphs used in SVG (default)
|
|
158
|
+
--full Embed complete font files
|
|
159
|
+
--source <name> Preferred font source (google|local|fontget|fnt)
|
|
160
|
+
--timeout <ms> Download timeout (default: 30000)
|
|
161
|
+
|
|
162
|
+
${colors.cyan}REPLACE OPTIONS:${colors.reset}
|
|
163
|
+
--map <file> Path to replacement YAML
|
|
164
|
+
--no-auto-download Don't download missing fonts automatically
|
|
165
|
+
|
|
166
|
+
${colors.cyan}EXTRACT OPTIONS:${colors.reset}
|
|
167
|
+
--extract-dir <dir> Directory for extracted fonts (default: ./fonts/)
|
|
168
|
+
--restore-links Convert back to external URL references
|
|
169
|
+
|
|
170
|
+
${colors.cyan}TEMPLATE OPTIONS:${colors.reset}
|
|
171
|
+
--template-output <file> Output path for template (default: stdout)
|
|
172
|
+
|
|
173
|
+
${colors.cyan}EXAMPLES:${colors.reset}
|
|
174
|
+
svgfonts embed icon.svg # Embed fonts with subsetting
|
|
175
|
+
svgfonts embed --full icon.svg # Embed complete fonts
|
|
176
|
+
svgfonts list document.svg # List all fonts in SVG
|
|
177
|
+
svgfonts replace --map fonts.yml *.svg # Apply replacement map
|
|
178
|
+
svgfonts extract -o ./fonts doc.svg # Extract embedded fonts
|
|
179
|
+
svgfonts interactive icon.svg # Interactive mode
|
|
180
|
+
svgfonts template > fonts.yml # Generate template
|
|
181
|
+
|
|
182
|
+
${colors.cyan}ENVIRONMENT:${colors.reset}
|
|
183
|
+
SVGM_REPLACEMENT_MAP Path to default replacement map YAML
|
|
184
|
+
|
|
185
|
+
${colors.cyan}SEE ALSO:${colors.reset}
|
|
186
|
+
svgm (general SVG optimization)
|
|
187
|
+
svg-matrix (matrix operations)
|
|
188
|
+
`;
|
|
189
|
+
|
|
190
|
+
// ============================================================================
|
|
191
|
+
// ARGUMENT PARSING
|
|
192
|
+
// ============================================================================
|
|
193
|
+
function parseArgs(args) {
|
|
194
|
+
const result = { ...DEFAULT_CONFIG };
|
|
195
|
+
let i = 0;
|
|
196
|
+
|
|
197
|
+
while (i < args.length) {
|
|
198
|
+
const arg = args[i];
|
|
199
|
+
|
|
200
|
+
switch (arg) {
|
|
201
|
+
case "-h":
|
|
202
|
+
case "--help":
|
|
203
|
+
console.log(HELP);
|
|
204
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
205
|
+
break;
|
|
206
|
+
|
|
207
|
+
case "--version":
|
|
208
|
+
console.log(VERSION);
|
|
209
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
210
|
+
break;
|
|
211
|
+
|
|
212
|
+
case "embed":
|
|
213
|
+
case "extract":
|
|
214
|
+
case "replace":
|
|
215
|
+
case "list":
|
|
216
|
+
case "interactive":
|
|
217
|
+
case "template":
|
|
218
|
+
if (!result.command) {
|
|
219
|
+
result.command = arg;
|
|
220
|
+
} else {
|
|
221
|
+
result.inputs.push(arg);
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
|
|
225
|
+
case "-i":
|
|
226
|
+
case "--input":
|
|
227
|
+
if (args[i + 1]) {
|
|
228
|
+
result.inputs.push(args[++i]);
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
|
|
232
|
+
case "-o":
|
|
233
|
+
case "--output":
|
|
234
|
+
result.output = args[++i];
|
|
235
|
+
break;
|
|
236
|
+
|
|
237
|
+
case "-r":
|
|
238
|
+
case "--recursive":
|
|
239
|
+
result.recursive = true;
|
|
240
|
+
break;
|
|
241
|
+
|
|
242
|
+
case "-q":
|
|
243
|
+
case "--quiet":
|
|
244
|
+
result.quiet = true;
|
|
245
|
+
break;
|
|
246
|
+
|
|
247
|
+
case "-v":
|
|
248
|
+
case "--verbose":
|
|
249
|
+
result.verbose = true;
|
|
250
|
+
break;
|
|
251
|
+
|
|
252
|
+
case "--no-backup":
|
|
253
|
+
result.noBackup = true;
|
|
254
|
+
break;
|
|
255
|
+
|
|
256
|
+
case "--validate":
|
|
257
|
+
result.validate = true;
|
|
258
|
+
break;
|
|
259
|
+
|
|
260
|
+
case "--dry-run":
|
|
261
|
+
result.dryRun = true;
|
|
262
|
+
break;
|
|
263
|
+
|
|
264
|
+
case "--no-color":
|
|
265
|
+
// Already handled in colors setup
|
|
266
|
+
break;
|
|
267
|
+
|
|
268
|
+
// Embed options
|
|
269
|
+
case "--subset":
|
|
270
|
+
result.subset = true;
|
|
271
|
+
result.full = false;
|
|
272
|
+
break;
|
|
273
|
+
|
|
274
|
+
case "--full":
|
|
275
|
+
result.full = true;
|
|
276
|
+
result.subset = false;
|
|
277
|
+
break;
|
|
278
|
+
|
|
279
|
+
case "--source":
|
|
280
|
+
result.source = args[++i];
|
|
281
|
+
break;
|
|
282
|
+
|
|
283
|
+
case "--timeout":
|
|
284
|
+
result.timeout = parseInt(args[++i], 10);
|
|
285
|
+
break;
|
|
286
|
+
|
|
287
|
+
// Replace options
|
|
288
|
+
case "--map":
|
|
289
|
+
result.mapFile = args[++i];
|
|
290
|
+
break;
|
|
291
|
+
|
|
292
|
+
case "--no-auto-download":
|
|
293
|
+
result.autoDownload = false;
|
|
294
|
+
break;
|
|
295
|
+
|
|
296
|
+
// Extract options
|
|
297
|
+
case "--extract-dir":
|
|
298
|
+
result.extractDir = args[++i];
|
|
299
|
+
break;
|
|
300
|
+
|
|
301
|
+
case "--restore-links":
|
|
302
|
+
result.restoreLinks = true;
|
|
303
|
+
break;
|
|
304
|
+
|
|
305
|
+
// Template options
|
|
306
|
+
case "--template-output":
|
|
307
|
+
result.templateOutput = args[++i];
|
|
308
|
+
break;
|
|
309
|
+
|
|
310
|
+
default:
|
|
311
|
+
// Assume it's an input file
|
|
312
|
+
if (!arg.startsWith("-")) {
|
|
313
|
+
result.inputs.push(arg);
|
|
314
|
+
} else {
|
|
315
|
+
logError(`Unknown option: ${arg}`);
|
|
316
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
i++;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Default command is 'embed' if inputs provided
|
|
323
|
+
if (!result.command && result.inputs.length > 0) {
|
|
324
|
+
result.command = "embed";
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return result;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// ============================================================================
|
|
331
|
+
// FILE UTILITIES
|
|
332
|
+
// ============================================================================
|
|
333
|
+
function collectSvgFiles(inputs, recursive = false) {
|
|
334
|
+
const files = [];
|
|
335
|
+
const visited = new Set();
|
|
336
|
+
|
|
337
|
+
for (const input of inputs) {
|
|
338
|
+
const absPath = isAbsolute(input) ? input : resolve(input);
|
|
339
|
+
|
|
340
|
+
if (!existsSync(absPath)) {
|
|
341
|
+
logWarn(`File not found: ${input}`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Detect symlink loops
|
|
346
|
+
try {
|
|
347
|
+
const realPath = realpathSync(absPath);
|
|
348
|
+
if (visited.has(realPath)) continue;
|
|
349
|
+
visited.add(realPath);
|
|
350
|
+
} catch {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const stat = statSync(absPath);
|
|
355
|
+
|
|
356
|
+
if (stat.isFile()) {
|
|
357
|
+
const ext = extname(absPath).toLowerCase();
|
|
358
|
+
if (CONSTANTS.SVG_EXTENSIONS.includes(ext)) {
|
|
359
|
+
files.push(absPath);
|
|
360
|
+
}
|
|
361
|
+
} else if (stat.isDirectory() && recursive) {
|
|
362
|
+
const entries = readdirSync(absPath);
|
|
363
|
+
const subPaths = entries.map((e) => join(absPath, e));
|
|
364
|
+
files.push(...collectSvgFiles(subPaths, recursive));
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return files;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ============================================================================
|
|
372
|
+
// COMMAND: LIST
|
|
373
|
+
// ============================================================================
|
|
374
|
+
async function cmdList(files) {
|
|
375
|
+
for (const file of files) {
|
|
376
|
+
const content = readFileSync(file, "utf8");
|
|
377
|
+
const doc = parseSVG(content);
|
|
378
|
+
|
|
379
|
+
const fonts = FontManager.listFonts(doc);
|
|
380
|
+
|
|
381
|
+
log(`\n${colors.bright}${basename(file)}${colors.reset}`);
|
|
382
|
+
log("─".repeat(60));
|
|
383
|
+
|
|
384
|
+
if (fonts.length === 0) {
|
|
385
|
+
log(` ${colors.dim}No fonts found${colors.reset}`);
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
log(
|
|
390
|
+
` ${colors.cyan}${"#".padEnd(4)}${"Font Family".padEnd(25)}${"Type".padEnd(12)}${"Size".padEnd(12)}Used Chars${colors.reset}`
|
|
391
|
+
);
|
|
392
|
+
log(" " + "─".repeat(56));
|
|
393
|
+
|
|
394
|
+
fonts.forEach((font, idx) => {
|
|
395
|
+
const num = `${idx + 1}.`.padEnd(4);
|
|
396
|
+
const family = font.family.slice(0, 24).padEnd(25);
|
|
397
|
+
const type = font.type.padEnd(12);
|
|
398
|
+
const size = font.size
|
|
399
|
+
? `${(font.size / 1024).toFixed(1)} KB`.padEnd(12)
|
|
400
|
+
: font.source
|
|
401
|
+
? `(${font.source.slice(0, 8)}...)`.padEnd(12)
|
|
402
|
+
: "".padEnd(12);
|
|
403
|
+
const chars = font.usedChars
|
|
404
|
+
? [...font.usedChars].slice(0, 20).join("") +
|
|
405
|
+
(font.usedChars.size > 20 ? "..." : "")
|
|
406
|
+
: "";
|
|
407
|
+
|
|
408
|
+
log(` ${num}${family}${type}${size}${chars}`);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ============================================================================
|
|
414
|
+
// COMMAND: EMBED
|
|
415
|
+
// ============================================================================
|
|
416
|
+
async function cmdEmbed(files) {
|
|
417
|
+
for (const file of files) {
|
|
418
|
+
logVerbose(`Processing: ${file}`);
|
|
419
|
+
|
|
420
|
+
const content = readFileSync(file, "utf8");
|
|
421
|
+
|
|
422
|
+
// Create backup
|
|
423
|
+
if (!config.noBackup && !config.dryRun) {
|
|
424
|
+
const backupPath = FontManager.createBackup(file, { noBackup: config.noBackup });
|
|
425
|
+
if (backupPath) {
|
|
426
|
+
logVerbose(`Backup created: ${backupPath}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (config.dryRun) {
|
|
431
|
+
log(`[dry-run] Would embed fonts in: ${file}`);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const doc = parseSVG(content);
|
|
436
|
+
|
|
437
|
+
// Use svg-toolbox embedExternalDependencies
|
|
438
|
+
const result = await SVGToolbox.embedExternalDependencies(doc, {
|
|
439
|
+
embedImages: false,
|
|
440
|
+
embedExternalSVGs: false,
|
|
441
|
+
embedCSS: true,
|
|
442
|
+
embedFonts: true,
|
|
443
|
+
embedScripts: false,
|
|
444
|
+
embedAudio: false,
|
|
445
|
+
subsetFonts: config.subset && !config.full,
|
|
446
|
+
timeout: config.timeout,
|
|
447
|
+
verbose: config.verbose,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
const output = serializeSVG(result);
|
|
451
|
+
|
|
452
|
+
// Validate if requested
|
|
453
|
+
if (config.validate) {
|
|
454
|
+
const validation = await FontManager.validateSvgAfterFontOperation(output, "embed");
|
|
455
|
+
if (!validation.valid) {
|
|
456
|
+
logError(`Validation failed for ${file}: ${validation.errors.join(", ")}`);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
for (const warn of validation.warnings) {
|
|
460
|
+
logWarn(warn);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const outputPath = config.output || file;
|
|
465
|
+
writeFileSync(outputPath, output);
|
|
466
|
+
logSuccess(`Embedded fonts: ${outputPath}`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// ============================================================================
|
|
471
|
+
// COMMAND: EXTRACT
|
|
472
|
+
// ============================================================================
|
|
473
|
+
async function cmdExtract(files) {
|
|
474
|
+
mkdirSync(config.extractDir, { recursive: true });
|
|
475
|
+
|
|
476
|
+
for (const file of files) {
|
|
477
|
+
logVerbose(`Processing: ${file}`);
|
|
478
|
+
|
|
479
|
+
const content = readFileSync(file, "utf8");
|
|
480
|
+
|
|
481
|
+
// Create backup
|
|
482
|
+
if (!config.noBackup && !config.dryRun) {
|
|
483
|
+
FontManager.createBackup(file, { noBackup: config.noBackup });
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (config.dryRun) {
|
|
487
|
+
log(`[dry-run] Would extract fonts from: ${file}`);
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const doc = parseSVG(content);
|
|
492
|
+
|
|
493
|
+
// Use svg-toolbox exportEmbeddedResources
|
|
494
|
+
const result = await SVGToolbox.exportEmbeddedResources(doc, {
|
|
495
|
+
outputDir: config.extractDir,
|
|
496
|
+
extractFonts: true,
|
|
497
|
+
extractImages: false,
|
|
498
|
+
extractStyles: false,
|
|
499
|
+
restoreGoogleFonts: config.restoreLinks,
|
|
500
|
+
});
|
|
501
|
+
|
|
502
|
+
const output = serializeSVG(result);
|
|
503
|
+
|
|
504
|
+
// Validate if requested
|
|
505
|
+
if (config.validate) {
|
|
506
|
+
const validation = await FontManager.validateSvgAfterFontOperation(output, "extract");
|
|
507
|
+
if (!validation.valid) {
|
|
508
|
+
logError(`Validation failed for ${file}: ${validation.errors.join(", ")}`);
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
const outputPath = config.output || file;
|
|
514
|
+
writeFileSync(outputPath, output);
|
|
515
|
+
logSuccess(`Extracted fonts to: ${config.extractDir}`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ============================================================================
|
|
520
|
+
// COMMAND: REPLACE
|
|
521
|
+
// ============================================================================
|
|
522
|
+
async function cmdReplace(files) {
|
|
523
|
+
// Load replacement map
|
|
524
|
+
const map = FontManager.loadReplacementMap(config.mapFile);
|
|
525
|
+
|
|
526
|
+
if (!map) {
|
|
527
|
+
logError(
|
|
528
|
+
"No replacement map found. Create one with 'svgfonts template' or specify --map"
|
|
529
|
+
);
|
|
530
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
log(`Using replacement map: ${map.path}`);
|
|
534
|
+
|
|
535
|
+
const replacementCount = Object.keys(map.replacements).length;
|
|
536
|
+
if (replacementCount === 0) {
|
|
537
|
+
logWarn("Replacement map has no font mappings defined");
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
log(`Found ${replacementCount} font replacement(s)`);
|
|
542
|
+
|
|
543
|
+
for (const file of files) {
|
|
544
|
+
logVerbose(`Processing: ${file}`);
|
|
545
|
+
|
|
546
|
+
const content = readFileSync(file, "utf8");
|
|
547
|
+
|
|
548
|
+
// Create backup
|
|
549
|
+
if (!config.noBackup && !config.dryRun) {
|
|
550
|
+
FontManager.createBackup(file, { noBackup: config.noBackup });
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (config.dryRun) {
|
|
554
|
+
log(`[dry-run] Would apply replacements to: ${file}`);
|
|
555
|
+
continue;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
const doc = parseSVG(content);
|
|
559
|
+
|
|
560
|
+
// Apply replacements
|
|
561
|
+
const replaceResult = FontManager.applyFontReplacements(doc, map.replacements);
|
|
562
|
+
|
|
563
|
+
if (replaceResult.modified) {
|
|
564
|
+
const output = serializeSVG(doc);
|
|
565
|
+
|
|
566
|
+
// Validate if requested
|
|
567
|
+
if (config.validate) {
|
|
568
|
+
const validation = await FontManager.validateSvgAfterFontOperation(
|
|
569
|
+
output,
|
|
570
|
+
"replace"
|
|
571
|
+
);
|
|
572
|
+
for (const warn of validation.warnings) {
|
|
573
|
+
logWarn(warn);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const outputPath = config.output || file;
|
|
578
|
+
writeFileSync(outputPath, output);
|
|
579
|
+
|
|
580
|
+
for (const r of replaceResult.replaced) {
|
|
581
|
+
log(` ${r.from} -> ${r.to}`);
|
|
582
|
+
}
|
|
583
|
+
logSuccess(`Replaced fonts: ${outputPath}`);
|
|
584
|
+
} else {
|
|
585
|
+
log(` No fonts replaced in ${basename(file)}`);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// ============================================================================
|
|
591
|
+
// COMMAND: TEMPLATE
|
|
592
|
+
// ============================================================================
|
|
593
|
+
function cmdTemplate() {
|
|
594
|
+
const template = FontManager.generateReplacementMapTemplate();
|
|
595
|
+
|
|
596
|
+
if (config.templateOutput) {
|
|
597
|
+
writeFileSync(config.templateOutput, template);
|
|
598
|
+
logSuccess(`Template written to: ${config.templateOutput}`);
|
|
599
|
+
} else {
|
|
600
|
+
console.log(template);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ============================================================================
|
|
605
|
+
// COMMAND: INTERACTIVE
|
|
606
|
+
// ============================================================================
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Box-drawing characters for consistent borders
|
|
610
|
+
*/
|
|
611
|
+
const BOX = {
|
|
612
|
+
tl: "┌",
|
|
613
|
+
tr: "┐",
|
|
614
|
+
bl: "└",
|
|
615
|
+
br: "┘",
|
|
616
|
+
h: "─",
|
|
617
|
+
v: "│",
|
|
618
|
+
ml: "├",
|
|
619
|
+
mr: "┤",
|
|
620
|
+
mt: "┬",
|
|
621
|
+
mb: "┴",
|
|
622
|
+
cross: "┼",
|
|
623
|
+
// Double line variants for headers
|
|
624
|
+
dh: "═",
|
|
625
|
+
dtl: "╔",
|
|
626
|
+
dtr: "╗",
|
|
627
|
+
dbl: "╚",
|
|
628
|
+
dbr: "╝",
|
|
629
|
+
dv: "║",
|
|
630
|
+
dml: "╠",
|
|
631
|
+
dmr: "╣",
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Draw a horizontal line with box characters
|
|
636
|
+
*/
|
|
637
|
+
function drawLine(width, left = BOX.ml, fill = BOX.h, right = BOX.mr) {
|
|
638
|
+
return left + fill.repeat(width - 2) + right;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Format a table row with padding
|
|
643
|
+
*/
|
|
644
|
+
function tableRow(cols, widths, sep = BOX.v) {
|
|
645
|
+
return (
|
|
646
|
+
sep +
|
|
647
|
+
cols.map((col, i) => ` ${String(col).slice(0, widths[i] - 2).padEnd(widths[i] - 2)} `).join(sep) +
|
|
648
|
+
sep
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Format file size for display
|
|
654
|
+
*/
|
|
655
|
+
function formatSize(bytes) {
|
|
656
|
+
if (!bytes) return "-";
|
|
657
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
658
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
659
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Get status indicator for font type
|
|
664
|
+
*/
|
|
665
|
+
function fontTypeIcon(type) {
|
|
666
|
+
switch (type) {
|
|
667
|
+
case "embedded":
|
|
668
|
+
return `${colors.green}●${colors.reset}`;
|
|
669
|
+
case "external":
|
|
670
|
+
return `${colors.yellow}○${colors.reset}`;
|
|
671
|
+
case "system":
|
|
672
|
+
return `${colors.cyan}◆${colors.reset}`;
|
|
673
|
+
default:
|
|
674
|
+
return `${colors.dim}?${colors.reset}`;
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
async function cmdInteractive(files) {
|
|
679
|
+
if (files.length === 0) {
|
|
680
|
+
logError("No input files specified for interactive mode");
|
|
681
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const file = files[0]; // Process one file at a time in interactive mode
|
|
685
|
+
const content = readFileSync(file, "utf8");
|
|
686
|
+
let doc = parseSVG(content);
|
|
687
|
+
let modified = false;
|
|
688
|
+
let filterText = "";
|
|
689
|
+
let selectedIdx = -1;
|
|
690
|
+
let scrollOffset = 0; // For scrolling through long font lists
|
|
691
|
+
let message = ""; // Status message to display
|
|
692
|
+
let messageType = "info"; // "info", "success", "error", "warn"
|
|
693
|
+
|
|
694
|
+
// Page size for font list (visible rows)
|
|
695
|
+
const PAGE_SIZE = 10;
|
|
696
|
+
|
|
697
|
+
// Undo history (stores serialized SVG states)
|
|
698
|
+
const history = [];
|
|
699
|
+
const MAX_HISTORY = 20;
|
|
700
|
+
|
|
701
|
+
const pushHistory = () => {
|
|
702
|
+
const state = serializeSVG(doc);
|
|
703
|
+
history.push(state);
|
|
704
|
+
if (history.length > MAX_HISTORY) {
|
|
705
|
+
history.shift();
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
const undo = () => {
|
|
710
|
+
if (history.length > 0) {
|
|
711
|
+
const prevState = history.pop();
|
|
712
|
+
doc = parseSVG(prevState);
|
|
713
|
+
return true;
|
|
714
|
+
}
|
|
715
|
+
return false;
|
|
716
|
+
};
|
|
717
|
+
|
|
718
|
+
const rl = createInterface({
|
|
719
|
+
input: process.stdin,
|
|
720
|
+
output: process.stdout,
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
const question = (prompt) =>
|
|
724
|
+
new Promise((resolve) => rl.question(prompt, resolve));
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* Read single keypress (for arrow key navigation)
|
|
728
|
+
*/
|
|
729
|
+
const readKey = () =>
|
|
730
|
+
new Promise((resolve) => {
|
|
731
|
+
const wasRaw = process.stdin.isRaw;
|
|
732
|
+
if (process.stdin.setRawMode) {
|
|
733
|
+
process.stdin.setRawMode(true);
|
|
734
|
+
}
|
|
735
|
+
process.stdin.resume();
|
|
736
|
+
process.stdin.once("data", (key) => {
|
|
737
|
+
if (process.stdin.setRawMode) {
|
|
738
|
+
process.stdin.setRawMode(wasRaw);
|
|
739
|
+
}
|
|
740
|
+
resolve(key.toString());
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
// Create backup before interactive session
|
|
745
|
+
if (!config.noBackup) {
|
|
746
|
+
const backupPath = FontManager.createBackup(file, { noBackup: config.noBackup });
|
|
747
|
+
if (backupPath) {
|
|
748
|
+
message = `Backup: ${basename(backupPath)}`;
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Calculate table widths
|
|
753
|
+
const TABLE_WIDTH = 76;
|
|
754
|
+
const COL_WIDTHS = [4, 26, 12, 10, 22]; // #, Font Family, Type, Size, Used Chars
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Display help screen
|
|
758
|
+
*/
|
|
759
|
+
const showHelp = async () => {
|
|
760
|
+
console.clear();
|
|
761
|
+
log(`\n${colors.bright}${BOX.dtl}${BOX.dh.repeat(TABLE_WIDTH - 2)}${BOX.dtr}${colors.reset}`);
|
|
762
|
+
log(`${colors.bright}${BOX.dv}${colors.reset} ${"INTERACTIVE MODE HELP".padStart(38).padEnd(TABLE_WIDTH - 4)} ${colors.bright}${BOX.dv}${colors.reset}`);
|
|
763
|
+
log(`${colors.bright}${BOX.dbl}${BOX.dh.repeat(TABLE_WIDTH - 2)}${BOX.dbr}${colors.reset}`);
|
|
764
|
+
|
|
765
|
+
log(`\n${colors.cyan}Navigation (for long font lists):${colors.reset}`);
|
|
766
|
+
log(` ${colors.bright}j${colors.reset}/${colors.bright}↓${colors.reset} Scroll down one line`);
|
|
767
|
+
log(` ${colors.bright}k${colors.reset}/${colors.bright}↑${colors.reset} Scroll up one line`);
|
|
768
|
+
log(` ${colors.bright}n${colors.reset} Page down (next page)`);
|
|
769
|
+
log(` ${colors.bright}p${colors.reset} Page up (previous page)`);
|
|
770
|
+
log(` ${colors.bright}g${colors.reset} Jump to top of list`);
|
|
771
|
+
log(` ${colors.bright}G${colors.reset} Jump to bottom of list`);
|
|
772
|
+
|
|
773
|
+
log(`\n${colors.cyan}Main Menu Commands:${colors.reset}`);
|
|
774
|
+
log(` ${colors.bright}1-99${colors.reset} Select font by number`);
|
|
775
|
+
log(` ${colors.bright}a${colors.reset} Apply action to all fonts`);
|
|
776
|
+
log(` ${colors.bright}e${colors.reset} Embed all external fonts (subset)`);
|
|
777
|
+
log(` ${colors.bright}E${colors.reset} Embed all external fonts (full)`);
|
|
778
|
+
log(` ${colors.bright}/${colors.reset} Search/filter fonts by name`);
|
|
779
|
+
log(` ${colors.bright}c${colors.reset} Clear filter`);
|
|
780
|
+
log(` ${colors.bright}u${colors.reset} Undo last change`);
|
|
781
|
+
log(` ${colors.bright}i${colors.reset} Show SVG info summary`);
|
|
782
|
+
log(` ${colors.bright}s${colors.reset} Save and exit`);
|
|
783
|
+
log(` ${colors.bright}q${colors.reset} Quit without saving`);
|
|
784
|
+
log(` ${colors.bright}h${colors.reset}/${colors.bright}?${colors.reset} Show this help`);
|
|
785
|
+
|
|
786
|
+
log(`\n${colors.cyan}Per-Font Commands:${colors.reset}`);
|
|
787
|
+
log(` ${colors.bright}e${colors.reset} Embed font (subset - smaller file)`);
|
|
788
|
+
log(` ${colors.bright}E${colors.reset} Embed font (full - all glyphs)`);
|
|
789
|
+
log(` ${colors.bright}r${colors.reset} Replace with another font`);
|
|
790
|
+
log(` ${colors.bright}x${colors.reset} Extract font to file`);
|
|
791
|
+
log(` ${colors.bright}d${colors.reset} Delete font from SVG`);
|
|
792
|
+
log(` ${colors.bright}v${colors.reset} Validate external URL`);
|
|
793
|
+
log(` ${colors.bright}c${colors.reset} Copy font details to clipboard`);
|
|
794
|
+
log(` ${colors.bright}b${colors.reset} Back to font list`);
|
|
795
|
+
|
|
796
|
+
log(`\n${colors.cyan}Legend:${colors.reset}`);
|
|
797
|
+
log(` ${colors.green}●${colors.reset} Embedded ${colors.yellow}○${colors.reset} External ${colors.cyan}◆${colors.reset} System ${colors.dim}?${colors.reset} Unknown`);
|
|
798
|
+
|
|
799
|
+
log(`\n${colors.dim}Press Enter to return...${colors.reset}`);
|
|
800
|
+
await question("");
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Show SVG info summary
|
|
805
|
+
*/
|
|
806
|
+
const showInfo = async () => {
|
|
807
|
+
console.clear();
|
|
808
|
+
const fonts = FontManager.listFonts(doc);
|
|
809
|
+
|
|
810
|
+
log(`\n${colors.bright}${BOX.dtl}${BOX.dh.repeat(TABLE_WIDTH - 2)}${BOX.dtr}${colors.reset}`);
|
|
811
|
+
log(`${colors.bright}${BOX.dv}${colors.reset} ${"SVG INFORMATION".padStart(38).padEnd(TABLE_WIDTH - 4)} ${colors.bright}${BOX.dv}${colors.reset}`);
|
|
812
|
+
log(`${colors.bright}${BOX.dbl}${BOX.dh.repeat(TABLE_WIDTH - 2)}${BOX.dbr}${colors.reset}`);
|
|
813
|
+
|
|
814
|
+
log(`\n${colors.cyan}File:${colors.reset} ${basename(file)}`);
|
|
815
|
+
log(`${colors.cyan}Path:${colors.reset} ${file}`);
|
|
816
|
+
|
|
817
|
+
const svgContent = serializeSVG(doc);
|
|
818
|
+
log(`${colors.cyan}Size:${colors.reset} ${formatSize(svgContent.length)}`);
|
|
819
|
+
|
|
820
|
+
log(`\n${colors.cyan}Font Summary:${colors.reset}`);
|
|
821
|
+
const embedded = fonts.filter((f) => f.type === "embedded").length;
|
|
822
|
+
const external = fonts.filter((f) => f.type === "external").length;
|
|
823
|
+
const system = fonts.filter((f) => f.type === "system").length;
|
|
824
|
+
const unknown = fonts.filter((f) => f.type === "unknown").length;
|
|
825
|
+
|
|
826
|
+
log(` Total fonts: ${fonts.length}`);
|
|
827
|
+
log(` ${colors.green}●${colors.reset} Embedded: ${embedded}`);
|
|
828
|
+
log(` ${colors.yellow}○${colors.reset} External: ${external}`);
|
|
829
|
+
log(` ${colors.cyan}◆${colors.reset} System: ${system}`);
|
|
830
|
+
if (unknown > 0) log(` ${colors.dim}?${colors.reset} Unknown: ${unknown}`);
|
|
831
|
+
|
|
832
|
+
// Calculate total embedded font size
|
|
833
|
+
const totalEmbedSize = fonts
|
|
834
|
+
.filter((f) => f.type === "embedded" && f.size)
|
|
835
|
+
.reduce((sum, f) => sum + f.size, 0);
|
|
836
|
+
|
|
837
|
+
if (totalEmbedSize > 0) {
|
|
838
|
+
log(`\n${colors.cyan}Embedded Font Size:${colors.reset} ${formatSize(totalEmbedSize)}`);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
// List unique characters used
|
|
842
|
+
const allChars = new Set();
|
|
843
|
+
fonts.forEach((f) => {
|
|
844
|
+
if (f.usedChars) {
|
|
845
|
+
for (const c of f.usedChars) allChars.add(c);
|
|
846
|
+
}
|
|
847
|
+
});
|
|
848
|
+
log(`${colors.cyan}Unique Characters:${colors.reset} ${allChars.size}`);
|
|
849
|
+
|
|
850
|
+
log(`\n${colors.dim}Press Enter to return...${colors.reset}`);
|
|
851
|
+
await question("");
|
|
852
|
+
};
|
|
853
|
+
|
|
854
|
+
/**
|
|
855
|
+
* Main interactive loop
|
|
856
|
+
*/
|
|
857
|
+
while (true) {
|
|
858
|
+
console.clear();
|
|
859
|
+
const fonts = FontManager.listFonts(doc);
|
|
860
|
+
|
|
861
|
+
// Apply filter
|
|
862
|
+
const filteredFonts = filterText
|
|
863
|
+
? fonts.filter((f) => f.family.toLowerCase().includes(filterText.toLowerCase()))
|
|
864
|
+
: fonts;
|
|
865
|
+
|
|
866
|
+
// Header
|
|
867
|
+
log(`${colors.bright}${BOX.dtl}${BOX.dh.repeat(TABLE_WIDTH - 2)}${BOX.dtr}${colors.reset}`);
|
|
868
|
+
const title = ` SVGFONTS INTERACTIVE `;
|
|
869
|
+
const modIndicator = modified ? `${colors.yellow}[MODIFIED]${colors.reset}` : "";
|
|
870
|
+
const headerText = `${title}${modIndicator}`.padEnd(TABLE_WIDTH - 4 + (modified ? 14 : 0));
|
|
871
|
+
log(`${colors.bright}${BOX.dv}${colors.reset}${headerText}${colors.bright}${BOX.dv}${colors.reset}`);
|
|
872
|
+
log(`${colors.bright}${BOX.dml}${BOX.dh.repeat(TABLE_WIDTH - 2)}${BOX.dmr}${colors.reset}`);
|
|
873
|
+
|
|
874
|
+
// File info row
|
|
875
|
+
const fileInfo = ` File: ${basename(file)} `;
|
|
876
|
+
const fontCount = ` Fonts: ${filteredFonts.length}${filterText ? ` (filtered)` : ``} `;
|
|
877
|
+
const undoCount = history.length > 0 ? ` Undo: ${history.length} ` : "";
|
|
878
|
+
log(
|
|
879
|
+
`${BOX.v}${fileInfo.padEnd(40)}${fontCount.padEnd(20)}${undoCount.padEnd(TABLE_WIDTH - 62)}${BOX.v}`
|
|
880
|
+
);
|
|
881
|
+
log(drawLine(TABLE_WIDTH, BOX.ml, BOX.h, BOX.mr));
|
|
882
|
+
|
|
883
|
+
// Filter indicator
|
|
884
|
+
if (filterText) {
|
|
885
|
+
log(`${BOX.v} ${colors.magenta}Filter: "${filterText}"${colors.reset}${" ".repeat(TABLE_WIDTH - filterText.length - 14)}${BOX.v}`);
|
|
886
|
+
log(drawLine(TABLE_WIDTH, BOX.ml, BOX.h, BOX.mr));
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Column headers
|
|
890
|
+
log(
|
|
891
|
+
tableRow(
|
|
892
|
+
[
|
|
893
|
+
`${colors.cyan}#${colors.reset}`,
|
|
894
|
+
`${colors.cyan}Font Family${colors.reset}`,
|
|
895
|
+
`${colors.cyan}Type${colors.reset}`,
|
|
896
|
+
`${colors.cyan}Size${colors.reset}`,
|
|
897
|
+
`${colors.cyan}Used Chars${colors.reset}`,
|
|
898
|
+
],
|
|
899
|
+
COL_WIDTHS
|
|
900
|
+
)
|
|
901
|
+
);
|
|
902
|
+
log(drawLine(TABLE_WIDTH, BOX.ml, BOX.h, BOX.mr));
|
|
903
|
+
|
|
904
|
+
// Font rows with pagination
|
|
905
|
+
if (filteredFonts.length === 0) {
|
|
906
|
+
const emptyMsg = filterText ? "No fonts match filter" : "No fonts found";
|
|
907
|
+
log(`${BOX.v} ${colors.dim}${emptyMsg.padEnd(TABLE_WIDTH - 4)}${colors.reset} ${BOX.v}`);
|
|
908
|
+
} else {
|
|
909
|
+
// Ensure scroll offset is valid
|
|
910
|
+
const maxOffset = Math.max(0, filteredFonts.length - PAGE_SIZE);
|
|
911
|
+
if (scrollOffset > maxOffset) scrollOffset = maxOffset;
|
|
912
|
+
if (scrollOffset < 0) scrollOffset = 0;
|
|
913
|
+
|
|
914
|
+
// Show visible fonts based on scroll position
|
|
915
|
+
const visibleFonts = filteredFonts.slice(scrollOffset, scrollOffset + PAGE_SIZE);
|
|
916
|
+
const hasMore = filteredFonts.length > PAGE_SIZE;
|
|
917
|
+
|
|
918
|
+
// Show scroll indicator if there are fonts above
|
|
919
|
+
if (scrollOffset > 0) {
|
|
920
|
+
log(`${BOX.v} ${colors.dim} ↑ ${scrollOffset} more above...${" ".repeat(TABLE_WIDTH - 24)}${colors.reset} ${BOX.v}`);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
visibleFonts.forEach((font, visibleIdx) => {
|
|
924
|
+
const actualIdx = scrollOffset + visibleIdx;
|
|
925
|
+
const num = `${actualIdx + 1}.`;
|
|
926
|
+
const icon = fontTypeIcon(font.type);
|
|
927
|
+
const family = font.family.slice(0, 22);
|
|
928
|
+
const typeStr = `${icon} ${font.type}`;
|
|
929
|
+
const size = formatSize(font.size);
|
|
930
|
+
const chars = font.usedChars
|
|
931
|
+
? [...font.usedChars].slice(0, 18).join("") + (font.usedChars.size > 18 ? "..." : "")
|
|
932
|
+
: "-";
|
|
933
|
+
|
|
934
|
+
// Highlight selected font
|
|
935
|
+
const isSelected = actualIdx === selectedIdx;
|
|
936
|
+
const highlight = isSelected ? colors.bright : "";
|
|
937
|
+
const resetH = isSelected ? colors.reset : "";
|
|
938
|
+
const selectMarker = isSelected ? "▶" : " ";
|
|
939
|
+
|
|
940
|
+
log(
|
|
941
|
+
`${BOX.v}${selectMarker}${highlight}${num.padEnd(3)}${resetH} ${BOX.v} ${highlight}${family.padEnd(24)}${resetH} ${BOX.v} ${typeStr.padEnd(10 + 9)} ${BOX.v} ${size.padEnd(8)} ${BOX.v} ${chars.padEnd(20)} ${BOX.v}`
|
|
942
|
+
);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
// Show scroll indicator if there are fonts below
|
|
946
|
+
const remaining = filteredFonts.length - scrollOffset - PAGE_SIZE;
|
|
947
|
+
if (remaining > 0) {
|
|
948
|
+
log(`${BOX.v} ${colors.dim} ↓ ${remaining} more below...${" ".repeat(TABLE_WIDTH - 24)}${colors.reset} ${BOX.v}`);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Show pagination info if list is long
|
|
952
|
+
if (hasMore) {
|
|
953
|
+
const currentPage = Math.floor(scrollOffset / PAGE_SIZE) + 1;
|
|
954
|
+
const totalPages = Math.ceil(filteredFonts.length / PAGE_SIZE);
|
|
955
|
+
const pageInfo = `Page ${currentPage}/${totalPages}`;
|
|
956
|
+
log(drawLine(TABLE_WIDTH, BOX.ml, BOX.h, BOX.mr));
|
|
957
|
+
log(`${BOX.v} ${colors.dim}${pageInfo}${colors.reset}${" ".repeat(TABLE_WIDTH - pageInfo.length - 4)} ${BOX.v}`);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Footer
|
|
962
|
+
log(`${BOX.bl}${BOX.h.repeat(TABLE_WIDTH - 2)}${BOX.br}`);
|
|
963
|
+
|
|
964
|
+
// Status message
|
|
965
|
+
if (message) {
|
|
966
|
+
const msgColor =
|
|
967
|
+
messageType === "success"
|
|
968
|
+
? colors.green
|
|
969
|
+
: messageType === "error"
|
|
970
|
+
? colors.red
|
|
971
|
+
: messageType === "warn"
|
|
972
|
+
? colors.yellow
|
|
973
|
+
: colors.dim;
|
|
974
|
+
log(`\n${msgColor}${message}${colors.reset}`);
|
|
975
|
+
message = "";
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
// Action menu - show arrow keys if list is long
|
|
979
|
+
const hasMoreFonts = filteredFonts.length > PAGE_SIZE;
|
|
980
|
+
if (hasMoreFonts) {
|
|
981
|
+
log(`\n${colors.cyan}Navigation:${colors.reset} ${colors.dim}[↑/↓]${colors.reset} scroll ${colors.dim}[j/k]${colors.reset} up/down ${colors.dim}[PgUp/PgDn]${colors.reset} page ${colors.dim}[g/G]${colors.reset} top/bottom`);
|
|
982
|
+
}
|
|
983
|
+
log(`${colors.cyan}Commands:${colors.reset} ${colors.dim}[1-99]${colors.reset} select ${colors.dim}[a]${colors.reset} all ${colors.dim}[e/E]${colors.reset} embed ${colors.dim}[/]${colors.reset} filter ${colors.dim}[u]${colors.reset} undo ${colors.dim}[s]${colors.reset} save ${colors.dim}[q]${colors.reset} quit ${colors.dim}[h]${colors.reset} help`);
|
|
984
|
+
|
|
985
|
+
const choice = await question(`\n${colors.bright}>${colors.reset} `);
|
|
986
|
+
|
|
987
|
+
// Handle vim-style navigation keys
|
|
988
|
+
if (choice === "j" || choice === "\x1b[B") {
|
|
989
|
+
// Down arrow or j - scroll down
|
|
990
|
+
scrollOffset = Math.min(scrollOffset + 1, Math.max(0, filteredFonts.length - PAGE_SIZE));
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
if (choice === "k" || choice === "\x1b[A") {
|
|
994
|
+
// Up arrow or k - scroll up
|
|
995
|
+
scrollOffset = Math.max(0, scrollOffset - 1);
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
if (choice === "g") {
|
|
999
|
+
// Go to top
|
|
1000
|
+
scrollOffset = 0;
|
|
1001
|
+
message = "Jumped to top";
|
|
1002
|
+
messageType = "info";
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
if (choice === "G") {
|
|
1006
|
+
// Go to bottom
|
|
1007
|
+
scrollOffset = Math.max(0, filteredFonts.length - PAGE_SIZE);
|
|
1008
|
+
message = "Jumped to bottom";
|
|
1009
|
+
messageType = "info";
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
if (choice === "n" || choice === "\x1b[6~") {
|
|
1013
|
+
// Page down (n or PgDn)
|
|
1014
|
+
scrollOffset = Math.min(scrollOffset + PAGE_SIZE, Math.max(0, filteredFonts.length - PAGE_SIZE));
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
if (choice === "p" || choice === "\x1b[5~") {
|
|
1018
|
+
// Page up (p or PgUp)
|
|
1019
|
+
scrollOffset = Math.max(0, scrollOffset - PAGE_SIZE);
|
|
1020
|
+
continue;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Handle commands
|
|
1024
|
+
if (choice === "q" || choice === "Q") {
|
|
1025
|
+
if (modified) {
|
|
1026
|
+
const confirm = await question(
|
|
1027
|
+
`${colors.yellow}Unsaved changes! Quit anyway? [y/N]${colors.reset} `
|
|
1028
|
+
);
|
|
1029
|
+
if (confirm.toLowerCase() !== "y") continue;
|
|
1030
|
+
}
|
|
1031
|
+
log("\nExiting without saving.");
|
|
1032
|
+
rl.close();
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if (choice === "s" || choice === "S") {
|
|
1037
|
+
if (modified) {
|
|
1038
|
+
const output = serializeSVG(doc);
|
|
1039
|
+
const outputPath = config.output || file;
|
|
1040
|
+
writeFileSync(outputPath, output);
|
|
1041
|
+
logSuccess(`\nSaved: ${outputPath}`);
|
|
1042
|
+
} else {
|
|
1043
|
+
log("\nNo changes to save.");
|
|
1044
|
+
}
|
|
1045
|
+
rl.close();
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (choice === "h" || choice === "?" || choice === "H") {
|
|
1050
|
+
await showHelp();
|
|
1051
|
+
continue;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
if (choice === "i" || choice === "I") {
|
|
1055
|
+
await showInfo();
|
|
1056
|
+
continue;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (choice === "/") {
|
|
1060
|
+
const searchTerm = await question(`${colors.cyan}Filter fonts:${colors.reset} `);
|
|
1061
|
+
filterText = searchTerm.trim();
|
|
1062
|
+
message = filterText ? `Filtering by: "${filterText}"` : "Filter cleared";
|
|
1063
|
+
messageType = "info";
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
if (choice === "c" || choice === "C") {
|
|
1068
|
+
filterText = "";
|
|
1069
|
+
message = "Filter cleared";
|
|
1070
|
+
messageType = "info";
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
if (choice === "u" || choice === "U") {
|
|
1075
|
+
if (undo()) {
|
|
1076
|
+
modified = history.length > 0;
|
|
1077
|
+
message = "Undo successful";
|
|
1078
|
+
messageType = "success";
|
|
1079
|
+
} else {
|
|
1080
|
+
message = "Nothing to undo";
|
|
1081
|
+
messageType = "warn";
|
|
1082
|
+
}
|
|
1083
|
+
continue;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (choice === "e") {
|
|
1087
|
+
log("\nEmbedding all external fonts (subset)...");
|
|
1088
|
+
pushHistory();
|
|
1089
|
+
doc = await SVGToolbox.embedExternalDependencies(doc, {
|
|
1090
|
+
embedFonts: true,
|
|
1091
|
+
embedImages: false,
|
|
1092
|
+
embedCSS: true,
|
|
1093
|
+
subsetFonts: true,
|
|
1094
|
+
verbose: config.verbose,
|
|
1095
|
+
});
|
|
1096
|
+
modified = true;
|
|
1097
|
+
message = "All fonts embedded (subset)";
|
|
1098
|
+
messageType = "success";
|
|
1099
|
+
continue;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
if (choice === "E") {
|
|
1103
|
+
log("\nEmbedding all external fonts (full)...");
|
|
1104
|
+
pushHistory();
|
|
1105
|
+
doc = await SVGToolbox.embedExternalDependencies(doc, {
|
|
1106
|
+
embedFonts: true,
|
|
1107
|
+
embedImages: false,
|
|
1108
|
+
embedCSS: true,
|
|
1109
|
+
subsetFonts: false,
|
|
1110
|
+
verbose: config.verbose,
|
|
1111
|
+
});
|
|
1112
|
+
modified = true;
|
|
1113
|
+
message = "All fonts embedded (full)";
|
|
1114
|
+
messageType = "success";
|
|
1115
|
+
continue;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (choice === "a" || choice === "A") {
|
|
1119
|
+
log(`\n${colors.cyan}Apply to all fonts:${colors.reset}`);
|
|
1120
|
+
log(` ${colors.dim}[e]${colors.reset} Embed all (subset)`);
|
|
1121
|
+
log(` ${colors.dim}[E]${colors.reset} Embed all (full)`);
|
|
1122
|
+
log(` ${colors.dim}[r]${colors.reset} Replace all with mapping`);
|
|
1123
|
+
log(` ${colors.dim}[d]${colors.reset} Delete all fonts`);
|
|
1124
|
+
log(` ${colors.dim}[b]${colors.reset} Back`);
|
|
1125
|
+
|
|
1126
|
+
const subChoice = await question(`\n${colors.bright}>${colors.reset} `);
|
|
1127
|
+
|
|
1128
|
+
if (subChoice === "e" || subChoice === "E") {
|
|
1129
|
+
log("\nEmbedding all fonts...");
|
|
1130
|
+
pushHistory();
|
|
1131
|
+
doc = await SVGToolbox.embedExternalDependencies(doc, {
|
|
1132
|
+
embedFonts: true,
|
|
1133
|
+
embedImages: false,
|
|
1134
|
+
embedCSS: true,
|
|
1135
|
+
subsetFonts: subChoice === "e",
|
|
1136
|
+
verbose: config.verbose,
|
|
1137
|
+
});
|
|
1138
|
+
modified = true;
|
|
1139
|
+
message = `All fonts embedded (${subChoice === "e" ? "subset" : "full"})`;
|
|
1140
|
+
messageType = "success";
|
|
1141
|
+
} else if (subChoice === "r") {
|
|
1142
|
+
const mapFile = await question(`${colors.cyan}Replacement map file (or Enter for default):${colors.reset} `);
|
|
1143
|
+
const map = FontManager.loadReplacementMap(mapFile.trim() || null);
|
|
1144
|
+
if (map) {
|
|
1145
|
+
pushHistory();
|
|
1146
|
+
const result = FontManager.applyFontReplacements(doc, map.replacements);
|
|
1147
|
+
if (result.modified) {
|
|
1148
|
+
modified = true;
|
|
1149
|
+
message = `Replaced ${result.replaced.length} font(s)`;
|
|
1150
|
+
messageType = "success";
|
|
1151
|
+
} else {
|
|
1152
|
+
message = "No fonts matched replacement map";
|
|
1153
|
+
messageType = "warn";
|
|
1154
|
+
}
|
|
1155
|
+
} else {
|
|
1156
|
+
message = "No replacement map found";
|
|
1157
|
+
messageType = "error";
|
|
1158
|
+
}
|
|
1159
|
+
} else if (subChoice === "d") {
|
|
1160
|
+
const confirm = await question(
|
|
1161
|
+
`${colors.red}Delete ALL fonts? This cannot be undone! [y/N]${colors.reset} `
|
|
1162
|
+
);
|
|
1163
|
+
if (confirm.toLowerCase() === "y") {
|
|
1164
|
+
pushHistory();
|
|
1165
|
+
// Remove all @font-face rules and font references
|
|
1166
|
+
const styleEls = doc.querySelectorAll?.("style") || [];
|
|
1167
|
+
for (const styleEl of styleEls) {
|
|
1168
|
+
if (styleEl.textContent) {
|
|
1169
|
+
styleEl.textContent = styleEl.textContent.replace(/@font-face\s*\{[^}]*\}/gi, "");
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
modified = true;
|
|
1173
|
+
message = "All fonts deleted";
|
|
1174
|
+
messageType = "success";
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
// Font selection by number
|
|
1181
|
+
const fontIdx = parseInt(choice, 10) - 1;
|
|
1182
|
+
if (fontIdx >= 0 && fontIdx < filteredFonts.length) {
|
|
1183
|
+
const selectedFont = filteredFonts[fontIdx];
|
|
1184
|
+
selectedIdx = fontIdx;
|
|
1185
|
+
|
|
1186
|
+
console.clear();
|
|
1187
|
+
log(`\n${colors.bright}${BOX.dtl}${BOX.dh.repeat(TABLE_WIDTH - 2)}${BOX.dtr}${colors.reset}`);
|
|
1188
|
+
log(`${colors.bright}${BOX.dv}${colors.reset} FONT DETAILS${" ".repeat(TABLE_WIDTH - 16)}${colors.bright}${BOX.dv}${colors.reset}`);
|
|
1189
|
+
log(`${colors.bright}${BOX.dbl}${BOX.dh.repeat(TABLE_WIDTH - 2)}${BOX.dbr}${colors.reset}`);
|
|
1190
|
+
|
|
1191
|
+
log(`\n${colors.cyan}Family:${colors.reset} ${selectedFont.family}`);
|
|
1192
|
+
log(`${colors.cyan}Type:${colors.reset} ${fontTypeIcon(selectedFont.type)} ${selectedFont.type}`);
|
|
1193
|
+
if (selectedFont.source) {
|
|
1194
|
+
const srcDisplay =
|
|
1195
|
+
selectedFont.source.length > 60
|
|
1196
|
+
? selectedFont.source.slice(0, 57) + "..."
|
|
1197
|
+
: selectedFont.source;
|
|
1198
|
+
log(`${colors.cyan}Source:${colors.reset} ${srcDisplay}`);
|
|
1199
|
+
}
|
|
1200
|
+
if (selectedFont.size) {
|
|
1201
|
+
log(`${colors.cyan}Size:${colors.reset} ${formatSize(selectedFont.size)}`);
|
|
1202
|
+
}
|
|
1203
|
+
if (selectedFont.usedChars && selectedFont.usedChars.size > 0) {
|
|
1204
|
+
const chars = [...selectedFont.usedChars].sort().join("");
|
|
1205
|
+
const charsDisplay = chars.length > 60 ? chars.slice(0, 57) + "..." : chars;
|
|
1206
|
+
log(`${colors.cyan}Chars:${colors.reset} ${charsDisplay} (${selectedFont.usedChars.size} unique)`);
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
log(`\n${colors.cyan}Actions:${colors.reset}`);
|
|
1210
|
+
log(` ${colors.dim}[e]${colors.reset} Embed (subset) ${colors.dim}[E]${colors.reset} Embed (full)`);
|
|
1211
|
+
log(` ${colors.dim}[r]${colors.reset} Replace font ${colors.dim}[x]${colors.reset} Extract to file`);
|
|
1212
|
+
log(` ${colors.dim}[d]${colors.reset} Delete font ${colors.dim}[v]${colors.reset} Validate URL`);
|
|
1213
|
+
log(` ${colors.dim}[c]${colors.reset} Copy details ${colors.dim}[b]${colors.reset} Back to list`);
|
|
1214
|
+
|
|
1215
|
+
const action = await question(`\n${colors.bright}>${colors.reset} `);
|
|
1216
|
+
|
|
1217
|
+
if (action === "e" || action === "E") {
|
|
1218
|
+
log(`\n${colors.dim}Embedding ${selectedFont.family}...${colors.reset}`);
|
|
1219
|
+
pushHistory();
|
|
1220
|
+
doc = await SVGToolbox.embedExternalDependencies(doc, {
|
|
1221
|
+
embedFonts: true,
|
|
1222
|
+
embedImages: false,
|
|
1223
|
+
embedCSS: true,
|
|
1224
|
+
subsetFonts: action === "e",
|
|
1225
|
+
verbose: config.verbose,
|
|
1226
|
+
});
|
|
1227
|
+
modified = true;
|
|
1228
|
+
message = `Embedded: ${selectedFont.family} (${action === "e" ? "subset" : "full"})`;
|
|
1229
|
+
messageType = "success";
|
|
1230
|
+
} else if (action === "r") {
|
|
1231
|
+
const newFont = await question(`${colors.cyan}Replace with:${colors.reset} `);
|
|
1232
|
+
if (newFont.trim()) {
|
|
1233
|
+
pushHistory();
|
|
1234
|
+
const replaceResult = FontManager.applyFontReplacements(doc, {
|
|
1235
|
+
[selectedFont.family]: newFont.trim(),
|
|
1236
|
+
});
|
|
1237
|
+
if (replaceResult.modified) {
|
|
1238
|
+
modified = true;
|
|
1239
|
+
message = `Replaced: ${selectedFont.family} -> ${newFont.trim()}`;
|
|
1240
|
+
messageType = "success";
|
|
1241
|
+
} else {
|
|
1242
|
+
message = "Font not found in document";
|
|
1243
|
+
messageType = "warn";
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
} else if (action === "x") {
|
|
1247
|
+
if (selectedFont.type === "embedded" && selectedFont.size) {
|
|
1248
|
+
const extractDir = await question(
|
|
1249
|
+
`${colors.cyan}Extract to directory [./fonts]:${colors.reset} `
|
|
1250
|
+
);
|
|
1251
|
+
const dir = extractDir.trim() || "./fonts";
|
|
1252
|
+
mkdirSync(dir, { recursive: true });
|
|
1253
|
+
|
|
1254
|
+
// Extract using svg-toolbox
|
|
1255
|
+
await SVGToolbox.exportEmbeddedResources(doc, {
|
|
1256
|
+
outputDir: dir,
|
|
1257
|
+
extractFonts: true,
|
|
1258
|
+
extractImages: false,
|
|
1259
|
+
});
|
|
1260
|
+
message = `Font extracted to: ${dir}`;
|
|
1261
|
+
messageType = "success";
|
|
1262
|
+
} else {
|
|
1263
|
+
message = "Only embedded fonts can be extracted";
|
|
1264
|
+
messageType = "warn";
|
|
1265
|
+
}
|
|
1266
|
+
} else if (action === "d") {
|
|
1267
|
+
const confirm = await question(
|
|
1268
|
+
`${colors.red}Delete ${selectedFont.family}? [y/N]${colors.reset} `
|
|
1269
|
+
);
|
|
1270
|
+
if (confirm.toLowerCase() === "y") {
|
|
1271
|
+
pushHistory();
|
|
1272
|
+
// Remove @font-face for this font
|
|
1273
|
+
const styleEls = doc.querySelectorAll?.("style") || [];
|
|
1274
|
+
for (const styleEl of styleEls) {
|
|
1275
|
+
if (styleEl.textContent) {
|
|
1276
|
+
const pattern = new RegExp(
|
|
1277
|
+
`@font-face\\s*\\{[^}]*font-family:\\s*['"]?${selectedFont.family.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"]?[^}]*\\}`,
|
|
1278
|
+
"gi"
|
|
1279
|
+
);
|
|
1280
|
+
styleEl.textContent = styleEl.textContent.replace(pattern, "");
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
modified = true;
|
|
1284
|
+
message = `Deleted: ${selectedFont.family}`;
|
|
1285
|
+
messageType = "success";
|
|
1286
|
+
}
|
|
1287
|
+
} else if (action === "v") {
|
|
1288
|
+
if (selectedFont.source && !selectedFont.source.startsWith("data:")) {
|
|
1289
|
+
log(`\n${colors.dim}Validating: ${selectedFont.source}${colors.reset}`);
|
|
1290
|
+
try {
|
|
1291
|
+
const response = await fetch(selectedFont.source, { method: "HEAD" });
|
|
1292
|
+
if (response.ok) {
|
|
1293
|
+
message = `Valid: ${response.status} ${response.statusText}`;
|
|
1294
|
+
messageType = "success";
|
|
1295
|
+
} else {
|
|
1296
|
+
message = `Invalid: HTTP ${response.status}`;
|
|
1297
|
+
messageType = "error";
|
|
1298
|
+
}
|
|
1299
|
+
} catch (err) {
|
|
1300
|
+
message = `Error: ${err.message}`;
|
|
1301
|
+
messageType = "error";
|
|
1302
|
+
}
|
|
1303
|
+
} else {
|
|
1304
|
+
message = "No external URL to validate";
|
|
1305
|
+
messageType = "warn";
|
|
1306
|
+
}
|
|
1307
|
+
} else if (action === "c") {
|
|
1308
|
+
// Copy font details to clipboard (platform-specific)
|
|
1309
|
+
const details = [
|
|
1310
|
+
`Font Family: ${selectedFont.family}`,
|
|
1311
|
+
`Type: ${selectedFont.type}`,
|
|
1312
|
+
selectedFont.source ? `Source: ${selectedFont.source}` : null,
|
|
1313
|
+
selectedFont.size ? `Size: ${formatSize(selectedFont.size)}` : null,
|
|
1314
|
+
selectedFont.usedChars
|
|
1315
|
+
? `Characters: ${[...selectedFont.usedChars].join("")}`
|
|
1316
|
+
: null,
|
|
1317
|
+
]
|
|
1318
|
+
.filter(Boolean)
|
|
1319
|
+
.join("\n");
|
|
1320
|
+
|
|
1321
|
+
try {
|
|
1322
|
+
// Try pbcopy (macOS), xclip (Linux), clip (Windows)
|
|
1323
|
+
const clipCmd =
|
|
1324
|
+
process.platform === "darwin"
|
|
1325
|
+
? "pbcopy"
|
|
1326
|
+
: process.platform === "win32"
|
|
1327
|
+
? "clip"
|
|
1328
|
+
: "xclip -selection clipboard";
|
|
1329
|
+
const { execSync } = await import("child_process");
|
|
1330
|
+
execSync(clipCmd, { input: details });
|
|
1331
|
+
message = "Font details copied to clipboard";
|
|
1332
|
+
messageType = "success";
|
|
1333
|
+
} catch {
|
|
1334
|
+
// Fallback: just show the details
|
|
1335
|
+
log(`\n${colors.dim}${details}${colors.reset}`);
|
|
1336
|
+
message = "Could not copy to clipboard (shown above)";
|
|
1337
|
+
messageType = "warn";
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
// 'b' or any other key returns to list
|
|
1341
|
+
selectedIdx = -1;
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// ============================================================================
|
|
1347
|
+
// MAIN
|
|
1348
|
+
// ============================================================================
|
|
1349
|
+
async function main() {
|
|
1350
|
+
config = parseArgs(process.argv.slice(2));
|
|
1351
|
+
|
|
1352
|
+
// Show help if no command and no inputs
|
|
1353
|
+
if (!config.command && config.inputs.length === 0) {
|
|
1354
|
+
console.log(HELP);
|
|
1355
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// Handle template command (no inputs needed)
|
|
1359
|
+
if (config.command === "template") {
|
|
1360
|
+
cmdTemplate();
|
|
1361
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
// Collect files
|
|
1365
|
+
const files = collectSvgFiles(config.inputs, config.recursive);
|
|
1366
|
+
|
|
1367
|
+
if (files.length === 0) {
|
|
1368
|
+
logError("No SVG files found");
|
|
1369
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
log(`Found ${files.length} SVG file(s)`);
|
|
1373
|
+
|
|
1374
|
+
// Execute command
|
|
1375
|
+
try {
|
|
1376
|
+
switch (config.command) {
|
|
1377
|
+
case "list":
|
|
1378
|
+
await cmdList(files);
|
|
1379
|
+
break;
|
|
1380
|
+
|
|
1381
|
+
case "embed":
|
|
1382
|
+
await cmdEmbed(files);
|
|
1383
|
+
break;
|
|
1384
|
+
|
|
1385
|
+
case "extract":
|
|
1386
|
+
await cmdExtract(files);
|
|
1387
|
+
break;
|
|
1388
|
+
|
|
1389
|
+
case "replace":
|
|
1390
|
+
await cmdReplace(files);
|
|
1391
|
+
break;
|
|
1392
|
+
|
|
1393
|
+
case "interactive":
|
|
1394
|
+
await cmdInteractive(files);
|
|
1395
|
+
break;
|
|
1396
|
+
|
|
1397
|
+
default:
|
|
1398
|
+
// Default to embed
|
|
1399
|
+
await cmdEmbed(files);
|
|
1400
|
+
}
|
|
1401
|
+
} catch (err) {
|
|
1402
|
+
logError(err.message);
|
|
1403
|
+
if (config.verbose) {
|
|
1404
|
+
console.error(err.stack);
|
|
1405
|
+
}
|
|
1406
|
+
process.exit(CONSTANTS.EXIT_ERROR);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
process.exit(CONSTANTS.EXIT_SUCCESS);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
main();
|