@ccheever/exact-renderer 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/package.json +118 -0
- package/src/__tests__/adapter-window-state.test.tsx +190 -0
- package/src/__tests__/attrs.test.ts +157 -0
- package/src/__tests__/classname.test.ts +332 -0
- package/src/__tests__/color.test.ts +169 -0
- package/src/__tests__/dom-mirror.test.ts +682 -0
- package/src/__tests__/dom-shim.test.ts +274 -0
- package/src/__tests__/fixtures/SvelteCounter.svelte +7 -0
- package/src/__tests__/fixtures/SvelteInput.svelte +8 -0
- package/src/__tests__/host-config.test.ts +51 -0
- package/src/__tests__/host-ops.test.ts +2234 -0
- package/src/__tests__/image-source.test.ts +135 -0
- package/src/__tests__/liquid-glass.test.ts +72 -0
- package/src/__tests__/multi-root.test.ts +118 -0
- package/src/__tests__/native-view-events.test.ts +102 -0
- package/src/__tests__/nodes.test.ts +399 -0
- package/src/__tests__/normalize.test.ts +576 -0
- package/src/__tests__/paragraph-lowering.test.tsx +144 -0
- package/src/__tests__/props.test.ts +518 -0
- package/src/__tests__/protocol-encoder.test.ts +732 -0
- package/src/__tests__/protocol-fixture-bytes.test.ts +41 -0
- package/src/__tests__/reconciler.test.tsx +241 -0
- package/src/__tests__/svelte-adapter.test.ts +166 -0
- package/src/__tests__/svg-source.test.ts +71 -0
- package/src/__tests__/tags.test.ts +354 -0
- package/src/__tests__/toggle.test.ts +441 -0
- package/src/__tests__/transitions.test.ts +106 -0
- package/src/__tests__/web-primitives.test.tsx +454 -0
- package/src/__tests__/window-hooks.test.tsx +447 -0
- package/src/adapter-contract.ts +68 -0
- package/src/attrs.ts +596 -0
- package/src/classname-contract.ts +87 -0
- package/src/classname-resolve.ts +553 -0
- package/src/classname-runtime.ts +29 -0
- package/src/components.ts +214 -0
- package/src/css-variable-context.ts +83 -0
- package/src/dom-hydration.ts +160 -0
- package/src/dom-mirror.ts +1459 -0
- package/src/dom-shim.ts +1736 -0
- package/src/group-context.ts +69 -0
- package/src/host-config.ts +431 -0
- package/src/host-ops.ts +3167 -0
- package/src/image-source.native.ts +703 -0
- package/src/image-source.ts +554 -0
- package/src/index.ts +278 -0
- package/src/inspector-runtime.ts +244 -0
- package/src/inspector.ts +3570 -0
- package/src/jsx-augmentations.ts +54 -0
- package/src/keyboard-avoidance.ts +217 -0
- package/src/native-primitives.ts +43 -0
- package/src/native-view-events.ts +322 -0
- package/src/native-view.ts +60 -0
- package/src/nodes/index.ts +41 -0
- package/src/nodes/node.ts +531 -0
- package/src/peer-context.ts +100 -0
- package/src/primitives.native.ts +8 -0
- package/src/primitives.ts +8 -0
- package/src/props/index.ts +14 -0
- package/src/props/normalize.ts +816 -0
- package/src/protocol/encoder.ts +940 -0
- package/src/protocol/index.ts +33 -0
- package/src/reconciler.ts +581 -0
- package/src/runtime.ts +11 -0
- package/src/safe-area.ts +543 -0
- package/src/solid.ts +490 -0
- package/src/style/color.js +1 -0
- package/src/style/color.ts +15 -0
- package/src/style/index.js +1 -0
- package/src/style/index.ts +22 -0
- package/src/style/normalize.js +1 -0
- package/src/style/normalize.ts +1426 -0
- package/src/svelte.ts +349 -0
- package/src/svg-source.ts +222 -0
- package/src/tags/index.ts +21 -0
- package/src/tags/tag-map.ts +289 -0
- package/src/text/paragraph-lowering.ts +310 -0
- package/src/types.ts +1175 -0
- package/src/vue.ts +535 -0
- package/src/web-host.ts +19 -0
- package/src/web-primitives.ts +1654 -0
package/src/svelte.ts
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exact Svelte Adapter
|
|
3
|
+
*
|
|
4
|
+
* RFC 0043 routes Svelte through the DOM shim rather than a framework-native
|
|
5
|
+
* host config. Modern Svelte still expects browser globals during runtime
|
|
6
|
+
* startup, so this adapter installs a narrow compatibility layer that points
|
|
7
|
+
* Svelte's `document`/`window` access at an Exact DOM handle.
|
|
8
|
+
*
|
|
9
|
+
* Important tradeoff:
|
|
10
|
+
* - The DOM shim itself is multi-root safe.
|
|
11
|
+
* - The Svelte runtime snapshots global DOM constructors on first client init.
|
|
12
|
+
* - To keep the adapter correct without forking Svelte internals, we allow one
|
|
13
|
+
* mounted Svelte root at a time. Additional roots can exist sequentially, but
|
|
14
|
+
* not concurrently.
|
|
15
|
+
*
|
|
16
|
+
* That is sufficient for the first-party Frameworks Lab and the common
|
|
17
|
+
* "single app root" Exact usage pattern, while keeping the adapter code small
|
|
18
|
+
* and understandable.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { Component, ComponentType } from 'svelte';
|
|
22
|
+
import {
|
|
23
|
+
flushSync,
|
|
24
|
+
mount as mountSvelteComponent,
|
|
25
|
+
settled,
|
|
26
|
+
tick,
|
|
27
|
+
unmount as unmountSvelteComponent,
|
|
28
|
+
} from 'svelte';
|
|
29
|
+
|
|
30
|
+
import { _clearHandlers } from './host-ops.js';
|
|
31
|
+
import {
|
|
32
|
+
ExactComment,
|
|
33
|
+
ExactDocument,
|
|
34
|
+
ExactDocumentFragment,
|
|
35
|
+
ExactElement,
|
|
36
|
+
ExactNodeBase,
|
|
37
|
+
ExactRootBodyElement,
|
|
38
|
+
ExactText,
|
|
39
|
+
ExactWindow,
|
|
40
|
+
createExactDOM,
|
|
41
|
+
type ExactDOMHandle,
|
|
42
|
+
} from './dom-shim.js';
|
|
43
|
+
|
|
44
|
+
type SvelteComponentLike<Props extends Record<string, any>, Exports extends Record<string, any>> =
|
|
45
|
+
| ComponentType<any>
|
|
46
|
+
| Component<Props, Exports, any>;
|
|
47
|
+
|
|
48
|
+
interface ExactSvelteRootState {
|
|
49
|
+
readonly rootId: number;
|
|
50
|
+
dom: ExactDOMHandle;
|
|
51
|
+
instance: Record<string, any> | null;
|
|
52
|
+
claimed: boolean;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ExactSvelteMountOptions<Props extends Record<string, any> = Record<string, any>> {
|
|
56
|
+
readonly rootId?: number;
|
|
57
|
+
readonly target?: ExactRootBodyElement | ExactElement | ExactDocument | ExactDocumentFragment;
|
|
58
|
+
readonly anchor?: ExactNodeBase | null;
|
|
59
|
+
readonly props?: Props;
|
|
60
|
+
readonly events?: Record<string, (event: unknown) => unknown>;
|
|
61
|
+
readonly context?: Map<any, any>;
|
|
62
|
+
readonly intro?: boolean;
|
|
63
|
+
readonly transformError?: (error: unknown) => unknown | Promise<unknown>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ExactSvelteRoot {
|
|
67
|
+
readonly rootId: number;
|
|
68
|
+
render<Props extends Record<string, any>, Exports extends Record<string, any>>(
|
|
69
|
+
component: SvelteComponentLike<Props, Exports>,
|
|
70
|
+
options?: Omit<ExactSvelteMountOptions<Props>, 'rootId'>,
|
|
71
|
+
): Exports;
|
|
72
|
+
unmount(): Promise<void>;
|
|
73
|
+
destroy(): Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const rootStates = new Map<number, ExactSvelteRootState>();
|
|
77
|
+
|
|
78
|
+
let activeMountedRootId: number | null = null;
|
|
79
|
+
let compatGlobalsInstalled = false;
|
|
80
|
+
|
|
81
|
+
const hostDocument =
|
|
82
|
+
typeof globalThis.document === 'object' && globalThis.document !== null
|
|
83
|
+
? globalThis.document
|
|
84
|
+
: undefined;
|
|
85
|
+
const hostWindow =
|
|
86
|
+
typeof globalThis.window === 'object' && globalThis.window !== null
|
|
87
|
+
? globalThis.window
|
|
88
|
+
: undefined;
|
|
89
|
+
const hostNavigator =
|
|
90
|
+
typeof globalThis.navigator === 'object' && globalThis.navigator !== null
|
|
91
|
+
? globalThis.navigator
|
|
92
|
+
: undefined;
|
|
93
|
+
|
|
94
|
+
function getRootState(rootId: number): ExactSvelteRootState {
|
|
95
|
+
let state = rootStates.get(rootId);
|
|
96
|
+
if (!state) {
|
|
97
|
+
state = {
|
|
98
|
+
rootId,
|
|
99
|
+
dom: createExactDOM({ rootId }),
|
|
100
|
+
instance: null,
|
|
101
|
+
claimed: false,
|
|
102
|
+
};
|
|
103
|
+
rootStates.set(rootId, state);
|
|
104
|
+
}
|
|
105
|
+
return state;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function getActiveMountedState(): ExactSvelteRootState | null {
|
|
109
|
+
if (activeMountedRootId === null) {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
return rootStates.get(activeMountedRootId) ?? null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resolveCurrentDocument(): ExactDocument | Document {
|
|
116
|
+
const active = getActiveMountedState();
|
|
117
|
+
if (active) {
|
|
118
|
+
return active.dom.document;
|
|
119
|
+
}
|
|
120
|
+
if (hostDocument) {
|
|
121
|
+
return hostDocument;
|
|
122
|
+
}
|
|
123
|
+
throw new Error('[ExactSvelte] No active Exact DOM document is available.');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function resolveCurrentWindow(): ExactWindow | Window {
|
|
127
|
+
const active = getActiveMountedState();
|
|
128
|
+
if (active) {
|
|
129
|
+
return active.dom.window;
|
|
130
|
+
}
|
|
131
|
+
if (hostWindow) {
|
|
132
|
+
return hostWindow;
|
|
133
|
+
}
|
|
134
|
+
throw new Error('[ExactSvelte] No active Exact DOM window is available.');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const documentProxy = new Proxy({} as ExactDocument, {
|
|
138
|
+
get(_target, prop) {
|
|
139
|
+
const document = resolveCurrentDocument() as unknown as Record<PropertyKey, unknown>;
|
|
140
|
+
const value = document[prop];
|
|
141
|
+
return typeof value === 'function' ? value.bind(document) : value;
|
|
142
|
+
},
|
|
143
|
+
set(_target, prop, value) {
|
|
144
|
+
(resolveCurrentDocument() as unknown as Record<PropertyKey, unknown>)[prop] = value;
|
|
145
|
+
return true;
|
|
146
|
+
},
|
|
147
|
+
has(_target, prop) {
|
|
148
|
+
return prop in (resolveCurrentDocument() as unknown as Record<PropertyKey, unknown>);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const windowProxy = new Proxy({} as ExactWindow, {
|
|
153
|
+
get(_target, prop) {
|
|
154
|
+
const window = resolveCurrentWindow() as unknown as Record<PropertyKey, unknown>;
|
|
155
|
+
const value = window[prop];
|
|
156
|
+
return typeof value === 'function' ? value.bind(window) : value;
|
|
157
|
+
},
|
|
158
|
+
set(_target, prop, value) {
|
|
159
|
+
(resolveCurrentWindow() as unknown as Record<PropertyKey, unknown>)[prop] = value;
|
|
160
|
+
return true;
|
|
161
|
+
},
|
|
162
|
+
has(_target, prop) {
|
|
163
|
+
return prop in (resolveCurrentWindow() as unknown as Record<PropertyKey, unknown>);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
function defineCompatGlobal(name: string, value: unknown): void {
|
|
168
|
+
const descriptor = Object.getOwnPropertyDescriptor(globalThis, name);
|
|
169
|
+
if (descriptor?.configurable === false) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
Object.defineProperty(globalThis, name, {
|
|
174
|
+
value,
|
|
175
|
+
writable: true,
|
|
176
|
+
configurable: true,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function ensureCompatGlobals(): void {
|
|
181
|
+
if (compatGlobalsInstalled) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// The adapter is meant for Exact's non-browser runtime. Overwriting a live
|
|
186
|
+
// browser DOM would make the host page incoherent, so fail loudly there.
|
|
187
|
+
if (hostDocument && hostDocument !== (documentProxy as unknown as Document)) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
'[ExactSvelte] The Exact Svelte adapter cannot replace an existing browser DOM. Use it from the Exact runtime, not inside a live web page.',
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
defineCompatGlobal('window', windowProxy);
|
|
194
|
+
defineCompatGlobal('document', documentProxy);
|
|
195
|
+
defineCompatGlobal('navigator', hostNavigator ?? { userAgent: 'ExactSvelte/1.0' });
|
|
196
|
+
|
|
197
|
+
// Svelte snapshots these constructor prototypes during client runtime init.
|
|
198
|
+
// They only need to describe the shim node surface, not a full browser DOM.
|
|
199
|
+
defineCompatGlobal('Node', ExactNodeBase);
|
|
200
|
+
defineCompatGlobal('Element', ExactElement);
|
|
201
|
+
defineCompatGlobal('Text', ExactText);
|
|
202
|
+
defineCompatGlobal('Comment', ExactComment);
|
|
203
|
+
defineCompatGlobal('Document', ExactDocument);
|
|
204
|
+
defineCompatGlobal('DocumentFragment', ExactDocumentFragment);
|
|
205
|
+
defineCompatGlobal('ShadowRoot', ExactDocumentFragment);
|
|
206
|
+
defineCompatGlobal('HTMLMediaElement', class HTMLMediaElementStub {});
|
|
207
|
+
|
|
208
|
+
compatGlobalsInstalled = true;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function activateMountedRoot(state: ExactSvelteRootState): void {
|
|
212
|
+
const active = getActiveMountedState();
|
|
213
|
+
if (active && active.rootId !== state.rootId && active.instance !== null) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
'[ExactSvelte] Only one mounted Svelte root is supported at a time because the Svelte client runtime captures global DOM state during initialization.',
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
activeMountedRootId = state.rootId;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function destroyRootState(state: ExactSvelteRootState): Promise<void> {
|
|
223
|
+
if (state.instance) {
|
|
224
|
+
await unmountMountedRoot(state);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
state.dom.destroy();
|
|
228
|
+
rootStates.delete(state.rootId);
|
|
229
|
+
|
|
230
|
+
if (activeMountedRootId === state.rootId) {
|
|
231
|
+
activeMountedRootId = null;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function unmountMountedRoot(state: ExactSvelteRootState): Promise<void> {
|
|
236
|
+
if (!state.instance) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
activateMountedRoot(state);
|
|
241
|
+
|
|
242
|
+
// `unmount()` returns a promise, but without outros the DOM removal happens
|
|
243
|
+
// synchronously. `flushSync()` keeps the Svelte runtime and the DOM shim in a
|
|
244
|
+
// deterministic state before we continue.
|
|
245
|
+
const unmountPromise = unmountSvelteComponent(state.instance);
|
|
246
|
+
flushSync();
|
|
247
|
+
await unmountPromise;
|
|
248
|
+
state.instance = null;
|
|
249
|
+
|
|
250
|
+
if (activeMountedRootId === state.rootId) {
|
|
251
|
+
activeMountedRootId = null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function mountIntoRoot<Props extends Record<string, any>, Exports extends Record<string, any>>(
|
|
256
|
+
state: ExactSvelteRootState,
|
|
257
|
+
component: SvelteComponentLike<Props, Exports>,
|
|
258
|
+
options: Omit<ExactSvelteMountOptions<Props>, 'rootId'> = {},
|
|
259
|
+
): Exports {
|
|
260
|
+
ensureCompatGlobals();
|
|
261
|
+
activateMountedRoot(state);
|
|
262
|
+
|
|
263
|
+
if (state.instance) {
|
|
264
|
+
// Rendering over an existing tree mirrors the React/Solid/Vue adapter
|
|
265
|
+
// ergonomics. We remove the previous component first and reuse the Exact
|
|
266
|
+
// root surface underneath it.
|
|
267
|
+
void unmountSvelteComponent(state.instance);
|
|
268
|
+
state.instance = null;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const instance = mountSvelteComponent(component as any, {
|
|
272
|
+
target: (options.target ?? state.dom.document.body) as any,
|
|
273
|
+
anchor: (options.anchor ?? undefined) as any,
|
|
274
|
+
props: options.props,
|
|
275
|
+
events: options.events,
|
|
276
|
+
context: options.context,
|
|
277
|
+
intro: options.intro,
|
|
278
|
+
transformError: options.transformError,
|
|
279
|
+
} as any);
|
|
280
|
+
|
|
281
|
+
state.instance = instance as Record<string, any>;
|
|
282
|
+
flushSync();
|
|
283
|
+
|
|
284
|
+
return instance as Exports;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
export function mount<Props extends Record<string, any>, Exports extends Record<string, any>>(
|
|
288
|
+
component: SvelteComponentLike<Props, Exports>,
|
|
289
|
+
options: ExactSvelteMountOptions<Props> = {},
|
|
290
|
+
): Exports {
|
|
291
|
+
const state = getRootState(options.rootId ?? 0);
|
|
292
|
+
return mountIntoRoot(state, component, options);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function render<Props extends Record<string, any>, Exports extends Record<string, any>>(
|
|
296
|
+
component: SvelteComponentLike<Props, Exports>,
|
|
297
|
+
options: Omit<ExactSvelteMountOptions<Props>, 'rootId'> = {},
|
|
298
|
+
): () => Promise<void> {
|
|
299
|
+
const state = getRootState(0);
|
|
300
|
+
mountIntoRoot(state, component, options);
|
|
301
|
+
return () => unmountMountedRoot(state);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export async function unmount(): Promise<void> {
|
|
305
|
+
const state = rootStates.get(0);
|
|
306
|
+
if (!state) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
await unmountMountedRoot(state);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export function createExactRoot(rootId: number): ExactSvelteRoot {
|
|
313
|
+
if (rootId === 0) {
|
|
314
|
+
throw new Error('createExactRoot: rootId must be > 0. Use render() for the default root.');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const state = getRootState(rootId);
|
|
318
|
+
if (state.claimed) {
|
|
319
|
+
throw new Error(`createExactRoot: rootId ${rootId} is already in use.`);
|
|
320
|
+
}
|
|
321
|
+
state.claimed = true;
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
rootId,
|
|
325
|
+
render(component, options) {
|
|
326
|
+
return mountIntoRoot(state, component, options);
|
|
327
|
+
},
|
|
328
|
+
async unmount() {
|
|
329
|
+
await unmountMountedRoot(state);
|
|
330
|
+
},
|
|
331
|
+
async destroy() {
|
|
332
|
+
state.claimed = false;
|
|
333
|
+
await destroyRootState(state);
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
export const createSvelteRoot = createExactRoot;
|
|
339
|
+
|
|
340
|
+
export async function reset(): Promise<void> {
|
|
341
|
+
for (const state of [...rootStates.values()]) {
|
|
342
|
+
await destroyRootState(state);
|
|
343
|
+
}
|
|
344
|
+
_clearHandlers();
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Re-export the small subset of the Svelte runtime that Exact app code commonly
|
|
348
|
+
// wants from the adapter entrypoint during demos and tests.
|
|
349
|
+
export { flushSync, settled, tick };
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { isAssetRef } from '@exact/core/assets-fonts-state';
|
|
2
|
+
|
|
3
|
+
import type { ImageCandidate, ImageSource, SvgSource } from './types.js';
|
|
4
|
+
|
|
5
|
+
export interface ResolvedSvgRequest {
|
|
6
|
+
readonly headers?: Record<string, string>;
|
|
7
|
+
readonly markup?: string;
|
|
8
|
+
readonly uri?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isInlineSvgSource(source: SvgSource | undefined): boolean {
|
|
12
|
+
return typeof source === 'string' &&
|
|
13
|
+
(source.trimStart().startsWith('<') || source.trimStart().startsWith('<?xml'));
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function resolveSvgSourceForDOM(source: SvgSource | undefined): string | undefined {
|
|
17
|
+
if (source == null) {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof source === 'string' && source.length > 0) {
|
|
22
|
+
return source;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (isAssetRef(source)) {
|
|
26
|
+
return source.url;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (typeof source === 'object' && source !== null && typeof source.uri === 'string' && source.uri.length > 0) {
|
|
30
|
+
return source.uri;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve an SVG source to a string the native image pipeline can load.
|
|
38
|
+
*
|
|
39
|
+
* Native renderers (the AppKit/UIKit presenters' `applyNativeImage`) load a
|
|
40
|
+
* glyph from a named asset, a file/remote URL, or a `data:` URI — they cannot
|
|
41
|
+
* consume a raw inline `<svg>` markup string the way the DOM mirror can (which
|
|
42
|
+
* parses the markup into a live `<svg>` host). Inline markup is therefore
|
|
43
|
+
* wrapped as a percent-encoded `data:image/svg+xml` URI: `encodeURIComponent`
|
|
44
|
+
* (not base64) keeps the transform dependency-free on Hermes and the payload
|
|
45
|
+
* inspectable. URL / asset / already-`data:` sources pass through unchanged so
|
|
46
|
+
* `NSImage`/`UIImage` can fetch them directly.
|
|
47
|
+
*/
|
|
48
|
+
export function resolveSvgSourceForNative(source: SvgSource | undefined): string | undefined {
|
|
49
|
+
const resolved = resolveSvgSourceForDOM(source);
|
|
50
|
+
if (resolved == null || resolved.length === 0) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (isInlineSvgSource(resolved)) {
|
|
55
|
+
return `data:image/svg+xml,${encodeURIComponent(resolved)}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return resolved;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function resolveSvgSourceForFetch(source: SvgSource | undefined): ResolvedSvgRequest | undefined {
|
|
62
|
+
if (source == null) {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof source === 'string' && isInlineSvgSource(source)) {
|
|
67
|
+
return { markup: source };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (typeof source === 'string') {
|
|
71
|
+
const decodedMarkup = decodeSvgDataUrl(source);
|
|
72
|
+
if (decodedMarkup !== undefined) {
|
|
73
|
+
return { markup: decodedMarkup };
|
|
74
|
+
}
|
|
75
|
+
return source !== '' ? { uri: source } : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (isAssetRef(source)) {
|
|
79
|
+
return { uri: source.url };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (typeof source === 'object' && source !== null && typeof source.uri === 'string' && source.uri.length > 0) {
|
|
83
|
+
const decodedMarkup = decodeSvgDataUrl(source.uri);
|
|
84
|
+
if (decodedMarkup !== undefined) {
|
|
85
|
+
return { markup: decodedMarkup };
|
|
86
|
+
}
|
|
87
|
+
return { uri: source.uri, headers: source.headers };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function coerceImageSourceToSvgSource(source: ImageSource | undefined): SvgSource | undefined {
|
|
94
|
+
if (source == null || typeof source === 'number') {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (typeof source === 'string') {
|
|
99
|
+
return isInlineSvgSource(source) || isSvgLikeUri(source) ? source : undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (isAssetRef(source)) {
|
|
103
|
+
return isSvgLikeUri(source.url) ? source : undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (isImageCandidateArraySource(source)) {
|
|
107
|
+
const candidate = source.find((entry) => isSvgImageCandidate(entry));
|
|
108
|
+
if (!candidate) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
uri: candidate.uri,
|
|
113
|
+
headers: candidate.headers,
|
|
114
|
+
cacheKey: candidate.cacheKey,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (typeof source === 'object' && source !== null && 'thumbhash' in source) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (isSvgUriSourceLike(source)) {
|
|
123
|
+
return isSvgLikeUri(source.uri) ? source : undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function filterSvgColors(
|
|
130
|
+
colors: Record<string, string> | undefined,
|
|
131
|
+
): Record<string, string> | undefined {
|
|
132
|
+
if (colors == null) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const filtered = Object.fromEntries(
|
|
137
|
+
Object.entries(colors).filter(([key, value]) => key.startsWith('--') && value.length > 0),
|
|
138
|
+
);
|
|
139
|
+
|
|
140
|
+
return Object.keys(filtered).length > 0 ? filtered : undefined;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getDefaultSvgPixelDensity(): number {
|
|
144
|
+
const devicePixelRatio = (
|
|
145
|
+
globalThis as typeof globalThis & { devicePixelRatio?: unknown }
|
|
146
|
+
).devicePixelRatio;
|
|
147
|
+
if (typeof devicePixelRatio === 'number' && isFinite(devicePixelRatio) && devicePixelRatio > 0) {
|
|
148
|
+
return devicePixelRatio;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const exact = (globalThis as { exact?: { screenScale?: unknown } }).exact;
|
|
152
|
+
if (typeof exact?.screenScale === 'number' && isFinite(exact.screenScale) && exact.screenScale > 0) {
|
|
153
|
+
return exact.screenScale;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function decodeSvgDataUrl(input: string): string | undefined {
|
|
160
|
+
if (!input.startsWith('data:image/svg+xml')) {
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const commaIndex = input.indexOf(',');
|
|
165
|
+
if (commaIndex === -1) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const metadata = input.slice(0, commaIndex).toLowerCase();
|
|
170
|
+
const payload = input.slice(commaIndex + 1);
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
if (metadata.includes(';base64')) {
|
|
174
|
+
if (typeof Buffer !== 'undefined') {
|
|
175
|
+
return Buffer.from(payload, 'base64').toString('utf8');
|
|
176
|
+
}
|
|
177
|
+
if (typeof Uint8Array !== 'undefined' && typeof TextDecoder !== 'undefined') {
|
|
178
|
+
const binary = atob(payload);
|
|
179
|
+
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
180
|
+
return new TextDecoder().decode(bytes);
|
|
181
|
+
}
|
|
182
|
+
return atob(payload);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return decodeURIComponent(payload);
|
|
186
|
+
} catch {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function isSvgLikeUri(uri: string): boolean {
|
|
192
|
+
const normalized = uri.trim();
|
|
193
|
+
if (normalized.length === 0) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (normalized.startsWith('data:image/svg+xml')) {
|
|
198
|
+
return true;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const withoutHash = normalized.split('#', 1)[0] ?? normalized;
|
|
202
|
+
const withoutQuery = withoutHash.split('?', 1)[0] ?? withoutHash;
|
|
203
|
+
return /\.svgz?$/i.test(withoutQuery);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isSvgImageCandidate(candidate: ImageCandidate): boolean {
|
|
207
|
+
return isSvgLikeUri(candidate.uri) || normalizeMimeType(candidate.type) === 'image/svg+xml';
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function isImageCandidateArraySource(source: ImageSource): source is readonly ImageCandidate[] {
|
|
211
|
+
return Array.isArray(source);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isSvgUriSourceLike(
|
|
215
|
+
source: Exclude<ImageSource, string | number | readonly ImageCandidate[]>,
|
|
216
|
+
): source is { uri: string; headers?: Record<string, string>; cacheKey?: string } {
|
|
217
|
+
return typeof source === 'object' && source !== null && 'uri' in source && typeof source.uri === 'string';
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizeMimeType(type: string | undefined): string {
|
|
221
|
+
return type?.split(';', 1)[0]?.trim().toLowerCase() ?? '';
|
|
222
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag Mapping - Public Exports
|
|
3
|
+
*
|
|
4
|
+
* This module re-exports the tag mapping types and functions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
export {
|
|
8
|
+
type NativeViewTagMetadata,
|
|
9
|
+
type NativeViewTagSelection,
|
|
10
|
+
type TagConfig,
|
|
11
|
+
getTagConfig,
|
|
12
|
+
isValidTag,
|
|
13
|
+
isWebTag,
|
|
14
|
+
isRNTag,
|
|
15
|
+
getWebTags,
|
|
16
|
+
getRNTags,
|
|
17
|
+
getAllTags,
|
|
18
|
+
registerNativeViewTag,
|
|
19
|
+
defaultTagConfig,
|
|
20
|
+
_clearNativeViewTagsForTesting,
|
|
21
|
+
} from './tag-map.js';
|