From 7411745154049e44c5f2d59846f753140e86a8f2 Mon Sep 17 00:00:00 2001 From: FAL Date: Thu, 13 Nov 2025 21:15:35 +0900 Subject: [PATCH 01/14] =?UTF-8?q?refactor:=20utils.js=20=E3=82=92=E3=83=A2?= =?UTF-8?q?=E3=82=B8=E3=83=A5=E3=83=BC=E3=83=AB=E5=88=86=E5=89=B2=E3=81=97?= =?UTF-8?q?=E3=81=A6=20src/utils/=20=E3=81=AB=E5=86=8D=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/CmapProcessor.js | 3 +-- src/TTFFont.js | 2 +- src/TrueTypeCollection.js | 2 +- src/aat/AATLookupTable.js | 2 +- src/layout/KernProcessor.js | 2 +- src/opentype/shapers/ArabicShaper.js | 2 +- src/opentype/shapers/IndicShaper.js | 2 +- src/opentype/shapers/UniversalShaper.js | 2 +- src/utils/arrays.js | 26 ++++++++++++++++++++++++ src/{utils.js => utils/decode.js} | 27 ------------------------- 10 files changed, 34 insertions(+), 36 deletions(-) create mode 100644 src/utils/arrays.js rename src/{utils.js => utils/decode.js} (70%) diff --git a/src/CmapProcessor.js b/src/CmapProcessor.js index 5d61759d..390cc944 100644 --- a/src/CmapProcessor.js +++ b/src/CmapProcessor.js @@ -1,7 +1,6 @@ -import { binarySearch } from './utils'; +import { binarySearch, range } from './utils/arrays'; import { encodingExists, getEncoding, getEncodingMapping } from './encodings'; import { cache } from './decorators'; -import { range } from './utils'; export default class CmapProcessor { constructor(cmapTable) { diff --git a/src/TTFFont.js b/src/TTFFont.js index 77f8a533..24d3c89d 100644 --- a/src/TTFFont.js +++ b/src/TTFFont.js @@ -13,7 +13,7 @@ import GlyphVariationProcessor from './glyph/GlyphVariationProcessor'; import TTFSubset from './subset/TTFSubset'; import CFFSubset from './subset/CFFSubset'; import BBox from './glyph/BBox'; -import { asciiDecoder } from './utils'; +import { asciiDecoder } from './utils/decode'; /** * This is the base class for all SFNT-based font formats in fontkit. diff --git a/src/TrueTypeCollection.js b/src/TrueTypeCollection.js index f9efb4d9..90540049 100644 --- a/src/TrueTypeCollection.js +++ b/src/TrueTypeCollection.js @@ -2,7 +2,7 @@ import * as r from 'restructure'; import TTFFont from './TTFFont'; import Directory from './tables/directory'; import tables from './tables'; -import { asciiDecoder } from './utils'; +import { asciiDecoder } from './utils/decode'; let TTCHeader = new r.VersionedStruct(r.uint32, { 0x00010000: { diff --git a/src/aat/AATLookupTable.js b/src/aat/AATLookupTable.js index 74f764c7..636a4166 100644 --- a/src/aat/AATLookupTable.js +++ b/src/aat/AATLookupTable.js @@ -1,5 +1,5 @@ import {cache} from '../decorators'; -import {range} from '../utils'; +import {range} from '../utils/arrays'; export default class AATLookupTable { constructor(table) { diff --git a/src/layout/KernProcessor.js b/src/layout/KernProcessor.js index eeb85d84..075062a6 100644 --- a/src/layout/KernProcessor.js +++ b/src/layout/KernProcessor.js @@ -1,6 +1,6 @@ // @ts-check -import { binarySearch } from '../utils'; +import { binarySearch } from '../utils/arrays'; export default class KernProcessor { /** diff --git a/src/opentype/shapers/ArabicShaper.js b/src/opentype/shapers/ArabicShaper.js index 07cbd391..d44ed33e 100644 --- a/src/opentype/shapers/ArabicShaper.js +++ b/src/opentype/shapers/ArabicShaper.js @@ -1,7 +1,7 @@ import DefaultShaper from './DefaultShaper'; import {getCategory} from 'unicode-properties'; import UnicodeTrie from 'unicode-trie'; -import { decodeBase64 } from '../../utils'; +import { decodeBase64 } from '../../utils/decode'; const trie = new UnicodeTrie(decodeBase64(require('fs').readFileSync(__dirname + '/data.trie', 'base64'))); const FEATURES = ['isol', 'fina', 'fin2', 'fin3', 'medi', 'med2', 'init']; diff --git a/src/opentype/shapers/IndicShaper.js b/src/opentype/shapers/IndicShaper.js index b9b3290e..bd5a3bea 100644 --- a/src/opentype/shapers/IndicShaper.js +++ b/src/opentype/shapers/IndicShaper.js @@ -14,7 +14,7 @@ import { HALANT_OR_COENG_FLAGS, INDIC_CONFIGS, INDIC_DECOMPOSITIONS } from './indic-data'; -import { decodeBase64 } from '../../utils'; +import { decodeBase64 } from '../../utils/decode'; const {decompositions} = useData; const trie = new UnicodeTrie(decodeBase64(require('fs').readFileSync(__dirname + '/indic.trie', 'base64'))); diff --git a/src/opentype/shapers/UniversalShaper.js b/src/opentype/shapers/UniversalShaper.js index cfec0557..ce28ecc6 100644 --- a/src/opentype/shapers/UniversalShaper.js +++ b/src/opentype/shapers/UniversalShaper.js @@ -3,7 +3,7 @@ import StateMachine from 'dfa'; import UnicodeTrie from 'unicode-trie'; import GlyphInfo from '../GlyphInfo'; import useData from './use.json'; -import { decodeBase64 } from '../../utils'; +import { decodeBase64 } from '../../utils/decode'; const {categories, decompositions} = useData; const trie = new UnicodeTrie(decodeBase64(require('fs').readFileSync(__dirname + '/use.trie', 'base64'))); diff --git a/src/utils/arrays.js b/src/utils/arrays.js new file mode 100644 index 00000000..f5bf9874 --- /dev/null +++ b/src/utils/arrays.js @@ -0,0 +1,26 @@ +export function binarySearch(arr, cmp) { + let min = 0; + let max = arr.length - 1; + while (min <= max) { + let mid = (min + max) >> 1; + let res = cmp(arr[mid]); + + if (res < 0) { + max = mid - 1; + } else if (res > 0) { + min = mid + 1; + } else { + return mid; + } + } + + return -1; +} + +export function range(index, end) { + let range = []; + while (index < end) { + range.push(index++); + } + return range; +} diff --git a/src/utils.js b/src/utils/decode.js similarity index 70% rename from src/utils.js rename to src/utils/decode.js index 72db5760..4ca06118 100644 --- a/src/utils.js +++ b/src/utils/decode.js @@ -1,30 +1,3 @@ -export function binarySearch(arr, cmp) { - let min = 0; - let max = arr.length - 1; - while (min <= max) { - let mid = (min + max) >> 1; - let res = cmp(arr[mid]); - - if (res < 0) { - max = mid - 1; - } else if (res > 0) { - min = mid + 1; - } else { - return mid; - } - } - - return -1; -} - -export function range(index, end) { - let range = []; - while (index < end) { - range.push(index++); - } - return range; -} - export const asciiDecoder = new TextDecoder('ascii'); // Based on https://github.com/niklasvh/base64-arraybuffer. MIT license. From b9e0dffe49eee29b9df24b316bf25bd88d73e461 Mon Sep 17 00:00:00 2001 From: FAL Date: Fri, 14 Nov 2025 00:52:16 +0900 Subject: [PATCH 02/14] chore: update typescript --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index e06faf9d..9e495132 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,7 +26,7 @@ "npm-run-all": "^4.1.5", "parcel": "2.0.0-canary.1713", "shx": "^0.3.4", - "typescript": "^5.7.2" + "typescript": "^5.9.3" } }, "node_modules/@babel/code-frame": { @@ -5993,10 +5993,11 @@ "dev": true }, "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index 624ba6ae..a6212331 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,6 @@ "npm-run-all": "^4.1.5", "parcel": "2.0.0-canary.1713", "shx": "^0.3.4", - "typescript": "^5.7.2" + "typescript": "^5.9.3" } } From e3f4c72b38da590c195524695848b3a1a4930815 Mon Sep 17 00:00:00 2001 From: FAL Date: Fri, 14 Nov 2025 02:01:42 +0900 Subject: [PATCH 03/14] =?UTF-8?q?cloneDeep()=20=E3=81=AE=E4=BB=A3=E6=9B=BF?= =?UTF-8?q?=E3=81=A8=E3=81=97=E3=81=A6=E7=8B=AC=E8=87=AA=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/clone.js | 123 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/utils/clone.js diff --git a/src/utils/clone.js b/src/utils/clone.js new file mode 100644 index 00000000..df6ba39b --- /dev/null +++ b/src/utils/clone.js @@ -0,0 +1,123 @@ +// @ts-check + +/** + * Deeply clones the value. + * + * - Supports primitives, arrays, and plain objects. + * - Handles circular references by tracking already-cloned objects. + * - Preserves object prototypes (Object.prototype or null). + * - Clones only enumerable properties (both string keys and symbols). + * - Does not support functions, class instances, or built-in objects + * (Date, RegExp, Map, Set, Buffer, etc.). + * + * @template T + * @param {T} value - The value to clone. + * @returns {T} A deep clone of the value. + * @throws {TypeError} If `value` contains unsupported types. + */ +export function cloneDeep(value) { + return cloneValue(value, new Map()); +} + +/** + * Recursively clones a value, tracking seen objects to handle circular references. + * + * @param {unknown} value + * @param {Map} seen + * @returns {any} + */ +function cloneValue(value, seen) { + if (isPrimitive(value)) { + return value; + } + + if (Array.isArray(value)) { + return cloneArray(value, seen); + } + + if (isCloneableObject(value)) { + return cloneObject(value, seen); + } + + throw new TypeError('cloneDeep only supports primitives, arrays, and plain objects'); +} + +/** + * Checks if a value is a primitive type + * (null, string, number, boolean, undefined, bigint, symbol). + * + * @param {unknown} value + * @returns {boolean} + */ +function isPrimitive(value) { + return value === null || (typeof value !== 'object' && typeof value !== 'function'); +} + +/** + * Checks if a value is a plain object (prototype is Object.prototype or null). + * + * @param {unknown} value + * @returns {value is Record} + */ +function isCloneableObject(value) { + if (value === null || typeof value !== 'object') { + return false; + } + + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +} + +/** + * Clones an array and recursively clones all its elements. + * + * @param {any[]} value + * @param {Map} seen + * @returns {any[]} + */ +function cloneArray(value, seen) { + if (seen.has(value)) { + return seen.get(value); + } + + const result = new Array(value.length); + seen.set(value, result); + + for (let i = 0; i < value.length; i++) { + result[i] = cloneValue(value[i], seen); + } + + return result; +} + +/** + * Clones a plain object, preserving its prototype and + * recursively cloning all properties and enumerable symbols. + * + * @param {Record} value + * @param {Map} seen + * @returns {Record} + */ +function cloneObject(value, seen) { + if (seen.has(value)) { + return seen.get(value); + } + + const result = Object.create(Object.getPrototypeOf(value)); + seen.set(value, result); + + for (const key of Object.keys(value)) { + result[key] = cloneValue(value[key], seen); + } + + const symbols = Object.getOwnPropertySymbols(value); + for (const symbol of symbols) { + const descriptor = Object.getOwnPropertyDescriptor(value, symbol); + if (descriptor && !descriptor.enumerable) { + continue; + } + result[symbol] = cloneValue(value[symbol], seen); + } + + return result; +} From 2964c92fbef0357bc6f4499374530fc810e827b2 Mon Sep 17 00:00:00 2001 From: FAL Date: Fri, 14 Nov 2025 02:03:17 +0900 Subject: [PATCH 04/14] =?UTF-8?q?test:=20cloneDeep()=20=E3=81=AE=E3=83=A6?= =?UTF-8?q?=E3=83=8B=E3=83=83=E3=83=88=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- test/unit/utils/clone.test.js | 90 +++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 test/unit/utils/clone.test.js diff --git a/package.json b/package.json index a6212331..2884c89e 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ ], "scripts": { "test": "run-s build mocha", - "mocha": "mocha", + "mocha": "mocha \"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" + "coverage": "c8 mocha \"test/**/*.js\"" }, "type": "module", "main": "dist/main.cjs", diff --git a/test/unit/utils/clone.test.js b/test/unit/utils/clone.test.js new file mode 100644 index 00000000..873d4321 --- /dev/null +++ b/test/unit/utils/clone.test.js @@ -0,0 +1,90 @@ +import assert from 'assert'; +import { cloneDeep } from '../../../src/utils/clone.js'; + +describe('unit test: clone', function () { + describe('cloneDeep', function () { + it('returns primitives as-is', function () { + assert.strictEqual(cloneDeep(42), 42); + assert.strictEqual(cloneDeep(null), null); + assert.strictEqual(cloneDeep('fontkit'), 'fontkit'); + }); + + it('clones nested arrays and objects', function () { + let source = [{ width: 123, names: ['a', 'b'] }, { width: 456, names: [] }]; + let copy = cloneDeep(source); + + assert.notStrictEqual(copy, source); + assert.notStrictEqual(copy[0], source[0]); + assert.notStrictEqual(copy[0].names, source[0].names); + assert.deepStrictEqual(copy, source); + + source[0].names.push('c'); + assert.deepStrictEqual(copy[0].names, ['a', 'b']); + }); + + it('preserves circular references', function () { + let source = { name: 'metrics' }; + source.self = source; + + let copy = cloneDeep(source); + assert.notStrictEqual(copy, source); + assert.strictEqual(copy.self, copy); + assert.deepStrictEqual({ name: copy.name }, { name: 'metrics' }); + }); + + it('preserves circular arrays', function () { + let source = []; + source[0] = source; + + let copy = cloneDeep(source); + assert.notStrictEqual(copy, source); + assert.strictEqual(copy[0], copy); + }); + + it('copies enumerable symbols and ignores hidden ones', function () { + let visible = Symbol('visible'); + let hidden = Symbol('hidden'); + + let head = {}; + Object.defineProperty(head, visible, { value: 'ok', enumerable: true }); + Object.defineProperty(head, hidden, { value: 'skip', enumerable: false }); + + let copy = cloneDeep(head); + assert.strictEqual(copy[visible], 'ok'); + assert.strictEqual(Object.prototype.hasOwnProperty.call(copy, hidden), false); + }); + + it('handles structures similar to font tables', function () { + let maxp = { + numGlyphs: 42, + version: 1.0, + stats: { + maxPoints: 120, + maxContours: 10 + } + }; + + let head = Object.assign(Object.create(null), { + indexToLocFormat: 0, + flags: { + baselineAtY0: true, + forcePPEMToInteger: false + }, + bbox: { xMin: -10, yMin: -20, xMax: 400, yMax: 800 } + }); + + let cloned = cloneDeep({ maxp, head }); + + assert.deepStrictEqual(cloned, { maxp, head }); + assert.notStrictEqual(cloned.maxp, maxp); + assert.notStrictEqual(cloned.head, head); + assert.notStrictEqual(cloned.head.bbox, head.bbox); + }); + + it('throws when encountering unsupported values', function () { + assert.throws(() => cloneDeep(new Date()), /only supports/); + assert.throws(() => cloneDeep(Buffer.from('a')), /only supports/); + assert.throws(() => cloneDeep(function noop() { }), /only supports/); + }); + }); +}); From 2f4934e0bc26f619bfcf897da2e75f69dd8cc535 Mon Sep 17 00:00:00 2001 From: FAL Date: Fri, 14 Nov 2025 02:17:06 +0900 Subject: [PATCH 05/14] =?UTF-8?q?refactor:=20cloneDeep()=20=E3=81=AE?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=AE=87=E6=89=80=E3=82=92=E5=A4=96=E9=83=A8?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=83=96=E3=83=A9=E3=83=AA=E7=89=88=E3=81=8B?= =?UTF-8?q?=E3=82=89=E7=8B=AC=E8=87=AA=E5=AE=9F=E8=A3=85=E7=89=88=E3=81=AB?= =?UTF-8?q?=E7=BD=AE=E3=81=8D=E6=8F=9B=E3=81=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/subset/TTFSubset.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/subset/TTFSubset.js b/src/subset/TTFSubset.js index fdeb5b25..29448fe7 100644 --- a/src/subset/TTFSubset.js +++ b/src/subset/TTFSubset.js @@ -1,4 +1,4 @@ -import cloneDeep from 'clone'; +import { cloneDeep } from '../utils/clone.js'; import Subset from './Subset'; import Directory from '../tables/directory'; import Tables from '../tables'; From e7ca385990f2005a59d3bf3f7d0e3daa01c2bcca Mon Sep 17 00:00:00 2001 From: FAL Date: Fri, 14 Nov 2025 02:21:58 +0900 Subject: [PATCH 06/14] npm uninstall clone --- package-lock.json | 2 +- package.json | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9e495132..56ca6fe1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "license": "MIT", "dependencies": { "@swc/helpers": "^0.5.12", - "clone": "^2.1.2", "dfa": "^1.2.0", "fast-deep-equal": "^3.1.3", "restructure": "^3.0.0", @@ -2508,6 +2507,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, "engines": { "node": ">=0.8" } diff --git a/package.json b/package.json index 2884c89e..0a9c156f 100644 --- a/package.json +++ b/package.json @@ -83,7 +83,6 @@ }, "dependencies": { "@swc/helpers": "^0.5.12", - "clone": "^2.1.2", "dfa": "^1.2.0", "fast-deep-equal": "^3.1.3", "restructure": "^3.0.0", From ee66736f0c68a7a781a43dd776f964a86c617dca Mon Sep 17 00:00:00 2001 From: FAL Date: Fri, 14 Nov 2025 04:15:50 +0900 Subject: [PATCH 07/14] =?UTF-8?q?fast-deep-equal=20=E3=81=AE=E4=BB=A3?= =?UTF-8?q?=E6=9B=BF=E3=81=A8=E3=81=97=E3=81=A6=E7=8B=AC=E8=87=AA=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/deep-equal.js | 56 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 src/utils/deep-equal.js diff --git a/src/utils/deep-equal.js b/src/utils/deep-equal.js new file mode 100644 index 00000000..40e098ab --- /dev/null +++ b/src/utils/deep-equal.js @@ -0,0 +1,56 @@ +// @ts-check + +/** + * @typedef {null | undefined | string | number | boolean | bigint | symbol} Primitive + */ + +/** + * Compares primitives (via `Object.is`) and arrays (recursively) for equality. + * Only accepts primitives or arrays composed of supported values. + * + * @param {Primitive | (Primitive | Primitive[])[]} left + * @param {Primitive | (Primitive | Primitive[])[]} right + * @returns {boolean} + */ +export function equalArray(left, right) { + if (isPrimitive(left)) { + if (isPrimitive(right)) { + return Object.is(left, right); + } + return false; + } else { + if (isPrimitive(right)) { + return false; + } + } + + if (Array.isArray(left)) { + if (Array.isArray(right)) { + if (left.length !== right.length) { + return false; + } + for (let i = 0; i < left.length; i++) { + if (!equalArray(left[i], right[i])) { + return false; + } + } + return true; + } else { + return false; + } + } else { + if (Array.isArray(right)) { + return false; + } + } + + throw new TypeError('equalArray only supports primitives and arrays'); +} + +/** + * @param {unknown} value + * @returns {value is Primitive} + */ +function isPrimitive(value) { + return value == null || (typeof value !== 'object' && typeof value !== 'function'); +} From 2a8af5d434d00645b5d3dd7c5a19df2ef0bdc0ac Mon Sep 17 00:00:00 2001 From: FAL Date: Fri, 14 Nov 2025 04:17:05 +0900 Subject: [PATCH 08/14] =?UTF-8?q?test:=20equalArray()=20=E3=81=AE=E3=83=A6?= =?UTF-8?q?=E3=83=8B=E3=83=83=E3=83=88=E3=83=86=E3=82=B9=E3=83=88=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/unit/utils/deep-equal.test.js | 39 ++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 test/unit/utils/deep-equal.test.js diff --git a/test/unit/utils/deep-equal.test.js b/test/unit/utils/deep-equal.test.js new file mode 100644 index 00000000..b3dc73d4 --- /dev/null +++ b/test/unit/utils/deep-equal.test.js @@ -0,0 +1,39 @@ +import assert from 'assert'; +import { equalArray } from '../../../src/utils/deep-equal.js'; + +describe('unit test: deep-equal', function () { + describe('equalArray', function () { + it('compares primitives via Object.is semantics', function () { + assert.strictEqual(equalArray(42, 42), true); + assert.strictEqual(equalArray(-0, 0), false); + assert.strictEqual(equalArray(NaN, NaN), true); + assert.strictEqual(equalArray(null, undefined), false); + }); + + it('compares nested arrays of supported values', function () { + assert.strictEqual(equalArray([1, 2, 3], [1, 2, 3]), true); + assert.strictEqual(equalArray([1, [2, 3], 4], [1, [2, 3], 4]), true); + assert.strictEqual(equalArray([1, [2, 3], 4], [1, [3, 2], 4]), false); + assert.strictEqual(equalArray([1, 2], [1, 2, 3]), false); + assert.strictEqual(equalArray([1, 2], 1), false); + }); + + it('returns false when only one operand is an array', function () { + assert.strictEqual(equalArray([1, 2], null), false); + assert.strictEqual(equalArray(undefined, [1, 2]), false); + assert.strictEqual(equalArray([1, 2], 1), false); + assert.strictEqual(equalArray(1, [1, 2]), false); + }); + + it('returns false when only one operand is primitive', function () { + assert.strictEqual(equalArray({ a: 1 }, null), false); + assert.strictEqual(equalArray(undefined, { a: 1 }), false); + }); + + it('throws on unsupported values', function () { + assert.throws(() => equalArray({ a: 1 }, { a: 1 }), /only supports primitives and arrays/); + assert.throws(() => equalArray([1, { a: 1 }], [1, { a: 1 }]), /only supports primitives and arrays/); + assert.throws(() => equalArray(Buffer.alloc(1), Buffer.alloc(1)), /only supports primitives and arrays/); + }); + }); +}); From d88a6c583f1b66075d9d0fc3079bd84c4a97d8be Mon Sep 17 00:00:00 2001 From: FAL Date: Fri, 14 Nov 2025 04:17:41 +0900 Subject: [PATCH 09/14] =?UTF-8?q?refactor:=20fast-deep-equal=20=E3=81=AE?= =?UTF-8?q?=E4=BD=BF=E7=94=A8=E7=AE=87=E6=89=80=E3=82=92=E7=8B=AC=E8=87=AA?= =?UTF-8?q?=E5=AE=9F=E8=A3=85=E7=89=88=E3=81=AB=E7=BD=AE=E3=81=8D=E6=8F=9B?= =?UTF-8?q?=E3=81=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cff/CFFDict.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cff/CFFDict.js b/src/cff/CFFDict.js index 569c1067..a333298b 100644 --- a/src/cff/CFFDict.js +++ b/src/cff/CFFDict.js @@ -1,7 +1,7 @@ -import isEqual from 'fast-deep-equal'; import * as r from 'restructure'; import CFFOperand from './CFFOperand'; import { PropertyDescriptor } from 'restructure'; +import { equalArray } from '../utils/deep-equal.js'; export default class CFFDict { constructor(ops = []) { @@ -108,7 +108,7 @@ export default class CFFDict { for (let k in this.fields) { let field = this.fields[k]; let val = dict[field[1]]; - if (val == null || isEqual(val, field[3])) { + if (val == null || equalArray(val, field[3])) { continue; } @@ -141,7 +141,7 @@ export default class CFFDict { for (let field of this.ops) { let val = dict[field[1]]; - if (val == null || isEqual(val, field[3])) { + if (val == null || equalArray(val, field[3])) { continue; } From 4eccc38f49054ba6472f5f6a8b6bf727c8e085d5 Mon Sep 17 00:00:00 2001 From: FAL Date: Fri, 14 Nov 2025 04:18:14 +0900 Subject: [PATCH 10/14] refactor: remove unused import --- src/cff/CFFDict.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cff/CFFDict.js b/src/cff/CFFDict.js index a333298b..4accb00d 100644 --- a/src/cff/CFFDict.js +++ b/src/cff/CFFDict.js @@ -1,4 +1,3 @@ -import * as r from 'restructure'; import CFFOperand from './CFFOperand'; import { PropertyDescriptor } from 'restructure'; import { equalArray } from '../utils/deep-equal.js'; From 5153b6773f4d1a02080cab600c5907a6cbc1443c Mon Sep 17 00:00:00 2001 From: FAL Date: Fri, 14 Nov 2025 04:20:31 +0900 Subject: [PATCH 11/14] npm uninstall fast-deep-equal --- package-lock.json | 6 ------ package.json | 1 - 2 files changed, 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 56ca6fe1..b0d0e6af 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,6 @@ "dependencies": { "@swc/helpers": "^0.5.12", "dfa": "^1.2.0", - "fast-deep-equal": "^3.1.3", "restructure": "^3.0.0", "unicode-properties": "^1.4.0", "unicode-trie": "^2.0.0" @@ -3171,11 +3170,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" - }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", diff --git a/package.json b/package.json index 0a9c156f..738b597a 100644 --- a/package.json +++ b/package.json @@ -84,7 +84,6 @@ "dependencies": { "@swc/helpers": "^0.5.12", "dfa": "^1.2.0", - "fast-deep-equal": "^3.1.3", "restructure": "^3.0.0", "unicode-properties": "^1.4.0", "unicode-trie": "^2.0.0" From 004f7fea23a21394829173d6cae924e293e45928 Mon Sep 17 00:00:00 2001 From: FAL Date: Fri, 14 Nov 2025 04:35:35 +0900 Subject: [PATCH 12/14] =?UTF-8?q?refactor:=20isPrimitive()=20=E3=82=92?= =?UTF-8?q?=E5=88=A5=E3=83=A2=E3=82=B8=E3=83=A5=E3=83=BC=E3=83=AB=E3=81=AB?= =?UTF-8?q?=E6=8A=BD=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/utils/clone.js | 13 ++----------- src/utils/deep-equal.js | 12 +++--------- src/utils/primitive.js | 15 +++++++++++++++ test/unit/utils/primitive.test.js | 30 ++++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 20 deletions(-) create mode 100644 src/utils/primitive.js create mode 100644 test/unit/utils/primitive.test.js diff --git a/src/utils/clone.js b/src/utils/clone.js index df6ba39b..a7da6ccc 100644 --- a/src/utils/clone.js +++ b/src/utils/clone.js @@ -1,5 +1,7 @@ // @ts-check +import { isPrimitive } from './primitive.js'; + /** * Deeply clones the value. * @@ -42,17 +44,6 @@ function cloneValue(value, seen) { throw new TypeError('cloneDeep only supports primitives, arrays, and plain objects'); } -/** - * Checks if a value is a primitive type - * (null, string, number, boolean, undefined, bigint, symbol). - * - * @param {unknown} value - * @returns {boolean} - */ -function isPrimitive(value) { - return value === null || (typeof value !== 'object' && typeof value !== 'function'); -} - /** * Checks if a value is a plain object (prototype is Object.prototype or null). * diff --git a/src/utils/deep-equal.js b/src/utils/deep-equal.js index 40e098ab..483df2ce 100644 --- a/src/utils/deep-equal.js +++ b/src/utils/deep-equal.js @@ -1,7 +1,9 @@ // @ts-check +import { isPrimitive } from './primitive.js'; + /** - * @typedef {null | undefined | string | number | boolean | bigint | symbol} Primitive + * @typedef {import('./primitive.js').Primitive} Primitive */ /** @@ -46,11 +48,3 @@ export function equalArray(left, right) { throw new TypeError('equalArray only supports primitives and arrays'); } - -/** - * @param {unknown} value - * @returns {value is Primitive} - */ -function isPrimitive(value) { - return value == null || (typeof value !== 'object' && typeof value !== 'function'); -} diff --git a/src/utils/primitive.js b/src/utils/primitive.js new file mode 100644 index 00000000..79169cc4 --- /dev/null +++ b/src/utils/primitive.js @@ -0,0 +1,15 @@ +// @ts-check + +/** + * @typedef {null | undefined | string | number | boolean | bigint | symbol} Primitive + */ + +/** + * Returns true when the value is a primitive (including null/undefined). + * + * @param {unknown} value + * @returns {value is Primitive} + */ +export function isPrimitive(value) { + return value == null || (typeof value !== 'object' && typeof value !== 'function'); +} diff --git a/test/unit/utils/primitive.test.js b/test/unit/utils/primitive.test.js new file mode 100644 index 00000000..59852cd1 --- /dev/null +++ b/test/unit/utils/primitive.test.js @@ -0,0 +1,30 @@ +import assert from 'assert'; +import { isPrimitive } from '../../../src/utils/primitive.js'; + +describe('unit test: primitive', function () { + describe('isPrimitive', function () { + it('recognizes primitive values', function () { + assert.strictEqual(isPrimitive(null), true); + assert.strictEqual(isPrimitive(undefined), true); + assert.strictEqual(isPrimitive(0), true); + assert.strictEqual(isPrimitive(-0), true); + assert.strictEqual(isPrimitive(NaN), true); + assert.strictEqual(isPrimitive('fontkit'), true); + assert.strictEqual(isPrimitive(true), true); + assert.strictEqual(isPrimitive(Symbol('s')), true); + assert.strictEqual(isPrimitive(10n), true); + }); + + it('rejects arrays and objects', function () { + assert.strictEqual(isPrimitive({}), false); + assert.strictEqual(isPrimitive(Object.create(null)), false); + assert.strictEqual(isPrimitive([]), false); + }); + + it('rejects functions and class instances', function () { + assert.strictEqual(isPrimitive(() => { }), false); + class Foo { } + assert.strictEqual(isPrimitive(new Foo()), false); + }); + }); +}); From dd14a1c8d0bc80b5cc88ef74741e968f15d1e039 Mon Sep 17 00:00:00 2001 From: FAL Date: Fri, 14 Nov 2025 04:35:54 +0900 Subject: [PATCH 13/14] docs: update changelog --- MODIFICATIONS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/MODIFICATIONS.md b/MODIFICATIONS.md index 4e92f0a8..c389ff21 100644 --- a/MODIFICATIONS.md +++ b/MODIFICATIONS.md @@ -8,6 +8,9 @@ - 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`) ## [2.0.4-mod.2025.2] From 386bafbef624554096a37075413463a9e8b19462 Mon Sep 17 00:00:00 2001 From: FAL Date: Fri, 14 Nov 2025 04:50:41 +0900 Subject: [PATCH 14/14] =?UTF-8?q?chore:=20=E6=97=A2=E5=AD=98=E5=AE=9F?= =?UTF-8?q?=E8=A3=85=E3=81=AB=E5=90=88=E3=82=8F=E3=81=9B=E3=81=A6=20import?= =?UTF-8?q?=20=E3=83=91=E3=82=B9=E3=81=AE=E6=8B=A1=E5=BC=B5=E5=AD=90?= =?UTF-8?q?=E3=82=92=E5=89=8A=E9=99=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cff/CFFDict.js | 2 +- src/subset/TTFSubset.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cff/CFFDict.js b/src/cff/CFFDict.js index 4accb00d..66fc0855 100644 --- a/src/cff/CFFDict.js +++ b/src/cff/CFFDict.js @@ -1,6 +1,6 @@ import CFFOperand from './CFFOperand'; import { PropertyDescriptor } from 'restructure'; -import { equalArray } from '../utils/deep-equal.js'; +import { equalArray } from '../utils/deep-equal'; export default class CFFDict { constructor(ops = []) { diff --git a/src/subset/TTFSubset.js b/src/subset/TTFSubset.js index 29448fe7..a3034868 100644 --- a/src/subset/TTFSubset.js +++ b/src/subset/TTFSubset.js @@ -1,4 +1,4 @@ -import { cloneDeep } from '../utils/clone.js'; +import { cloneDeep } from '../utils/clone'; import Subset from './Subset'; import Directory from '../tables/directory'; import Tables from '../tables';