@vyaz/core 0.0.1

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.
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Canvas API polyfill for Bun/Node.js via @napi-rs/canvas.
3
+ * Required by @chenglou/pretext in server environments (Bun/Node.js without DOM).
4
+ *
5
+ * Two levels of polyfill:
6
+ * 1. globalThis.document.createElement('canvas') — pretext uses this
7
+ * inside prepare() to create a temporary Canvas and call measureText.
8
+ * 2. OffscreenCanvas — for some libraries and early versions.
9
+ *
10
+ * Exports enableOfficeTextMeasure / disableOfficeTextMeasure —
11
+ * ctx.measureText override for fontkit-based measurements in Office mode.
12
+ */
13
+ // ⚠️ Dynamic import — prevents esbuild from resolving @napi-rs/canvas at bundle time.
14
+ // This module is a native Node.js addon. In the browser, Canvas APIs are already available.
15
+ let _createCanvas: ((w: number, h: number) => any) | null = null;
16
+
17
+ try {
18
+ // Dynamic require — kept as eval to avoid esbuild static analysis
19
+ const mod: any = (Function('return require("@napi-rs/canvas")'))();
20
+ _createCanvas = mod.createCanvas;
21
+ } catch {
22
+ // Browser — document.createElement('canvas') is available natively, no polyfill needed
23
+ _createCanvas = null;
24
+ }
25
+
26
+ // ── Polyfill document.createElement('canvas') ────────────────────────────
27
+ // Pretext internally calls document.createElement('canvas') during prepare(),
28
+ // to create a temporary Canvas and obtain a 2d context for measureText.
29
+ // Without this polyfill, prepare() crashes with "document is not defined" in Bun.
30
+
31
+ if (typeof globalThis.document === 'undefined') {
32
+ (globalThis as any).document = {
33
+ createElement: (tag: string) => {
34
+ if (tag === 'canvas' && _createCanvas) return _createCanvas(1, 1);
35
+ // For other elements — empty stub
36
+ return {};
37
+ },
38
+ };
39
+ }
40
+
41
+ // ── Polyfill OffscreenCanvas ─────────────────────────────────────────────
42
+ // @napi-rs/canvas does not provide OffscreenCanvas, so we create a shim.
43
+
44
+ if (typeof globalThis.OffscreenCanvas === 'undefined' && _createCanvas) {
45
+ (globalThis as any).OffscreenCanvas = class OffscreenCanvasShim {
46
+ private _canvas: any;
47
+ private _w: number;
48
+ private _h: number;
49
+
50
+ constructor(width: number, height: number) {
51
+ this._w = width;
52
+ this._h = height;
53
+ this._canvas = _createCanvas!(width, height);
54
+ }
55
+
56
+ get width(): number { return this._w; }
57
+ set width(v: number) { this._w = v; this._canvas.width = v; }
58
+ get height(): number { return this._h; }
59
+ set height(v: number) { this._h = v; this._canvas.height = v; }
60
+
61
+ getContext(type: string, attrs?: any): any {
62
+ return this._canvas.getContext(type, attrs);
63
+ }
64
+
65
+ async convertToBlob({ type: _type }: { type?: string } = {}): Promise<Blob> {
66
+ // @napi-rs/canvas toBuffer always returns PNG buffer
67
+ const buffer = this._canvas.toBuffer('image/png');
68
+ return new Blob([buffer], { type: 'image/png' });
69
+ }
70
+ };
71
+ }
72
+
73
+ // ── Office measureText override ─────────────────────────────────────
74
+
75
+ const originalMeasureText = (globalThis as any).CanvasRenderingContext2D
76
+ ?.prototype?.measureText as
77
+ | ((text: string) => TextMetrics)
78
+ | undefined;
79
+
80
+ /** fontCache: Map<family_weight_style, fontkit.Font> */
81
+ let officeFontCache: Map<string, any> | null = null;
82
+ let officeEnabled = false;
83
+
84
+ /**
85
+ * Parse ctx.font string like "italic bold 16px Arial" → { family, size, weight }
86
+ */
87
+ function parseFont(fontStr: string): { family: string; size: number; weight: string } | null {
88
+ // "italic bold 16px Arial" or "bold 16px Inter" or "16px Arial"
89
+ const pxMatch = fontStr.match(/(\d+(?:\.\d+)?)px\s+(.+)/);
90
+ if (!pxMatch) return null;
91
+ const size = parseFloat(pxMatch[1]);
92
+ // Simplified: everything after px is the family
93
+ const family = pxMatch[2].trim();
94
+ // Parse weight from beginning
95
+ const weightMatch = fontStr.match(/\b(bold|italic|\d{3})\b/);
96
+ const weight = weightMatch ? (weightMatch[1] === 'bold' ? 'bold' : weightMatch[1]) : 'normal';
97
+ return { family, size, weight };
98
+ }
99
+
100
+ /** Cache key: "${family}_${weight}_normal" */
101
+ function cacheKey(family: string, weight: string): string {
102
+ return `${family}_${weight}_normal`;
103
+ }
104
+
105
+ function officeMeasureText(this: any, text: string): TextMetrics {
106
+ if (!officeFontCache) {
107
+ return originalMeasureText?.call(this, text) ?? createEmptyMetrics();
108
+ }
109
+
110
+ const parsed = parseFont(this.font);
111
+ if (!parsed) {
112
+ return originalMeasureText?.call(this, text) ?? createEmptyMetrics();
113
+ }
114
+
115
+ const key = cacheKey(parsed.family, parsed.weight);
116
+ const font = officeFontCache.get(key);
117
+ if (!font) {
118
+ return originalMeasureText?.call(this, text) ?? createEmptyMetrics();
119
+ }
120
+
121
+ const scale = parsed.size / font.unitsPerEm;
122
+ let totalWidth = 0;
123
+
124
+ for (let i = 0; i < text.length; i++) {
125
+ const codePoint = text.codePointAt(i)!;
126
+ const glyph = font.glyphForCodePoint(codePoint);
127
+ if (glyph) {
128
+ totalWidth += glyph.advanceWidth * scale;
129
+ } else {
130
+ // Fallback for missing glyph: use original
131
+ if (originalMeasureText) {
132
+ return originalMeasureText.call(this, text);
133
+ }
134
+ totalWidth += parsed.size * 0.5; // rough estimate
135
+ }
136
+ // Skip surrogate pairs
137
+ if (codePoint > 0xffff) i++;
138
+ }
139
+
140
+ return createMetricsObject(totalWidth);
141
+ }
142
+
143
+ function createMetricsObject(width: number): TextMetrics {
144
+ return {
145
+ width,
146
+ actualBoundingBoxAscent: 0,
147
+ actualBoundingBoxDescent: 0,
148
+ fontBoundingBoxAscent: 0,
149
+ fontBoundingBoxDescent: 0,
150
+ actualBoundingBoxLeft: 0,
151
+ actualBoundingBoxRight: width,
152
+ } as unknown as TextMetrics;
153
+ }
154
+
155
+ function createEmptyMetrics(): TextMetrics {
156
+ return createMetricsObject(0);
157
+ }
158
+
159
+ /**
160
+ * Enable Office measurement: replaces ctx.measureText with fontkit-based version.
161
+ * @param fontCache — Map key->font from FontMetricsProvider
162
+ */
163
+ export function enableOfficeTextMeasure(fontCache: Map<string, any>): void {
164
+ if (officeEnabled) return;
165
+ officeFontCache = fontCache;
166
+ officeEnabled = true;
167
+
168
+ const CtxProto = (globalThis as any).CanvasRenderingContext2D?.prototype;
169
+ if (CtxProto && originalMeasureText) {
170
+ CtxProto.measureText = officeMeasureText;
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Restore original ctx.measureText.
176
+ */
177
+ export function disableOfficeTextMeasure(): void {
178
+ if (!officeEnabled) return;
179
+ officeEnabled = false;
180
+ officeFontCache = null;
181
+
182
+ const CtxProto = (globalThis as any).CanvasRenderingContext2D?.prototype;
183
+ if (CtxProto && originalMeasureText) {
184
+ CtxProto.measureText = originalMeasureText;
185
+ }
186
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Minimal type declarations for fontkit.
3
+ * fontkit does not ship native TypeScript types.
4
+ */
5
+
6
+ declare module 'fontkit' {
7
+ interface BBox {
8
+ minX: number;
9
+ minY: number;
10
+ maxX: number;
11
+ maxY: number;
12
+ }
13
+
14
+ interface Glyph {
15
+ id: number;
16
+ advanceWidth: number;
17
+ bbox: BBox;
18
+ name: string;
19
+ path: any;
20
+ }
21
+
22
+ interface TTFFont {
23
+ unitsPerEm: number;
24
+ ascent: number;
25
+ descent: number;
26
+ capHeight: number;
27
+ xHeight: number;
28
+ lineGap: number;
29
+ underlinePosition: number;
30
+ underlineThickness: number;
31
+ familyName: string;
32
+ subfamilyName: string;
33
+ postscriptName: string;
34
+ format: string;
35
+ glyphsForString(text: string): Glyph[];
36
+ getGlyph(glyphId: number): Glyph;
37
+ }
38
+
39
+ function create(buffer: Buffer, postscriptName?: string): TTFFont;
40
+ function open(filename: string, postscriptName?: string): Promise<TTFFont>;
41
+ function openSync(filename: string, postscriptName?: string): TTFFont;
42
+
43
+ export { create, open, openSync, TTFFont, Glyph, BBox };
44
+ }