normalize-metrics 0.1.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/LICENSE +21 -0
- package/README.md +68 -0
- package/bin/normalize-metrics.js +17 -0
- package/dist/args.js +97 -0
- package/dist/config.js +99 -0
- package/dist/discover.js +70 -0
- package/dist/engine.js +65 -0
- package/dist/fs-exists.js +10 -0
- package/dist/glob.js +59 -0
- package/dist/index.js +220 -0
- package/dist/package.json +1 -0
- package/dist/paths.js +22 -0
- package/dist/progress.js +184 -0
- package/dist/report.js +45 -0
- package/dist/runtime.js +49 -0
- package/dist/types.js +1 -0
- package/lib/engine.py +319 -0
- package/package.json +66 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { basename, resolve } from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { HELP, modeOf, parseArgs } from "./args.js";
|
|
4
|
+
import { emptyConfig, findConfig, mergeConfig } from "./config.js";
|
|
5
|
+
import { discover, discoverFromInclude } from "./discover.js";
|
|
6
|
+
import { runEngine } from "./engine.js";
|
|
7
|
+
import { formatOffset, outputPath } from "./paths.js";
|
|
8
|
+
import { formatReports } from "./report.js";
|
|
9
|
+
import { ProgressStack } from "./progress.js";
|
|
10
|
+
import { EULA, invocationDir, packageVersion, resolvePython } from "./runtime.js";
|
|
11
|
+
import { pathExists } from "./fs-exists.js";
|
|
12
|
+
function phaseFromEngine(phase) {
|
|
13
|
+
if (phase === "rewriting")
|
|
14
|
+
return "rewriting";
|
|
15
|
+
if (phase === "writing" || phase === "done")
|
|
16
|
+
return "writing";
|
|
17
|
+
return "analyzing";
|
|
18
|
+
}
|
|
19
|
+
function destLabel(dest) {
|
|
20
|
+
return basename(dest);
|
|
21
|
+
}
|
|
22
|
+
async function processFile(file, options) {
|
|
23
|
+
const dest = outputPath(file, options.config, options.inPlace);
|
|
24
|
+
if (file.kind === "ttc") {
|
|
25
|
+
const reason = "TTC collections are skipped in v1";
|
|
26
|
+
options.stack.update(file.abs, { percent: 0, phase: "skipped", detail: reason });
|
|
27
|
+
return { status: "skipped", reason };
|
|
28
|
+
}
|
|
29
|
+
options.stack.update(file.abs, { percent: 4, phase: "analyzing", detail: "analyzing" });
|
|
30
|
+
try {
|
|
31
|
+
const inspect = options.mode !== "write";
|
|
32
|
+
const result = await runEngine({
|
|
33
|
+
python: options.python,
|
|
34
|
+
input: file.abs,
|
|
35
|
+
output: inspect ? undefined : dest,
|
|
36
|
+
inspect,
|
|
37
|
+
skipIfGood: options.mode === "write",
|
|
38
|
+
onProgress: (event) => {
|
|
39
|
+
options.stack.update(file.abs, {
|
|
40
|
+
percent: event.percent,
|
|
41
|
+
phase: phaseFromEngine(event.phase),
|
|
42
|
+
detail: event.phase,
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
if (result.skip) {
|
|
47
|
+
options.stack.update(file.abs, { percent: 0, phase: "skipped", detail: result.skip });
|
|
48
|
+
return { status: "skipped", reason: result.skip };
|
|
49
|
+
}
|
|
50
|
+
return finishResult(file.abs, dest, result, options.mode, options.stack);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
const message = error instanceof Error ? error.message.split("\n").at(-1) || "failed" : "failed";
|
|
54
|
+
options.stack.update(file.abs, { percent: 0, phase: "failed", detail: message });
|
|
55
|
+
return { status: "failed", message };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function alreadyGoodDetail(result) {
|
|
59
|
+
return `already good ${formatOffset(result.before?.offset ?? result.after?.offset)}`;
|
|
60
|
+
}
|
|
61
|
+
function finishResult(id, dest, result, mode, stack) {
|
|
62
|
+
const offset = formatOffset(result.before?.offset);
|
|
63
|
+
if (result.off === false) {
|
|
64
|
+
stack.update(id, { percent: 100, phase: "ok", detail: alreadyGoodDetail(result) });
|
|
65
|
+
return { status: "ok", result };
|
|
66
|
+
}
|
|
67
|
+
if (mode === "check") {
|
|
68
|
+
stack.update(id, { percent: 100, phase: "off", detail: `off ${offset}` });
|
|
69
|
+
return { status: "off", result };
|
|
70
|
+
}
|
|
71
|
+
if (mode === "dry-run") {
|
|
72
|
+
stack.update(id, {
|
|
73
|
+
percent: 100,
|
|
74
|
+
phase: "done",
|
|
75
|
+
detail: `→ ${destLabel(dest)} ${offset} → ${formatOffset(result.after?.offset)}`,
|
|
76
|
+
});
|
|
77
|
+
return { status: "would-write", dest, result };
|
|
78
|
+
}
|
|
79
|
+
stack.update(id, {
|
|
80
|
+
percent: 100,
|
|
81
|
+
phase: "done",
|
|
82
|
+
detail: `→ ${destLabel(dest)}`,
|
|
83
|
+
});
|
|
84
|
+
return { status: "wrote", dest, result };
|
|
85
|
+
}
|
|
86
|
+
function summarize(outcomes, mode) {
|
|
87
|
+
const counts = {
|
|
88
|
+
wrote: outcomes.filter((item) => item.status === "wrote").length,
|
|
89
|
+
would: outcomes.filter((item) => item.status === "would-write").length,
|
|
90
|
+
ok: outcomes.filter((item) => item.status === "ok").length,
|
|
91
|
+
off: outcomes.filter((item) => item.status === "off").length,
|
|
92
|
+
skipped: outcomes.filter((item) => item.status === "skipped").length,
|
|
93
|
+
failed: outcomes.filter((item) => item.status === "failed").length,
|
|
94
|
+
};
|
|
95
|
+
const parts = [];
|
|
96
|
+
if (mode === "write") {
|
|
97
|
+
parts.push(`${counts.wrote} wrote`);
|
|
98
|
+
if (counts.ok)
|
|
99
|
+
parts.push(`${counts.ok} already good`);
|
|
100
|
+
}
|
|
101
|
+
if (mode === "dry-run") {
|
|
102
|
+
parts.push(`${counts.would} would write`);
|
|
103
|
+
if (counts.ok)
|
|
104
|
+
parts.push(`${counts.ok} already good`);
|
|
105
|
+
}
|
|
106
|
+
if (mode === "check") {
|
|
107
|
+
parts.push(`${counts.off} off`);
|
|
108
|
+
parts.push(`${counts.ok} ok`);
|
|
109
|
+
}
|
|
110
|
+
if (counts.skipped)
|
|
111
|
+
parts.push(`${counts.skipped} skipped`);
|
|
112
|
+
if (counts.failed)
|
|
113
|
+
parts.push(`${counts.failed} failed`);
|
|
114
|
+
return parts.join(" · ");
|
|
115
|
+
}
|
|
116
|
+
function exitCode(outcomes, mode) {
|
|
117
|
+
if (outcomes.some((item) => item.status === "failed"))
|
|
118
|
+
return 1;
|
|
119
|
+
if (mode === "check" && outcomes.some((item) => item.status === "off"))
|
|
120
|
+
return 1;
|
|
121
|
+
if (outcomes.length === 0)
|
|
122
|
+
return 1;
|
|
123
|
+
return 0;
|
|
124
|
+
}
|
|
125
|
+
async function resolveTargets(args, cwd, config) {
|
|
126
|
+
if (args.path) {
|
|
127
|
+
const target = resolve(cwd, args.path);
|
|
128
|
+
if (!(await pathExists(target))) {
|
|
129
|
+
throw new Error(`Not found: ${args.path}`);
|
|
130
|
+
}
|
|
131
|
+
return discover(target, config);
|
|
132
|
+
}
|
|
133
|
+
if (config.include.length) {
|
|
134
|
+
return discoverFromInclude(cwd, config);
|
|
135
|
+
}
|
|
136
|
+
throw new Error("Point it at a file or a folder. See --help.");
|
|
137
|
+
}
|
|
138
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
139
|
+
let args;
|
|
140
|
+
try {
|
|
141
|
+
args = parseArgs(argv);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
console.error(error instanceof Error ? error.message : error);
|
|
145
|
+
return 2;
|
|
146
|
+
}
|
|
147
|
+
if (args.help) {
|
|
148
|
+
process.stdout.write(HELP);
|
|
149
|
+
return 0;
|
|
150
|
+
}
|
|
151
|
+
if (args.version) {
|
|
152
|
+
process.stdout.write(`${await packageVersion()}\n`);
|
|
153
|
+
return 0;
|
|
154
|
+
}
|
|
155
|
+
const cwd = invocationDir();
|
|
156
|
+
let config = emptyConfig();
|
|
157
|
+
try {
|
|
158
|
+
config = mergeConfig(await findConfig(cwd, args.configPath), args, cwd);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
console.error(error instanceof Error ? error.message : error);
|
|
162
|
+
return 2;
|
|
163
|
+
}
|
|
164
|
+
if (!config.suffix && !args.inPlace) {
|
|
165
|
+
console.error("A suffix is required unless you pass --in-place. Default is -normalized.");
|
|
166
|
+
return 2;
|
|
167
|
+
}
|
|
168
|
+
let files;
|
|
169
|
+
try {
|
|
170
|
+
files = await resolveTargets(args, cwd, config);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
console.error(error instanceof Error ? error.message : error);
|
|
174
|
+
return 1;
|
|
175
|
+
}
|
|
176
|
+
if (!files.length) {
|
|
177
|
+
console.error("No .otf, .ttf, .woff, or .woff2 files found.");
|
|
178
|
+
return 1;
|
|
179
|
+
}
|
|
180
|
+
let python;
|
|
181
|
+
try {
|
|
182
|
+
python = await resolvePython();
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
console.error(error instanceof Error ? error.message : error);
|
|
186
|
+
return 1;
|
|
187
|
+
}
|
|
188
|
+
const mode = modeOf(args);
|
|
189
|
+
const stack = new ProgressStack();
|
|
190
|
+
const onInterrupt = () => {
|
|
191
|
+
stack.restore();
|
|
192
|
+
process.exit(130);
|
|
193
|
+
};
|
|
194
|
+
process.on("SIGINT", onInterrupt);
|
|
195
|
+
process.on("SIGTERM", onInterrupt);
|
|
196
|
+
stack.identify(files.map((file) => ({ id: file.abs, label: files.length > 1 ? file.rel : file.name })), EULA);
|
|
197
|
+
const outcomes = [];
|
|
198
|
+
for (const file of files) {
|
|
199
|
+
outcomes.push(await processFile(file, { python, mode, inPlace: args.inPlace, config, stack }));
|
|
200
|
+
}
|
|
201
|
+
await stack.finish();
|
|
202
|
+
process.off("SIGINT", onInterrupt);
|
|
203
|
+
process.off("SIGTERM", onInterrupt);
|
|
204
|
+
const reports = formatReports(outcomes);
|
|
205
|
+
if (reports)
|
|
206
|
+
process.stdout.write(`\n${reports}\n`);
|
|
207
|
+
const summary = summarize(outcomes, mode);
|
|
208
|
+
if (summary) {
|
|
209
|
+
const stream = process.stderr.isTTY ? process.stderr : process.stdout;
|
|
210
|
+
stream.write(`\n${summary}\n`);
|
|
211
|
+
}
|
|
212
|
+
return exitCode(outcomes, mode);
|
|
213
|
+
}
|
|
214
|
+
const invoked = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
215
|
+
if (invoked) {
|
|
216
|
+
main().then((code) => process.exit(code), (error) => {
|
|
217
|
+
console.error(error instanceof Error ? error.message : error);
|
|
218
|
+
process.exit(1);
|
|
219
|
+
});
|
|
220
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { basename, dirname, extname, join } from "node:path";
|
|
2
|
+
export function outputPath(file, config, inPlace) {
|
|
3
|
+
if (inPlace)
|
|
4
|
+
return file.abs;
|
|
5
|
+
const ext = extname(file.name);
|
|
6
|
+
const stem = basename(file.name, ext);
|
|
7
|
+
const named = `${stem}${config.suffix}${ext}`;
|
|
8
|
+
if (config.outDir) {
|
|
9
|
+
return join(config.outDir, dirname(file.rel), named);
|
|
10
|
+
}
|
|
11
|
+
return join(dirname(file.abs), named);
|
|
12
|
+
}
|
|
13
|
+
export function formatOffset(value) {
|
|
14
|
+
if (value == null || Number.isNaN(value))
|
|
15
|
+
return "";
|
|
16
|
+
const abs = Math.abs(value).toFixed(1);
|
|
17
|
+
if (value > 0)
|
|
18
|
+
return `+${abs}‰`;
|
|
19
|
+
if (value < 0)
|
|
20
|
+
return `−${abs}‰`;
|
|
21
|
+
return "0‰";
|
|
22
|
+
}
|
package/dist/progress.js
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { styleText } from "node:util";
|
|
2
|
+
const BAR_WIDTH = 20;
|
|
3
|
+
const HIDE_CURSOR = "\x1b[?25l";
|
|
4
|
+
const SHOW_CURSOR = "\x1b[?25h";
|
|
5
|
+
const CLEAR_LINE = "\x1b[2K";
|
|
6
|
+
export function renderBar(percent, width = BAR_WIDTH) {
|
|
7
|
+
const clamped = Math.max(0, Math.min(100, percent));
|
|
8
|
+
const filled = Math.round((clamped / 100) * width);
|
|
9
|
+
return "-".repeat(filled) + "·".repeat(width - filled);
|
|
10
|
+
}
|
|
11
|
+
export function formatPercent(percent) {
|
|
12
|
+
return `${String(Math.round(Math.max(0, Math.min(100, percent)))).padStart(3, " ")}%`;
|
|
13
|
+
}
|
|
14
|
+
function truncate(value, width) {
|
|
15
|
+
if (value.length <= width)
|
|
16
|
+
return value;
|
|
17
|
+
if (width <= 1)
|
|
18
|
+
return "…";
|
|
19
|
+
return `${value.slice(0, width - 1)}…`;
|
|
20
|
+
}
|
|
21
|
+
function paint(text, phase, tty) {
|
|
22
|
+
if (!tty)
|
|
23
|
+
return text;
|
|
24
|
+
if (phase === "queued" || phase === "skipped")
|
|
25
|
+
return styleText("dim", text);
|
|
26
|
+
if (phase === "failed" || phase === "off")
|
|
27
|
+
return styleText("red", text);
|
|
28
|
+
if (phase === "done" || phase === "ok")
|
|
29
|
+
return styleText("green", text);
|
|
30
|
+
return text;
|
|
31
|
+
}
|
|
32
|
+
export function formatRow(row, nameWidth, columns, tty = false) {
|
|
33
|
+
const name = truncate(row.label, nameWidth).padEnd(nameWidth, " ");
|
|
34
|
+
const budget = Math.max(40, columns);
|
|
35
|
+
if (row.phase === "skipped" || row.phase === "failed") {
|
|
36
|
+
const detail = truncate(row.detail, Math.max(8, budget - nameWidth - 12));
|
|
37
|
+
return paint(`${name} ${row.phase.padEnd(7, " ")} ${detail}`, row.phase, tty);
|
|
38
|
+
}
|
|
39
|
+
const bar = renderBar(row.percent);
|
|
40
|
+
const pct = formatPercent(row.percent);
|
|
41
|
+
const used = nameWidth + 2 + BAR_WIDTH + 2 + pct.length;
|
|
42
|
+
const detail = row.detail ? ` ${truncate(row.detail, Math.max(0, budget - used - 2))}` : "";
|
|
43
|
+
return paint(`${name} ${bar} ${pct}${detail}`, row.phase, tty);
|
|
44
|
+
}
|
|
45
|
+
function nameWidthOf(rows, columns) {
|
|
46
|
+
const longest = rows.reduce((max, row) => Math.max(max, row.label.length), 0);
|
|
47
|
+
const usable = Math.max(columns, 80);
|
|
48
|
+
return Math.min(Math.max(longest, 8), Math.max(16, usable - BAR_WIDTH - 16));
|
|
49
|
+
}
|
|
50
|
+
export class ProgressStack {
|
|
51
|
+
rows = new Map();
|
|
52
|
+
order = [];
|
|
53
|
+
stream;
|
|
54
|
+
tty;
|
|
55
|
+
painted = 0;
|
|
56
|
+
timer = null;
|
|
57
|
+
displayed = new Map();
|
|
58
|
+
targets = new Map();
|
|
59
|
+
warning = "";
|
|
60
|
+
constructor(stream = process.stderr) {
|
|
61
|
+
this.stream = stream;
|
|
62
|
+
this.tty = Boolean(stream.isTTY);
|
|
63
|
+
}
|
|
64
|
+
columns() {
|
|
65
|
+
const reported = this.stream.columns ?? 0;
|
|
66
|
+
return reported >= 60 ? reported : 80;
|
|
67
|
+
}
|
|
68
|
+
identify(files, warning) {
|
|
69
|
+
this.warning = warning;
|
|
70
|
+
for (const file of files) {
|
|
71
|
+
this.order.push(file.id);
|
|
72
|
+
this.rows.set(file.id, {
|
|
73
|
+
id: file.id,
|
|
74
|
+
label: file.label,
|
|
75
|
+
percent: 0,
|
|
76
|
+
phase: "queued",
|
|
77
|
+
detail: "",
|
|
78
|
+
});
|
|
79
|
+
this.displayed.set(file.id, 0);
|
|
80
|
+
this.targets.set(file.id, 0);
|
|
81
|
+
}
|
|
82
|
+
if (this.tty) {
|
|
83
|
+
this.stream.write(HIDE_CURSOR);
|
|
84
|
+
if (this.warning)
|
|
85
|
+
this.stream.write(`${styleText("dim", this.warning)}\n\n`);
|
|
86
|
+
this.paint(true);
|
|
87
|
+
this.timer = setInterval(() => this.tick(), 50);
|
|
88
|
+
}
|
|
89
|
+
else if (this.warning) {
|
|
90
|
+
this.stream.write(`${this.warning}\n`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
update(id, patch) {
|
|
94
|
+
const row = this.rows.get(id);
|
|
95
|
+
if (!row)
|
|
96
|
+
return;
|
|
97
|
+
if (patch.phase)
|
|
98
|
+
row.phase = patch.phase;
|
|
99
|
+
if (patch.detail != null)
|
|
100
|
+
row.detail = patch.detail;
|
|
101
|
+
if (patch.percent != null) {
|
|
102
|
+
row.percent = patch.percent;
|
|
103
|
+
this.targets.set(id, patch.percent);
|
|
104
|
+
if (!this.tty)
|
|
105
|
+
this.displayed.set(id, patch.percent);
|
|
106
|
+
}
|
|
107
|
+
if (this.tty)
|
|
108
|
+
this.paint(false);
|
|
109
|
+
else if (patch.phase && ["done", "skipped", "failed", "off", "ok"].includes(patch.phase)) {
|
|
110
|
+
this.stream.write(`${formatRow({ ...row, percent: this.displayed.get(id) ?? row.percent }, row.label.length, 80, false)}\n`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async finish() {
|
|
114
|
+
if (!this.tty)
|
|
115
|
+
return;
|
|
116
|
+
const started = Date.now();
|
|
117
|
+
while (Date.now() - started < 800 && this.needsEase()) {
|
|
118
|
+
this.tick();
|
|
119
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
120
|
+
}
|
|
121
|
+
for (const id of this.order) {
|
|
122
|
+
const row = this.rows.get(id);
|
|
123
|
+
if (row)
|
|
124
|
+
this.displayed.set(id, row.percent);
|
|
125
|
+
}
|
|
126
|
+
this.paint(false);
|
|
127
|
+
if (this.timer)
|
|
128
|
+
clearInterval(this.timer);
|
|
129
|
+
this.timer = null;
|
|
130
|
+
this.stream.write(SHOW_CURSOR);
|
|
131
|
+
}
|
|
132
|
+
restore() {
|
|
133
|
+
if (this.timer)
|
|
134
|
+
clearInterval(this.timer);
|
|
135
|
+
this.timer = null;
|
|
136
|
+
if (this.tty)
|
|
137
|
+
this.stream.write(SHOW_CURSOR);
|
|
138
|
+
}
|
|
139
|
+
needsEase() {
|
|
140
|
+
return this.order.some((id) => {
|
|
141
|
+
const shown = this.displayed.get(id) ?? 0;
|
|
142
|
+
const target = this.targets.get(id) ?? 0;
|
|
143
|
+
return Math.abs(target - shown) > 0.6;
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
tick() {
|
|
147
|
+
let moved = false;
|
|
148
|
+
for (const id of this.order) {
|
|
149
|
+
const shown = this.displayed.get(id) ?? 0;
|
|
150
|
+
const target = this.targets.get(id) ?? 0;
|
|
151
|
+
if (Math.abs(target - shown) < 0.4) {
|
|
152
|
+
if (shown !== target) {
|
|
153
|
+
this.displayed.set(id, target);
|
|
154
|
+
moved = true;
|
|
155
|
+
}
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const next = shown + (target - shown) * 0.28;
|
|
159
|
+
this.displayed.set(id, next);
|
|
160
|
+
moved = true;
|
|
161
|
+
}
|
|
162
|
+
if (moved)
|
|
163
|
+
this.paint(false);
|
|
164
|
+
}
|
|
165
|
+
snapshot() {
|
|
166
|
+
return this.order.map((id) => {
|
|
167
|
+
const row = this.rows.get(id);
|
|
168
|
+
return { ...row, percent: this.displayed.get(id) ?? row.percent };
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
paint(first) {
|
|
172
|
+
const columns = this.columns();
|
|
173
|
+
const rows = this.snapshot();
|
|
174
|
+
const nameWidth = nameWidthOf(rows, columns);
|
|
175
|
+
const lines = rows.map((row) => formatRow(row, nameWidth, columns, true));
|
|
176
|
+
if (!first && this.painted > 0) {
|
|
177
|
+
this.stream.write(`\x1b[${this.painted}A`);
|
|
178
|
+
}
|
|
179
|
+
for (const line of lines) {
|
|
180
|
+
this.stream.write(`${CLEAR_LINE}${line}\n`);
|
|
181
|
+
}
|
|
182
|
+
this.painted = lines.length;
|
|
183
|
+
}
|
|
184
|
+
}
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const COL = 32;
|
|
2
|
+
function usedHeight(metrics) {
|
|
3
|
+
return metrics.ascent + Math.abs(metrics.descent) + metrics.lineGap;
|
|
4
|
+
}
|
|
5
|
+
function formatPercent(value) {
|
|
6
|
+
const rounded = Math.round(Math.abs(value) * 10) / 10;
|
|
7
|
+
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
8
|
+
}
|
|
9
|
+
function leadingChange(before, after) {
|
|
10
|
+
const from = usedHeight(before);
|
|
11
|
+
const to = usedHeight(after);
|
|
12
|
+
if (from <= 0 || Math.abs(to - from) / from < 0.0005) {
|
|
13
|
+
return "Leading change: unchanged";
|
|
14
|
+
}
|
|
15
|
+
const percent = ((to - from) / from) * 100;
|
|
16
|
+
const label = percent > 0 ? "bigger" : "smaller";
|
|
17
|
+
return `Leading change: ${formatPercent(percent)}% ${label}`;
|
|
18
|
+
}
|
|
19
|
+
function row(left, right) {
|
|
20
|
+
return `${left.padEnd(COL, " ")}| ${right}`;
|
|
21
|
+
}
|
|
22
|
+
export function formatReport(result) {
|
|
23
|
+
const before = result.before;
|
|
24
|
+
const after = result.after;
|
|
25
|
+
if (!before || !after)
|
|
26
|
+
return null;
|
|
27
|
+
const name = result.family?.trim() || "Font";
|
|
28
|
+
return [
|
|
29
|
+
name,
|
|
30
|
+
row("Before", "After"),
|
|
31
|
+
row(`Centered: ${before.centered}%`, `Centered: ${after.centered}%`),
|
|
32
|
+
leadingChange(before, after),
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
export function formatReports(outcomes) {
|
|
36
|
+
const blocks = [];
|
|
37
|
+
for (const item of outcomes) {
|
|
38
|
+
if (!("result" in item) || !item.result)
|
|
39
|
+
continue;
|
|
40
|
+
const block = formatReport(item.result);
|
|
41
|
+
if (block)
|
|
42
|
+
blocks.push(block);
|
|
43
|
+
}
|
|
44
|
+
return blocks.join("\n\n");
|
|
45
|
+
}
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { readFile } from "node:fs/promises";
|
|
5
|
+
export const EULA = "Rewriting a licensed font and redistributing the result may violate the EULA. This tool does not legalize the file.";
|
|
6
|
+
export function packageRoot() {
|
|
7
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
}
|
|
9
|
+
/** Directory the user ran the command from. npm scripts otherwise start at the package root. */
|
|
10
|
+
export function invocationDir() {
|
|
11
|
+
return process.env.INIT_CWD || process.cwd();
|
|
12
|
+
}
|
|
13
|
+
export function enginePath() {
|
|
14
|
+
return join(packageRoot(), "lib", "engine.py");
|
|
15
|
+
}
|
|
16
|
+
export async function packageVersion() {
|
|
17
|
+
const raw = await readFile(join(packageRoot(), "package.json"), "utf8");
|
|
18
|
+
return JSON.parse(raw).version ?? "0.0.0";
|
|
19
|
+
}
|
|
20
|
+
function run(command, args) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const child = spawn(command, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
23
|
+
let stdout = "";
|
|
24
|
+
let stderr = "";
|
|
25
|
+
child.stdout.on("data", (chunk) => {
|
|
26
|
+
stdout += chunk.toString();
|
|
27
|
+
});
|
|
28
|
+
child.stderr.on("data", (chunk) => {
|
|
29
|
+
stderr += chunk.toString();
|
|
30
|
+
});
|
|
31
|
+
child.on("error", reject);
|
|
32
|
+
child.on("close", (code) => {
|
|
33
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
export async function resolvePython() {
|
|
38
|
+
for (const command of ["python3", "python"]) {
|
|
39
|
+
try {
|
|
40
|
+
const probe = await run(command, ["-c", "import fontTools, sys; print(sys.executable)"]);
|
|
41
|
+
if (probe.code === 0 && probe.stdout.trim())
|
|
42
|
+
return command;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// try the next name
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw new Error("The engine needs python3 with fontTools (implementation detail — not a pip product install).");
|
|
49
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|