@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.
- package/LICENSE.md +6 -0
- package/dist/bridge.js +4 -0
- package/dist/bridge.js.map +7 -0
- package/dist/effindom.v2.manifest.json +68 -0
- package/dist/fonts/NotoColorEmoji.ttf +0 -0
- package/dist/fonts/NotoEmoji-Regular.ttf +0 -0
- package/dist/fonts/NotoSans-Bold.ttf +0 -0
- package/dist/fonts/NotoSans-BoldItalic.ttf +0 -0
- package/dist/fonts/NotoSans-Italic.ttf +0 -0
- package/dist/fonts/NotoSans-Regular.ttf +0 -0
- package/dist/fonts/NotoSansMono-Bold.ttf +0 -0
- package/dist/fonts/NotoSansMono-Regular.ttf +0 -0
- package/dist/fonts/NotoSansSymbols2-Regular.ttf +0 -0
- package/dist/harness.js +2 -0
- package/dist/harness.js.map +7 -0
- package/dist/index.html +53 -0
- package/dist/runtime/effindom-core-v2.wasm32-simd.JQXIaRaN0-JahfIVFiSLE49WzzCENvef_2EDEm09nJs.wasm +0 -0
- package/dist/runtime/effindom-core-v2.wasm32-simd.y7RzpkMARiFeRkpgiqKQsAfv4Hf17NYdpni-6aLNhMs.js.symbols +10079 -0
- package/dist/runtime/effindom-core-v2.wasm32-simd.yhT7DGUv4soEv4W91WVZl3T7T_ecKojk5_IcnwL79a0.js +1 -0
- package/dist/runtime/effindom-core-v2.wasm32.JSfMkp9ertJzSZxA-_xz3yacrJUhswxlwbqbJLRIuqw.wasm +0 -0
- package/dist/runtime/effindom-core-v2.wasm32.xNgsQv7dCwf8Uy-PfJSoRNyk9-q1OSogUwkk5g6ZBjk.js.symbols +10088 -0
- package/dist/runtime/effindom-core-v2.wasm32.yhT7DGUv4soEv4W91WVZl3T7T_ecKojk5_IcnwL79a0.js +1 -0
- package/dist/runtime/effindom-core-v2.wasm64-simd.GkByf-CPorNOs1CORny_8JjVk8Z3piiFq92r-uw1Syc.js +1 -0
- package/dist/runtime/effindom-core-v2.wasm64-simd.p4P98oRu2wEWxtRRW8RHr27JhGeWvWlziZXDM_z3Nc4.js.symbols +10286 -0
- package/dist/runtime/effindom-core-v2.wasm64-simd.y75FYXRwhQrpaDGYbZWrohGDv0AmjTb-EjXwOjBIgnM.wasm +0 -0
- package/dist/runtime/effindom-core-v2.wasm64.GkByf-CPorNOs1CORny_8JjVk8Z3piiFq92r-uw1Syc.js +1 -0
- package/dist/runtime/effindom-core-v2.wasm64.emhE1_CJs4_zXp8wiQS_5lYpUQ0OchmXgxksi0ykaBs.js.symbols +10298 -0
- package/dist/runtime/effindom-core-v2.wasm64.sO-Yu70cfN8Qs3a5iEp6cbFPaiOchqcMKUzryu4npNo.wasm +0 -0
- package/dist/runtime/effindom-ui-v2.wasm32-simd.0Mas1XD03eYvemryTioWaZOBuBA5ij7MFlTa8CgEZWs.wasm +0 -0
- package/dist/runtime/effindom-ui-v2.wasm32-simd.ThSDClMnSWdwf9d89JZfYor0G1Z6OxR4lOc75rNRuD4.js.symbols +1890 -0
- package/dist/runtime/effindom-ui-v2.wasm32-simd.wved0xEV4EKXVNBU3Sx7giD4faxD2YII9sQ2N_wCP4I.js +2 -0
- package/dist/runtime/effindom-ui-v2.wasm32.H7kYg99bT9ADGh0uUvj6H9Dk1L058nVFLv_4R79IXW8.js.symbols +1900 -0
- package/dist/runtime/effindom-ui-v2.wasm32.tp53X7nHfG_EUq29naDyElfnqhMw2D1Tr1T-BJAYO7w.wasm +0 -0
- package/dist/runtime/effindom-ui-v2.wasm32.wved0xEV4EKXVNBU3Sx7giD4faxD2YII9sQ2N_wCP4I.js +2 -0
- package/dist/runtime/effindom-ui-v2.wasm64-simd.86tk9Z3xIpgTOykET_8Nn9iUVJnp1AzOHW4fVQRGtQE.wasm +0 -0
- package/dist/runtime/effindom-ui-v2.wasm64-simd.RQaXil22Chu63-vxK9oOuX8wUY044kbo190oYIbBU4M.js.symbols +1918 -0
- package/dist/runtime/effindom-ui-v2.wasm64-simd.ZS1KEAg0XQex-VXkfgpBHE8MIoqPF8qpaf8nOjANb_U.js +2 -0
- package/dist/runtime/effindom-ui-v2.wasm64.YSwpMFbr-Q1SBe0Ze8mub1u1PqsvSz3QIYuA3eaUMME.js.symbols +1924 -0
- package/dist/runtime/effindom-ui-v2.wasm64.ZS1KEAg0XQex-VXkfgpBHE8MIoqPF8qpaf8nOjANb_U.js +2 -0
- package/dist/runtime/effindom-ui-v2.wasm64.ioQ9DuM6gR_EjlfRHdF8EvNPBcKCs0PQbbY9-cjTV6Y.wasm +0 -0
- package/dist/runtime/icudt_minimal.962CX1q0-Nbv-OqXPaub5piYTOLumUk-nEvemcvvnpw.dat +0 -0
- package/package.json +62 -0
- package/scripts/build.sh +279 -0
- package/scripts/build_assets.sh +51 -0
- package/scripts/font_assets.sh +52 -0
- package/scripts/generate_manifest.py +121 -0
- package/scripts/stage_package_assets.sh +42 -0
- package/src/bridge/commit-policy.ts +10 -0
- package/src/bridge/events/canvas-geometry.ts +78 -0
- package/src/bridge/events/key-router.ts +187 -0
- package/src/bridge/events/pointer-router.ts +619 -0
- package/src/bridge/events/semantic-hit-testing.ts +27 -0
- package/src/bridge/events.ts +54 -0
- package/src/bridge/find-dialog.ts +690 -0
- package/src/bridge/find-session.ts +158 -0
- package/src/bridge/font-catalog.ts +51 -0
- package/src/bridge/google-fonts.ts +63 -0
- package/src/bridge/incremental-font-packages.ts +216 -0
- package/src/bridge/init.ts +77 -0
- package/src/bridge/interaction/editor-model.ts +371 -0
- package/src/bridge/interaction/editor-mutations.ts +495 -0
- package/src/bridge/interaction/editor-session.ts +628 -0
- package/src/bridge/interaction/logs.ts +23 -0
- package/src/bridge/interaction/text-encoding.ts +51 -0
- package/src/bridge/interaction.ts +86 -0
- package/src/bridge/local-types.ts +105 -0
- package/src/bridge/platform.ts +68 -0
- package/src/bridge/pointer-move-coalescer.ts +41 -0
- package/src/bridge/pull-to-refresh.ts +124 -0
- package/src/bridge/render-loop.ts +268 -0
- package/src/bridge/runtime/asset-manager.ts +202 -0
- package/src/bridge/runtime/find-controller.ts +269 -0
- package/src/bridge/runtime/font-manager.ts +691 -0
- package/src/bridge/runtime/open-canvas-api.ts +72 -0
- package/src/bridge/runtime/semantic-controller.ts +133 -0
- package/src/bridge/runtime/text-documents.ts +234 -0
- package/src/bridge/runtime.ts +315 -0
- package/src/bridge/touch-gesture.ts +159 -0
- package/src/bridge/utils/assets.ts +572 -0
- package/src/bridge/utils/backends.ts +163 -0
- package/src/bridge/utils/encoding.ts +128 -0
- package/src/bridge/utils/fetch.ts +147 -0
- package/src/bridge/utils/heap.ts +118 -0
- package/src/bridge.ts +93 -0
- package/src/clipboard.ts +139 -0
- package/src/core-types.ts +595 -0
- package/src/find-on-page.ts +284 -0
- package/src/harness.ts +53 -0
- package/src/index.ts +40 -0
- package/src/open-canvas.ts +108 -0
- package/src/runtime-config.ts +96 -0
- package/src/semantic.ts +905 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
OpenCanvasFindMatch,
|
|
3
|
+
OpenCanvasFindOptions,
|
|
4
|
+
OpenCanvasFindResults,
|
|
5
|
+
OpenCanvasResolvedFindOptions,
|
|
6
|
+
OpenCanvasTextDocument,
|
|
7
|
+
} from '../core-types';
|
|
8
|
+
|
|
9
|
+
const combiningMarkPattern = /\p{Mark}+/gu;
|
|
10
|
+
const wordLikeCharPattern = /[\p{Letter}\p{Number}\p{Mark}_]/u;
|
|
11
|
+
const textEncoder = new TextEncoder();
|
|
12
|
+
|
|
13
|
+
interface SearchToken {
|
|
14
|
+
readonly start: number;
|
|
15
|
+
readonly end: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface SearchText {
|
|
19
|
+
readonly text: string;
|
|
20
|
+
readonly tokens: readonly SearchToken[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const DEFAULT_OPEN_CANVAS_FIND_OPTIONS: OpenCanvasResolvedFindOptions = {
|
|
24
|
+
highlightAll: false,
|
|
25
|
+
matchCase: false,
|
|
26
|
+
matchDiacritics: false,
|
|
27
|
+
wholeWords: false,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export function normalizeOpenCanvasFindOptions(
|
|
31
|
+
options?: OpenCanvasFindOptions,
|
|
32
|
+
): OpenCanvasResolvedFindOptions {
|
|
33
|
+
return {
|
|
34
|
+
highlightAll: options?.highlightAll ?? DEFAULT_OPEN_CANVAS_FIND_OPTIONS.highlightAll,
|
|
35
|
+
matchCase: options?.matchCase ?? DEFAULT_OPEN_CANVAS_FIND_OPTIONS.matchCase,
|
|
36
|
+
matchDiacritics: options?.matchDiacritics ?? DEFAULT_OPEN_CANVAS_FIND_OPTIONS.matchDiacritics,
|
|
37
|
+
wholeWords: options?.wholeWords ?? DEFAULT_OPEN_CANVAS_FIND_OPTIONS.wholeWords,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function utf8ByteOffsetFromCodeUnitIndex(text: string, codeUnitIndex: number): number {
|
|
42
|
+
const clampedIndex = Math.max(0, Math.min(codeUnitIndex, text.length));
|
|
43
|
+
return textEncoder.encode(text.slice(0, clampedIndex)).byteLength;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function codePointBefore(text: string, boundary: number): string | null {
|
|
47
|
+
if (boundary <= 0) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
let start = boundary - 1;
|
|
51
|
+
const low = text.charCodeAt(start);
|
|
52
|
+
if (
|
|
53
|
+
start > 0 &&
|
|
54
|
+
low >= 0xDC00 &&
|
|
55
|
+
low <= 0xDFFF
|
|
56
|
+
) {
|
|
57
|
+
const high = text.charCodeAt(start - 1);
|
|
58
|
+
if (high >= 0xD800 && high <= 0xDBFF) {
|
|
59
|
+
start -= 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return text.slice(start, boundary);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function codePointAt(text: string, boundary: number): string | null {
|
|
66
|
+
const codePoint = text.codePointAt(boundary);
|
|
67
|
+
return codePoint === undefined ? null : String.fromCodePoint(codePoint);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function isWordLikeChar(value: string | null): boolean {
|
|
71
|
+
return value !== null && wordLikeCharPattern.test(value);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function isWholeWordMatch(text: string, start: number, end: number): boolean {
|
|
75
|
+
return !isWordLikeChar(codePointBefore(text, start)) &&
|
|
76
|
+
!isWordLikeChar(codePointAt(text, end));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function buildSearchText(
|
|
80
|
+
source: string,
|
|
81
|
+
options: OpenCanvasResolvedFindOptions,
|
|
82
|
+
): SearchText {
|
|
83
|
+
const tokens: SearchToken[] = [];
|
|
84
|
+
let output = '';
|
|
85
|
+
let sourceIndex = 0;
|
|
86
|
+
for (const segment of source) {
|
|
87
|
+
const start = sourceIndex;
|
|
88
|
+
sourceIndex += segment.length;
|
|
89
|
+
const end = sourceIndex;
|
|
90
|
+
let searchable = options.matchDiacritics
|
|
91
|
+
? segment
|
|
92
|
+
: segment.normalize('NFD').replace(combiningMarkPattern, '');
|
|
93
|
+
searchable = options.matchCase ? searchable : searchable.toLocaleLowerCase();
|
|
94
|
+
output += searchable;
|
|
95
|
+
for (let index = 0; index < searchable.length; index += 1) {
|
|
96
|
+
tokens.push({ start, end });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { text: output, tokens };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function findTextInOpenCanvasDocuments(
|
|
103
|
+
documents: readonly OpenCanvasTextDocument[],
|
|
104
|
+
query: string,
|
|
105
|
+
options?: OpenCanvasFindOptions,
|
|
106
|
+
): OpenCanvasFindResults {
|
|
107
|
+
const resolvedOptions = normalizeOpenCanvasFindOptions(options);
|
|
108
|
+
if (query.length === 0) {
|
|
109
|
+
return {
|
|
110
|
+
query,
|
|
111
|
+
options: resolvedOptions,
|
|
112
|
+
matches: [],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const normalizedQuery = buildSearchText(query, resolvedOptions).text;
|
|
117
|
+
if (normalizedQuery.length === 0) {
|
|
118
|
+
return {
|
|
119
|
+
query,
|
|
120
|
+
options: resolvedOptions,
|
|
121
|
+
matches: [],
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const matches: OpenCanvasFindMatch[] = [];
|
|
126
|
+
for (const document of documents) {
|
|
127
|
+
const searchableDocument = buildSearchText(document.text, resolvedOptions);
|
|
128
|
+
let searchStart = 0;
|
|
129
|
+
while (searchStart <= searchableDocument.text.length - normalizedQuery.length) {
|
|
130
|
+
const matchStart = searchableDocument.text.indexOf(normalizedQuery, searchStart);
|
|
131
|
+
if (matchStart < 0) {
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
const matchEnd = matchStart + normalizedQuery.length;
|
|
135
|
+
const startToken = searchableDocument.tokens[matchStart];
|
|
136
|
+
const endToken = searchableDocument.tokens[matchEnd - 1];
|
|
137
|
+
if (startToken === undefined || endToken === undefined) {
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
const sourceStart = startToken.start;
|
|
141
|
+
const sourceEnd = endToken.end;
|
|
142
|
+
if (!resolvedOptions.wholeWords || isWholeWordMatch(document.text, sourceStart, sourceEnd)) {
|
|
143
|
+
matches.push({
|
|
144
|
+
handle: document.handle,
|
|
145
|
+
start: utf8ByteOffsetFromCodeUnitIndex(document.text, sourceStart),
|
|
146
|
+
end: utf8ByteOffsetFromCodeUnitIndex(document.text, sourceEnd),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
searchStart = matchStart + Math.max(normalizedQuery.length, 1);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
query,
|
|
155
|
+
options: resolvedOptions,
|
|
156
|
+
matches,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
export interface BridgeFontDefinition {
|
|
2
|
+
readonly id: number;
|
|
3
|
+
readonly assetFile: string;
|
|
4
|
+
readonly fallbackIds: readonly number[];
|
|
5
|
+
readonly preload: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const BUILT_IN_FONT_BODY = 1;
|
|
9
|
+
export const BUILT_IN_FONT_HEADING = 2;
|
|
10
|
+
export const BUILT_IN_FONT_SYMBOLS = 3;
|
|
11
|
+
export const BUILT_IN_FONT_EMOJI = 4;
|
|
12
|
+
export const BUILT_IN_FONT_MONO = 5;
|
|
13
|
+
export const BUILT_IN_FONT_MONO_BOLD = 6;
|
|
14
|
+
export const BUILT_IN_FONT_BODY_ITALIC = 9;
|
|
15
|
+
export const BUILT_IN_FONT_BODY_BOLD_ITALIC = 10;
|
|
16
|
+
|
|
17
|
+
const BUILT_IN_BRIDGE_FONTS: readonly BridgeFontDefinition[] = [
|
|
18
|
+
{ id: BUILT_IN_FONT_BODY, assetFile: 'NotoSans-Regular.ttf', fallbackIds: [BUILT_IN_FONT_EMOJI, BUILT_IN_FONT_SYMBOLS], preload: true },
|
|
19
|
+
{ id: BUILT_IN_FONT_HEADING, assetFile: 'NotoSans-Bold.ttf', fallbackIds: [BUILT_IN_FONT_EMOJI, BUILT_IN_FONT_SYMBOLS], preload: true },
|
|
20
|
+
{ id: BUILT_IN_FONT_BODY_ITALIC, assetFile: 'NotoSans-Italic.ttf', fallbackIds: [BUILT_IN_FONT_EMOJI, BUILT_IN_FONT_SYMBOLS], preload: true },
|
|
21
|
+
{ id: BUILT_IN_FONT_BODY_BOLD_ITALIC, assetFile: 'NotoSans-BoldItalic.ttf', fallbackIds: [BUILT_IN_FONT_EMOJI, BUILT_IN_FONT_SYMBOLS], preload: true },
|
|
22
|
+
{ id: BUILT_IN_FONT_SYMBOLS, assetFile: 'NotoSansSymbols2-Regular.ttf', fallbackIds: [], preload: true },
|
|
23
|
+
{ id: BUILT_IN_FONT_EMOJI, assetFile: 'NotoEmoji-Regular.ttf', fallbackIds: [], preload: true },
|
|
24
|
+
{ id: BUILT_IN_FONT_MONO, assetFile: 'NotoSansMono-Regular.ttf', fallbackIds: [BUILT_IN_FONT_EMOJI, BUILT_IN_FONT_SYMBOLS], preload: false },
|
|
25
|
+
{ id: BUILT_IN_FONT_MONO_BOLD, assetFile: 'NotoSansMono-Bold.ttf', fallbackIds: [BUILT_IN_FONT_EMOJI, BUILT_IN_FONT_SYMBOLS], preload: false },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
export const STARTUP_BRIDGE_FONTS: readonly BridgeFontDefinition[] = BUILT_IN_BRIDGE_FONTS.filter((font) => font.preload);
|
|
29
|
+
|
|
30
|
+
export function getBuiltInBridgeFont(fontId: number): BridgeFontDefinition | undefined {
|
|
31
|
+
return BUILT_IN_BRIDGE_FONTS.find((font) => font.id === fontId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getBridgeAssetBaseUrl(): string {
|
|
35
|
+
const runtimeConfig = window as Window & {
|
|
36
|
+
__effindomRuntime?: { manifestUrl?: string };
|
|
37
|
+
};
|
|
38
|
+
const manifestUrl = runtimeConfig.__effindomRuntime?.manifestUrl;
|
|
39
|
+
if (typeof manifestUrl === 'string' && manifestUrl.length > 0) {
|
|
40
|
+
return new URL('./', new URL(manifestUrl, document.baseURI)).toString();
|
|
41
|
+
}
|
|
42
|
+
const currentScript = document.currentScript;
|
|
43
|
+
if (currentScript instanceof HTMLScriptElement && currentScript.src.length > 0) {
|
|
44
|
+
return new URL('./', currentScript.src).toString();
|
|
45
|
+
}
|
|
46
|
+
return new URL('./', document.baseURI).toString();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getBridgeAssetUrl(assetFile: string): string {
|
|
50
|
+
return new URL(`../fonts/${assetFile}`, getBridgeAssetBaseUrl()).toString();
|
|
51
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
export interface GoogleFontShardBytes {
|
|
2
|
+
readonly url: string;
|
|
3
|
+
readonly bytes: Uint8Array;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
const GOOGLE_FONTS_STYLESHEET_ORIGIN = 'https://fonts.googleapis.com';
|
|
7
|
+
const GOOGLE_FONTS_BINARY_ORIGIN = 'https://fonts.gstatic.com';
|
|
8
|
+
|
|
9
|
+
function ensureLinkTag(rel: string, href: string, crossOrigin?: '' | 'anonymous'): void {
|
|
10
|
+
const existing = document.head.querySelector(`link[rel="${rel}"][href="${href}"]`);
|
|
11
|
+
if (existing instanceof HTMLLinkElement) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const link = document.createElement('link');
|
|
15
|
+
link.rel = rel;
|
|
16
|
+
link.href = href;
|
|
17
|
+
if (crossOrigin !== undefined) {
|
|
18
|
+
link.crossOrigin = crossOrigin;
|
|
19
|
+
}
|
|
20
|
+
document.head.appendChild(link);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function ensureGoogleFontShardPreconnect(): void {
|
|
24
|
+
ensureLinkTag('preconnect', GOOGLE_FONTS_STYLESHEET_ORIGIN);
|
|
25
|
+
ensureLinkTag('preconnect', GOOGLE_FONTS_BINARY_ORIGIN, 'anonymous');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function buildGoogleFontStylesheetUrl(googleFamily: string, text: string): string {
|
|
29
|
+
const params = new URLSearchParams();
|
|
30
|
+
params.set('family', `${googleFamily}:wght@400`);
|
|
31
|
+
params.set('text', text);
|
|
32
|
+
params.set('display', 'swap');
|
|
33
|
+
return `${GOOGLE_FONTS_STYLESHEET_ORIGIN}/css2?${params.toString()}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseGoogleFontBinaryUrl(stylesheetText: string): string {
|
|
37
|
+
const match = /src:\s*url\(([^)]+)\)\s*format\((['"]?)(woff2|woff|truetype|opentype)\2\)/.exec(stylesheetText);
|
|
38
|
+
if (match?.[1] === undefined) {
|
|
39
|
+
throw new Error('Google Fonts stylesheet did not expose a usable shard URL.');
|
|
40
|
+
}
|
|
41
|
+
return match[1].trim();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function fetchGoogleFontShardBytes(
|
|
45
|
+
googleFamily: string,
|
|
46
|
+
text: string,
|
|
47
|
+
): Promise<GoogleFontShardBytes> {
|
|
48
|
+
ensureGoogleFontShardPreconnect();
|
|
49
|
+
const stylesheetUrl = buildGoogleFontStylesheetUrl(googleFamily, text);
|
|
50
|
+
const stylesheetResponse = await fetch(stylesheetUrl, { mode: 'cors' });
|
|
51
|
+
if (!stylesheetResponse.ok) {
|
|
52
|
+
throw new Error(`Failed to fetch Google Fonts stylesheet ${stylesheetUrl}: ${String(stylesheetResponse.status)}`);
|
|
53
|
+
}
|
|
54
|
+
const binaryUrl = parseGoogleFontBinaryUrl(await stylesheetResponse.text());
|
|
55
|
+
const binaryResponse = await fetch(binaryUrl);
|
|
56
|
+
if (!binaryResponse.ok) {
|
|
57
|
+
throw new Error(`Failed to fetch font ${binaryUrl}: ${String(binaryResponse.status)}`);
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
url: binaryUrl,
|
|
61
|
+
bytes: new Uint8Array(await binaryResponse.arrayBuffer()),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BUILT_IN_FONT_BODY,
|
|
3
|
+
BUILT_IN_FONT_BODY_BOLD_ITALIC,
|
|
4
|
+
BUILT_IN_FONT_BODY_ITALIC,
|
|
5
|
+
BUILT_IN_FONT_HEADING,
|
|
6
|
+
BUILT_IN_FONT_MONO,
|
|
7
|
+
BUILT_IN_FONT_MONO_BOLD,
|
|
8
|
+
} from './font-catalog';
|
|
9
|
+
|
|
10
|
+
export const UI_MISSING_FONT_COVERAGE_UNKNOWN = 0;
|
|
11
|
+
export const UI_MISSING_FONT_COVERAGE_ARABIC = 1;
|
|
12
|
+
export const UI_MISSING_FONT_COVERAGE_THAI = 2;
|
|
13
|
+
export const UI_MISSING_FONT_COVERAGE_CJK = 3;
|
|
14
|
+
export const UI_MISSING_FONT_COVERAGE_SUPPLEMENTAL = 4;
|
|
15
|
+
|
|
16
|
+
export interface ResolvedIncrementalFontShardRequest {
|
|
17
|
+
readonly packageId: string;
|
|
18
|
+
readonly coverageKind: number;
|
|
19
|
+
readonly familyKey: string;
|
|
20
|
+
readonly googleFamily: string;
|
|
21
|
+
readonly text: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const AUTO_EXTENDABLE_PRIMARY_FONT_IDS = new Set<number>([
|
|
25
|
+
BUILT_IN_FONT_BODY,
|
|
26
|
+
BUILT_IN_FONT_HEADING,
|
|
27
|
+
BUILT_IN_FONT_BODY_ITALIC,
|
|
28
|
+
BUILT_IN_FONT_BODY_BOLD_ITALIC,
|
|
29
|
+
BUILT_IN_FONT_MONO,
|
|
30
|
+
BUILT_IN_FONT_MONO_BOLD,
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
interface SupplementalFamilyDefinition {
|
|
34
|
+
readonly familyKey: string;
|
|
35
|
+
readonly googleFamily: string;
|
|
36
|
+
readonly ranges: readonly [number, number][];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SUPPLEMENTAL_FAMILIES: readonly SupplementalFamilyDefinition[] = [
|
|
40
|
+
{ familyKey: 'hebrew', googleFamily: 'Noto Sans Hebrew', ranges: [[0x0590, 0x05FF]] },
|
|
41
|
+
{ familyKey: 'armenian', googleFamily: 'Noto Sans Armenian', ranges: [[0x0530, 0x058F]] },
|
|
42
|
+
{ familyKey: 'georgian', googleFamily: 'Noto Sans Georgian', ranges: [[0x10A0, 0x10FF], [0x2D00, 0x2D2F]] },
|
|
43
|
+
{ familyKey: 'devanagari', googleFamily: 'Noto Sans Devanagari', ranges: [[0x0900, 0x097F]] },
|
|
44
|
+
{ familyKey: 'bengali', googleFamily: 'Noto Sans Bengali', ranges: [[0x0980, 0x09FF]] },
|
|
45
|
+
{ familyKey: 'gurmukhi', googleFamily: 'Noto Sans Gurmukhi', ranges: [[0x0A00, 0x0A7F]] },
|
|
46
|
+
{ familyKey: 'gujarati', googleFamily: 'Noto Sans Gujarati', ranges: [[0x0A80, 0x0AFF]] },
|
|
47
|
+
{ familyKey: 'oriya', googleFamily: 'Noto Sans Oriya', ranges: [[0x0B00, 0x0B7F]] },
|
|
48
|
+
{ familyKey: 'tamil', googleFamily: 'Noto Sans Tamil', ranges: [[0x0B80, 0x0BFF]] },
|
|
49
|
+
{ familyKey: 'telugu', googleFamily: 'Noto Sans Telugu', ranges: [[0x0C00, 0x0C7F]] },
|
|
50
|
+
{ familyKey: 'kannada', googleFamily: 'Noto Sans Kannada', ranges: [[0x0C80, 0x0CFF]] },
|
|
51
|
+
{ familyKey: 'malayalam', googleFamily: 'Noto Sans Malayalam', ranges: [[0x0D00, 0x0D7F]] },
|
|
52
|
+
{ familyKey: 'sinhala', googleFamily: 'Noto Sans Sinhala', ranges: [[0x0D80, 0x0DFF]] },
|
|
53
|
+
{ familyKey: 'lao', googleFamily: 'Noto Sans Lao', ranges: [[0x0E80, 0x0EFF]] },
|
|
54
|
+
{ familyKey: 'myanmar', googleFamily: 'Noto Sans Myanmar', ranges: [[0x1000, 0x109F]] },
|
|
55
|
+
{ familyKey: 'khmer', googleFamily: 'Noto Sans Khmer', ranges: [[0x1780, 0x17FF]] },
|
|
56
|
+
{ familyKey: 'ethiopic', googleFamily: 'Noto Sans Ethiopic', ranges: [[0x1200, 0x137F], [0x1380, 0x139F], [0x2D80, 0x2DDF]] },
|
|
57
|
+
] as const;
|
|
58
|
+
|
|
59
|
+
function inRanges(codePoint: number, ranges: readonly [number, number][]): boolean {
|
|
60
|
+
return ranges.some(([start, end]) => codePoint >= start && codePoint <= end);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function uniqueCodePoints(text: string): readonly string[] {
|
|
64
|
+
const seen = new Set<string>();
|
|
65
|
+
const ordered: string[] = [];
|
|
66
|
+
for (const character of text) {
|
|
67
|
+
if (seen.has(character)) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
seen.add(character);
|
|
71
|
+
ordered.push(character);
|
|
72
|
+
}
|
|
73
|
+
return ordered;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isCjkPunctuation(codePoint: number): boolean {
|
|
77
|
+
return (codePoint >= 0x3000 && codePoint <= 0x303F)
|
|
78
|
+
|| (codePoint >= 0xFF01 && codePoint <= 0xFF0F)
|
|
79
|
+
|| (codePoint >= 0xFF1A && codePoint <= 0xFF20)
|
|
80
|
+
|| (codePoint >= 0xFF3B && codePoint <= 0xFF40)
|
|
81
|
+
|| (codePoint >= 0xFF5B && codePoint <= 0xFF65);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function buildShardRequest(
|
|
85
|
+
packageId: string,
|
|
86
|
+
coverageKind: number,
|
|
87
|
+
familyKey: string,
|
|
88
|
+
googleFamily: string,
|
|
89
|
+
characters: readonly string[],
|
|
90
|
+
): ResolvedIncrementalFontShardRequest | null {
|
|
91
|
+
if (characters.length === 0) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const text = characters.join('');
|
|
95
|
+
return {
|
|
96
|
+
packageId,
|
|
97
|
+
coverageKind,
|
|
98
|
+
familyKey,
|
|
99
|
+
googleFamily,
|
|
100
|
+
text,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function resolveCjkRequests(sampleText: string): readonly ResolvedIncrementalFontShardRequest[] {
|
|
105
|
+
const codePoints = uniqueCodePoints(sampleText);
|
|
106
|
+
const kana: string[] = [];
|
|
107
|
+
const hangul: string[] = [];
|
|
108
|
+
const han: string[] = [];
|
|
109
|
+
const punctuation: string[] = [];
|
|
110
|
+
for (const character of codePoints) {
|
|
111
|
+
const codePoint = character.codePointAt(0);
|
|
112
|
+
if (codePoint === undefined) {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (isCjkPunctuation(codePoint)) {
|
|
116
|
+
punctuation.push(character);
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
if ((codePoint >= 0x3040 && codePoint <= 0x30FF) || (codePoint >= 0x31F0 && codePoint <= 0x31FF)) {
|
|
120
|
+
kana.push(character);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (codePoint >= 0xAC00 && codePoint <= 0xD7AF) {
|
|
124
|
+
hangul.push(character);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (
|
|
128
|
+
(codePoint >= 0x3400 && codePoint <= 0x4DBF)
|
|
129
|
+
|| (codePoint >= 0x4E00 && codePoint <= 0x9FFF)
|
|
130
|
+
|| (codePoint >= 0xF900 && codePoint <= 0xFAFF)
|
|
131
|
+
) {
|
|
132
|
+
han.push(character);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const requests: ResolvedIncrementalFontShardRequest[] = [];
|
|
137
|
+
if (kana.length > 0) {
|
|
138
|
+
requests.push(
|
|
139
|
+
buildShardRequest('cjk-sans', UI_MISSING_FONT_COVERAGE_CJK, 'cjk-jp', 'Noto Sans JP', [...kana, ...han, ...punctuation])!,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (hangul.length > 0) {
|
|
143
|
+
requests.push(
|
|
144
|
+
buildShardRequest('cjk-sans', UI_MISSING_FONT_COVERAGE_CJK, 'cjk-kr', 'Noto Sans KR', [...hangul, ...(kana.length === 0 ? han : []), ...punctuation])!,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (han.length > 0 && kana.length === 0 && hangul.length === 0) {
|
|
148
|
+
requests.push(
|
|
149
|
+
buildShardRequest('cjk-sans', UI_MISSING_FONT_COVERAGE_CJK, 'cjk-sc', 'Noto Sans SC', [...han, ...punctuation])!,
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
if (requests.length === 0 && punctuation.length > 0) {
|
|
153
|
+
requests.push(
|
|
154
|
+
buildShardRequest('cjk-sans', UI_MISSING_FONT_COVERAGE_CJK, 'cjk-sc', 'Noto Sans SC', punctuation)!,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return requests;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function resolveSupplementalRequests(sampleText: string): readonly ResolvedIncrementalFontShardRequest[] {
|
|
161
|
+
const codePoints = uniqueCodePoints(sampleText);
|
|
162
|
+
const textByFamily = new Map<string, string[]>();
|
|
163
|
+
for (const character of codePoints) {
|
|
164
|
+
const codePoint = character.codePointAt(0);
|
|
165
|
+
if (codePoint === undefined) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const family = SUPPLEMENTAL_FAMILIES.find((entry) => inRanges(codePoint, entry.ranges));
|
|
169
|
+
if (family === undefined) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const current = textByFamily.get(family.familyKey) ?? [];
|
|
173
|
+
current.push(character);
|
|
174
|
+
textByFamily.set(family.familyKey, current);
|
|
175
|
+
}
|
|
176
|
+
return SUPPLEMENTAL_FAMILIES
|
|
177
|
+
.map((family) => buildShardRequest(
|
|
178
|
+
'supplemental-sans',
|
|
179
|
+
UI_MISSING_FONT_COVERAGE_SUPPLEMENTAL,
|
|
180
|
+
family.familyKey,
|
|
181
|
+
family.googleFamily,
|
|
182
|
+
textByFamily.get(family.familyKey) ?? [],
|
|
183
|
+
))
|
|
184
|
+
.filter((request): request is ResolvedIncrementalFontShardRequest => request !== null);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function resolveIncrementalFontPackageRequests(
|
|
188
|
+
primaryFontId: number,
|
|
189
|
+
coverageKind: number,
|
|
190
|
+
sampleText: string,
|
|
191
|
+
): readonly ResolvedIncrementalFontShardRequest[] {
|
|
192
|
+
if (!AUTO_EXTENDABLE_PRIMARY_FONT_IDS.has(primaryFontId) || sampleText.length === 0) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
const uniqueText = uniqueCodePoints(sampleText).join('');
|
|
196
|
+
if (uniqueText.length === 0) {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
if (coverageKind === UI_MISSING_FONT_COVERAGE_ARABIC) {
|
|
200
|
+
return [
|
|
201
|
+
buildShardRequest('arabic-sans', coverageKind, 'arabic-core', 'Noto Naskh Arabic', Array.from(uniqueText))!,
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
if (coverageKind === UI_MISSING_FONT_COVERAGE_THAI) {
|
|
205
|
+
return [
|
|
206
|
+
buildShardRequest('thai-sans', coverageKind, 'thai-core', 'Noto Sans Thai', Array.from(uniqueText))!,
|
|
207
|
+
];
|
|
208
|
+
}
|
|
209
|
+
if (coverageKind === UI_MISSING_FONT_COVERAGE_CJK) {
|
|
210
|
+
return resolveCjkRequests(uniqueText);
|
|
211
|
+
}
|
|
212
|
+
if (coverageKind === UI_MISSING_FONT_COVERAGE_SUPPLEMENTAL) {
|
|
213
|
+
return resolveSupplementalRequests(uniqueText);
|
|
214
|
+
}
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { BridgeRuntime } from '../core-types';
|
|
2
|
+
import type { PreparedRuntimeAssets, BridgeInteractionState } from './local-types';
|
|
3
|
+
import { buildBackendLadder, createErrorWithCause, loadCoreModule, loadIcuData, loadUiModule, showIcuError } from './utils/assets';
|
|
4
|
+
import { initRenderer } from './utils/backends';
|
|
5
|
+
import { ensureCanvasLogicalSize, installEventHandlers } from './events';
|
|
6
|
+
import { getBridgeAssetUrl, STARTUP_BRIDGE_FONTS } from './font-catalog';
|
|
7
|
+
import { createBridgeRuntime } from './runtime';
|
|
8
|
+
import { installRenderLoop } from './render-loop';
|
|
9
|
+
|
|
10
|
+
export interface BridgeSession {
|
|
11
|
+
readonly runtime: BridgeRuntime;
|
|
12
|
+
destroy(): void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface BridgeSessionOptions {
|
|
16
|
+
readonly interactionState: BridgeInteractionState;
|
|
17
|
+
readonly preparedAssets: PreparedRuntimeAssets;
|
|
18
|
+
readonly runtimeRef: { current: BridgeRuntime | null };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function requireCanvas(id: string): HTMLCanvasElement {
|
|
22
|
+
const element = document.getElementById(id);
|
|
23
|
+
if (!(element instanceof HTMLCanvasElement)) {
|
|
24
|
+
throw new Error(`Expected #${id} canvas.`);
|
|
25
|
+
}
|
|
26
|
+
return element;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function createBridgeSession(options: BridgeSessionOptions): Promise<BridgeSession> {
|
|
30
|
+
const canvas = requireCanvas('fui-canvas');
|
|
31
|
+
ensureCanvasLogicalSize(canvas);
|
|
32
|
+
|
|
33
|
+
const { interactionState, preparedAssets, runtimeRef } = options;
|
|
34
|
+
window.__bridgeLoaderInfo = preparedAssets.loaderInfo;
|
|
35
|
+
const [core, ui] = await Promise.all([
|
|
36
|
+
loadCoreModule(preparedAssets.coreBundle, preparedAssets.coreWasm, canvas, preparedAssets.loaderInfo),
|
|
37
|
+
loadUiModule(preparedAssets.uiBundle, preparedAssets.uiWasm, preparedAssets.loaderInfo),
|
|
38
|
+
]);
|
|
39
|
+
const runtimeState = createBridgeRuntime(core, ui, canvas, interactionState, preparedAssets.loaderInfo);
|
|
40
|
+
const runtime = runtimeState.runtime;
|
|
41
|
+
runtimeRef.current = runtime;
|
|
42
|
+
|
|
43
|
+
const dpr = Math.max(1, window.devicePixelRatio || 1);
|
|
44
|
+
const fallbackLadder = buildBackendLadder(preparedAssets.loaderInfo.requestedRendererBackend);
|
|
45
|
+
await initRenderer(core, canvas, dpr, preparedAssets.loaderInfo, fallbackLadder);
|
|
46
|
+
ui._ui_reset();
|
|
47
|
+
try {
|
|
48
|
+
await loadIcuData(ui, preparedAssets);
|
|
49
|
+
} catch (error: unknown) {
|
|
50
|
+
const message = error instanceof Error ? error.message : 'Failed to load text engine.';
|
|
51
|
+
showIcuError(message);
|
|
52
|
+
throw createErrorWithCause(message, error);
|
|
53
|
+
}
|
|
54
|
+
runtime.updateCanvasSize();
|
|
55
|
+
const disposeEventHandlers = installEventHandlers(runtime, interactionState);
|
|
56
|
+
const disposeRenderLoop = installRenderLoop(runtime, preparedAssets.loaderInfo, fallbackLadder);
|
|
57
|
+
await Promise.all(
|
|
58
|
+
STARTUP_BRIDGE_FONTS.map((font) => runtime.registerFont({
|
|
59
|
+
id: font.id,
|
|
60
|
+
url: getBridgeAssetUrl(font.assetFile),
|
|
61
|
+
fallbackIds: font.fallbackIds,
|
|
62
|
+
})),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
delete window.__bridgeError;
|
|
66
|
+
return {
|
|
67
|
+
runtime,
|
|
68
|
+
destroy: () => {
|
|
69
|
+
disposeEventHandlers();
|
|
70
|
+
disposeRenderLoop();
|
|
71
|
+
runtimeState.destroy();
|
|
72
|
+
if (runtimeRef.current === runtime) {
|
|
73
|
+
runtimeRef.current = null;
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|