@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.
Files changed (80) hide show
  1. package/package.json +118 -0
  2. package/src/__tests__/adapter-window-state.test.tsx +190 -0
  3. package/src/__tests__/attrs.test.ts +157 -0
  4. package/src/__tests__/classname.test.ts +332 -0
  5. package/src/__tests__/color.test.ts +169 -0
  6. package/src/__tests__/dom-mirror.test.ts +682 -0
  7. package/src/__tests__/dom-shim.test.ts +274 -0
  8. package/src/__tests__/fixtures/SvelteCounter.svelte +7 -0
  9. package/src/__tests__/fixtures/SvelteInput.svelte +8 -0
  10. package/src/__tests__/host-config.test.ts +51 -0
  11. package/src/__tests__/host-ops.test.ts +2234 -0
  12. package/src/__tests__/image-source.test.ts +135 -0
  13. package/src/__tests__/liquid-glass.test.ts +72 -0
  14. package/src/__tests__/multi-root.test.ts +118 -0
  15. package/src/__tests__/native-view-events.test.ts +102 -0
  16. package/src/__tests__/nodes.test.ts +399 -0
  17. package/src/__tests__/normalize.test.ts +576 -0
  18. package/src/__tests__/paragraph-lowering.test.tsx +144 -0
  19. package/src/__tests__/props.test.ts +518 -0
  20. package/src/__tests__/protocol-encoder.test.ts +732 -0
  21. package/src/__tests__/protocol-fixture-bytes.test.ts +41 -0
  22. package/src/__tests__/reconciler.test.tsx +241 -0
  23. package/src/__tests__/svelte-adapter.test.ts +166 -0
  24. package/src/__tests__/svg-source.test.ts +71 -0
  25. package/src/__tests__/tags.test.ts +354 -0
  26. package/src/__tests__/toggle.test.ts +441 -0
  27. package/src/__tests__/transitions.test.ts +106 -0
  28. package/src/__tests__/web-primitives.test.tsx +454 -0
  29. package/src/__tests__/window-hooks.test.tsx +447 -0
  30. package/src/adapter-contract.ts +68 -0
  31. package/src/attrs.ts +596 -0
  32. package/src/classname-contract.ts +87 -0
  33. package/src/classname-resolve.ts +553 -0
  34. package/src/classname-runtime.ts +29 -0
  35. package/src/components.ts +214 -0
  36. package/src/css-variable-context.ts +83 -0
  37. package/src/dom-hydration.ts +160 -0
  38. package/src/dom-mirror.ts +1459 -0
  39. package/src/dom-shim.ts +1736 -0
  40. package/src/group-context.ts +69 -0
  41. package/src/host-config.ts +431 -0
  42. package/src/host-ops.ts +3167 -0
  43. package/src/image-source.native.ts +703 -0
  44. package/src/image-source.ts +554 -0
  45. package/src/index.ts +278 -0
  46. package/src/inspector-runtime.ts +244 -0
  47. package/src/inspector.ts +3570 -0
  48. package/src/jsx-augmentations.ts +54 -0
  49. package/src/keyboard-avoidance.ts +217 -0
  50. package/src/native-primitives.ts +43 -0
  51. package/src/native-view-events.ts +322 -0
  52. package/src/native-view.ts +60 -0
  53. package/src/nodes/index.ts +41 -0
  54. package/src/nodes/node.ts +531 -0
  55. package/src/peer-context.ts +100 -0
  56. package/src/primitives.native.ts +8 -0
  57. package/src/primitives.ts +8 -0
  58. package/src/props/index.ts +14 -0
  59. package/src/props/normalize.ts +816 -0
  60. package/src/protocol/encoder.ts +940 -0
  61. package/src/protocol/index.ts +33 -0
  62. package/src/reconciler.ts +581 -0
  63. package/src/runtime.ts +11 -0
  64. package/src/safe-area.ts +543 -0
  65. package/src/solid.ts +490 -0
  66. package/src/style/color.js +1 -0
  67. package/src/style/color.ts +15 -0
  68. package/src/style/index.js +1 -0
  69. package/src/style/index.ts +22 -0
  70. package/src/style/normalize.js +1 -0
  71. package/src/style/normalize.ts +1426 -0
  72. package/src/svelte.ts +349 -0
  73. package/src/svg-source.ts +222 -0
  74. package/src/tags/index.ts +21 -0
  75. package/src/tags/tag-map.ts +289 -0
  76. package/src/text/paragraph-lowering.ts +310 -0
  77. package/src/types.ts +1175 -0
  78. package/src/vue.ts +535 -0
  79. package/src/web-host.ts +19 -0
  80. 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';