@shadow-garden/bapbong-measuring 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Le Phuoc Minh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,25 @@
1
+ # @shadow-garden/bapbong-measuring
2
+
3
+ Text measurement + font-metrics, behind a small injectable interface. The layout
4
+ engine and selection math never touch the canvas directly — they call a
5
+ `MeasureText` / `MeasureMetrics` provided by this package, so they stay testable
6
+ in a headless (no-DOM) environment.
7
+
8
+ - **Scope:** `scope:measuring`
9
+ - **Depends on:** `@shadow-garden/bapbong-contracts`
10
+
11
+ ## What it provides
12
+
13
+ - **`createCanvasMeasurer()` / `createCanvasMetrics()`** — real measurement via a
14
+ shared offscreen `CanvasRenderingContext2D` (`ctx.measureText`,
15
+ ascent/descent), with a per-font cache.
16
+ - **`createApproxMeasurer()` / `createApproxMetrics()`** — DOM-free approximations
17
+ (average advance widths) for unit tests and SSR.
18
+ - **`fontToCss(spec)`** — `FontSpec` → CSS `font` shorthand.
19
+
20
+ ## Build / test
21
+
22
+ ```sh
23
+ pnpm nx build @shadow-garden/bapbong-measuring
24
+ pnpm nx test @shadow-garden/bapbong-measuring
25
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,184 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // packages/measuring/src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ FontRegistry: () => FontRegistry,
34
+ createApproxMeasurer: () => createApproxMeasurer,
35
+ createApproxMetrics: () => createApproxMetrics,
36
+ createCanvasMeasurer: () => createCanvasMeasurer,
37
+ createCanvasMetrics: () => createCanvasMetrics,
38
+ createFontRegistryMeasurer: () => createFontRegistryMeasurer,
39
+ createFontRegistryMetrics: () => createFontRegistryMetrics,
40
+ ensureFontsLoaded: () => ensureFontsLoaded,
41
+ fontToCss: () => fontToCss
42
+ });
43
+ module.exports = __toCommonJS(index_exports);
44
+
45
+ // packages/measuring/src/lib/measuring.ts
46
+ var PT_TO_PX = 96 / 72;
47
+ function fontToCss(font) {
48
+ const style = font.italic ? "italic " : "";
49
+ const weight = font.bold ? "700 " : "400 ";
50
+ return `${style}${weight}${font.sizePt}pt ${font.family}`;
51
+ }
52
+ function createCanvasMeasurer() {
53
+ const canvas = document.createElement("canvas");
54
+ const ctx = canvas.getContext("2d");
55
+ if (!ctx) throw new Error("bapbong-measuring: 2D canvas context unavailable");
56
+ return (text, font) => {
57
+ ctx.font = fontToCss(font);
58
+ return ctx.measureText(text).width;
59
+ };
60
+ }
61
+ function createApproxMeasurer(factor = 0.5) {
62
+ return (text, font) => text.length * font.sizePt * factor;
63
+ }
64
+ function createCanvasMetrics() {
65
+ const canvas = document.createElement("canvas");
66
+ const ctx = canvas.getContext("2d");
67
+ if (!ctx) throw new Error("bapbong-measuring: 2D canvas context unavailable");
68
+ return (font) => {
69
+ ctx.font = fontToCss(font);
70
+ const m = ctx.measureText("Mg");
71
+ const ascent = m.fontBoundingBoxAscent ?? m.actualBoundingBoxAscent;
72
+ const descent = m.fontBoundingBoxDescent ?? m.actualBoundingBoxDescent;
73
+ return { ascent, descent };
74
+ };
75
+ }
76
+ function createApproxMetrics() {
77
+ return (font) => {
78
+ const em = font.sizePt * PT_TO_PX;
79
+ return { ascent: em * 0.8, descent: em * 0.2 };
80
+ };
81
+ }
82
+ async function ensureFontsLoaded(families, sizePt = 12) {
83
+ const fonts = globalThis.document?.fonts;
84
+ if (!fonts?.load) return;
85
+ const variants = ["", "italic ", "700 ", "italic 700 "];
86
+ const loads = [];
87
+ for (const family of new Set(families)) {
88
+ for (const variant of variants) {
89
+ loads.push(fonts.load(`${variant}${sizePt}pt "${family}"`).catch(() => []));
90
+ }
91
+ }
92
+ await Promise.all(loads);
93
+ }
94
+
95
+ // packages/measuring/src/lib/font-registry.ts
96
+ var opentype = __toESM(require("opentype.js"), 1);
97
+ var PT_TO_PX2 = 96 / 72;
98
+ function faceKey(family, bold, italic) {
99
+ return `${family.toLowerCase()}|${bold ? "b" : ""}|${italic ? "i" : ""}`;
100
+ }
101
+ var FontRegistry = class {
102
+ faces = /* @__PURE__ */ new Map();
103
+ /** Per-face memo: code point → the file that serves it (or its .notdef fallback). */
104
+ glyphMemo = /* @__PURE__ */ new Map();
105
+ /** Register a parsed opentype font for a family + variant (appends to the
106
+ * face's file list; register subset files in coverage-priority order). */
107
+ register(family, variant, font) {
108
+ const key = faceKey(family, !!variant.bold, !!variant.italic);
109
+ const list = this.faces.get(key);
110
+ if (list) list.push(font);
111
+ else this.faces.set(key, [font]);
112
+ this.glyphMemo.clear();
113
+ }
114
+ /** Parse font-file bytes (TTF/OTF/WOFF) and register the resulting face. */
115
+ registerBytes(family, variant, bytes) {
116
+ this.register(family, variant, opentype.parse(bytes));
117
+ }
118
+ /** The files backing a spec: the exact family+bold+italic face, else the
119
+ * family's regular face, else empty. */
120
+ files(spec) {
121
+ return this.faces.get(faceKey(spec.family, spec.bold, spec.italic)) ?? this.faces.get(faceKey(spec.family, false, false)) ?? [];
122
+ }
123
+ /** Whether any registered file can serve this spec. */
124
+ has(spec) {
125
+ return this.files(spec).length > 0;
126
+ }
127
+ /** The face's first file, for shared vertical metrics (all subset files of one
128
+ * face carry the same hhea/head metrics), or null. */
129
+ primary(spec) {
130
+ return this.files(spec)[0] ?? null;
131
+ }
132
+ /** The file in the face that has a glyph for `codePoint`, else the first file
133
+ * (whose .notdef advance is used), else null. Memoised per face. */
134
+ fileForCodePoint(spec, codePoint) {
135
+ const list = this.files(spec);
136
+ if (list.length === 0) return null;
137
+ if (list.length === 1) return list[0];
138
+ const key = faceKey(spec.family, spec.bold, spec.italic);
139
+ let memo = this.glyphMemo.get(key);
140
+ if (!memo) {
141
+ memo = /* @__PURE__ */ new Map();
142
+ this.glyphMemo.set(key, memo);
143
+ }
144
+ const cached = memo.get(codePoint);
145
+ if (cached) return cached;
146
+ const ch = String.fromCodePoint(codePoint);
147
+ const found = list.find((f) => f.charToGlyph(ch).index !== 0) ?? list[0];
148
+ memo.set(codePoint, found);
149
+ return found;
150
+ }
151
+ };
152
+ function createFontRegistryMeasurer(registry, fallback) {
153
+ return (text, font) => {
154
+ if (!registry.has(font)) return fallback(text, font);
155
+ const px = font.sizePt * PT_TO_PX2;
156
+ let total = 0;
157
+ for (const ch of text) {
158
+ const file = registry.fileForCodePoint(font, ch.codePointAt(0) ?? 0);
159
+ if (!file) continue;
160
+ total += (file.charToGlyph(ch).advanceWidth ?? 0) / file.unitsPerEm * px;
161
+ }
162
+ return total;
163
+ };
164
+ }
165
+ function createFontRegistryMetrics(registry, fallback) {
166
+ return (font) => {
167
+ const face = registry.primary(font);
168
+ if (!face) return fallback(font);
169
+ const scale = font.sizePt * PT_TO_PX2 / face.unitsPerEm;
170
+ return { ascent: face.ascender * scale, descent: Math.abs(face.descender) * scale };
171
+ };
172
+ }
173
+ // Annotate the CommonJS export names for ESM import in node:
174
+ 0 && (module.exports = {
175
+ FontRegistry,
176
+ createApproxMeasurer,
177
+ createApproxMetrics,
178
+ createCanvasMeasurer,
179
+ createCanvasMetrics,
180
+ createFontRegistryMeasurer,
181
+ createFontRegistryMetrics,
182
+ ensureFontsLoaded,
183
+ fontToCss
184
+ });
@@ -0,0 +1,3 @@
1
+ export * from './lib/measuring.js';
2
+ export * from './lib/font-registry.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC;AACnC,cAAc,wBAAwB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,139 @@
1
+ // packages/measuring/src/lib/measuring.ts
2
+ var PT_TO_PX = 96 / 72;
3
+ function fontToCss(font) {
4
+ const style = font.italic ? "italic " : "";
5
+ const weight = font.bold ? "700 " : "400 ";
6
+ return `${style}${weight}${font.sizePt}pt ${font.family}`;
7
+ }
8
+ function createCanvasMeasurer() {
9
+ const canvas = document.createElement("canvas");
10
+ const ctx = canvas.getContext("2d");
11
+ if (!ctx) throw new Error("bapbong-measuring: 2D canvas context unavailable");
12
+ return (text, font) => {
13
+ ctx.font = fontToCss(font);
14
+ return ctx.measureText(text).width;
15
+ };
16
+ }
17
+ function createApproxMeasurer(factor = 0.5) {
18
+ return (text, font) => text.length * font.sizePt * factor;
19
+ }
20
+ function createCanvasMetrics() {
21
+ const canvas = document.createElement("canvas");
22
+ const ctx = canvas.getContext("2d");
23
+ if (!ctx) throw new Error("bapbong-measuring: 2D canvas context unavailable");
24
+ return (font) => {
25
+ ctx.font = fontToCss(font);
26
+ const m = ctx.measureText("Mg");
27
+ const ascent = m.fontBoundingBoxAscent ?? m.actualBoundingBoxAscent;
28
+ const descent = m.fontBoundingBoxDescent ?? m.actualBoundingBoxDescent;
29
+ return { ascent, descent };
30
+ };
31
+ }
32
+ function createApproxMetrics() {
33
+ return (font) => {
34
+ const em = font.sizePt * PT_TO_PX;
35
+ return { ascent: em * 0.8, descent: em * 0.2 };
36
+ };
37
+ }
38
+ async function ensureFontsLoaded(families, sizePt = 12) {
39
+ const fonts = globalThis.document?.fonts;
40
+ if (!fonts?.load) return;
41
+ const variants = ["", "italic ", "700 ", "italic 700 "];
42
+ const loads = [];
43
+ for (const family of new Set(families)) {
44
+ for (const variant of variants) {
45
+ loads.push(fonts.load(`${variant}${sizePt}pt "${family}"`).catch(() => []));
46
+ }
47
+ }
48
+ await Promise.all(loads);
49
+ }
50
+
51
+ // packages/measuring/src/lib/font-registry.ts
52
+ import * as opentype from "opentype.js";
53
+ var PT_TO_PX2 = 96 / 72;
54
+ function faceKey(family, bold, italic) {
55
+ return `${family.toLowerCase()}|${bold ? "b" : ""}|${italic ? "i" : ""}`;
56
+ }
57
+ var FontRegistry = class {
58
+ faces = /* @__PURE__ */ new Map();
59
+ /** Per-face memo: code point → the file that serves it (or its .notdef fallback). */
60
+ glyphMemo = /* @__PURE__ */ new Map();
61
+ /** Register a parsed opentype font for a family + variant (appends to the
62
+ * face's file list; register subset files in coverage-priority order). */
63
+ register(family, variant, font) {
64
+ const key = faceKey(family, !!variant.bold, !!variant.italic);
65
+ const list = this.faces.get(key);
66
+ if (list) list.push(font);
67
+ else this.faces.set(key, [font]);
68
+ this.glyphMemo.clear();
69
+ }
70
+ /** Parse font-file bytes (TTF/OTF/WOFF) and register the resulting face. */
71
+ registerBytes(family, variant, bytes) {
72
+ this.register(family, variant, opentype.parse(bytes));
73
+ }
74
+ /** The files backing a spec: the exact family+bold+italic face, else the
75
+ * family's regular face, else empty. */
76
+ files(spec) {
77
+ return this.faces.get(faceKey(spec.family, spec.bold, spec.italic)) ?? this.faces.get(faceKey(spec.family, false, false)) ?? [];
78
+ }
79
+ /** Whether any registered file can serve this spec. */
80
+ has(spec) {
81
+ return this.files(spec).length > 0;
82
+ }
83
+ /** The face's first file, for shared vertical metrics (all subset files of one
84
+ * face carry the same hhea/head metrics), or null. */
85
+ primary(spec) {
86
+ return this.files(spec)[0] ?? null;
87
+ }
88
+ /** The file in the face that has a glyph for `codePoint`, else the first file
89
+ * (whose .notdef advance is used), else null. Memoised per face. */
90
+ fileForCodePoint(spec, codePoint) {
91
+ const list = this.files(spec);
92
+ if (list.length === 0) return null;
93
+ if (list.length === 1) return list[0];
94
+ const key = faceKey(spec.family, spec.bold, spec.italic);
95
+ let memo = this.glyphMemo.get(key);
96
+ if (!memo) {
97
+ memo = /* @__PURE__ */ new Map();
98
+ this.glyphMemo.set(key, memo);
99
+ }
100
+ const cached = memo.get(codePoint);
101
+ if (cached) return cached;
102
+ const ch = String.fromCodePoint(codePoint);
103
+ const found = list.find((f) => f.charToGlyph(ch).index !== 0) ?? list[0];
104
+ memo.set(codePoint, found);
105
+ return found;
106
+ }
107
+ };
108
+ function createFontRegistryMeasurer(registry, fallback) {
109
+ return (text, font) => {
110
+ if (!registry.has(font)) return fallback(text, font);
111
+ const px = font.sizePt * PT_TO_PX2;
112
+ let total = 0;
113
+ for (const ch of text) {
114
+ const file = registry.fileForCodePoint(font, ch.codePointAt(0) ?? 0);
115
+ if (!file) continue;
116
+ total += (file.charToGlyph(ch).advanceWidth ?? 0) / file.unitsPerEm * px;
117
+ }
118
+ return total;
119
+ };
120
+ }
121
+ function createFontRegistryMetrics(registry, fallback) {
122
+ return (font) => {
123
+ const face = registry.primary(font);
124
+ if (!face) return fallback(font);
125
+ const scale = font.sizePt * PT_TO_PX2 / face.unitsPerEm;
126
+ return { ascent: face.ascender * scale, descent: Math.abs(face.descender) * scale };
127
+ };
128
+ }
129
+ export {
130
+ FontRegistry,
131
+ createApproxMeasurer,
132
+ createApproxMetrics,
133
+ createCanvasMeasurer,
134
+ createCanvasMetrics,
135
+ createFontRegistryMeasurer,
136
+ createFontRegistryMetrics,
137
+ ensureFontsLoaded,
138
+ fontToCss
139
+ };
@@ -0,0 +1,54 @@
1
+ import * as opentype from 'opentype.js';
2
+ import type { FontSpec, MeasureMetrics, MeasureText } from '@shadow-garden/bapbong-contracts';
3
+ /** One requestable font variant. Missing flags default to the regular face. */
4
+ export interface FontVariant {
5
+ bold?: boolean;
6
+ italic?: boolean;
7
+ }
8
+ /**
9
+ * Holds parsed font faces keyed by family + bold/italic and resolves per-glyph
10
+ * advance widths + vertical metrics from the font files. Widths derived here are
11
+ * byte-for-byte identical on every platform and WebView engine — that
12
+ * determinism is the whole reason layout is measured from fonts rather than the
13
+ * browser's `measureText`.
14
+ *
15
+ * A single face may be backed by **several files** because webfonts are commonly
16
+ * shipped subsetted by `unicode-range` (e.g. `@fontsource` splits latin /
17
+ * latin-ext / vietnamese). Each requested character is routed to the first file
18
+ * in the face that actually contains its glyph.
19
+ */
20
+ export declare class FontRegistry {
21
+ private readonly faces;
22
+ /** Per-face memo: code point → the file that serves it (or its .notdef fallback). */
23
+ private readonly glyphMemo;
24
+ /** Register a parsed opentype font for a family + variant (appends to the
25
+ * face's file list; register subset files in coverage-priority order). */
26
+ register(family: string, variant: FontVariant, font: opentype.Font): void;
27
+ /** Parse font-file bytes (TTF/OTF/WOFF) and register the resulting face. */
28
+ registerBytes(family: string, variant: FontVariant, bytes: ArrayBuffer): void;
29
+ /** The files backing a spec: the exact family+bold+italic face, else the
30
+ * family's regular face, else empty. */
31
+ private files;
32
+ /** Whether any registered file can serve this spec. */
33
+ has(spec: FontSpec): boolean;
34
+ /** The face's first file, for shared vertical metrics (all subset files of one
35
+ * face carry the same hhea/head metrics), or null. */
36
+ primary(spec: FontSpec): opentype.Font | null;
37
+ /** The file in the face that has a glyph for `codePoint`, else the first file
38
+ * (whose .notdef advance is used), else null. Memoised per face. */
39
+ fileForCodePoint(spec: FontSpec, codePoint: number): opentype.Font | null;
40
+ }
41
+ /**
42
+ * A {@link MeasureText} that sums glyph advance widths from a registered face —
43
+ * engine-independent, unlike canvas `measureText`. Each character is routed to
44
+ * the subset file that contains its glyph. Families absent from the registry
45
+ * defer to `fallback` (e.g. a canvas measurer). Kerning is not applied, matching
46
+ * Word's default line-breaking (kerning-for-fonts is off by default).
47
+ */
48
+ export declare function createFontRegistryMeasurer(registry: FontRegistry, fallback: MeasureText): MeasureText;
49
+ /**
50
+ * A {@link MeasureMetrics} reading a registered face's hhea ascent/descent
51
+ * (scaled to px), deferring to `fallback` for unknown families.
52
+ */
53
+ export declare function createFontRegistryMetrics(registry: FontRegistry, fallback: MeasureMetrics): MeasureMetrics;
54
+ //# sourceMappingURL=font-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"font-registry.d.ts","sourceRoot":"","sources":["../../src/lib/font-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,QAAQ,MAAM,aAAa,CAAC;AACxC,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAI9F,+EAA+E;AAC/E,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAOD;;;;;;;;;;;GAWG;AACH,qBAAa,YAAY;IACvB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAsC;IAC5D,qFAAqF;IACrF,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAiD;IAE3E;+EAC2E;IAC3E,QAAQ,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,GAAG,IAAI;IAQzE,4EAA4E;IAC5E,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,GAAG,IAAI;IAI7E;6CACyC;IACzC,OAAO,CAAC,KAAK;IAQb,uDAAuD;IACvD,GAAG,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO;IAI5B;2DACuD;IACvD,OAAO,CAAC,IAAI,EAAE,QAAQ,GAAG,QAAQ,CAAC,IAAI,GAAG,IAAI;IAI7C;yEACqE;IACrE,gBAAgB,CAAC,IAAI,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,GAAG,QAAQ,CAAC,IAAI,GAAG,IAAI;CAiB1E;AAED;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,WAAW,GAAG,WAAW,CAYrG;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,YAAY,EAAE,QAAQ,EAAE,cAAc,GAAG,cAAc,CAO1G"}
@@ -0,0 +1,32 @@
1
+ import type { FontSpec, MeasureMetrics, MeasureText } from '@shadow-garden/bapbong-contracts';
2
+ /** CSS font shorthand for a FontSpec, e.g. "italic 700 11pt Arial". */
3
+ export declare function fontToCss(font: FontSpec): string;
4
+ /**
5
+ * Canvas-backed text measurer (browser). Reuses a single offscreen 2D context,
6
+ * which is the accurate way to size text before painting it on canvas.
7
+ */
8
+ export declare function createCanvasMeasurer(): MeasureText;
9
+ /**
10
+ * Headless approximate measurer (no DOM): width ≈ chars × sizePt × factor.
11
+ * For SSR/tests, or as a first-paint estimate before fonts load.
12
+ */
13
+ export declare function createApproxMeasurer(factor?: number): MeasureText;
14
+ /**
15
+ * Canvas-backed vertical metrics (browser). Reads the font bounding box so the
16
+ * layout engine can build baseline-accurate line boxes.
17
+ */
18
+ export declare function createCanvasMetrics(): MeasureMetrics;
19
+ /**
20
+ * Headless approximate metrics (no DOM): ascent ≈ 0.8·em, descent ≈ 0.2·em.
21
+ * For SSR/tests, or before web fonts load.
22
+ */
23
+ export declare function createApproxMetrics(): MeasureMetrics;
24
+ /**
25
+ * Wait for the given font families to be usable before measuring — otherwise
26
+ * `measureText` runs against fallback fonts and every wrap/pagination
27
+ * coordinate is wrong once the real font arrives. Loads the four common
28
+ * variants per family. Resolves immediately where the CSS Font Loading API is
29
+ * unavailable (headless/tests) or for fonts the browser can't load.
30
+ */
31
+ export declare function ensureFontsLoaded(families: string[], sizePt?: number): Promise<void>;
32
+ //# sourceMappingURL=measuring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"measuring.d.ts","sourceRoot":"","sources":["../../src/lib/measuring.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,cAAc,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAI9F,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAIhD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,IAAI,WAAW,CAQlD;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,MAAM,SAAM,GAAG,WAAW,CAE9D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,cAAc,CAWpD;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,IAAI,cAAc,CAKpD;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,MAAM,SAAK,GAAG,OAAO,CAAC,IAAI,CAAC,CAYtF"}
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@shadow-garden/bapbong-measuring",
3
+ "version": "0.1.0",
4
+ "description": "bapbong — text measurement (canvas) + font helpers",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/shadowgarden-app/bapbong.git",
9
+ "directory": "packages/measuring"
10
+ },
11
+ "type": "module",
12
+ "main": "./dist/index.cjs",
13
+ "module": "./dist/index.js",
14
+ "types": "./dist/index.d.ts",
15
+ "exports": {
16
+ "./package.json": "./package.json",
17
+ ".": {
18
+ "@shadow-garden/source": "./src/index.ts",
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js",
21
+ "require": "./dist/index.cjs",
22
+ "default": "./dist/index.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "!**/*.tsbuildinfo"
28
+ ],
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "nx": {
33
+ "tags": [
34
+ "scope:measuring"
35
+ ],
36
+ "targets": {
37
+ "build": {
38
+ "executor": "@nx/esbuild:esbuild",
39
+ "outputs": [
40
+ "{options.outputPath}"
41
+ ],
42
+ "options": {
43
+ "outputPath": "packages/measuring/dist",
44
+ "main": "packages/measuring/src/index.ts",
45
+ "tsConfig": "packages/measuring/tsconfig.lib.json",
46
+ "format": [
47
+ "esm",
48
+ "cjs"
49
+ ],
50
+ "declarationRootDir": "packages/measuring/src",
51
+ "external": [
52
+ "@shadow-garden/bapbong-contracts"
53
+ ]
54
+ }
55
+ }
56
+ }
57
+ },
58
+ "dependencies": {
59
+ "opentype.js": "^2.0.0",
60
+ "@shadow-garden/bapbong-contracts": "^0.1.0"
61
+ }
62
+ }