@effindomv2/runtime 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.
Files changed (92) hide show
  1. package/LICENSE.md +6 -0
  2. package/dist/bridge.js +4 -0
  3. package/dist/bridge.js.map +7 -0
  4. package/dist/effindom.v2.manifest.json +68 -0
  5. package/dist/fonts/NotoColorEmoji.ttf +0 -0
  6. package/dist/fonts/NotoEmoji-Regular.ttf +0 -0
  7. package/dist/fonts/NotoSans-Bold.ttf +0 -0
  8. package/dist/fonts/NotoSans-BoldItalic.ttf +0 -0
  9. package/dist/fonts/NotoSans-Italic.ttf +0 -0
  10. package/dist/fonts/NotoSans-Regular.ttf +0 -0
  11. package/dist/fonts/NotoSansMono-Bold.ttf +0 -0
  12. package/dist/fonts/NotoSansMono-Regular.ttf +0 -0
  13. package/dist/fonts/NotoSansSymbols2-Regular.ttf +0 -0
  14. package/dist/harness.js +2 -0
  15. package/dist/harness.js.map +7 -0
  16. package/dist/index.html +53 -0
  17. package/dist/runtime/effindom-core-v2.wasm32-simd.JQXIaRaN0-JahfIVFiSLE49WzzCENvef_2EDEm09nJs.wasm +0 -0
  18. package/dist/runtime/effindom-core-v2.wasm32-simd.y7RzpkMARiFeRkpgiqKQsAfv4Hf17NYdpni-6aLNhMs.js.symbols +10079 -0
  19. package/dist/runtime/effindom-core-v2.wasm32-simd.yhT7DGUv4soEv4W91WVZl3T7T_ecKojk5_IcnwL79a0.js +1 -0
  20. package/dist/runtime/effindom-core-v2.wasm32.JSfMkp9ertJzSZxA-_xz3yacrJUhswxlwbqbJLRIuqw.wasm +0 -0
  21. package/dist/runtime/effindom-core-v2.wasm32.xNgsQv7dCwf8Uy-PfJSoRNyk9-q1OSogUwkk5g6ZBjk.js.symbols +10088 -0
  22. package/dist/runtime/effindom-core-v2.wasm32.yhT7DGUv4soEv4W91WVZl3T7T_ecKojk5_IcnwL79a0.js +1 -0
  23. package/dist/runtime/effindom-core-v2.wasm64-simd.GkByf-CPorNOs1CORny_8JjVk8Z3piiFq92r-uw1Syc.js +1 -0
  24. package/dist/runtime/effindom-core-v2.wasm64-simd.p4P98oRu2wEWxtRRW8RHr27JhGeWvWlziZXDM_z3Nc4.js.symbols +10286 -0
  25. package/dist/runtime/effindom-core-v2.wasm64-simd.y75FYXRwhQrpaDGYbZWrohGDv0AmjTb-EjXwOjBIgnM.wasm +0 -0
  26. package/dist/runtime/effindom-core-v2.wasm64.GkByf-CPorNOs1CORny_8JjVk8Z3piiFq92r-uw1Syc.js +1 -0
  27. package/dist/runtime/effindom-core-v2.wasm64.emhE1_CJs4_zXp8wiQS_5lYpUQ0OchmXgxksi0ykaBs.js.symbols +10298 -0
  28. package/dist/runtime/effindom-core-v2.wasm64.sO-Yu70cfN8Qs3a5iEp6cbFPaiOchqcMKUzryu4npNo.wasm +0 -0
  29. package/dist/runtime/effindom-ui-v2.wasm32-simd.0Mas1XD03eYvemryTioWaZOBuBA5ij7MFlTa8CgEZWs.wasm +0 -0
  30. package/dist/runtime/effindom-ui-v2.wasm32-simd.ThSDClMnSWdwf9d89JZfYor0G1Z6OxR4lOc75rNRuD4.js.symbols +1890 -0
  31. package/dist/runtime/effindom-ui-v2.wasm32-simd.wved0xEV4EKXVNBU3Sx7giD4faxD2YII9sQ2N_wCP4I.js +2 -0
  32. package/dist/runtime/effindom-ui-v2.wasm32.H7kYg99bT9ADGh0uUvj6H9Dk1L058nVFLv_4R79IXW8.js.symbols +1900 -0
  33. package/dist/runtime/effindom-ui-v2.wasm32.tp53X7nHfG_EUq29naDyElfnqhMw2D1Tr1T-BJAYO7w.wasm +0 -0
  34. package/dist/runtime/effindom-ui-v2.wasm32.wved0xEV4EKXVNBU3Sx7giD4faxD2YII9sQ2N_wCP4I.js +2 -0
  35. package/dist/runtime/effindom-ui-v2.wasm64-simd.86tk9Z3xIpgTOykET_8Nn9iUVJnp1AzOHW4fVQRGtQE.wasm +0 -0
  36. package/dist/runtime/effindom-ui-v2.wasm64-simd.RQaXil22Chu63-vxK9oOuX8wUY044kbo190oYIbBU4M.js.symbols +1918 -0
  37. package/dist/runtime/effindom-ui-v2.wasm64-simd.ZS1KEAg0XQex-VXkfgpBHE8MIoqPF8qpaf8nOjANb_U.js +2 -0
  38. package/dist/runtime/effindom-ui-v2.wasm64.YSwpMFbr-Q1SBe0Ze8mub1u1PqsvSz3QIYuA3eaUMME.js.symbols +1924 -0
  39. package/dist/runtime/effindom-ui-v2.wasm64.ZS1KEAg0XQex-VXkfgpBHE8MIoqPF8qpaf8nOjANb_U.js +2 -0
  40. package/dist/runtime/effindom-ui-v2.wasm64.ioQ9DuM6gR_EjlfRHdF8EvNPBcKCs0PQbbY9-cjTV6Y.wasm +0 -0
  41. package/dist/runtime/icudt_minimal.962CX1q0-Nbv-OqXPaub5piYTOLumUk-nEvemcvvnpw.dat +0 -0
  42. package/package.json +62 -0
  43. package/scripts/build.sh +279 -0
  44. package/scripts/build_assets.sh +51 -0
  45. package/scripts/font_assets.sh +52 -0
  46. package/scripts/generate_manifest.py +121 -0
  47. package/scripts/stage_package_assets.sh +42 -0
  48. package/src/bridge/commit-policy.ts +10 -0
  49. package/src/bridge/events/canvas-geometry.ts +78 -0
  50. package/src/bridge/events/key-router.ts +187 -0
  51. package/src/bridge/events/pointer-router.ts +619 -0
  52. package/src/bridge/events/semantic-hit-testing.ts +27 -0
  53. package/src/bridge/events.ts +54 -0
  54. package/src/bridge/find-dialog.ts +690 -0
  55. package/src/bridge/find-session.ts +158 -0
  56. package/src/bridge/font-catalog.ts +51 -0
  57. package/src/bridge/google-fonts.ts +63 -0
  58. package/src/bridge/incremental-font-packages.ts +216 -0
  59. package/src/bridge/init.ts +77 -0
  60. package/src/bridge/interaction/editor-model.ts +371 -0
  61. package/src/bridge/interaction/editor-mutations.ts +495 -0
  62. package/src/bridge/interaction/editor-session.ts +628 -0
  63. package/src/bridge/interaction/logs.ts +23 -0
  64. package/src/bridge/interaction/text-encoding.ts +51 -0
  65. package/src/bridge/interaction.ts +86 -0
  66. package/src/bridge/local-types.ts +105 -0
  67. package/src/bridge/platform.ts +68 -0
  68. package/src/bridge/pointer-move-coalescer.ts +41 -0
  69. package/src/bridge/pull-to-refresh.ts +124 -0
  70. package/src/bridge/render-loop.ts +268 -0
  71. package/src/bridge/runtime/asset-manager.ts +202 -0
  72. package/src/bridge/runtime/find-controller.ts +269 -0
  73. package/src/bridge/runtime/font-manager.ts +691 -0
  74. package/src/bridge/runtime/open-canvas-api.ts +72 -0
  75. package/src/bridge/runtime/semantic-controller.ts +133 -0
  76. package/src/bridge/runtime/text-documents.ts +234 -0
  77. package/src/bridge/runtime.ts +315 -0
  78. package/src/bridge/touch-gesture.ts +159 -0
  79. package/src/bridge/utils/assets.ts +572 -0
  80. package/src/bridge/utils/backends.ts +163 -0
  81. package/src/bridge/utils/encoding.ts +128 -0
  82. package/src/bridge/utils/fetch.ts +147 -0
  83. package/src/bridge/utils/heap.ts +118 -0
  84. package/src/bridge.ts +93 -0
  85. package/src/clipboard.ts +139 -0
  86. package/src/core-types.ts +595 -0
  87. package/src/find-on-page.ts +284 -0
  88. package/src/harness.ts +53 -0
  89. package/src/index.ts +40 -0
  90. package/src/open-canvas.ts +108 -0
  91. package/src/runtime-config.ts +96 -0
  92. package/src/semantic.ts +905 -0
@@ -0,0 +1,284 @@
1
+ const textEncoder = new TextEncoder();
2
+
3
+ export interface FindOnPageDocument {
4
+ readonly handle: string;
5
+ readonly text: string;
6
+ }
7
+
8
+ export interface ResolvedFindSelection {
9
+ readonly handle: string;
10
+ readonly start: number;
11
+ readonly end: number;
12
+ }
13
+
14
+ function utf8ByteLength(text: string): number {
15
+ return textEncoder.encode(text).byteLength;
16
+ }
17
+
18
+ function closestElement(node: Node | null): Element | null {
19
+ if (node instanceof Element) {
20
+ return node;
21
+ }
22
+ return node?.parentElement ?? null;
23
+ }
24
+
25
+ function utf8ByteOffsetFromCodeUnitIndex(text: string, codeUnitIndex: number): number {
26
+ const clampedIndex = Math.max(0, Math.min(codeUnitIndex, text.length));
27
+ return utf8ByteLength(text.slice(0, clampedIndex));
28
+ }
29
+
30
+ function codeUnitIndexFromUtf8ByteOffset(text: string, byteOffset: number): number {
31
+ const target = Math.max(0, Math.min(byteOffset, utf8ByteLength(text)));
32
+ let currentByteOffset = 0;
33
+ let currentIndex = 0;
34
+ while (currentIndex < text.length) {
35
+ const codePoint = text.codePointAt(currentIndex) ?? 0;
36
+ const nextIndex = currentIndex + (codePoint > 0xFFFF ? 2 : 1);
37
+ const nextByteOffset = utf8ByteOffsetFromCodeUnitIndex(text, nextIndex);
38
+ if (nextByteOffset > target) {
39
+ break;
40
+ }
41
+ currentIndex = nextIndex;
42
+ currentByteOffset = nextByteOffset;
43
+ if (currentByteOffset === target) {
44
+ break;
45
+ }
46
+ }
47
+ return currentIndex;
48
+ }
49
+
50
+ function readBoundaryCodeUnitOffset(fragment: HTMLElement, container: Node, offset: number): number | null {
51
+ const range = document.createRange();
52
+ range.selectNodeContents(fragment);
53
+ try {
54
+ range.setEnd(container, offset);
55
+ } catch {
56
+ return null;
57
+ }
58
+ return range.toString().length;
59
+ }
60
+
61
+ function ensureProjectedElement(
62
+ layer: HTMLElement,
63
+ byHandle: Map<string, HTMLDivElement>,
64
+ canvasId: string,
65
+ entry: FindOnPageDocument,
66
+ ): HTMLDivElement {
67
+ const existing = byHandle.get(entry.handle);
68
+ if (existing !== undefined) {
69
+ return existing;
70
+ }
71
+
72
+ const created = document.createElement('div');
73
+ created.setAttribute('data-ed-find-fragment', '1');
74
+ created.setAttribute('data-ed-canvas-id', canvasId);
75
+ created.style.display = 'block';
76
+ created.style.position = 'relative';
77
+ created.style.whiteSpace = 'pre-wrap';
78
+ created.style.wordBreak = 'break-word';
79
+ created.style.pointerEvents = 'none';
80
+ created.style.userSelect = 'text';
81
+ layer.appendChild(created);
82
+ byHandle.set(entry.handle, created);
83
+ return created;
84
+ }
85
+
86
+ export class FindOnPageProjector {
87
+ private readonly layer: HTMLDivElement;
88
+ private readonly content: HTMLDivElement;
89
+ private readonly canvasId: string;
90
+ private readonly elementsByHandle = new Map<string, HTMLDivElement>();
91
+
92
+ public constructor(canvas: HTMLCanvasElement, canvasId: string) {
93
+ const parent = canvas.parentElement;
94
+ if (!(parent instanceof HTMLElement)) {
95
+ throw new Error('Expected scene shell for Find-on-Page projection.');
96
+ }
97
+
98
+ const layer = document.createElement('div');
99
+ layer.id = `find-on-page-layer-${canvasId}`;
100
+ layer.style.position = 'absolute';
101
+ layer.style.left = '0';
102
+ layer.style.top = '0';
103
+ layer.style.pointerEvents = 'none';
104
+ layer.style.opacity = '0';
105
+ layer.style.overflow = 'hidden';
106
+ layer.style.userSelect = 'text';
107
+ layer.setAttribute('aria-hidden', 'true');
108
+ layer.setAttribute('data-ed-find-root', '1');
109
+ layer.setAttribute('data-ed-canvas-id', canvasId);
110
+
111
+ const content = document.createElement('div');
112
+ content.id = `find-on-page-content-${canvasId}`;
113
+ content.style.position = 'absolute';
114
+ content.style.left = '0';
115
+ content.style.top = '0';
116
+ content.style.width = '100%';
117
+ content.style.minHeight = '100%';
118
+ content.style.pointerEvents = 'none';
119
+ content.style.userSelect = 'text';
120
+ content.style.whiteSpace = 'pre-wrap';
121
+ content.style.color = 'transparent';
122
+ content.style.background = 'transparent';
123
+ layer.appendChild(content);
124
+ parent.appendChild(layer);
125
+
126
+ this.layer = layer;
127
+ this.content = content;
128
+ this.canvasId = canvasId;
129
+ }
130
+
131
+ public syncSize(logicalWidth: number, logicalHeight: number): void {
132
+ const width = `${String(logicalWidth)}px`;
133
+ const height = `${String(logicalHeight)}px`;
134
+ this.layer.style.width = width;
135
+ this.layer.style.height = height;
136
+ }
137
+
138
+ public update(documents: readonly FindOnPageDocument[]): void {
139
+ const seenHandles = new Set<string>();
140
+ const orderedElements: HTMLDivElement[] = [];
141
+
142
+ for (const entry of documents) {
143
+ seenHandles.add(entry.handle);
144
+ const element = ensureProjectedElement(this.content, this.elementsByHandle, this.canvasId, entry);
145
+ element.setAttribute('data-ed-handle', entry.handle);
146
+ element.setAttribute('data-ed-start', '0');
147
+ element.setAttribute('data-ed-end', String(utf8ByteLength(entry.text)));
148
+ if (element.textContent !== entry.text) {
149
+ element.textContent = entry.text;
150
+ }
151
+ orderedElements.push(element);
152
+ }
153
+
154
+ this.content.replaceChildren(...orderedElements);
155
+ for (const [handle] of this.elementsByHandle.entries()) {
156
+ if (seenHandles.has(handle)) {
157
+ continue;
158
+ }
159
+ this.elementsByHandle.delete(handle);
160
+ }
161
+ }
162
+
163
+ public resolveSelection(selection: Selection | null): ResolvedFindSelection | null {
164
+ if (
165
+ selection === null ||
166
+ selection.rangeCount === 0 ||
167
+ selection.isCollapsed ||
168
+ document.activeElement?.getAttribute('data-effindom-hidden-editor') === 'true'
169
+ ) {
170
+ return null;
171
+ }
172
+
173
+ const range = selection.getRangeAt(0);
174
+ const startElement = closestElement(range.startContainer);
175
+ const endElement = closestElement(range.endContainer);
176
+ if (
177
+ startElement?.closest('[data-effindom-hidden-editor="true"]') !== null ||
178
+ endElement?.closest('[data-effindom-hidden-editor="true"]') !== null
179
+ ) {
180
+ return null;
181
+ }
182
+
183
+ const startRoot = startElement?.closest('[data-ed-find-root="1"]');
184
+ const endRoot = endElement?.closest('[data-ed-find-root="1"]');
185
+ const startFragment = startElement?.closest('[data-ed-find-fragment="1"]');
186
+ const endFragment = endElement?.closest('[data-ed-find-fragment="1"]');
187
+ if (
188
+ !(startRoot instanceof HTMLElement) ||
189
+ !(endRoot instanceof HTMLElement) ||
190
+ !(startFragment instanceof HTMLElement) ||
191
+ !(endFragment instanceof HTMLElement) ||
192
+ startRoot !== endRoot ||
193
+ startFragment !== endFragment ||
194
+ startRoot.dataset.edCanvasId !== this.canvasId ||
195
+ startFragment.dataset.edCanvasId !== this.canvasId
196
+ ) {
197
+ return null;
198
+ }
199
+
200
+ const handle = startFragment.dataset.edHandle;
201
+ const fragmentStart = Number(startFragment.dataset.edStart);
202
+ const fragmentEnd = Number(startFragment.dataset.edEnd);
203
+ if (
204
+ handle === undefined ||
205
+ !Number.isFinite(fragmentStart) ||
206
+ !Number.isFinite(fragmentEnd) ||
207
+ fragmentStart < 0 ||
208
+ fragmentEnd < fragmentStart
209
+ ) {
210
+ return null;
211
+ }
212
+
213
+ const text = startFragment.textContent ?? '';
214
+ const localStart = readBoundaryCodeUnitOffset(startFragment, range.startContainer, range.startOffset);
215
+ const localEnd = readBoundaryCodeUnitOffset(startFragment, range.endContainer, range.endOffset);
216
+ if (localStart === null || localEnd === null) {
217
+ return null;
218
+ }
219
+
220
+ const start = fragmentStart + utf8ByteOffsetFromCodeUnitIndex(text, localStart);
221
+ const end = fragmentStart + utf8ByteOffsetFromCodeUnitIndex(text, localEnd);
222
+ if (start >= end || end > fragmentEnd) {
223
+ return null;
224
+ }
225
+
226
+ return {
227
+ handle,
228
+ start,
229
+ end,
230
+ };
231
+ }
232
+
233
+ public selectMatch(match: ResolvedFindSelection | null): boolean {
234
+ const selection = document.getSelection();
235
+ if (selection === null) {
236
+ return false;
237
+ }
238
+ if (match === null) {
239
+ selection.removeAllRanges();
240
+ return true;
241
+ }
242
+
243
+ const fragment = this.elementsByHandle.get(match.handle);
244
+ if (!(fragment instanceof HTMLElement)) {
245
+ return false;
246
+ }
247
+
248
+ const fragmentStart = Number(fragment.dataset.edStart);
249
+ const fragmentEnd = Number(fragment.dataset.edEnd);
250
+ if (
251
+ !Number.isFinite(fragmentStart) ||
252
+ !Number.isFinite(fragmentEnd) ||
253
+ match.start < fragmentStart ||
254
+ match.end > fragmentEnd ||
255
+ match.start >= match.end
256
+ ) {
257
+ return false;
258
+ }
259
+
260
+ const text = fragment.textContent ?? '';
261
+ const localStart = match.start - fragmentStart;
262
+ const localEnd = match.end - fragmentStart;
263
+ const startIndex = codeUnitIndexFromUtf8ByteOffset(text, localStart);
264
+ const endIndex = codeUnitIndexFromUtf8ByteOffset(text, localEnd);
265
+ const textNode = fragment.firstChild instanceof Text
266
+ ? fragment.firstChild
267
+ : document.createTextNode(text);
268
+ if (fragment.firstChild !== textNode) {
269
+ fragment.replaceChildren(textNode);
270
+ }
271
+
272
+ const range = document.createRange();
273
+ range.setStart(textNode, startIndex);
274
+ range.setEnd(textNode, endIndex);
275
+ selection.removeAllRanges();
276
+ selection.addRange(range);
277
+ return true;
278
+ }
279
+
280
+ public destroy(): void {
281
+ this.elementsByHandle.clear();
282
+ this.layer.remove();
283
+ }
284
+ }
package/src/harness.ts ADDED
@@ -0,0 +1,53 @@
1
+ import type { BridgeRuntime } from './core-types';
2
+ import { handleToBigInt } from './bridge/utils/encoding';
3
+
4
+ const UI_SIZE_UNIT_PIXEL = 0;
5
+ const UI_NODE_FLEX_BOX = 0;
6
+
7
+ function waitForFrame(): Promise<void> {
8
+ return new Promise<void>((resolve) => {
9
+ requestAnimationFrame(() => {
10
+ requestAnimationFrame(() => {
11
+ resolve();
12
+ });
13
+ });
14
+ });
15
+ }
16
+
17
+ async function runSmokeHarness(runtime: BridgeRuntime): Promise<void> {
18
+ const { canvas, ui } = runtime;
19
+ const rect = canvas.getBoundingClientRect();
20
+ const logicalWidth = rect.width;
21
+ const logicalHeight = rect.height;
22
+ const rootHandle = handleToBigInt(ui._ui_create_node(UI_NODE_FLEX_BOX));
23
+ if (rootHandle === 0n) {
24
+ throw new Error('ui_create_node returned UI_INVALID_HANDLE.');
25
+ }
26
+
27
+ ui._ui_set_root(rootHandle);
28
+ ui._ui_set_width(rootHandle, logicalWidth, UI_SIZE_UNIT_PIXEL);
29
+ ui._ui_set_height(rootHandle, logicalHeight, UI_SIZE_UNIT_PIXEL);
30
+ ui._ui_set_bg_color(rootHandle, 0xff0000ff);
31
+ ui._ui_commit_frame();
32
+
33
+ const commandWords = runtime.syncCommandBufferToCore();
34
+ await waitForFrame();
35
+
36
+ window.__bridgeState = {
37
+ commandWordCount: commandWords.length,
38
+ commandWords: Array.from(commandWords),
39
+ rootHandle: rootHandle.toString(),
40
+ };
41
+ window.__bridgeReady = true;
42
+ delete window.__bridgeError;
43
+ }
44
+
45
+ void window.EffinDomBrowserBridge?.ready.then(async (runtime) => {
46
+ await runSmokeHarness(runtime);
47
+ }).catch((error: unknown) => {
48
+ const message = error instanceof Error ? error.message : String(error);
49
+ window.__bridgeError = message;
50
+ throw error;
51
+ });
52
+
53
+ export {};
package/src/index.ts ADDED
@@ -0,0 +1,40 @@
1
+ export type {
2
+ BridgeRuntime,
3
+ BridgeState,
4
+ EffinDomCallbacks,
5
+ WasmHandleLike,
6
+ } from './core-types';
7
+
8
+ export {
9
+ computeModifiers,
10
+ handleToBigInt,
11
+ toHeapPointer,
12
+ normalizePointerForWasm,
13
+ pointerToHeapOffset,
14
+ } from './bridge/utils/encoding';
15
+
16
+ export {
17
+ getPointerPosition,
18
+ } from './bridge/events/canvas-geometry';
19
+
20
+ export {
21
+ writeBytesToHeap,
22
+ } from './bridge/utils/heap';
23
+
24
+ export type {
25
+ EffinDomRuntimeAssetUrls,
26
+ EffinDomRuntimeConfig,
27
+ } from './runtime-config';
28
+
29
+ export {
30
+ EFFINDOM_RUNTIME_ARTIFACT_DIR,
31
+ EFFINDOM_RUNTIME_BRIDGE_SCRIPT,
32
+ EFFINDOM_RUNTIME_DIST_DIR,
33
+ EFFINDOM_RUNTIME_FONTS_DIR,
34
+ EFFINDOM_RUNTIME_HARNESS_SCRIPT,
35
+ EFFINDOM_RUNTIME_MANIFEST_FILE,
36
+ applyRuntimeConfig,
37
+ createRuntimeConfig,
38
+ createRuntimeConfigScript,
39
+ resolveRuntimeAssetUrls,
40
+ } from './runtime-config';
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Opaque host-side identity for a retained node or document snapshot.
3
+ *
4
+ * The runtime owns handles as 64-bit values. The browser bridge exposes them as strings so:
5
+ * - JavaScript never loses precision on wasm64 / native 64-bit handles
6
+ * - extensions and hosts can persist and compare identities without reaching into wasm internals
7
+ *
8
+ * A handle is:
9
+ * - stable for the lifetime of the realized retained node
10
+ * - local to the current runtime instance
11
+ * - not a DOM node id, CSS selector, or application-level business id
12
+ */
13
+ export type OpenCanvasHandle = string;
14
+
15
+ export interface SemanticBounds {
16
+ readonly x: number;
17
+ readonly y: number;
18
+ readonly width: number;
19
+ readonly height: number;
20
+ }
21
+
22
+ export interface SemanticState {
23
+ readonly checked?: 'false' | 'true' | 'mixed';
24
+ readonly selected?: boolean;
25
+ readonly expanded?: boolean;
26
+ readonly disabled?: boolean;
27
+ readonly readonly?: boolean;
28
+ readonly multiline?: boolean;
29
+ readonly orientation?: 'horizontal' | 'vertical';
30
+ readonly valueNow?: number;
31
+ readonly valueMin?: number;
32
+ readonly valueMax?: number;
33
+ }
34
+
35
+ /**
36
+ * Flattened semantic snapshot of one retained node as exposed to hosts.
37
+ *
38
+ * This is intentionally a bridge-friendly read model rather than a DOM clone:
39
+ * - `role` is the engine enum value
40
+ * - `roleName` is the host-readable role label
41
+ * - `handle` is the retained node identity token
42
+ * - `bounds` are logical canvas bounds for the realized node snapshot
43
+ * - `label` is the semantic label/text payload currently exposed for this node
44
+ * - `state` carries role-relevant flags without forcing consumers to inspect bridge DOM internals
45
+ */
46
+ export interface SemanticNode {
47
+ readonly role: number;
48
+ readonly roleName: string;
49
+ readonly handle: OpenCanvasHandle;
50
+ readonly bounds: SemanticBounds;
51
+ readonly label: string;
52
+ readonly state: SemanticState;
53
+ }
54
+
55
+ export interface OpenCanvasTextDocument {
56
+ readonly handle: OpenCanvasHandle;
57
+ readonly text: string;
58
+ }
59
+
60
+ export interface OpenCanvasFindMatch {
61
+ readonly handle: OpenCanvasHandle;
62
+ readonly start: number;
63
+ readonly end: number;
64
+ }
65
+
66
+ export interface OpenCanvasFindOptions {
67
+ readonly highlightAll?: boolean;
68
+ readonly matchCase?: boolean;
69
+ readonly matchDiacritics?: boolean;
70
+ readonly wholeWords?: boolean;
71
+ }
72
+
73
+ export interface OpenCanvasResolvedFindOptions {
74
+ readonly highlightAll: boolean;
75
+ readonly matchCase: boolean;
76
+ readonly matchDiacritics: boolean;
77
+ readonly wholeWords: boolean;
78
+ }
79
+
80
+ export interface OpenCanvasFindResults {
81
+ readonly query: string;
82
+ readonly options: OpenCanvasResolvedFindOptions;
83
+ readonly matches: readonly OpenCanvasFindMatch[];
84
+ }
85
+
86
+ export interface OpenCanvasFindState extends OpenCanvasFindResults {
87
+ readonly activeMatchIndex: number;
88
+ }
89
+
90
+ /**
91
+ * Current shipped browser-host contract.
92
+ *
93
+ * This interface only covers the runtime surface that is actually wired today.
94
+ * Proposed mutation and callback/event contracts are documented in
95
+ * `docs/v2/browser-bridge/OPEN_CANVAS_API.md` until they are implemented.
96
+ */
97
+ export interface OpenCanvasApi {
98
+ getSemanticTree(): SemanticNode[];
99
+ getBoundingBox(handle: OpenCanvasHandle): SemanticBounds | null;
100
+ getTextVisibleBounds(handle: OpenCanvasHandle): SemanticBounds | null;
101
+ getTextDocument(handle: OpenCanvasHandle): OpenCanvasTextDocument | null;
102
+ getRangeRects(handle: OpenCanvasHandle, start: number, end: number): readonly SemanticBounds[];
103
+ findText(query: string, options?: OpenCanvasFindOptions): OpenCanvasFindResults;
104
+ setFindState(state: OpenCanvasFindState | null, revealActive?: boolean): boolean;
105
+ getFindState(): OpenCanvasFindState | null;
106
+ setFindMatch(match: OpenCanvasFindMatch | null): boolean;
107
+ revealRange(handle: OpenCanvasHandle, start: number, end: number): boolean;
108
+ }
@@ -0,0 +1,96 @@
1
+ export const EFFINDOM_RUNTIME_DIST_DIR = 'dist';
2
+ export const EFFINDOM_RUNTIME_MANIFEST_FILE = 'effindom.v2.manifest.json';
3
+ export const EFFINDOM_RUNTIME_BRIDGE_SCRIPT = 'bridge.js';
4
+ export const EFFINDOM_RUNTIME_HARNESS_SCRIPT = 'harness.js';
5
+ export const EFFINDOM_RUNTIME_ARTIFACT_DIR = 'runtime';
6
+ export const EFFINDOM_RUNTIME_FONTS_DIR = 'fonts';
7
+
8
+ export interface EffinDomRuntimeConfig {
9
+ readonly manifestUrl: string;
10
+ }
11
+
12
+ export interface EffinDomRuntimeAssetUrls {
13
+ readonly packageBaseUrl: string;
14
+ readonly distBaseUrl: string;
15
+ readonly manifestUrl: string;
16
+ readonly bridgeScriptUrl: string;
17
+ readonly harnessScriptUrl: string;
18
+ readonly runtimeBaseUrl: string;
19
+ readonly fontsBaseUrl: string;
20
+ }
21
+
22
+ interface RuntimeWindowLike {
23
+ __effindomRuntime?: Partial<EffinDomRuntimeConfig>;
24
+ }
25
+
26
+ function ensureTrailingSlash(url: string): string {
27
+ return url.endsWith('/') ? url : `${url}/`;
28
+ }
29
+
30
+ function resolveFromBase(
31
+ value: string | URL,
32
+ base?: string | URL,
33
+ ): URL {
34
+ if (value instanceof URL) {
35
+ return new URL(value.toString());
36
+ }
37
+ if (base !== undefined) {
38
+ return new URL(value, base);
39
+ }
40
+ if (typeof document !== 'undefined') {
41
+ return new URL(value, document.baseURI);
42
+ }
43
+ return new URL(value);
44
+ }
45
+
46
+ export function resolveRuntimeAssetUrls(
47
+ packageBaseUrl: string | URL,
48
+ relativeTo?: string | URL,
49
+ ): EffinDomRuntimeAssetUrls {
50
+ const packageBase = resolveFromBase(packageBaseUrl, relativeTo);
51
+ packageBase.pathname = ensureTrailingSlash(packageBase.pathname);
52
+ const distBase = new URL(`${EFFINDOM_RUNTIME_DIST_DIR}/`, packageBase);
53
+ const runtimeBase = new URL(`${EFFINDOM_RUNTIME_ARTIFACT_DIR}/`, distBase);
54
+ const fontsBase = new URL(`${EFFINDOM_RUNTIME_FONTS_DIR}/`, distBase);
55
+
56
+ return {
57
+ packageBaseUrl: packageBase.toString(),
58
+ distBaseUrl: distBase.toString(),
59
+ manifestUrl: new URL(EFFINDOM_RUNTIME_MANIFEST_FILE, distBase).toString(),
60
+ bridgeScriptUrl: new URL(EFFINDOM_RUNTIME_BRIDGE_SCRIPT, distBase).toString(),
61
+ harnessScriptUrl: new URL(EFFINDOM_RUNTIME_HARNESS_SCRIPT, distBase).toString(),
62
+ runtimeBaseUrl: runtimeBase.toString(),
63
+ fontsBaseUrl: fontsBase.toString(),
64
+ };
65
+ }
66
+
67
+ export function createRuntimeConfig(
68
+ packageBaseUrl: string | URL,
69
+ relativeTo?: string | URL,
70
+ ): EffinDomRuntimeConfig {
71
+ const urls = resolveRuntimeAssetUrls(packageBaseUrl, relativeTo);
72
+ return {
73
+ manifestUrl: urls.manifestUrl,
74
+ };
75
+ }
76
+
77
+ export function applyRuntimeConfig(
78
+ config: EffinDomRuntimeConfig,
79
+ target?: RuntimeWindowLike,
80
+ ): EffinDomRuntimeConfig {
81
+ const destination = target ?? (typeof window !== 'undefined' ? (window as RuntimeWindowLike) : undefined);
82
+ if (destination === undefined) {
83
+ throw new Error('applyRuntimeConfig requires a browser window-like target outside browser contexts.');
84
+ }
85
+ destination.__effindomRuntime = Object.assign({}, destination.__effindomRuntime, config);
86
+ return {
87
+ manifestUrl: destination.__effindomRuntime.manifestUrl ?? config.manifestUrl,
88
+ };
89
+ }
90
+
91
+ export function createRuntimeConfigScript(
92
+ config: EffinDomRuntimeConfig,
93
+ ): string {
94
+ const manifest = JSON.stringify(config.manifestUrl);
95
+ return `window.__effindomRuntime = Object.assign({}, window.__effindomRuntime, {\n manifestUrl: ${manifest},\n});\n`;
96
+ }