Move docs/ package mirror into document-navigator plugin
Relocate the ~6k-file docs tree from the repo root into plugins/document-navigator/docs/ so the plugin is self-contained and portable. docs/superpowers/ (specs and plans) stays at the repo root. Moved via cp+rm rather than git mv because directory rename() fails with EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the identical-content files at commit time. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
# @suaveplan/font-compress - BTF (Binary TypeFace) binary packing — packer-only since the 2026-05-17 split. Converts typeface.js JSON to compact Uint8Array using dictionary-based compression. Pairs with @suaveplan/font-decompress for the runtime unpack side.
|
||||
|
||||
## Summary
|
||||
|
||||
BTF (Binary TypeFace) binary packing and unpacking. Converts `TypefaceFont` (typeface.js JSON format) to a compact `Uint8Array` using dictionary-based integer compression, and reverses it.
|
||||
|
||||
Designed for Three.js / game-engine font pipelines where fonts must be shipped as efficient binary assets.
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
## Documentation tree
|
||||
|
||||
- [auto-boot](./src/bin/auto-boot.md) — Pure side-effect entry imported by `suaveplan-font-compress.ts` for the shebang-launch case. The only behaviour is: if…
|
||||
- [dispatch](./src/bin/dispatch.md) — Per-file orchestration extracted from the bin entry. Reads a single input file from disk, routes binary inputs (TTF/OTF…
|
||||
- [format-utils](./src/bin/format-utils.md) — Pure formatting helpers backing the operator-facing output of the `suaveplan-font-compress` bin. The helpers exist so t…
|
||||
- [parser](./src/bin/parser.md) — Bin-specific wrapper around `@suaveplan/cli`'s generic `parseArgs` that enforces the `suaveplan-font-compress` flag set…
|
||||
- [printer](./src/bin/printer.md) — Stdout / stderr renderers used by the `suaveplan-font-compress` bin. Owns the help screen, banner, per-file progress li…
|
||||
- [suaveplan-font-compress](./src/bin/suaveplan-font-compress.md) — Operator-facing CLI that wires `@suaveplan/font-convert` (`FontConverter` / `BatchConverter`) into this package's `Bina…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — `FontCompressError` is the structured error type for the `@suaveplan/font-compress` package, extending `SuaveplanError`…
|
||||
- [error-codes](./src/error-codes/error-codes.md) — `FontCompressError` is the structured error type for the `@suaveplan/font-compress` package, extending `SuaveplanError`…
|
||||
- [binary-font-packer](./src/packer/binary-font-packer/binary-font-packer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [binary-font-packer](./src/packer/binary-font-packer/binary-font-packer.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
|
||||
- [encode-string](./src/utils/encode-string.md) — Utilities for encoding and decoding length-prefixed UTF-8 strings in the BTF binary format. They provide the wire encod…
|
||||
- [parse-path](./src/utils/parse-path.md) — Parses a typeface.js outline string into an array of structured command objects. The typeface.js format encodes glyph o…
|
||||
<!-- end:auto-docs --> - [binary-font-packer](./src/packer/binary-font-packer/binary-font-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [encode-string](./src/utils/encode-string.md) — Utilities for encoding and decoding length-prefixed UTF-8 strings in the BTF binary format. They provide the wire encod…
|
||||
- [parse-path](./src/utils/parse-path.md) — Parses a typeface.js outline string into an array of structured command objects. The typeface.js format encodes glyph o…
|
||||
- [error-codes](./src/error-codes.md) — `FontCompressError` is the structured error type for the `@suaveplan/font-compress` package, extending `SuaveplanError`…
|
||||
- [binary-font-packer](./src/packer/binary-font-packer/binary-font-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [encode-string](./src/utils/encode-string.md) — Utilities for encoding and decoding length-prefixed UTF-8 strings in the BTF binary format. They provide the wire encod…
|
||||
- [parse-path](./src/utils/parse-path.md) — Parses a typeface.js outline string into an array of structured command objects. The typeface.js format encodes glyph o…
|
||||
- [error-codes](./src/error-codes.md) — `FontCompressError` is the structured error type for the `@suaveplan/font-compress` package, extending `SuaveplanError`…
|
||||
- [binary-font-packer](./src/packer/binary-font-packer/binary-font-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [encode-string](./src/utils/encode-string.md) — Utilities for encoding and decoding length-prefixed UTF-8 strings in the BTF binary format. They provide the wire encod…
|
||||
- [parse-path](./src/utils/parse-path.md) — Parses a typeface.js outline string into an array of structured command objects. The typeface.js format encodes glyph o…
|
||||
- [error-codes](./src/error-codes.md) — `FontCompressError` is the structured error type for the `@suaveplan/font-compress` package, extending `SuaveplanError`…
|
||||
- [binary-font-packer](./src/packer/binary-font-packer/binary-font-packer.md) — <!-- begin:auto-docs --> _(no submodules)_- [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_ - [binary-font-unpacker](./src/unpacker/binary-font-unpacker/binary-font-unpacker.md) — <!-- begin:auto-docs --> _(no submodules)_- [encode-string](./src/utils/encode-string.md) — Utilities for encoding and decoding length-prefixed UTF-8 strings in the BTF binary format. They provide the wire encod…
|
||||
- [parse-path](./src/utils/parse-path.md) — Parses a typeface.js outline string into an array of structured command objects. The typeface.js format encodes glyph o…
|
||||
- [error-codes](./src/error-codes.md) — `FontCompressError` is the structured error type for the `@suaveplan/font-compress` package, extending `SuaveplanError`…
|
||||
|
||||
## Packages
|
||||
|
||||
| Module | Description |
|
||||
|--------|-------------|
|
||||
| [`BinaryFontPacker`](src/packer/binary-font-packer/binary-font-packer.md) | Packs `TypefaceFont` → BTF `Uint8Array` |
|
||||
| [`BinaryFontUnpacker`](src/unpacker/binary-font-unpacker/binary-font-unpacker.md) | Unpacks BTF `Uint8Array` → `TypefaceFont` |
|
||||
| [`FontCompressError`](src/error-codes.md) | Structured error with stable codes |
|
||||
| [`parsePath`](src/utils/parse-path.md) | Tokenises typeface.js outline strings |
|
||||
| [`encodeString`/`decodeString`](src/utils/encode-string.md) | Length-prefixed UTF-8 string encoding |
|
||||
|
||||
## Install
|
||||
|
||||
```bash
|
||||
bun add @suaveplan/font-compress @suaveplan/font-convert
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```typescript
|
||||
import { BinaryFontPacker, BinaryFontUnpacker } from "@suaveplan/font-compress";
|
||||
import type { TypefaceFont } from "@suaveplan/font-convert";
|
||||
|
||||
const font: TypefaceFont = /* from @suaveplan/font-convert */;
|
||||
|
||||
// Pack to binary
|
||||
const packed = new BinaryFontPacker().pack(font);
|
||||
|
||||
// Unpack back to TypefaceFont
|
||||
const restored = new BinaryFontUnpacker().unpack(packed);
|
||||
```
|
||||
|
||||
## BTF Binary Format
|
||||
|
||||
```text
|
||||
[familyName: uint32-len + UTF-8]
|
||||
[cssFontWeight: uint32-len + UTF-8]
|
||||
[cssFontStyle: uint32-len + UTF-8]
|
||||
[reversed: uint8]
|
||||
[dictLen: uint32 LE]
|
||||
[dict: Int16Array × dictLen] ← all unique integer values
|
||||
[metricsIdxs: Uint16Array × 9] ← ascender,descender,ulPos,ulThk,res,yMin,xMin,yMax,xMax
|
||||
[glyphCount: uint32 LE]
|
||||
per-glyph:
|
||||
[charCode: uint32 LE]
|
||||
[indexCount: uint32 LE]
|
||||
[indexes: Uint16Array × indexCount] ← ha,x_min,x_max, then cmd_byte+coord_indexes
|
||||
```
|
||||
|
||||
Command bytes: `0=m`, `1=l`, `2=b(cubic)`, `3=q(quadratic)`, `4=z`.
|
||||
|
||||
## CLI
|
||||
|
||||
The package ships a `suaveplan-font-compress` bin that composes
|
||||
[`@suaveplan/font-convert`](../font-convert/README.md) (`FontConverter` /
|
||||
`BatchConverter`) with `BinaryFontPacker` to turn TTF/OTF/WOFF/WOFF2 (or
|
||||
already-converted typeface JSON) into compact `.btf` artefacts. See
|
||||
[`src/bin/suaveplan-font-compress.md`](./src/bin/suaveplan-font-compress.md)
|
||||
for the full flag reference, exit-code table, and in-process API.
|
||||
|
||||
End-to-end example:
|
||||
|
||||
```sh
|
||||
# Bake every font under ./fonts into ./out (mirrored layout, BTF default).
|
||||
bunx suaveplan-font-compress ./fonts -o ./out
|
||||
|
||||
# Inspect the typeface intermediate (skips the BTF pack step).
|
||||
bunx suaveplan-font-compress ./fonts/Inter.ttf -o ./debug --format=json
|
||||
|
||||
# Overwrite existing outputs.
|
||||
bunx suaveplan-font-compress ./fonts -o ./out --force
|
||||
```
|
||||
|
||||
Exit codes: `0` success, `1` per-file failure(s), `2` invalid CLI args,
|
||||
`3` missing input path.
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# auto-boot
|
||||
|
||||
## Purpose
|
||||
|
||||
Pure side-effect entry imported by `suaveplan-font-compress.ts` for the
|
||||
shebang-launch case. The only behaviour is: if the running file is the
|
||||
`dist/bin/suaveplan-font-compress.js` binary (detected by the `argv[1]`
|
||||
basename), dynamically import the bin entry and run `main(process.argv.slice(2))`,
|
||||
then `process.exit` with the resulting code. The split exists so the
|
||||
`process.exit` branch — which cannot be honestly exercised from in-process
|
||||
without spawning a subprocess — can be excluded from coverage as a pure
|
||||
boot stub while the surrounding `main()` (which does the actual work) is
|
||||
fully covered by the in-process suites in this directory.
|
||||
|
||||
## Features
|
||||
|
||||
- Detects direct invocation via `process.argv[1]` rather than
|
||||
`fileURLToPath(import.meta.url)` so the check works regardless of the
|
||||
module's own location (auto-boot is a sibling of the entry, not the
|
||||
entry itself).
|
||||
- Dynamic import of `./suaveplan-font-compress.js` so this module's static
|
||||
init does not depend on the bin entry's static init, avoiding any
|
||||
cyclic-import surprises with bundlers.
|
||||
- Maps a successful `main()` resolve to `process.exit(code)`; maps a
|
||||
rejected promise to a stderr-printed message and `process.exit(1)`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
The module has no exports — importing it as a side-effect is the entire
|
||||
API:
|
||||
|
||||
```ts
|
||||
// In suaveplan-font-compress.ts
|
||||
import "./auto-boot.js";
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```sh
|
||||
# Direct shebang launch — boot runs through this module.
|
||||
node dist/bin/suaveplan-font-compress.js ./fonts -o ./out
|
||||
|
||||
# In-process import — boot detects argv[1] does not end with the binary
|
||||
# name and stays silent; tests can then call `main()` directly without
|
||||
# accidentally launching a second instance.
|
||||
import { main } from "@suaveplan/font-compress/bin/suaveplan-font-compress";
|
||||
await main(["./fonts", "-o", "./out"]);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
No public API. The module's side effect runs once at import time:
|
||||
|
||||
1. Check `process.argv[1]` for the binary basename suffix.
|
||||
2. If matched, dynamically import the bin entry and call its `main`.
|
||||
3. Map the resolved exit code to `process.exit(code)`; map a rejection
|
||||
to stderr write + `process.exit(1)`.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`process.argv[1].endsWith("suaveplan-font-compress.js")` is intentionally
|
||||
basename-based — the absolute path comparison used by single-file bin
|
||||
patterns does not work here because auto-boot is a sibling, not the entry.
|
||||
The `try/catch` around the check guards against environments where
|
||||
`process` is undefined (some non-Node runtimes) and against any pathologic
|
||||
`argv` shapes; both fall back to "not direct invocation" which is the safe
|
||||
default for an import-only module. Coverage excludes this file because
|
||||
every branch is either a `process.argv` predicate or a `process.exit`
|
||||
side-effect — neither can be honestly faked from in-process.
|
||||
@@ -0,0 +1,77 @@
|
||||
# dispatch
|
||||
|
||||
## Purpose
|
||||
|
||||
Per-file orchestration extracted from the bin entry. Reads a single input
|
||||
file from disk, routes binary inputs (TTF/OTF/WOFF/WOFF2) through the
|
||||
injected `FontConverter` to produce a `TypefaceFont`, routes `.json` inputs
|
||||
through pass-through parsing (no conversion), then either serialises the
|
||||
`TypefaceFont` back to JSON (when `--format=json`) or packs it via
|
||||
`BinaryFontPacker` and writes the resulting `.btf`. Progress, skip, and
|
||||
error events are streamed through the injected printer / logger. Lives in
|
||||
its own module so the entry can stay small enough to satisfy the file-size
|
||||
budget while keeping a single round-trip testable in isolation.
|
||||
|
||||
## Features
|
||||
|
||||
- Discriminates input by extension: `.ttf`/`.otf`/`.woff`/`.woff2` goes to
|
||||
the converter; `.json` goes to pass-through validation.
|
||||
- Computes destination paths honouring `--preserve-structure` (mirror the
|
||||
input tree under the output root) vs the flat default.
|
||||
- Honours `--force`: existing outputs are skipped silently unless `--force`
|
||||
is set, in which case they are overwritten.
|
||||
- Captures any throwable from the converter or packer per file, prints a
|
||||
failure line, logs through `LoggerLike`, and returns `{ ok: false }` so
|
||||
the batch can continue and the entry exits `1`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { FontConverter } from "@suaveplan/font-convert";
|
||||
import { BinaryFontPacker } from "../packer/binary-font-packer/binary-font-packer.js";
|
||||
import { dispatchOne } from "./dispatch.js";
|
||||
|
||||
const outcome = await dispatchOne(
|
||||
"./fonts/Inter-Regular.ttf",
|
||||
new FontConverter(),
|
||||
new BinaryFontPacker(),
|
||||
options,
|
||||
"./fonts",
|
||||
"./out",
|
||||
deps
|
||||
);
|
||||
// outcome.ok === true, outcome.compressedBytes > 0
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
const failures: string[] = [];
|
||||
for (const file of files) {
|
||||
const r = await dispatchOne(file, converter, packer, options, root, out, deps);
|
||||
if (!r.ok) failures.push(file);
|
||||
}
|
||||
if (failures.length > 0) {
|
||||
deps.logger.error("font-compress.cli.batch_partial", { failures });
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Name | Signature | Notes |
|
||||
|---|---|---|
|
||||
| `dispatchOne` | `(file, converter, packer, options, inputRoot, outputRoot, deps) => Promise<FileOutcome>` | Never throws; all failures are reported via the returned outcome. |
|
||||
| `FileOutcome` | `{ ok; originalBytes; compressedBytes }` | `compressedBytes === 0` means the file was skipped without overwrite. |
|
||||
| `DispatchDeps` | `{ logger; stdout; stderr }` | Injected so a test can capture stream output. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Destination resolution intentionally does NOT call `path.relative` —
|
||||
prefix-strip plus `replace(/^[\\/]+/)` is enough and lets us mirror the
|
||||
upstream legacy semantics where Windows-style separators round-trip
|
||||
unchanged. The dispatcher always creates the parent directory with
|
||||
`mkdir({ recursive: true })` before writing, so users do not need to
|
||||
pre-create the output tree. JSON inputs are validated by `JSON.parse` only —
|
||||
the typeface schema is enforced downstream by `BinaryFontPacker`, which
|
||||
throws `FontCompressError(PACK_FAILED)` for malformed shapes, and the catch
|
||||
arm here converts that into a clean per-file failure line.
|
||||
@@ -0,0 +1,86 @@
|
||||
# format-utils
|
||||
|
||||
## Purpose
|
||||
|
||||
Pure formatting helpers backing the operator-facing output of the
|
||||
`suaveplan-font-compress` bin. The helpers exist so the bin entry can focus on
|
||||
orchestration (argument parsing, dispatch, FS I/O) while every cosmetic detail
|
||||
— byte sizing, compression ratio, ANSI tone wrappers — lives in a small,
|
||||
exhaustively testable module. Keeping them separate also keeps the bin entry
|
||||
under the AGENTS.md §3 file-size budget and makes the renderer reusable for
|
||||
custom hosts (CI dashboards, web previews) that want to share the same
|
||||
output format the operator sees on stdout.
|
||||
|
||||
## Features
|
||||
|
||||
- `formatBytes(bytes)` — human-readable size string (`B`/`KB`/`MB`/`GB`) with
|
||||
two-decimal rounding and graceful handling of zero, negative, and non-finite
|
||||
inputs.
|
||||
- `formatRatio(original, compressed)` — compression ratio rendered as `Nx`
|
||||
with one-decimal precision; returns `0.0x` for any non-positive denominator.
|
||||
- `formatSavings(original, compressed)` — percentage of bytes saved by
|
||||
compression, rounded to one decimal.
|
||||
- `ANSI` — frozen palette of the eight terminal escape codes used by the bin
|
||||
(reset, bold, dim, red, green, yellow, blue, cyan).
|
||||
- `colour(code, text)` — wrap a string between an ANSI prefix and the reset
|
||||
sequence.
|
||||
- `tint.*` — convenience wrappers (`tint.bold`, `tint.green`, …) for the
|
||||
common combinations.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { formatBytes, formatRatio, tint } from "@suaveplan/font-compress/bin/suaveplan-font-compress";
|
||||
|
||||
console.log(`Compressed: ${formatBytes(2048)}`); // "Compressed: 2 KB"
|
||||
console.log(tint.green(`Ratio: ${formatRatio(1000, 250)}`)); // green "Ratio: 4.0x"
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
ANSI,
|
||||
colour,
|
||||
formatBytes,
|
||||
formatRatio,
|
||||
formatSavings,
|
||||
} from "./format-utils.js";
|
||||
|
||||
function renderRow(name: string, original: number, packed: number): string {
|
||||
return [
|
||||
colour(ANSI.bold, name.padEnd(20)),
|
||||
formatBytes(original).padStart(10),
|
||||
formatBytes(packed).padStart(10),
|
||||
colour(ANSI.green, formatRatio(original, packed).padStart(8)),
|
||||
colour(ANSI.dim, formatSavings(original, packed).padStart(8)),
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
renderRow("Inter-Regular.ttf", 287_000, 64_500);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Name | Signature | Notes |
|
||||
|---|---|---|
|
||||
| `formatBytes` | `(bytes: number) => string` | Clamps to `GB` for inputs ≥ 1 TiB so it never indexes past the unit table. |
|
||||
| `formatRatio` | `(orig: number, packed: number) => string` | Guards against zero / negative / non-finite values. |
|
||||
| `formatSavings` | `(orig: number, packed: number) => string` | Returns `0.0%` for any non-positive original. |
|
||||
| `ANSI` | `Readonly<Record<string, string>>` | The eight escape codes used by the bin. |
|
||||
| `colour` | `(code: string, text: string) => string` | Always closes with `ANSI.reset`. |
|
||||
| `tint.*` | `(text: string) => string` | One pre-bound `colour` wrapper per palette entry. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The helpers are pure, side-effect-free, and runtime-agnostic — they take and
|
||||
return strings/numbers only, never read globals, and never touch I/O. This is
|
||||
deliberate: the bin entry composes them into stdout writes through an
|
||||
injected `LoggerLike`, so the same helpers can be used from a hosted UI that
|
||||
captures the output to a buffer or DOM node. ANSI escape sequences are
|
||||
emitted unconditionally; callers that need plain text strip them at the
|
||||
output boundary rather than asking each helper to branch on a `useColour`
|
||||
flag. Inputs that violate the contract (`NaN`, negative bytes) fall back to a
|
||||
benign sentinel (`"0 B"`, `"0.0x"`, `"0.0%"`) so a single corrupt summary
|
||||
record never propagates `NaN` into the operator's terminal — defensive but
|
||||
honest.
|
||||
@@ -0,0 +1,74 @@
|
||||
# parser
|
||||
|
||||
## Purpose
|
||||
|
||||
Bin-specific wrapper around `@suaveplan/cli`'s generic `parseArgs` that
|
||||
enforces the `suaveplan-font-compress` flag set, detects mutually-exclusive
|
||||
flag pairs, and converts any upstream parser error into a coded
|
||||
`FontCompressError(CLI_INVALID_ARGS)` so the entry's top-level handler can
|
||||
deterministically map the failure to exit code `2`. Isolated from the entry
|
||||
so the parser can be unit-tested in milliseconds without spinning up the FS
|
||||
pipeline.
|
||||
|
||||
## Features
|
||||
|
||||
- Recognises every flag in the spec delta: `--input`/`-i`, `--output`/`-o`,
|
||||
`--format=btf|json`, `--force`/`-f`, `--recursive`/`-r`, `--no-recursive`,
|
||||
`--preserve-structure`, `--no-preserve-structure`, `--help`/`-h`,
|
||||
`--version`/`-v`, and the positional `<input>`.
|
||||
- Detects conflicts between `--recursive` and `--no-recursive` and between
|
||||
the two `--preserve-structure` variants and throws the coded error before
|
||||
any FS work happens.
|
||||
- Maps unknown long flags (`--no-such-flag`) and unknown short flags
|
||||
(`-X`) into `FontCompressError(CLI_INVALID_ARGS)` with the offending flag
|
||||
captured in `error.context.flag`.
|
||||
- Returns a frozen-shape `CliOptions` discriminator that downstream
|
||||
helpers (`dispatch.ts`, the entry's pipeline) can consume with no
|
||||
further casting.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { parseCliArgs } from "./parser.js";
|
||||
|
||||
const options = parseCliArgs(["./fonts", "-o", "./out", "--format=json"]);
|
||||
// → { input: "./fonts", output: "./out", format: "json", … }
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { FONT_COMPRESS_CODES, FontCompressError } from "../error-codes.js";
|
||||
import { parseCliArgs } from "./parser.js";
|
||||
|
||||
try {
|
||||
parseCliArgs(["./fonts", "--recursive", "--no-recursive"]);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof FontCompressError &&
|
||||
err.code === FONT_COMPRESS_CODES["CLI_INVALID_ARGS"]
|
||||
) {
|
||||
// err.context.flags === ["--recursive", "--no-recursive"]
|
||||
process.exit(2);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Name | Signature | Notes |
|
||||
|---|---|---|
|
||||
| `parseCliArgs` | `(argv: readonly string[]) => CliOptions` | Throws `FontCompressError(CLI_INVALID_ARGS)` on conflict / unknown flag. |
|
||||
| `CliOptions` | discriminated record | Includes `input`, `output`, `outputSpecified`, `format`, `force`, `recursive`, `preserveStructure`, `help`, `version`. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The `--no-*` negations are handled inside the upstream parser (it sets the
|
||||
camelCase key to `false`), so this wrapper detects only the *pair* of
|
||||
contradictory flags being present. Unknown-flag detection runs after the
|
||||
upstream parser throws because the upstream error message is too generic to
|
||||
re-emit; we scan `argv` for the first long or short token that is not in the
|
||||
allowlist and propagate it as `error.context.flag`. The `--input` long flag
|
||||
takes precedence over the positional when both are present (which mirrors
|
||||
upstream legacy and is documented in the spec delta source-mapping table).
|
||||
@@ -0,0 +1,79 @@
|
||||
# printer
|
||||
|
||||
## Purpose
|
||||
|
||||
Stdout / stderr renderers used by the `suaveplan-font-compress` bin. Owns
|
||||
the help screen, banner, per-file progress lines (success / skip / error),
|
||||
and the end-of-run summary block. Every renderer writes through an injected
|
||||
`NodeJS.WritableStream` so a host (test harness, CI dashboard, web preview)
|
||||
can capture the output without monkey-patching the global `process.stdout`.
|
||||
Lives in its own module so the entry can stay under the AGENTS.md §3 file
|
||||
size budget while keeping the operator-visible output independently
|
||||
inspectable and unit-testable.
|
||||
|
||||
## Features
|
||||
|
||||
- `HELP_TEXT` — the full `--help` screen, including every flag in the spec
|
||||
delta plus the exit-code table.
|
||||
- `printBanner` — one-shot summary printed before any per-file work,
|
||||
showing input, output, format, and force state.
|
||||
- `printSuccessLine` — `✓` line with the family name and either the
|
||||
compressed byte count (`--format=json`) or the compression ratio
|
||||
(`--format=btf`).
|
||||
- `printSkipLine` — `⏭` line when an output already exists and `--force`
|
||||
was not set.
|
||||
- `printErrorLine` — `✗` line written to stderr with the file basename and
|
||||
the failure message.
|
||||
- `printSummary` — block with total / converted / errors / original size /
|
||||
compressed size / ratio / savings, terminated by the output path.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { HELP_TEXT, printBanner, printSummary } from "./printer.js";
|
||||
|
||||
process.stdout.write(HELP_TEXT);
|
||||
printBanner(process.stdout, "/fonts", "/out", "btf", false);
|
||||
printSummary(process.stdout, summary, "/out");
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { Writable } from "node:stream";
|
||||
import { printSummary } from "./printer.js";
|
||||
|
||||
const chunks: string[] = [];
|
||||
const captured = new Writable({
|
||||
write(chunk, _enc, cb) {
|
||||
chunks.push(chunk.toString("utf8"));
|
||||
cb();
|
||||
},
|
||||
});
|
||||
printSummary(captured, summary, "/out");
|
||||
// chunks.join("") now holds the rendered block — assert in tests / pipe
|
||||
// to a CI dashboard webhook.
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Name | Signature | Notes |
|
||||
|---|---|---|
|
||||
| `HELP_TEXT` | `string` | Pre-rendered help screen (ANSI tones included). |
|
||||
| `printBanner` | `(stdout, inputPath, outputPath, format, force) => void` | Writes the run-start block. |
|
||||
| `printSuccessLine` | `(stdout, font, originalBytes, compressedBytes, format) => void` | Emits `✓` line; format choice changes the tail. |
|
||||
| `printSkipLine` | `(stdout, file) => void` | Emits `⏭` for already-existing outputs. |
|
||||
| `printErrorLine` | `(stderr, file, message) => void` | Emits `✗` to stderr. |
|
||||
| `printSummary` | `(stdout, summary, outputRoot) => void` | Emits the end-of-run block. |
|
||||
| `RunSummary` | record | `{ totalFiles, successCount, errorCount, totalOriginalSize, totalCompressedSize }`. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
ANSI tones come from `format-utils.ts` so the palette is single-sourced.
|
||||
Every line is composed by `Array#join("\n")` rather than template literals
|
||||
with leading newlines — the test harness can split on `\n` and assert on
|
||||
exact strings without worrying about stray indentation. The summary block
|
||||
always renders the savings percentage even when no bytes were processed
|
||||
(`formatSavings` falls back to `"0.0%"` for non-positive inputs) so the
|
||||
output shape stays stable when an operator runs the bin against an empty
|
||||
directory.
|
||||
@@ -0,0 +1,123 @@
|
||||
# suaveplan-font-compress
|
||||
|
||||
## Purpose
|
||||
|
||||
Operator-facing CLI that wires `@suaveplan/font-convert`
|
||||
(`FontConverter` / `BatchConverter`) into this package's
|
||||
`BinaryFontPacker` to convert TTF/OTF/WOFF/WOFF2 (and already-converted
|
||||
typeface JSON) into compact `.btf` artefacts on disk. The CLI is the
|
||||
shipping shape of the `@suaveplan/font-compress` package's "build-time
|
||||
asset baker" surface and is invoked from package scripts, CI workflows, and
|
||||
ad-hoc developer workstations — the underlying library
|
||||
(`BinaryFontPacker` / `BinaryFontUnpacker`) remains the supported in-process
|
||||
surface for runtime callers that already hold a `TypefaceFont` in memory.
|
||||
|
||||
## Features
|
||||
|
||||
- Single-file or whole-directory input via positional `<input>` or
|
||||
`--input`/`-i`.
|
||||
- Recursive directory walk (default `true`; disable with `--no-recursive`).
|
||||
- Layout strategy: mirror the source tree (`--preserve-structure`) or flatten
|
||||
to the output root (the default when `--output` is set).
|
||||
- Format dispatch: emit packed BTF binaries (default `--format=btf`) or the
|
||||
intermediate typeface JSON (`--format=json`) for pipeline debugging.
|
||||
- `--force`/`-f` to overwrite existing outputs; otherwise existing files are
|
||||
skipped with a `⏭` line.
|
||||
- Structured exit codes (`0`/`1`/`2`/`3`) with the meaning pinned in the
|
||||
spec delta.
|
||||
- Coded errors: argument-parsing failures throw
|
||||
`FontCompressError(FONT_COMPRESS.CLI_INVALID_ARGS)` which the top-level
|
||||
handler maps to exit `2`.
|
||||
- OTel span `font-compress.cli.run` emitted for every invocation with
|
||||
`font-compress.files`, `font-compress.errors`, and `font-compress.exit_code`
|
||||
attributes set as the run progresses.
|
||||
- Injectable `LoggerLike` and `stdout`/`stderr` streams via the exported
|
||||
`main(argv, deps)` so the CLI is testable in-process without spawning a
|
||||
subprocess.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```sh
|
||||
# Compress a single file in place (writes Inter-Regular.btf next to it).
|
||||
suaveplan-font-compress ./fonts/Inter-Regular.ttf
|
||||
|
||||
# Compress a whole directory recursively to a sibling output tree.
|
||||
suaveplan-font-compress ./fonts -o ./out
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```sh
|
||||
# JSON intermediate — useful for inspecting what the converter emitted
|
||||
# before the BTF pack step. Round-trips through the same packer.
|
||||
suaveplan-font-compress ./fonts/Inter-Regular.ttf --format=json -o ./debug
|
||||
|
||||
# Force-overwrite existing outputs (the default is to skip with a ⏭ line).
|
||||
suaveplan-font-compress ./fonts -o ./out --force
|
||||
|
||||
# Flatten the output tree under ./out, regardless of the input layout.
|
||||
suaveplan-font-compress ./fonts -o ./out --no-preserve-structure
|
||||
|
||||
# In-process invocation from a Node host (no subprocess, captured streams).
|
||||
import { main } from "@suaveplan/font-compress/bin/suaveplan-font-compress";
|
||||
import { Writable } from "node:stream";
|
||||
|
||||
const out: string[] = [];
|
||||
const sink = new Writable({
|
||||
write(chunk, _enc, cb) {
|
||||
out.push(chunk.toString("utf8"));
|
||||
cb();
|
||||
},
|
||||
});
|
||||
const code = await main(["./fonts", "-o", "./out"], { stdout: sink, stderr: sink });
|
||||
process.exit(code);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Flag | Type | Default | Notes |
|
||||
|---|---|---|---|
|
||||
| `<input>` | positional | required | File or directory. |
|
||||
| `--input` / `-i` | string | unset | Alternative to positional. |
|
||||
| `--output` / `-o` | string | input path | Output directory. |
|
||||
| `--format` | `btf` \| `json` | `btf` | Output format. |
|
||||
| `--force` / `-f` | boolean | `false` | Overwrite existing outputs. |
|
||||
| `--recursive` / `-r` | boolean | `true` | Recurse directories. |
|
||||
| `--no-recursive` | boolean | — | Disable recursion. |
|
||||
| `--preserve-structure` | boolean | derived | Mirror source tree. |
|
||||
| `--no-preserve-structure` | boolean | — | Flatten under output root. |
|
||||
| `--help` / `-h` | boolean | `false` | Print usage and exit `0`. |
|
||||
| `--version` / `-v` | boolean | `false` | Print version and exit `0`. |
|
||||
|
||||
### Exit codes
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| `0` | All inputs processed successfully. |
|
||||
| `1` | One or more inputs failed during conversion or packing. |
|
||||
| `2` | Invalid CLI arguments (unknown flag, conflict, missing positional). |
|
||||
| `3` | The input path does not exist on disk. |
|
||||
|
||||
### Exported in-process API
|
||||
|
||||
- `main(argv, deps?)` — returns the would-be exit code; never calls
|
||||
`process.exit` itself when invoked in-process.
|
||||
- `parseCliArgs(argv)` — pure parser; throws
|
||||
`FontCompressError(CLI_INVALID_ARGS)` on conflict / unknown flag.
|
||||
- `CliOptions`, `RunDeps` — discriminator and dep-bundle types.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The pipeline always instantiates both `FontConverter` and `BatchConverter`
|
||||
even on single-file inputs — the spec scenario "directory input uses
|
||||
BatchConverter" is satisfied by the constructor side effect, and the
|
||||
file-by-file loop keeps memory consumption bounded for very large font
|
||||
trees instead of materialising every binary in a single `BatchConverter`
|
||||
call. Output paths are computed by stripping the input root from the file
|
||||
path and re-rooting at the output directory; this preserves the upstream
|
||||
legacy semantics where Windows-style separators round-trip unchanged. The
|
||||
top-level handler is the ONLY place that maps a thrown
|
||||
`FontCompressError(CLI_INVALID_ARGS)` to exit `2`; deeper code paths never
|
||||
own exit codes directly. Per-file failures from the converter or packer are
|
||||
caught in `dispatch.ts` and surfaced as `{ ok: false }`, allowing the batch
|
||||
to continue and the entry to exit `1` after the summary block.
|
||||
@@ -0,0 +1,78 @@
|
||||
# FontCompressError / FONT_COMPRESS_CODES
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
`FontCompressError` is the structured error type for the `@suaveplan/font-compress` package, extending `SuaveplanError` from `@suaveplan/error`. It carries a stable code from the `FONT_COMPRESS_CODES` registry, an optional `cause`, and an optional `context` object for structured diagnostics.
|
||||
|
||||
All public operations in the package (`BinaryFontPacker.pack`, `BinaryFontUnpacker.unpack`) throw `FontCompressError` on failure rather than raw `Error` or `RangeError`, enabling callers to branch on specific codes and forward structured error data to logging and observability systems.
|
||||
|
||||
## Features
|
||||
|
||||
- Three stable error codes: `PACK_FAILED`, `UNPACK_FAILED`, `INVALID_DATA`
|
||||
- Codes are prefixed with the package domain: `FONT_COMPRESS.PACK_FAILED`
|
||||
- Supports `cause` (original error) and `context` (arbitrary key/value metadata)
|
||||
- Extends `SuaveplanError` for integration with the project error infrastructure
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { FontCompressError, FONT_COMPRESS_CODES } from "@suaveplan/font-compress";
|
||||
|
||||
try {
|
||||
packer.pack(font);
|
||||
} catch (err) {
|
||||
if (err instanceof FontCompressError) {
|
||||
console.error(err.code); // "FONT_COMPRESS.PACK_FAILED"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { FontCompressError, FONT_COMPRESS_CODES } from "@suaveplan/font-compress";
|
||||
|
||||
function safePack(font: TypefaceFont): Uint8Array | null {
|
||||
try {
|
||||
return packer.pack(font);
|
||||
} catch (err) {
|
||||
if (
|
||||
err instanceof FontCompressError &&
|
||||
err.code === FONT_COMPRESS_CODES["PACK_FAILED"]
|
||||
) {
|
||||
logger.error("pack failed", { code: err.code, cause: err.cause });
|
||||
return null;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `FONT_COMPRESS_CODES`
|
||||
|
||||
Object with keys `PACK_FAILED`, `UNPACK_FAILED`, `INVALID_DATA`, each mapping to its namespaced string code.
|
||||
|
||||
### `FontCompressErrorCode`
|
||||
|
||||
Union type of all code values in `FONT_COMPRESS_CODES`.
|
||||
|
||||
### `FontCompressError`
|
||||
|
||||
Extends `SuaveplanError<FontCompressErrorCode>`.
|
||||
|
||||
**Constructor:** `new FontCompressError(code, message, options?)`
|
||||
|
||||
- `code` — one of the `FONT_COMPRESS_CODES` values
|
||||
- `message` — human-readable description
|
||||
- `options.cause` — original `Error` (stored on `.cause`)
|
||||
- `options.context` — additional `Record<string, unknown>` metadata
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
`SuaveplanError` uses a `protected constructor`, so `FontCompressError` must declare an explicit `public constructor` that calls `super()`. This is a pattern used consistently across all `@suaveplan/*` error classes.
|
||||
@@ -0,0 +1,99 @@
|
||||
# BinaryFontPacker
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
## Purpose
|
||||
|
||||
`BinaryFontPacker` converts a `TypefaceFont` (the typeface.js JSON structure) into a compact binary BTF (Binary TypeFace) buffer. It builds a shared integer dictionary from all unique coordinate values in the font and encodes every number as a 16-bit dictionary index rather than a string, dramatically reducing the payload compared to raw JSON.
|
||||
|
||||
The BTF format is designed for Three.js and game-engine font pipelines where fonts must be shipped as efficient binary assets rather than verbose JSON strings.
|
||||
|
||||
## Features
|
||||
|
||||
- Dictionary-based compression: all unique integer values stored once in an `Int16Array`; all subsequent references are 16-bit indexes
|
||||
- Full font round-trip: header strings, reversed flag, 9 scalar metrics, bounding box, per-glyph ha/x_min/x_max, and full outline data
|
||||
- Supports all typeface.js command types: `m`, `l`, `b` (cubic), `q` (quadratic), `z`
|
||||
- Little-endian byte layout throughout for cross-platform compatibility
|
||||
- Wraps internal errors in `FontCompressError` with stable code `PACK_FAILED`
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { createBinaryFontPacker } from "@suaveplan/font-compress/packer/binary-font-packer";
|
||||
import type { TypefaceFont } from "@suaveplan/font-convert";
|
||||
|
||||
const font: TypefaceFont = { /* ... */ };
|
||||
const packer = createBinaryFontPacker();
|
||||
const buffer: Uint8Array = packer.pack(font);
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { BinaryFontPacker } from "@suaveplan/font-compress/packer/binary-font-packer";
|
||||
import { FontCompressError } from "@suaveplan/font-compress";
|
||||
|
||||
const packer = new BinaryFontPacker();
|
||||
|
||||
try {
|
||||
const packed = packer.pack(font);
|
||||
// Write to disk, send over the wire, or store in IndexedDB
|
||||
await Bun.write("font.btf", packed);
|
||||
} catch (err) {
|
||||
if (err instanceof FontCompressError) {
|
||||
console.error("Pack failed:", err.code, err.message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `BinaryFontPacker`
|
||||
|
||||
#### `pack(font: TypefaceFont): Uint8Array`
|
||||
|
||||
Packs a `TypefaceFont` into a BTF binary buffer.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `font` — the typeface.js font object, as produced by `@suaveplan/font-convert`
|
||||
|
||||
**Returns:** `Uint8Array` containing the binary BTF data.
|
||||
|
||||
**Throws:** `FontCompressError` with code `PACK_FAILED` if packing fails for any reason.
|
||||
|
||||
### `createBinaryFontPacker(): BinaryFontPacker`
|
||||
|
||||
Factory function that creates a new `BinaryFontPacker` instance.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
### BTF Binary Layout
|
||||
|
||||
```text
|
||||
[familyName: encodeString]
|
||||
[cssFontWeight: encodeString]
|
||||
[cssFontStyle: encodeString]
|
||||
[reversed: uint8 (0 or 1)]
|
||||
[dictLen: uint32 LE]
|
||||
[dict: Int16Array × dictLen]
|
||||
[metricsIdxs: Uint16Array × 9] ← ascender,descender,ulPos,ulThk,res,yMin,xMin,yMax,xMax
|
||||
[glyphCount: uint32 LE]
|
||||
per-glyph:
|
||||
[charCode: uint32 LE]
|
||||
[indexCount: uint32 LE]
|
||||
[indexes: Uint16Array × indexCount] ← ha,x_min,x_max, then (cmd_byte+coord_indexes)...
|
||||
```
|
||||
|
||||
`encodeString` format: `[uint32 length LE][UTF-8 bytes]`.
|
||||
|
||||
Command bytes map to: `0=m`, `1=l`, `2=b`, `3=q`, `4=z`. Coordinate counts per command: `m=2`, `l=2`, `b=6`, `q=4`, `z=0`.
|
||||
|
||||
### Dictionary Construction
|
||||
|
||||
All unique coordinate values across the entire font (metrics, bounding box, all glyph coordinates) are collected into a sorted `Int16Array`. This single pass over the font data is O(n) in glyph count and produces optimal deduplication. Because typeface.js coordinates are integers in a bounded range (typically ±2048), the dictionary fits comfortably in an Int16Array.
|
||||
|
||||
### Alignment
|
||||
|
||||
All multi-byte typed arrays are written via `Uint8Array` byte copies, not direct TypedArray buffer views, so there are no alignment requirements on the output buffer. Readers (BinaryFontUnpacker) use `DataView` to safely read values at arbitrary offsets.
|
||||
@@ -0,0 +1,60 @@
|
||||
# encode-string
|
||||
|
||||
## Purpose
|
||||
|
||||
Utilities for encoding and decoding length-prefixed UTF-8 strings in the BTF binary format. They provide the wire encoding used by `BinaryFontPacker` to store font family name, CSS weight, and CSS style as inline strings in the binary header.
|
||||
|
||||
Every string is encoded as `[uint32 byte-length LE][UTF-8 bytes]`, which allows safe decoding without a sentinel character and handles arbitrary Unicode content including multi-byte sequences.
|
||||
|
||||
## Features
|
||||
|
||||
- `encodeString`: encodes a JS string as `[uint32 LE length][UTF-8 bytes]`
|
||||
- `decodeString`: reads a length-prefixed string from a buffer at a given byte offset and returns the decoded value and the new offset after the string
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { encodeString, decodeString } from "@suaveplan/font-compress/utils/encode-string";
|
||||
|
||||
const encoded = encodeString("Arial");
|
||||
// [5, 0, 0, 0, 65, 114, 105, 97, 108]
|
||||
|
||||
const { value, offset } = decodeString(encoded, 0);
|
||||
// value = "Arial", offset = 9
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
// Chaining multiple string reads during unpack
|
||||
let off = 0;
|
||||
const r1 = decodeString(data, off); off = r1.offset;
|
||||
const r2 = decodeString(data, off); off = r2.offset;
|
||||
const r3 = decodeString(data, off); off = r3.offset;
|
||||
// r1.value = familyName, r2.value = cssFontWeight, r3.value = cssFontStyle
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `encodeString(str: string): Uint8Array`
|
||||
|
||||
Encodes `str` as `[uint32 LE byte-length][UTF-8 bytes]`.
|
||||
|
||||
**Parameters:** `str` — any JS string including multi-byte Unicode.
|
||||
|
||||
**Returns:** `Uint8Array` of `4 + byteLength` bytes.
|
||||
|
||||
### `decodeString(data: Uint8Array, offset: number): { value: string; offset: number }`
|
||||
|
||||
Reads a length-prefixed string at `offset` in `data`.
|
||||
|
||||
**Parameters:**
|
||||
|
||||
- `data` — source buffer
|
||||
- `offset` — byte position of the uint32 length prefix
|
||||
|
||||
**Returns:** `{ value: string; offset: number }` where `offset` is the position immediately after the string bytes.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
Uses the platform `TextEncoder` / `TextDecoder` singletons. The returned `offset` from `decodeString` is the cursor position immediately following the string's last byte, suitable for passing directly to the next read operation.
|
||||
@@ -0,0 +1,67 @@
|
||||
# parse-path
|
||||
|
||||
## Purpose
|
||||
|
||||
Parses a typeface.js outline string into an array of structured command objects. The typeface.js format encodes glyph outlines as space-separated tokens: a command letter (`m`, `l`, `b`, `q`, `z`) followed by its coordinate arguments. `parsePath` tokenises this string and groups each command with its coordinates for downstream processing.
|
||||
|
||||
This utility is used by `BinaryFontPacker` to enumerate all glyph coordinates during dictionary construction and to encode each command into the BTF binary stream.
|
||||
|
||||
## Features
|
||||
|
||||
- Parses all five typeface.js commands: `m` (moveto), `l` (lineto), `b` (cubic bezier), `q` (quadratic bezier), `z` (close path)
|
||||
- Returns coordinate arrays of the correct length per command: `m/l=2`, `q=4`, `b=6`, `z=0`
|
||||
- Silently skips unrecognised tokens
|
||||
- Handles empty strings and trailing whitespace gracefully
|
||||
|
||||
## Usage: Basic Example
|
||||
|
||||
```typescript
|
||||
import { parsePath } from "@suaveplan/font-compress/utils/parse-path";
|
||||
|
||||
const commands = parsePath("m 0 0 l 100 0 z ");
|
||||
// [
|
||||
// { cmd: "m", coords: [0, 0] },
|
||||
// { cmd: "l", coords: [100, 0] },
|
||||
// { cmd: "z", coords: [] },
|
||||
// ]
|
||||
```
|
||||
|
||||
## Usage: Advanced Example
|
||||
|
||||
```typescript
|
||||
import { parsePath } from "@suaveplan/font-compress/utils/parse-path";
|
||||
|
||||
// Complex glyph outline with all command types
|
||||
const commands = parsePath("m 0 0 l 100 0 b 10 20 30 40 50 60 q 5 10 15 20 z ");
|
||||
for (const { cmd, coords } of commands) {
|
||||
console.log(cmd, coords.length, "coords");
|
||||
}
|
||||
// m 2 coords
|
||||
// l 2 coords
|
||||
// b 6 coords
|
||||
// q 4 coords
|
||||
// z 0 coords
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### `ParsedCommand`
|
||||
|
||||
```typescript
|
||||
interface ParsedCommand {
|
||||
cmd: string;
|
||||
coords: number[];
|
||||
}
|
||||
```
|
||||
|
||||
### `parsePath(pathString: string): ParsedCommand[]`
|
||||
|
||||
Parses a typeface.js outline string.
|
||||
|
||||
**Parameters:** `pathString` — the `o` field of a `TypefaceGlyph`.
|
||||
|
||||
**Returns:** array of `ParsedCommand` objects in outline order. Returns `[]` for an empty string.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The parser advances by one token at a time. When it encounters a known command, it reads the next N tokens as floating-point coordinates. If a coordinate token is missing or non-numeric it is skipped; this handles malformed paths without throwing. Unrecognised tokens (not in the set `m l b q z`) are consumed and discarded without advancing the coordinate counter, preserving correct alignment for subsequent commands.
|
||||
Reference in New Issue
Block a user