@simonklee/opentui-tex 0.2.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.
@@ -0,0 +1,3 @@
1
+ import type { MathBox, MathNode } from "./math-types.js";
2
+ export declare function layoutMath(node: MathNode, displayMode: boolean): MathBox;
3
+ export declare function boxToString(box: MathBox, widthMax: number, heightMax: number): string;
@@ -0,0 +1,4 @@
1
+ import type { MathNode } from "./math-types.js";
2
+ export declare const MAX_NESTING_DEPTH = 256;
3
+ export declare function parseMath(source: string): MathNode;
4
+ export declare function parseMathIncomplete(source: string): MathNode;
@@ -0,0 +1,11 @@
1
+ import type { AccentKind, SymbolRole } from "./math-types.js";
2
+ export interface SymbolDefinition {
3
+ value: string;
4
+ role?: SymbolRole;
5
+ }
6
+ export declare const symbols: Readonly<Record<string, SymbolDefinition>>;
7
+ export declare const operators: Readonly<Record<string, string>>;
8
+ export declare const namedOperators: Set<string>;
9
+ export declare const delimiters: Readonly<Record<string, string>>;
10
+ export declare const accents: Readonly<Record<string, AccentKind>>;
11
+ export declare const spacing: Readonly<Record<string, number>>;
@@ -0,0 +1,59 @@
1
+ export type SymbolRole = "ordinary" | "binary" | "relation" | "operator" | "punctuation" | "opening" | "closing";
2
+ export type MathEnvironment = "matrix" | "pmatrix" | "bmatrix" | "Bmatrix" | "vmatrix" | "Vmatrix" | "cases" | "aligned" | "align" | "gathered" | "gather" | "smallmatrix" | "array";
3
+ export type AccentKind = "hat" | "widehat" | "bar" | "overline" | "underline" | "vec" | "tilde" | "dot" | "ddot";
4
+ export type MathNode = {
5
+ type: "row";
6
+ body: MathNode[];
7
+ } | {
8
+ type: "symbol";
9
+ value: string;
10
+ role?: SymbolRole;
11
+ } | {
12
+ type: "text";
13
+ value: string;
14
+ } | {
15
+ type: "space";
16
+ width: number;
17
+ } | {
18
+ type: "fraction";
19
+ numerator: MathNode;
20
+ denominator: MathNode;
21
+ bar: boolean;
22
+ } | {
23
+ type: "root";
24
+ body: MathNode;
25
+ index?: MathNode;
26
+ } | {
27
+ type: "scripts";
28
+ base: MathNode;
29
+ superscript?: MathNode;
30
+ subscript?: MathNode;
31
+ } | {
32
+ type: "delimited";
33
+ left: string;
34
+ body: MathNode;
35
+ right: string;
36
+ } | {
37
+ type: "matrix";
38
+ rows: MathNode[][];
39
+ environment: MathEnvironment;
40
+ } | {
41
+ type: "accent";
42
+ accent: AccentKind;
43
+ body: MathNode;
44
+ } | {
45
+ type: "operator";
46
+ value: string;
47
+ limits: boolean;
48
+ } | {
49
+ type: "overunder";
50
+ base: MathNode;
51
+ over?: MathNode;
52
+ under?: MathNode;
53
+ };
54
+ export interface MathBox {
55
+ width: number;
56
+ height: number;
57
+ baseline: number;
58
+ cells: Array<Array<string | undefined>>;
59
+ }
@@ -0,0 +1,18 @@
1
+ export type Pointer = number | bigint;
2
+ type PointerArgument = Pointer | Uint8Array;
3
+ export interface NativeSymbols {
4
+ texInit(): number;
5
+ texRender(source: PointerArgument, sourceLength: number, display: number, foreground: PointerArgument, background: PointerArgument): Pointer | null;
6
+ texResultStatus(handle: Pointer): number;
7
+ texResultPixels(handle: Pointer): Pointer | null;
8
+ texResultPixelsLength(handle: Pointer): number;
9
+ texResultWidth(handle: Pointer): number;
10
+ texResultHeight(handle: Pointer): number;
11
+ texResultDestroy(handle: Pointer): void;
12
+ }
13
+ interface NativeLibrary {
14
+ symbols: NativeSymbols;
15
+ toArrayBuffer(pointer: Pointer, length: number): ArrayBuffer;
16
+ }
17
+ export declare function openNativeLibrary(path: string): NativeLibrary;
18
+ export {};
@@ -0,0 +1,15 @@
1
+ import { NativeImage } from "@opentui/core";
2
+ export declare class NativeTexRenderer {
3
+ private readonly cache;
4
+ private readonly queue;
5
+ private queueTimer;
6
+ private rendering;
7
+ private destroyed;
8
+ constructor();
9
+ renderAsync(formula: string, display: boolean, foreground: string, background: string, signal?: AbortSignal): Promise<NativeImage>;
10
+ destroy(): void;
11
+ private renderSync;
12
+ private getCached;
13
+ private scheduleQueue;
14
+ private discardAbortedJobs;
15
+ }
@@ -0,0 +1,8 @@
1
+ import type { TexBackend, TexRenderOutput, TexRenderRequest } from "../backend.js";
2
+ import { NativeTexRenderer } from "./native-renderer.js";
3
+ export declare class NativeTexBackend implements TexBackend {
4
+ private readonly renderer;
5
+ constructor(renderer?: NativeTexRenderer);
6
+ render(request: TexRenderRequest): Promise<TexRenderOutput>;
7
+ destroy(): void;
8
+ }
@@ -0,0 +1 @@
1
+ export declare function resolveNativeLibrary(): Promise<string>;
@@ -0,0 +1,273 @@
1
+ // @bun
2
+ var __require = import.meta.require;
3
+
4
+ // src/native/native-renderer.ts
5
+ import { isMainThread } from "worker_threads";
6
+ import { NativeImage } from "@opentui/core";
7
+
8
+ // src/native/ffi.ts
9
+ import { createRequire } from "module";
10
+ var require2 = createRequire(import.meta.url);
11
+ function openNativeLibrary(path) {
12
+ if (globalThis.Bun) {
13
+ const ffi2 = require2("bun:ffi");
14
+ const { u8, u32, ptr, void: voidType } = ffi2.FFIType;
15
+ const library2 = ffi2.dlopen(path, {
16
+ texInit: { args: [], returns: u32 },
17
+ texRender: { args: [ptr, u32, u8, ptr, ptr], returns: ptr },
18
+ texResultStatus: { args: [ptr], returns: u32 },
19
+ texResultPixels: { args: [ptr], returns: ptr },
20
+ texResultPixelsLength: { args: [ptr], returns: u32 },
21
+ texResultWidth: { args: [ptr], returns: u32 },
22
+ texResultHeight: { args: [ptr], returns: u32 },
23
+ texResultDestroy: { args: [ptr], returns: voidType }
24
+ });
25
+ return {
26
+ symbols: library2.symbols,
27
+ toArrayBuffer: (pointer, length) => ffi2.toArrayBuffer(pointer, 0, length)
28
+ };
29
+ }
30
+ const ffi = require2("node:ffi");
31
+ const library = ffi.dlopen(path, {
32
+ texInit: { arguments: [], return: "u32" },
33
+ texRender: { arguments: ["pointer", "u32", "u8", "pointer", "pointer"], return: "pointer" },
34
+ texResultStatus: { arguments: ["pointer"], return: "u32" },
35
+ texResultPixels: { arguments: ["pointer"], return: "pointer" },
36
+ texResultPixelsLength: { arguments: ["pointer"], return: "u32" },
37
+ texResultWidth: { arguments: ["pointer"], return: "u32" },
38
+ texResultHeight: { arguments: ["pointer"], return: "u32" },
39
+ texResultDestroy: { arguments: ["pointer"], return: "void" }
40
+ });
41
+ return {
42
+ symbols: library.functions,
43
+ toArrayBuffer: (pointer, length) => ffi.toArrayBuffer(pointer, length, true)
44
+ };
45
+ }
46
+
47
+ // src/native/platform.ts
48
+ function packagePath(loaded) {
49
+ return typeof loaded === "string" ? loaded : loaded.default;
50
+ }
51
+ function linuxLibc() {
52
+ const configured = process.env.OPENTUI_LIBC;
53
+ if (configured === "gnu" || configured === "glibc")
54
+ return "gnu";
55
+ if (configured === "musl")
56
+ return "musl";
57
+ if (configured)
58
+ throw new Error(`Unsupported OPENTUI_LIBC value: ${configured}`);
59
+ const report = process.report?.getReport();
60
+ return report?.header?.glibcVersionRuntime ? "gnu" : "musl";
61
+ }
62
+ async function resolveNativeLibrary() {
63
+ if (process.env.OPENTUI_LATEX_NATIVE_PATH)
64
+ return process.env.OPENTUI_LATEX_NATIVE_PATH;
65
+ const abi = process.platform === "linux" ? linuxLibc() : undefined;
66
+ const suffix = `${process.platform}-${process.arch}${abi === "musl" ? "-musl" : ""}`;
67
+ if (suffix === "darwin-x64")
68
+ return packagePath(await import("@simonklee/opentui-tex-native-darwin-x64"));
69
+ if (suffix === "darwin-arm64")
70
+ return packagePath(await import("@simonklee/opentui-tex-native-darwin-arm64"));
71
+ if (suffix === "win32-x64")
72
+ return packagePath(await import("@simonklee/opentui-tex-native-win32-x64"));
73
+ if (suffix === "win32-arm64")
74
+ return packagePath(await import("@simonklee/opentui-tex-native-win32-arm64"));
75
+ if (suffix === "linux-x64")
76
+ return packagePath(await import("@simonklee/opentui-tex-native-linux-x64"));
77
+ if (suffix === "linux-x64-musl")
78
+ return packagePath(await import("@simonklee/opentui-tex-native-linux-x64-musl"));
79
+ if (suffix === "linux-arm64")
80
+ return packagePath(await import("@simonklee/opentui-tex-native-linux-arm64"));
81
+ if (suffix === "linux-arm64-musl")
82
+ return packagePath(await import("@simonklee/opentui-tex-native-linux-arm64-musl"));
83
+ throw new Error(`Unsupported native renderer platform: ${process.platform}-${process.arch}`);
84
+ }
85
+
86
+ // src/native/native-renderer.ts
87
+ var CACHE_ENTRIES_MAX = 64;
88
+ var QUEUE_ENTRIES_MAX = 128;
89
+ var SOURCE_SIZE_MAX = 4096;
90
+ var STATUS_MESSAGES = [
91
+ "ok",
92
+ "invalid native renderer argument",
93
+ "invalid TeX formula",
94
+ "invalid SVG output",
95
+ "rendered dimensions exceed limits",
96
+ "native renderer ran out of memory",
97
+ "internal native renderer error"
98
+ ];
99
+ var nativePromise;
100
+ async function getNative() {
101
+ nativePromise ??= resolveNativeLibrary().then((path) => {
102
+ const loaded = openNativeLibrary(path);
103
+ const status = loaded.symbols.texInit();
104
+ if (status !== 0)
105
+ throw new Error(STATUS_MESSAGES[status] ?? `Native renderer initialization failed with status ${status}`);
106
+ return loaded;
107
+ }).catch((error) => {
108
+ nativePromise = undefined;
109
+ throw error;
110
+ });
111
+ return nativePromise;
112
+ }
113
+ function rgb(hex) {
114
+ if (!/^#[0-9a-f]{6}$/i.test(hex))
115
+ throw new Error(`Invalid RGB color: ${hex}`);
116
+ return Uint8Array.of(Number.parseInt(hex.slice(1, 3), 16), Number.parseInt(hex.slice(3, 5), 16), Number.parseInt(hex.slice(5, 7), 16));
117
+ }
118
+ function copyResult(symbols, toArrayBuffer, handle) {
119
+ const status = symbols.texResultStatus(handle);
120
+ if (status !== 0)
121
+ throw new Error(STATUS_MESSAGES[status] ?? `Native renderer failed with status ${status}`);
122
+ const pixelsPointer = symbols.texResultPixels(handle);
123
+ const pixelsLength = symbols.texResultPixelsLength(handle);
124
+ const width = symbols.texResultWidth(handle);
125
+ const height = symbols.texResultHeight(handle);
126
+ if (!pixelsPointer || width === 0 || height === 0)
127
+ throw new Error("Native renderer returned an empty image");
128
+ const expectedLength = width * height * 4;
129
+ if (pixelsLength !== expectedLength)
130
+ throw new Error("Native renderer returned an invalid pixel length");
131
+ const pixels = new Uint8Array(toArrayBuffer(pixelsPointer, pixelsLength));
132
+ return NativeImage.fromRgba(pixels, width, height, width * 4);
133
+ }
134
+
135
+ class NativeTexRenderer {
136
+ cache = new Map;
137
+ queue = [];
138
+ queueTimer = null;
139
+ rendering = false;
140
+ destroyed = false;
141
+ constructor() {
142
+ if (!isMainThread)
143
+ throw new Error("NativeTexRenderer is single-threaded and must be loaded on the main thread");
144
+ }
145
+ async renderAsync(formula, display, foreground, background, signal) {
146
+ if (this.destroyed)
147
+ throw new Error("Native renderer is destroyed");
148
+ if (signal?.aborted)
149
+ throw signal.reason ?? new Error("Native render cancelled");
150
+ const key = `${display ? "D" : "I"}\x00${foreground}\x00${background}\x00${formula}`;
151
+ const cached = this.getCached(key);
152
+ if (cached)
153
+ return cached;
154
+ this.discardAbortedJobs();
155
+ if (this.queue.length >= QUEUE_ENTRIES_MAX)
156
+ throw new Error(`Native render queue exceeds ${QUEUE_ENTRIES_MAX} formulas`);
157
+ const promise = new Promise((resolve, reject) => {
158
+ this.queue.push({ formula, display, foreground, background, signal, resolve, reject });
159
+ });
160
+ this.scheduleQueue();
161
+ return promise;
162
+ }
163
+ destroy() {
164
+ if (this.destroyed)
165
+ return;
166
+ this.destroyed = true;
167
+ if (this.queueTimer)
168
+ clearTimeout(this.queueTimer);
169
+ this.queueTimer = null;
170
+ for (const job of this.queue.splice(0))
171
+ job.reject(new Error("Native renderer is destroyed"));
172
+ for (const image of this.cache.values())
173
+ image.dispose();
174
+ this.cache.clear();
175
+ }
176
+ async renderSync(formula, display, foreground, background, signal) {
177
+ if (this.destroyed)
178
+ throw new Error("Native renderer is destroyed");
179
+ const key = `${display ? "D" : "I"}\x00${foreground}\x00${background}\x00${formula}`;
180
+ const cached = this.getCached(key);
181
+ if (cached)
182
+ return cached;
183
+ const source = new TextEncoder().encode(formula);
184
+ if (source.byteLength === 0 || source.byteLength > SOURCE_SIZE_MAX)
185
+ throw new Error(`Formula must contain between 1 and ${SOURCE_SIZE_MAX} UTF-8 bytes`);
186
+ const foregroundRgb = rgb(foreground);
187
+ const backgroundRgb = rgb(background);
188
+ const library = await getNative();
189
+ if (this.destroyed)
190
+ throw new Error("Native renderer is destroyed");
191
+ if (signal?.aborted)
192
+ throw signal.reason ?? new Error("Native render cancelled");
193
+ const handle = library.symbols.texRender(source, source.byteLength, display ? 1 : 0, foregroundRgb, backgroundRgb);
194
+ if (!handle)
195
+ throw new Error("Native renderer could not allocate a result");
196
+ let image;
197
+ try {
198
+ image = copyResult(library.symbols, library.toArrayBuffer, handle);
199
+ } finally {
200
+ library.symbols.texResultDestroy(handle);
201
+ }
202
+ if (this.destroyed || signal?.aborted) {
203
+ image.dispose();
204
+ if (this.destroyed)
205
+ throw new Error("Native renderer is destroyed");
206
+ throw signal?.reason ?? new Error("Native render cancelled");
207
+ }
208
+ this.cache.set(key, image);
209
+ if (this.cache.size > CACHE_ENTRIES_MAX) {
210
+ const [oldest, evicted] = this.cache.entries().next().value;
211
+ this.cache.delete(oldest);
212
+ evicted.dispose();
213
+ }
214
+ return image.retain();
215
+ }
216
+ getCached(key) {
217
+ const image = this.cache.get(key);
218
+ if (!image)
219
+ return null;
220
+ this.cache.delete(key);
221
+ this.cache.set(key, image);
222
+ return image.retain();
223
+ }
224
+ scheduleQueue() {
225
+ if (this.queueTimer || this.rendering || this.queue.length === 0)
226
+ return;
227
+ this.queueTimer = setTimeout(() => {
228
+ this.queueTimer = null;
229
+ const job = this.queue.shift();
230
+ if (!job)
231
+ return;
232
+ if (job.signal?.aborted)
233
+ job.reject(job.signal.reason ?? new Error("Native render cancelled"));
234
+ else {
235
+ this.rendering = true;
236
+ this.renderSync(job.formula, job.display, job.foreground, job.background, job.signal).then(job.resolve, job.reject).finally(() => {
237
+ this.rendering = false;
238
+ this.scheduleQueue();
239
+ });
240
+ return;
241
+ }
242
+ this.scheduleQueue();
243
+ }, 0);
244
+ }
245
+ discardAbortedJobs() {
246
+ for (let index = this.queue.length - 1;index >= 0; index -= 1) {
247
+ const job = this.queue[index];
248
+ if (!job.signal?.aborted)
249
+ continue;
250
+ this.queue.splice(index, 1);
251
+ job.reject(job.signal.reason ?? new Error("Native render cancelled"));
252
+ }
253
+ }
254
+ }
255
+
256
+ // src/native/native-tex-backend.ts
257
+ class NativeTexBackend {
258
+ renderer;
259
+ constructor(renderer = new NativeTexRenderer) {
260
+ this.renderer = renderer;
261
+ }
262
+ async render(request) {
263
+ const image = await this.renderer.renderAsync(request.formula, request.display, request.foreground, request.background, request.signal);
264
+ return { kind: "image", image };
265
+ }
266
+ destroy() {
267
+ this.renderer.destroy();
268
+ }
269
+ }
270
+ export {
271
+ NativeTexRenderer,
272
+ NativeTexBackend
273
+ };
@@ -0,0 +1,2 @@
1
+ export { NativeTexBackend } from "./native/native-tex-backend.js";
2
+ export { NativeTexRenderer } from "./native/native-renderer.js";
package/dist/native.js ADDED
@@ -0,0 +1,273 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
4
+ // src/native/native-renderer.ts
5
+ import { isMainThread } from "node:worker_threads";
6
+ import { NativeImage } from "@opentui/core";
7
+
8
+ // src/native/ffi.ts
9
+ import { createRequire as createRequire2 } from "node:module";
10
+ var require2 = createRequire2(import.meta.url);
11
+ function openNativeLibrary(path) {
12
+ if (globalThis.Bun) {
13
+ const ffi2 = require2("bun:ffi");
14
+ const { u8, u32, ptr, void: voidType } = ffi2.FFIType;
15
+ const library2 = ffi2.dlopen(path, {
16
+ texInit: { args: [], returns: u32 },
17
+ texRender: { args: [ptr, u32, u8, ptr, ptr], returns: ptr },
18
+ texResultStatus: { args: [ptr], returns: u32 },
19
+ texResultPixels: { args: [ptr], returns: ptr },
20
+ texResultPixelsLength: { args: [ptr], returns: u32 },
21
+ texResultWidth: { args: [ptr], returns: u32 },
22
+ texResultHeight: { args: [ptr], returns: u32 },
23
+ texResultDestroy: { args: [ptr], returns: voidType }
24
+ });
25
+ return {
26
+ symbols: library2.symbols,
27
+ toArrayBuffer: (pointer, length) => ffi2.toArrayBuffer(pointer, 0, length)
28
+ };
29
+ }
30
+ const ffi = require2("node:ffi");
31
+ const library = ffi.dlopen(path, {
32
+ texInit: { arguments: [], return: "u32" },
33
+ texRender: { arguments: ["pointer", "u32", "u8", "pointer", "pointer"], return: "pointer" },
34
+ texResultStatus: { arguments: ["pointer"], return: "u32" },
35
+ texResultPixels: { arguments: ["pointer"], return: "pointer" },
36
+ texResultPixelsLength: { arguments: ["pointer"], return: "u32" },
37
+ texResultWidth: { arguments: ["pointer"], return: "u32" },
38
+ texResultHeight: { arguments: ["pointer"], return: "u32" },
39
+ texResultDestroy: { arguments: ["pointer"], return: "void" }
40
+ });
41
+ return {
42
+ symbols: library.functions,
43
+ toArrayBuffer: (pointer, length) => ffi.toArrayBuffer(pointer, length, true)
44
+ };
45
+ }
46
+
47
+ // src/native/platform.ts
48
+ function packagePath(loaded) {
49
+ return typeof loaded === "string" ? loaded : loaded.default;
50
+ }
51
+ function linuxLibc() {
52
+ const configured = process.env.OPENTUI_LIBC;
53
+ if (configured === "gnu" || configured === "glibc")
54
+ return "gnu";
55
+ if (configured === "musl")
56
+ return "musl";
57
+ if (configured)
58
+ throw new Error(`Unsupported OPENTUI_LIBC value: ${configured}`);
59
+ const report = process.report?.getReport();
60
+ return report?.header?.glibcVersionRuntime ? "gnu" : "musl";
61
+ }
62
+ async function resolveNativeLibrary() {
63
+ if (process.env.OPENTUI_LATEX_NATIVE_PATH)
64
+ return process.env.OPENTUI_LATEX_NATIVE_PATH;
65
+ const abi = process.platform === "linux" ? linuxLibc() : undefined;
66
+ const suffix = `${process.platform}-${process.arch}${abi === "musl" ? "-musl" : ""}`;
67
+ if (suffix === "darwin-x64")
68
+ return packagePath(await import("@simonklee/opentui-tex-native-darwin-x64"));
69
+ if (suffix === "darwin-arm64")
70
+ return packagePath(await import("@simonklee/opentui-tex-native-darwin-arm64"));
71
+ if (suffix === "win32-x64")
72
+ return packagePath(await import("@simonklee/opentui-tex-native-win32-x64"));
73
+ if (suffix === "win32-arm64")
74
+ return packagePath(await import("@simonklee/opentui-tex-native-win32-arm64"));
75
+ if (suffix === "linux-x64")
76
+ return packagePath(await import("@simonklee/opentui-tex-native-linux-x64"));
77
+ if (suffix === "linux-x64-musl")
78
+ return packagePath(await import("@simonklee/opentui-tex-native-linux-x64-musl"));
79
+ if (suffix === "linux-arm64")
80
+ return packagePath(await import("@simonklee/opentui-tex-native-linux-arm64"));
81
+ if (suffix === "linux-arm64-musl")
82
+ return packagePath(await import("@simonklee/opentui-tex-native-linux-arm64-musl"));
83
+ throw new Error(`Unsupported native renderer platform: ${process.platform}-${process.arch}`);
84
+ }
85
+
86
+ // src/native/native-renderer.ts
87
+ var CACHE_ENTRIES_MAX = 64;
88
+ var QUEUE_ENTRIES_MAX = 128;
89
+ var SOURCE_SIZE_MAX = 4096;
90
+ var STATUS_MESSAGES = [
91
+ "ok",
92
+ "invalid native renderer argument",
93
+ "invalid TeX formula",
94
+ "invalid SVG output",
95
+ "rendered dimensions exceed limits",
96
+ "native renderer ran out of memory",
97
+ "internal native renderer error"
98
+ ];
99
+ var nativePromise;
100
+ async function getNative() {
101
+ nativePromise ??= resolveNativeLibrary().then((path) => {
102
+ const loaded = openNativeLibrary(path);
103
+ const status = loaded.symbols.texInit();
104
+ if (status !== 0)
105
+ throw new Error(STATUS_MESSAGES[status] ?? `Native renderer initialization failed with status ${status}`);
106
+ return loaded;
107
+ }).catch((error) => {
108
+ nativePromise = undefined;
109
+ throw error;
110
+ });
111
+ return nativePromise;
112
+ }
113
+ function rgb(hex) {
114
+ if (!/^#[0-9a-f]{6}$/i.test(hex))
115
+ throw new Error(`Invalid RGB color: ${hex}`);
116
+ return Uint8Array.of(Number.parseInt(hex.slice(1, 3), 16), Number.parseInt(hex.slice(3, 5), 16), Number.parseInt(hex.slice(5, 7), 16));
117
+ }
118
+ function copyResult(symbols, toArrayBuffer, handle) {
119
+ const status = symbols.texResultStatus(handle);
120
+ if (status !== 0)
121
+ throw new Error(STATUS_MESSAGES[status] ?? `Native renderer failed with status ${status}`);
122
+ const pixelsPointer = symbols.texResultPixels(handle);
123
+ const pixelsLength = symbols.texResultPixelsLength(handle);
124
+ const width = symbols.texResultWidth(handle);
125
+ const height = symbols.texResultHeight(handle);
126
+ if (!pixelsPointer || width === 0 || height === 0)
127
+ throw new Error("Native renderer returned an empty image");
128
+ const expectedLength = width * height * 4;
129
+ if (pixelsLength !== expectedLength)
130
+ throw new Error("Native renderer returned an invalid pixel length");
131
+ const pixels = new Uint8Array(toArrayBuffer(pixelsPointer, pixelsLength));
132
+ return NativeImage.fromRgba(pixels, width, height, width * 4);
133
+ }
134
+
135
+ class NativeTexRenderer {
136
+ cache = new Map;
137
+ queue = [];
138
+ queueTimer = null;
139
+ rendering = false;
140
+ destroyed = false;
141
+ constructor() {
142
+ if (!isMainThread)
143
+ throw new Error("NativeTexRenderer is single-threaded and must be loaded on the main thread");
144
+ }
145
+ async renderAsync(formula, display, foreground, background, signal) {
146
+ if (this.destroyed)
147
+ throw new Error("Native renderer is destroyed");
148
+ if (signal?.aborted)
149
+ throw signal.reason ?? new Error("Native render cancelled");
150
+ const key = `${display ? "D" : "I"}\x00${foreground}\x00${background}\x00${formula}`;
151
+ const cached = this.getCached(key);
152
+ if (cached)
153
+ return cached;
154
+ this.discardAbortedJobs();
155
+ if (this.queue.length >= QUEUE_ENTRIES_MAX)
156
+ throw new Error(`Native render queue exceeds ${QUEUE_ENTRIES_MAX} formulas`);
157
+ const promise = new Promise((resolve, reject) => {
158
+ this.queue.push({ formula, display, foreground, background, signal, resolve, reject });
159
+ });
160
+ this.scheduleQueue();
161
+ return promise;
162
+ }
163
+ destroy() {
164
+ if (this.destroyed)
165
+ return;
166
+ this.destroyed = true;
167
+ if (this.queueTimer)
168
+ clearTimeout(this.queueTimer);
169
+ this.queueTimer = null;
170
+ for (const job of this.queue.splice(0))
171
+ job.reject(new Error("Native renderer is destroyed"));
172
+ for (const image of this.cache.values())
173
+ image.dispose();
174
+ this.cache.clear();
175
+ }
176
+ async renderSync(formula, display, foreground, background, signal) {
177
+ if (this.destroyed)
178
+ throw new Error("Native renderer is destroyed");
179
+ const key = `${display ? "D" : "I"}\x00${foreground}\x00${background}\x00${formula}`;
180
+ const cached = this.getCached(key);
181
+ if (cached)
182
+ return cached;
183
+ const source = new TextEncoder().encode(formula);
184
+ if (source.byteLength === 0 || source.byteLength > SOURCE_SIZE_MAX)
185
+ throw new Error(`Formula must contain between 1 and ${SOURCE_SIZE_MAX} UTF-8 bytes`);
186
+ const foregroundRgb = rgb(foreground);
187
+ const backgroundRgb = rgb(background);
188
+ const library = await getNative();
189
+ if (this.destroyed)
190
+ throw new Error("Native renderer is destroyed");
191
+ if (signal?.aborted)
192
+ throw signal.reason ?? new Error("Native render cancelled");
193
+ const handle = library.symbols.texRender(source, source.byteLength, display ? 1 : 0, foregroundRgb, backgroundRgb);
194
+ if (!handle)
195
+ throw new Error("Native renderer could not allocate a result");
196
+ let image;
197
+ try {
198
+ image = copyResult(library.symbols, library.toArrayBuffer, handle);
199
+ } finally {
200
+ library.symbols.texResultDestroy(handle);
201
+ }
202
+ if (this.destroyed || signal?.aborted) {
203
+ image.dispose();
204
+ if (this.destroyed)
205
+ throw new Error("Native renderer is destroyed");
206
+ throw signal?.reason ?? new Error("Native render cancelled");
207
+ }
208
+ this.cache.set(key, image);
209
+ if (this.cache.size > CACHE_ENTRIES_MAX) {
210
+ const [oldest, evicted] = this.cache.entries().next().value;
211
+ this.cache.delete(oldest);
212
+ evicted.dispose();
213
+ }
214
+ return image.retain();
215
+ }
216
+ getCached(key) {
217
+ const image = this.cache.get(key);
218
+ if (!image)
219
+ return null;
220
+ this.cache.delete(key);
221
+ this.cache.set(key, image);
222
+ return image.retain();
223
+ }
224
+ scheduleQueue() {
225
+ if (this.queueTimer || this.rendering || this.queue.length === 0)
226
+ return;
227
+ this.queueTimer = setTimeout(() => {
228
+ this.queueTimer = null;
229
+ const job = this.queue.shift();
230
+ if (!job)
231
+ return;
232
+ if (job.signal?.aborted)
233
+ job.reject(job.signal.reason ?? new Error("Native render cancelled"));
234
+ else {
235
+ this.rendering = true;
236
+ this.renderSync(job.formula, job.display, job.foreground, job.background, job.signal).then(job.resolve, job.reject).finally(() => {
237
+ this.rendering = false;
238
+ this.scheduleQueue();
239
+ });
240
+ return;
241
+ }
242
+ this.scheduleQueue();
243
+ }, 0);
244
+ }
245
+ discardAbortedJobs() {
246
+ for (let index = this.queue.length - 1;index >= 0; index -= 1) {
247
+ const job = this.queue[index];
248
+ if (!job.signal?.aborted)
249
+ continue;
250
+ this.queue.splice(index, 1);
251
+ job.reject(job.signal.reason ?? new Error("Native render cancelled"));
252
+ }
253
+ }
254
+ }
255
+
256
+ // src/native/native-tex-backend.ts
257
+ class NativeTexBackend {
258
+ renderer;
259
+ constructor(renderer = new NativeTexRenderer) {
260
+ this.renderer = renderer;
261
+ }
262
+ async render(request) {
263
+ const image = await this.renderer.renderAsync(request.formula, request.display, request.foreground, request.background, request.signal);
264
+ return { kind: "image", image };
265
+ }
266
+ destroy() {
267
+ this.renderer.destroy();
268
+ }
269
+ }
270
+ export {
271
+ NativeTexRenderer,
272
+ NativeTexBackend
273
+ };
@@ -0,0 +1,8 @@
1
+ import { BindingTexRenderable } from "./binding-tex-renderable.js";
2
+ declare module "@opentui/react" {
3
+ interface OpenTUIComponents {
4
+ tex: typeof BindingTexRenderable;
5
+ }
6
+ }
7
+ export declare function registerTex(): void;
8
+ export * from "./index.js";