diff --git a/AGENTS.md b/AGENTS.md index abbcff54..4a2201c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,7 @@ - Better Unicode variation selector (UVS) handling - Richer shaping controls - Vertical metrics accuracy + - Better error reporting - Predictable type definitions for consumers ## Repository Orientation @@ -18,6 +19,9 @@ ## Toolchain & Commands - Use Node 20+ and npm (repo ships `package-lock.json`). Install with `npm install`. +- Two TypeScript configs ship with the repo: + - `tsconfig.json` configures editor tooling for the codebase. It enables `allowJs`, `strictNullChecks`, and decorators, but keeps `checkJs` disabled so files opt in via `// @ts-check`. + - `tsconfig-types.json` extends the base config and only emits declarations for `src/index.ts` and `src/node.ts` when running `npm run build:types`. - Fast feedback loop: - `npm run build:js` → Parcel build into `dist/`. - `npm run build:types` → `tsc --project tsconfig-types.json` (follows `src/index.ts` & `src/node.ts`, pulls in JS via `allowJs`). diff --git a/MODIFICATIONS.md b/MODIFICATIONS.md index c389ff21..5c3b1759 100644 --- a/MODIFICATIONS.md +++ b/MODIFICATIONS.md @@ -2,20 +2,33 @@ ## [Unreleased] +### Feature/Dependency Removals + - Remove WOFF format support - Also removing the `tiny-inflate` dependency (the indirect dependency may remain) - Remove WOFF2 format support - Also removing the `brotli` dependency - Remove DFont format support -- Simplify the published TypeScript types by inlining the concrete format exports (`TTFFont`/`TrueTypeCollection`) in place of the old aliases (`Font`/`FontCollection`). - Remove the dependencies below by replacing them with new internal helpers (no API changes): - `clone` (used by `TTFSubset`) - `fast-deep-equal` (used by `CFFDict`) +### Error Improvements + +- Replace generic `Error`s with new specific `FontkitError` subclasses +- Fix some error messages +- Rename table decode logging helpers from `logErrors`/`isLoggingErrors` to `logWarnings`/`isLoggingWarnings` and downgrade emitted messages to warnings + +### Other Changes + +- Simplify the published TypeScript types by inlining the concrete format exports (`TTFFont`/`TrueTypeCollection`) in place of the old aliases (`Font`/`FontCollection`). + + ## [2.0.4-mod.2025.2] - Improve performance of `CmapProcessor#lookupNonDefaultUVS` by caching variation selector records from `cmap` format 14 subtable + ## [2.0.4-mod.2025.1] - Fix glyph mapping using the cmap format 14 subtable, improving support for UVS in methods like `TTFFont#glyphsForString` diff --git a/jsconfig.json b/jsconfig.json deleted file mode 100644 index 546bcbe9..00000000 --- a/jsconfig.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "compilerOptions": { - "strictNullChecks": true, - "target": "es2015", - "experimentalDecorators": true - } -} diff --git a/package-lock.json b/package-lock.json index b0d0e6af..77bf8fe5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "npm-run-all": "^4.1.5", "parcel": "2.0.0-canary.1713", "shx": "^0.3.4", + "tsx": "^4.20.6", "typescript": "^5.9.3" } }, @@ -141,6 +142,448 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -3149,6 +3592,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/escalade": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", @@ -3339,6 +3824,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-tsconfig": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", @@ -5246,6 +5744,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/restructure": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/restructure/-/restructure-3.0.2.tgz", @@ -5895,6 +6403,26 @@ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, + "node_modules/tsx": { + "version": "4.20.6", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.6.tgz", + "integrity": "sha512-ytQKuwgmrrkDTFP4LjR0ToE2nqgy886GpvRSpU0JAnrdBYppuY5rLkRUYPU1yCryb24SsKBTL/hlDQAEFVwtZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", diff --git a/package.json b/package.json index 738b597a..f9d31b5f 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ ], "scripts": { "test": "run-s build mocha", - "mocha": "mocha \"test/**/*.js\"", + "mocha": "mocha --node-option import=tsx \"test/**/*.js\"", "build": "run-s build:*", "build:js": "parcel build", "build:types": "tsc --project tsconfig-types.json", @@ -22,7 +22,7 @@ "trie:use": "node src/opentype/shapers/gen-use.js", "trie:indic": "node src/opentype/shapers/gen-indic.js", "clean": "shx rm -rf src/opentype/shapers/data.trie src/opentype/shapers/use.trie src/opentype/shapers/use.json src/opentype/shapers/indic.trie src/opentype/shapers/indic.json dist types", - "coverage": "c8 mocha \"test/**/*.js\"" + "coverage": "c8 mocha --node-option import=tsx \"test/**/*.js\"" }, "type": "module", "main": "dist/main.cjs", @@ -97,6 +97,7 @@ "npm-run-all": "^4.1.5", "parcel": "2.0.0-canary.1713", "shx": "^0.3.4", + "tsx": "^4.20.6", "typescript": "^5.9.3" } } diff --git a/src/CmapProcessor.js b/src/CmapProcessor.js index 390cc944..c5d7d301 100644 --- a/src/CmapProcessor.js +++ b/src/CmapProcessor.js @@ -1,6 +1,7 @@ import { binarySearch, range } from './utils/arrays'; import { encodingExists, getEncoding, getEncodingMapping } from './encodings'; import { cache } from './decorators'; +import { AssertionError, InvalidFontDataError, UnsupportedFontDataError } from './errors'; export default class CmapProcessor { constructor(cmapTable) { @@ -33,7 +34,7 @@ export default class CmapProcessor { } if (!this.cmap) { - throw new Error("Could not find a supported cmap table"); + throw new UnsupportedFontDataError('Could not find a supported cmap table'); } this.uvs = this.findSubtable(cmapTable, [[0, 5]]); @@ -73,6 +74,10 @@ export default class CmapProcessor { case 0: return cmap.codeMap.get(codepoint) || 0; + case 2: + // Microsoft OpenType spec says "This format is not commonly used today." + throw new UnsupportedFontDataError('Unsupported cmap format 2'); + case 4: { let min = 0; let max = cmap.segCount - 1; @@ -105,7 +110,8 @@ export default class CmapProcessor { } case 8: - throw new Error('TODO: cmap format 8'); + // TODO: support format 8 + throw new UnsupportedFontDataError('Unsupported cmap format 8'); case 6: case 10: @@ -137,10 +143,10 @@ export default class CmapProcessor { case 14: // Format 14 is handled separately via the uvs property - throw new Error('Unexpected cmap format 14'); + throw new AssertionError('Unexpected cmap format 14'); default: - throw new Error(`Unknown cmap format ${cmap.version}`); + throw new InvalidFontDataError(`Unknown cmap format ${cmap.version}`); } } @@ -179,6 +185,10 @@ export default class CmapProcessor { case 0: return range(0, cmap.codeMap.length); + case 2: + // Microsoft OpenType spec says "This format is not commonly used today." + throw new UnsupportedFontDataError('Unsupported cmap format 2'); + case 4: { let res = []; let endCodes = cmap.endCode.toArray(); @@ -192,7 +202,8 @@ export default class CmapProcessor { } case 8: - throw new Error('TODO: cmap format 8'); + // TODO: support format 8 + throw new UnsupportedFontDataError('Unsupported cmap format 8'); case 6: case 10: @@ -210,10 +221,10 @@ export default class CmapProcessor { case 14: // Format 14 is handled separately via the uvs property - throw new Error('Unexpected cmap format 14'); + throw new AssertionError('Unexpected cmap format 14'); default: - throw new Error(`Unknown cmap format ${cmap.version}`); + throw new InvalidFontDataError(`Unknown cmap format ${cmap.version}`); } } @@ -317,6 +328,12 @@ export default class CmapProcessor { return res; } + case 2: + case 6: + case 8: + case 10: + throw new UnsupportedFontDataError(`Unsupported cmap format ${cmap.version}`); + case 12: { let res = []; for (let group of cmap.groups.toArray()) { @@ -339,8 +356,12 @@ export default class CmapProcessor { return res; } + case 14: + // Format 14 is handled separately via the uvs property + throw new AssertionError('Unexpected cmap format 14'); + default: - throw new Error(`Unknown cmap format ${cmap.version}`); + throw new InvalidFontDataError(`Unknown cmap format ${cmap.version}`); } } } diff --git a/src/TTFFont.js b/src/TTFFont.js index 24d3c89d..aa29dbd0 100644 --- a/src/TTFFont.js +++ b/src/TTFFont.js @@ -1,6 +1,6 @@ import * as r from 'restructure'; import { cache } from './decorators'; -import { isLoggingErrors, getDefaultLanguage as getGlobalDefaultLanguage } from './base'; +import { isLoggingWarnings, getDefaultLanguage as getGlobalDefaultLanguage } from './base'; import Directory from './tables/directory'; import tables from './tables/index'; import CmapProcessor from './CmapProcessor'; @@ -14,6 +14,7 @@ import TTFSubset from './subset/TTFSubset'; import CFFSubset from './subset/CFFSubset'; import BBox from './glyph/BBox'; import { asciiDecoder } from './utils/decode'; +import { InvalidCallerInputError } from './errors'; /** * This is the base class for all SFNT-based font formats in fontkit. @@ -81,9 +82,9 @@ export default class TTFFont { try { this._tables[table.tag] = this._decodeTable(table); } catch (e) { - if (isLoggingErrors()) { - console.error(`Error decoding table ${table.tag}`); - console.error(e.stack); + if (isLoggingWarnings()) { + console.warn(`Failed to decode table ${table.tag}`); + console.warn(e.stack); } } } @@ -102,7 +103,7 @@ export default class TTFFont { } _decodeDirectory() { - return this.directory = Directory.decode(this.stream, {_startOffset: 0}); + return this.directory = Directory.decode(this.stream, { _startOffset: 0 }); } _decodeTable(table) { @@ -125,12 +126,12 @@ export default class TTFFont { if (record) { // Attempt to retrieve the entry, depending on which translation is available: return ( - record[lang] - || record[this.defaultLanguage] - || record[getGlobalDefaultLanguage()] - || record['en'] - || record[Object.keys(record)[0]] // Seriously, ANY language would be fine - || null + record[lang] + || record[this.defaultLanguage] + || record[getGlobalDefaultLanguage()] + || record['en'] + || record[Object.keys(record)[0]] // Seriously, ANY language would be fine + || null ); } @@ -561,7 +562,9 @@ export default class TTFFont { */ getVariation(settings) { if (!(this.directory.tables.fvar && ((this.directory.tables.gvar && this.directory.tables.glyf) || this.directory.tables.CFF2))) { - throw new Error('Variations require a font with the fvar, gvar and glyf, or CFF2 tables.'); + throw new InvalidCallerInputError( + 'Variations require a font with the fvar, gvar and glyf, or CFF2 tables.' + ); } if (typeof settings === 'string') { @@ -569,7 +572,9 @@ export default class TTFFont { } if (typeof settings !== 'object') { - throw new Error('Variation settings must be either a variation name or settings object.'); + throw new InvalidCallerInputError( + 'Variation settings must be either a variation name or settings object.' + ); } // normalize the coordinates diff --git a/src/TrueTypeCollection.js b/src/TrueTypeCollection.js index 90540049..1a40bc8c 100644 --- a/src/TrueTypeCollection.js +++ b/src/TrueTypeCollection.js @@ -1,8 +1,7 @@ import * as r from 'restructure'; import TTFFont from './TTFFont'; -import Directory from './tables/directory'; -import tables from './tables'; import { asciiDecoder } from './utils/decode'; +import { AssertionError } from './errors'; let TTCHeader = new r.VersionedStruct(r.uint32, { 0x00010000: { @@ -31,7 +30,8 @@ export default class TrueTypeCollection { constructor(stream) { this.stream = stream; if (stream.readString(4) !== 'ttcf') { - throw new Error('Not a TrueType collection'); + // Should be unreachable: probe() verifies the TTC tag before construction. + throw new AssertionError('Not a TrueType collection'); } this.header = TTCHeader.decode(stream); diff --git a/src/aat/AATLookupTable.js b/src/aat/AATLookupTable.js index 636a4166..d0cc9854 100644 --- a/src/aat/AATLookupTable.js +++ b/src/aat/AATLookupTable.js @@ -1,5 +1,6 @@ import {cache} from '../decorators'; import {range} from '../utils/arrays'; +import { InvalidFontDataError } from '../errors'; export default class AATLookupTable { constructor(table) { @@ -70,7 +71,7 @@ export default class AATLookupTable { return this.table.values[glyph - this.table.firstGlyph]; default: - throw new Error(`Unknown lookup table format: ${this.table.version}`); + throw new InvalidFontDataError(`Unknown lookup table format: ${this.table.version}`); } } @@ -117,7 +118,7 @@ export default class AATLookupTable { } default: - throw new Error(`Unknown lookup table format: ${this.table.version}`); + throw new InvalidFontDataError(`Unknown lookup table format: ${this.table.version}`); } return res; diff --git a/src/aat/AATMorxProcessor.js b/src/aat/AATMorxProcessor.js index 94744e1a..3cadc69d 100644 --- a/src/aat/AATMorxProcessor.js +++ b/src/aat/AATMorxProcessor.js @@ -1,6 +1,7 @@ import AATStateMachine from './AATStateMachine'; import AATLookupTable from './AATLookupTable'; import {cache} from '../decorators'; +import { InvalidFontDataError, UnsupportedFontDataError } from '../errors'; // indic replacement flags const MARK_FIRST = 0x8000; @@ -122,7 +123,7 @@ export default class AATMorxProcessor { case 5: return this.processGlyphInsertion; default: - throw new Error(`Invalid morx subtable type: ${this.subtable.type}`); + throw new InvalidFontDataError(`Invalid morx subtable type: ${this.subtable.type}`); } } @@ -297,7 +298,7 @@ export default class AATMorxProcessor { let reverse = !!(subtable.coverage & REVERSE_DIRECTION); if (reverse) { - throw new Error('Reverse subtable, not supported.'); + throw new UnsupportedFontDataError('Reverse MORX subtable not supported.'); } this.subtable = subtable; @@ -425,6 +426,6 @@ function reorderGlyphs(glyphs, verb, firstGlyph, lastGlyph) { return swap(glyphs, [firstGlyph, 2], [lastGlyph, 2], true, true); default: - throw new Error(`Unknown verb: ${verb}`); + throw new InvalidFontDataError(`Unknown verb: ${verb}`); } } diff --git a/src/base.js b/src/base.js index b2a7a70d..ebbdd0d9 100644 --- a/src/base.js +++ b/src/base.js @@ -2,20 +2,21 @@ // @ts-ignore import { DecodeStream } from 'restructure'; +import { UnsupportedFontFileFormatError } from './errors'; // ----------------------------------------------------------------------------- -let loggingErrors = false; +let loggingWarnings = false; -export function isLoggingErrors() { - return loggingErrors; +export function isLoggingWarnings() { + return loggingWarnings; } /** * @param {boolean} flag */ -export function logErrors(flag) { - loggingErrors = flag; +export function logWarnings(flag) { + loggingWarnings = flag; } // ----------------------------------------------------------------------------- @@ -45,7 +46,9 @@ export function create(buffer, postscriptName) { } } - throw new Error('Unknown font format'); + throw new UnsupportedFontFileFormatError( + 'Unsupported font file format: no registered reader recognized the data' + ); } // ----------------------------------------------------------------------------- diff --git a/src/cff/CFFDict.js b/src/cff/CFFDict.js index 66fc0855..82bb2656 100644 --- a/src/cff/CFFDict.js +++ b/src/cff/CFFDict.js @@ -1,6 +1,7 @@ import CFFOperand from './CFFOperand'; import { PropertyDescriptor } from 'restructure'; import { equalArray } from '../utils/deep-equal'; +import { InvalidFontDataError } from '../errors'; export default class CFFDict { constructor(ops = []) { @@ -73,7 +74,7 @@ export default class CFFDict { let field = this.fields[b]; if (!field) { - throw new Error(`Unknown operator ${b}`); + throw new InvalidFontDataError(`Unknown operator ${b}`); } let val = this.decodeOperands(field[2], stream, ret, operands); diff --git a/src/cff/CFFFont.js b/src/cff/CFFFont.js index c6c6809b..84f48aa8 100644 --- a/src/cff/CFFFont.js +++ b/src/cff/CFFFont.js @@ -3,6 +3,7 @@ import CFFIndex from './CFFIndex'; import CFFTop from './CFFTop'; import CFFPrivateDict from './CFFPrivateDict'; import standardStrings from './CFFStandardStrings'; +import { InvalidFontDataError } from '../errors'; class CFFFont { constructor(stream) { @@ -24,7 +25,7 @@ class CFFFont { if (this.version < 2) { if (this.topDictIndex.length !== 1) { - throw new Error("Only a single font is allowed in CFF"); + throw new InvalidFontDataError('Only a single font is allowed in CFF'); } this.topDict = this.topDictIndex[0]; @@ -134,7 +135,7 @@ class CFFFont { } } default: - throw new Error(`Unknown FDSelect version: ${this.topDict.FDSelect.version}`); + throw new InvalidFontDataError(`Unknown FDSelect version: ${this.topDict.FDSelect.version}`); } } diff --git a/src/cff/CFFIndex.js b/src/cff/CFFIndex.js index fea5eaf3..dddcaa15 100644 --- a/src/cff/CFFIndex.js +++ b/src/cff/CFFIndex.js @@ -1,4 +1,5 @@ import * as r from 'restructure'; +import { AssertionError, InvalidFontDataError } from '../errors'; export default class CFFIndex { constructor(type) { @@ -34,7 +35,7 @@ export default class CFFIndex { } else if (offSize === 4) { offsetType = r.uint32; } else { - throw new Error(`Bad offset size in CFFIndex: ${offSize} ${stream.pos}`); + throw new InvalidFontDataError(`Bad offset size in CFFIndex: ${offSize} ${stream.pos}`); } let ret = []; @@ -90,7 +91,7 @@ export default class CFFIndex { } else if (offset <= 0xffffffff) { offsetType = r.uint32; } else { - throw new Error("Bad offset in CFFIndex"); + throw new AssertionError('CFFIndex size overflow'); } size += 1 + offsetType.size() * (arr.length + 1); @@ -126,7 +127,7 @@ export default class CFFIndex { } else if (offset <= 0xffffffff) { offsetType = r.uint32; } else { - throw new Error("Bad offset in CFFIndex"); + throw new AssertionError('CFFIndex encode offset overflow'); } // write offset size diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 00000000..7ccf2c8e --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,95 @@ +/** + * Error type codes recognized by `@denkiyagi/fontkit`. + */ +export const FontkitErrorTypes = { + /** @see {@link UnsupportedFontFileFormatError} */ + UNSUPPORTED_FONT_FILE_FORMAT: 'UNSUPPORTED_FONT_FILE_FORMAT', + + /** @see {@link UnsupportedFontDataError} */ + UNSUPPORTED_FONT_DATA: 'UNSUPPORTED_FONT_DATA', + + /** @see {@link InvalidFontDataError} */ + INVALID_FONT_DATA: 'INVALID_FONT_DATA', + + /** @see {@link InvalidCallerInputError} */ + INVALID_CALLER_INPUT: 'INVALID_CALLER_INPUT', + + /** @see {@link AssertionError} */ + ASSERTION: 'ASSERTION', +} as const; + +/** + * Error type code recognized by `@denkiyagi/fontkit`. + */ +export type FontkitErrorType = typeof FontkitErrorTypes[keyof typeof FontkitErrorTypes]; + +/** + * Error explicitly thrown by `@denkiyagi/fontkit`. + */ +export class FontkitError extends Error { + /** + * Machine-readable classification for the failure. + */ + readonly type: FontkitErrorType; + + constructor(type: FontkitErrorType, message: string, options?: ErrorOptions) { + super(message, options); + + this.name = new.target.name; + this.type = type; + } +} + +/** + * Buffer cannot be recognized as any supported font container, + * or represents a packaging variant we intentionally do not handle. + */ +export class UnsupportedFontFileFormatError extends FontkitError { + constructor(message: string, options?: ErrorOptions) { + super(FontkitErrorTypes.UNSUPPORTED_FONT_FILE_FORMAT, message, options); + } +} + +/** + * The font decodes successfully but requests optional tables/features that + * our engine has not implemented yet (e.g., cmap format 8, GSUB lookup type 8). + */ +export class UnsupportedFontDataError extends FontkitError { + constructor(message: string, options?: ErrorOptions) { + super(FontkitErrorTypes.UNSUPPORTED_FONT_DATA, message, options); + } +} + +/** + * Encoded data violates the applicable specification or is structurally + * inconsistent (bad offsets, illegal operators, corrupt tuples, ...). + */ +export class InvalidFontDataError extends FontkitError { + constructor(message: string, options?: ErrorOptions) { + super(FontkitErrorTypes.INVALID_FONT_DATA, message, options); + } +} + +/** + * Public API or shared helper received parameters outside its contract + * (wrong types, missing setup call, unsupported descriptors). + * Caller may be user code or an upstream subsystem. + * + * Scenarios that can only be triggered by internal misuse (no external entry point) + * are classified as `AssertionError` instead. + */ +export class InvalidCallerInputError extends FontkitError { + constructor(message: string, options?: ErrorOptions) { + super(FontkitErrorTypes.INVALID_CALLER_INPUT, message, options); + } +} + +/** + * Invariant breach that can only originate from an internal bug; + * the scenario should never happen when inputs are valid. + */ +export class AssertionError extends FontkitError { + constructor(message: string, options?: ErrorOptions) { + super(FontkitErrorTypes.ASSERTION, message, options); + } +} diff --git a/src/glyph/CFFGlyph.js b/src/glyph/CFFGlyph.js index 1ac182f1..d45f2913 100644 --- a/src/glyph/CFFGlyph.js +++ b/src/glyph/CFFGlyph.js @@ -1,5 +1,6 @@ import Glyph from './Glyph'; import Path from './Path'; +import { InvalidFontDataError } from '../errors'; /** * Represents an OpenType PostScript glyph, in the Compact Font Format. @@ -180,7 +181,7 @@ export default class CFFGlyph extends Glyph { case 15: { // vsindex if (cff.version < 2) { - throw new Error('vsindex operator not supported in CFF v1'); + throw new InvalidFontDataError('vsindex operator not supported in CFF v1'); } vsindex = stack.pop(); @@ -189,11 +190,11 @@ export default class CFFGlyph extends Glyph { case 16: { // blend if (cff.version < 2) { - throw new Error('blend operator not supported in CFF v1'); + throw new InvalidFontDataError('blend operator not supported in CFF v1'); } if (!variationProcessor) { - throw new Error('blend operator in non-variation font'); + throw new InvalidFontDataError('blend operator in non-variation font'); } let blendVector = variationProcessor.getBlendVector(vstore, vsindex); @@ -571,12 +572,12 @@ export default class CFFGlyph extends Glyph { break; default: - throw new Error(`Unknown op: 12 ${op}`); + throw new InvalidFontDataError(`Unknown op: 12 ${op}`); } break; default: - throw new Error(`Unknown op: ${op}`); + throw new InvalidFontDataError(`Unknown op: ${op}`); } } else if (op < 247) { diff --git a/src/glyph/GlyphVariationProcessor.js b/src/glyph/GlyphVariationProcessor.js index 71c36878..1c19fdae 100644 --- a/src/glyph/GlyphVariationProcessor.js +++ b/src/glyph/GlyphVariationProcessor.js @@ -1,3 +1,5 @@ +import { InvalidFontDataError } from '../errors'; + const TUPLES_SHARE_POINT_NUMBERS = 0x8000; const TUPLE_COUNT_MASK = 0x0fff; const EMBEDDED_TUPLE_COORD = 0x8000; @@ -105,7 +107,7 @@ export default class GlyphVariationProcessor { } else { if ((tupleIndex & TUPLE_INDEX_MASK) >= gvar.globalCoordCount) { - throw new Error('Invalid gvar table'); + throw new InvalidFontDataError('gvar tuple references invalid shared coordinate index'); } var tupleCoords = gvar.globalCoords[tupleIndex & TUPLE_INDEX_MASK]; diff --git a/src/glyph/TTFGlyph.js b/src/glyph/TTFGlyph.js index 8dc1c75b..bb458b69 100644 --- a/src/glyph/TTFGlyph.js +++ b/src/glyph/TTFGlyph.js @@ -2,6 +2,7 @@ import Glyph from './Glyph'; import Path from './Path'; import BBox from './BBox'; import * as r from 'restructure'; +import { InvalidFontDataError } from '../errors'; // The header for both simple and composite glyphs let GlyfHeader = new r.Struct({ @@ -376,7 +377,7 @@ export default class TTFGlyph extends Glyph { var curvePt = null; } else { - throw new Error("Unknown TTF path state"); + throw new InvalidFontDataError('Inconsistent on/off-curve sequence in glyf contour'); } } diff --git a/src/index.ts b/src/index.ts index 04c29161..a5cbf4de 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,8 @@ registerFormat(TrueTypeCollection); export * from './base'; export { DefaultShaper } from './base'; // Explicit export for preventing tree-shaking +export * from './errors'; + export type { default as TTFFont } from './TTFFont'; export type { default as TrueTypeCollection } from './TrueTypeCollection'; export type { default as Glyph } from './glyph/Glyph'; diff --git a/src/layout/KernProcessor.js b/src/layout/KernProcessor.js index 075062a6..f284c421 100644 --- a/src/layout/KernProcessor.js +++ b/src/layout/KernProcessor.js @@ -1,5 +1,6 @@ // @ts-check +import { UnsupportedFontDataError } from '../errors'; import { binarySearch } from '../utils/arrays'; export default class KernProcessor { @@ -52,7 +53,7 @@ export default class KernProcessor { break; default: - throw new Error(`Unsupported kerning table version ${table.version}`); + throw new UnsupportedFontDataError(`Unsupported kerning table version ${table.version}`); } let val = 0; @@ -94,7 +95,7 @@ export default class KernProcessor { break; default: - throw new Error(`Unsupported kerning sub-table format ${table.format}`); + throw new UnsupportedFontDataError(`Unsupported kerning sub-table format ${table.format}`); } // Microsoft supports the override flag, which resets the result diff --git a/src/node.ts b/src/node.ts index d34de7e9..34681279 100644 --- a/src/node.ts +++ b/src/node.ts @@ -9,6 +9,8 @@ registerFormat(TrueTypeCollection); export * from './base'; export { DefaultShaper } from './base'; // Explicit export for preventing tree-shaking +export * from './errors'; + export type { default as TTFFont } from './TTFFont'; export type { default as TrueTypeCollection } from './TrueTypeCollection'; export type { default as Glyph } from './glyph/Glyph'; diff --git a/src/opentype/GPOSProcessor.js b/src/opentype/GPOSProcessor.js index c0852450..8c704936 100644 --- a/src/opentype/GPOSProcessor.js +++ b/src/opentype/GPOSProcessor.js @@ -1,5 +1,6 @@ import GlyphPosition from '../layout/GlyphPosition'; import OTProcessor from './OTProcessor'; +import { InvalidFontDataError } from '../errors'; /** * Null object for `GlyphPosition`. @@ -278,7 +279,7 @@ export default class GPOSProcessor extends OTProcessor { return this.applyLookup(table.lookupType, table.extension); default: - throw new Error(`Unsupported GPOS table: ${lookupType}`); + throw new InvalidFontDataError(`Unknown GPOS lookupType: ${lookupType}`); } } diff --git a/src/opentype/GSUBProcessor.js b/src/opentype/GSUBProcessor.js index f5672d5d..eaed2096 100644 --- a/src/opentype/GSUBProcessor.js +++ b/src/opentype/GSUBProcessor.js @@ -1,5 +1,6 @@ import OTProcessor from './OTProcessor'; import GlyphInfo from './GlyphInfo'; +import { InvalidFontDataError, UnsupportedFontDataError } from '../errors'; export default class GSUBProcessor extends OTProcessor { applyLookup(lookupType, table) { @@ -185,8 +186,11 @@ export default class GSUBProcessor extends OTProcessor { case 7: // Extension Substitution return this.applyLookup(table.lookupType, table.extension); + case 8: // Reverse Chaining Contextual Single Substitution + throw new UnsupportedFontDataError('GSUB lookupType 8 is not supported'); + default: - throw new Error(`GSUB lookupType ${lookupType} is not supported`); + throw new InvalidFontDataError(`Unknown GSUB lookupType: ${lookupType}`); } } } diff --git a/src/opentype/OTLayoutEngine.js b/src/opentype/OTLayoutEngine.js index 51d8b68c..0eb2c160 100644 --- a/src/opentype/OTLayoutEngine.js +++ b/src/opentype/OTLayoutEngine.js @@ -5,6 +5,7 @@ import * as Shapers from './shapers/index'; import GlyphInfo from './GlyphInfo'; import GSUBProcessor from './GSUBProcessor'; import GPOSProcessor from './GPOSProcessor'; +import { AssertionError } from '../errors'; export default class OTLayoutEngine { /** @@ -63,7 +64,8 @@ export default class OTLayoutEngine { */ substitute(glyphRun) { if (this.glyphInfos == null || this.plan == null) { - throw new Error('setup() must be called before substitute()'); + // Internal guard: shapers invoke substitute() only after setup(). + throw new AssertionError('setup() must be called before substitute()'); } if (this.GSUBProcessor) { @@ -80,7 +82,8 @@ export default class OTLayoutEngine { */ position(glyphRun) { if (this.glyphInfos == null || this.plan == null || this.shaper == null) { - throw new Error('setup() must be called before position()'); + // Internal guard: setup() must run before any positioning work. + throw new AssertionError('setup() must be called before position()'); } let appliedFeatures = null; @@ -116,7 +119,8 @@ export default class OTLayoutEngine { */ zeroMarkAdvances(positions) { if (this.glyphInfos == null) { - throw new Error('setup() must be called before zeroMarkAdvances()'); + // Internal guard: zeroing advances only happens after setup(). + throw new AssertionError('setup() must be called before zeroMarkAdvances()'); } for (let i = 0; i < this.glyphInfos.length; i++) { diff --git a/src/opentype/OTProcessor.js b/src/opentype/OTProcessor.js index 1442b43d..5e5ef397 100644 --- a/src/opentype/OTProcessor.js +++ b/src/opentype/OTProcessor.js @@ -1,8 +1,12 @@ import GlyphIterator from './GlyphIterator'; import * as Script from '../layout/Script'; +import { AssertionError } from '../errors'; const DEFAULT_SCRIPTS = ['DFLT', 'dflt', 'latn']; +/** + * @abstract + */ export default class OTProcessor { constructor(font, table) { this.font = font; @@ -218,8 +222,11 @@ export default class OTProcessor { } } + /** + * @abstract + */ applyLookup(lookup, table) { - throw new Error('applyLookup must be implemented by subclasses'); + throw new AssertionError('applyLookup must be implemented by subclasses'); } applyLookupList(lookupRecords) { diff --git a/src/opentype/ShapingPlan.js b/src/opentype/ShapingPlan.js index 9717022d..71de4ff2 100644 --- a/src/opentype/ShapingPlan.js +++ b/src/opentype/ShapingPlan.js @@ -1,5 +1,7 @@ // @ts-check +import { AssertionError, InvalidCallerInputError } from '../errors'; + /** * ShapingPlans are used by the OpenType shapers to store which * features should by applied, and in what order to apply them. @@ -51,7 +53,7 @@ export default class ShapingPlan { } } } else { - throw new Error('Invalid data type of stage in ShapingPlan#stages'); + throw new AssertionError('Invalid data type of stage in ShapingPlan#stages'); } } @@ -76,7 +78,7 @@ export default class ShapingPlan { this._addFeatures(arg.global || [], true); this._addFeatures(arg.local || [], false); } else { - throw new Error('Unsupported argument to ShapingPlan#add'); + throw new InvalidCallerInputError('Unsupported argument to ShapingPlan#add'); } } @@ -112,7 +114,7 @@ export default class ShapingPlan { delete this.allFeatures[tag]; delete this.globalFeatures[tag]; } else { - throw new Error('Invalid data type of stage in ShapingPlan#stages'); + throw new AssertionError('Invalid data type of stage in ShapingPlan#stages'); } } } diff --git a/src/subset/CFFSubset.js b/src/subset/CFFSubset.js index 1c865cab..19837794 100644 --- a/src/subset/CFFSubset.js +++ b/src/subset/CFFSubset.js @@ -1,7 +1,7 @@ import Subset from './Subset'; import CFFTop from '../cff/CFFTop'; -import CFFPrivateDict from '../cff/CFFPrivateDict'; import standardStrings from '../cff/CFFStandardStrings'; +import { AssertionError } from '../errors'; export default class CFFSubset extends Subset { /** @@ -14,7 +14,8 @@ export default class CFFSubset extends Subset { this.cff = this.font['CFF ']; if (!this.cff) { - throw new Error('Not a CFF Font'); + // Subset constructors are only called after format probing, so this flags an invariant breach. + throw new AssertionError('CFFSubset requires a font with a CFF table'); } } diff --git a/src/subset/Subset.js b/src/subset/Subset.js index a80ab599..5d0d5704 100644 --- a/src/subset/Subset.js +++ b/src/subset/Subset.js @@ -1,5 +1,10 @@ // @ts-check +import { AssertionError } from '../errors'; + +/** + * @abstract + */ export default class Subset { /** * @type {('TTF' | 'CFF' | 'UNKNOWN')} @@ -47,9 +52,9 @@ export default class Subset { } /** - * @returns {Uint8Array} + * @abstract */ encode() { - throw new Error('Not implemented'); + throw new AssertionError('Subset.encode() must be overridden by subclasses'); } } diff --git a/src/types.ts b/src/types.ts index 16d05f69..9fbf1032 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,59 +1,59 @@ -import type TTFFont from './TTFFont.js'; +import type TTFFont from './TTFFont.js'; import type GlyphInfo from './opentype/GlyphInfo.js'; import type ShapingPlan from './opentype/ShapingPlan.js'; - -export type { GlyphInfo, ShapingPlan }; - -/** - * Advanced parameters for `TTFFont#layout` and `LayoutEngine#layout`. - */ -export type LayoutAdvancedParams = { - /** - * If not provided, `fontkit` attempts to detect the script from the string. - */ - script?: string; - - /** - * If not provided, `fontkit` uses the default language of the script. - */ - language?: string; - - /** - * If not provided, `fontkit` uses the default direction of the script. - */ - direction?: 'ltr' | 'rtl'; - - /** - * If not provided, `fontkit` chooses its own prepared `Shaper` based on the script. - */ - shaper?: Shaper; - - /** - * Set to `true` to skip position adjustment for each individual glyph. - * This results in `GlyphRun#positions` being `null`. - */ - skipPerGlyphPositioning?: boolean; -}; - -export interface Shaper { - zeroMarkWidths?: 'NONE' | 'BEFORE_GPOS' | 'AFTER_GPOS'; - - plan( - plan: ShapingPlan, - glyphs: GlyphInfo[], - userFeatures: string[] | Record - ): void; - - assignFeatures(plan: ShapingPlan, glyphs: GlyphInfo[]): void; -} - -/** - * Element of `ShapingPlan#stages` that is either an array of feature tags or a single `ShapingPlanStageFunction`. - */ -export type ShapingPlanStage = string[] | ShapingPlanStageFunction; - -export type ShapingPlanStageFunction = ( - font: TTFFont, - glyphs: GlyphInfo[], - plan: ShapingPlan -) => void; + +export type { GlyphInfo, ShapingPlan }; + +/** + * Advanced parameters for `TTFFont#layout` and `LayoutEngine#layout`. + */ +export type LayoutAdvancedParams = { + /** + * If not provided, `fontkit` attempts to detect the script from the string. + */ + script?: string; + + /** + * If not provided, `fontkit` uses the default language of the script. + */ + language?: string; + + /** + * If not provided, `fontkit` uses the default direction of the script. + */ + direction?: 'ltr' | 'rtl'; + + /** + * If not provided, `fontkit` chooses its own prepared `Shaper` based on the script. + */ + shaper?: Shaper; + + /** + * Set to `true` to skip position adjustment for each individual glyph. + * This results in `GlyphRun#positions` being `null`. + */ + skipPerGlyphPositioning?: boolean; +}; + +export interface Shaper { + zeroMarkWidths?: 'NONE' | 'BEFORE_GPOS' | 'AFTER_GPOS'; + + plan( + plan: ShapingPlan, + glyphs: GlyphInfo[], + userFeatures: string[] | Record + ): void; + + assignFeatures(plan: ShapingPlan, glyphs: GlyphInfo[]): void; +} + +/** + * Element of `ShapingPlan#stages` that is either an array of feature tags or a single `ShapingPlanStageFunction`. + */ +export type ShapingPlanStage = string[] | ShapingPlanStageFunction; + +export type ShapingPlanStageFunction = ( + font: TTFFont, + glyphs: GlyphInfo[], + plan: ShapingPlan +) => void; diff --git a/src/utils/clone.js b/src/utils/clone.js index a7da6ccc..802d1d83 100644 --- a/src/utils/clone.js +++ b/src/utils/clone.js @@ -1,5 +1,6 @@ // @ts-check +import { AssertionError } from '../errors'; import { isPrimitive } from './primitive.js'; /** @@ -41,7 +42,8 @@ function cloneValue(value, seen) { return cloneObject(value, seen); } - throw new TypeError('cloneDeep only supports primitives, arrays, and plain objects'); + // Internal misuse if we reach here + throw new AssertionError('cloneDeep only supports primitives, arrays, and plain objects'); } /** diff --git a/src/utils/deep-equal.js b/src/utils/deep-equal.js index 483df2ce..d0658570 100644 --- a/src/utils/deep-equal.js +++ b/src/utils/deep-equal.js @@ -1,5 +1,6 @@ // @ts-check +import { AssertionError } from '../errors'; import { isPrimitive } from './primitive.js'; /** @@ -46,5 +47,6 @@ export function equalArray(left, right) { } } - throw new TypeError('equalArray only supports primitives and arrays'); + // Internal misuse if we reach here + throw new AssertionError('equalArray only supports primitives and arrays'); } diff --git a/test/index.js b/test/index.js index 73e29390..73a5ac1e 100644 --- a/test/index.js +++ b/test/index.js @@ -32,14 +32,17 @@ describe('fontkit', function () { }); it('should error when opening an invalid font asynchronously', async function () { - assert.rejects( + await assert.rejects( fontkit.open(new URL(import.meta.url)), - 'Unknown font format' + fontkit.UnsupportedFontFileFormatError ); }); it('should error when opening an invalid font synchronously', function () { - assert.throws(() => fontkit.openSync(new URL(import.meta.url)), /Unknown font format/); + assert.throws( + () => fontkit.openSync(new URL(import.meta.url)), + fontkit.UnsupportedFontFileFormatError + ); }); it('should get collection objects for ttc fonts', function () { diff --git a/tsconfig-types.json b/tsconfig-types.json index 7fa57e1e..e40ef199 100644 --- a/tsconfig-types.json +++ b/tsconfig-types.json @@ -1,13 +1,13 @@ { + "extends": "./tsconfig.json", "compilerOptions": { + "noEmit": false, "declaration": true, "emitDeclarationOnly": true, - "allowJs": true, - "target": "ESNext", - "outDir": "types", + "outDir": "types" }, "include": [ "src/index.ts", - "src/node.ts", + "src/node.ts" ] } diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..fe7eb799 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "experimentalDecorators": true, + "allowJs": true, + "checkJs": false, + "strictNullChecks": true, + "skipLibCheck": true, + "noEmit": true + }, + "include": [ + "src/**/*.ts", + "src/**/*.js" + ] +}