@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
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tag Mapping
|
|
3
|
+
*
|
|
4
|
+
* This module defines how user-facing tags (both web and React Native)
|
|
5
|
+
* map to canonical internal types and their default properties.
|
|
6
|
+
*
|
|
7
|
+
* Design Principles:
|
|
8
|
+
* - Web semantics are primary (flexDirection defaults to 'row')
|
|
9
|
+
* - RN tags are aliases that use RN defaults (flexDirection defaults to 'column')
|
|
10
|
+
* - All tags resolve to a small set of canonical types
|
|
11
|
+
*
|
|
12
|
+
* Canonical Types:
|
|
13
|
+
* - view: Generic container (maps to View node type)
|
|
14
|
+
* - text: Text content (maps to Text node type)
|
|
15
|
+
* - image: Image display (maps to Image node type)
|
|
16
|
+
* - svg: SVG display (maps to Svg node type)
|
|
17
|
+
* - scroll: Scrollable container (maps to ScrollView node type)
|
|
18
|
+
* - list: Host-owned virtual list container (maps to List node type)
|
|
19
|
+
* - input: Text input (maps to TextInput node type)
|
|
20
|
+
* - pressable: Touchable wrapper (RN Pressable maps to Pressable; web tags
|
|
21
|
+
* render as View with event bindings)
|
|
22
|
+
* - nativeView: Module-backed native view, including first-party controls
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type {
|
|
26
|
+
CanonicalTagType,
|
|
27
|
+
FlexDirection,
|
|
28
|
+
NativeViewSelectionGesturePolicy,
|
|
29
|
+
NativeViewSelectionTier,
|
|
30
|
+
} from '../types.js';
|
|
31
|
+
import { NodeType } from '@exact/core/protocol/opcodes';
|
|
32
|
+
|
|
33
|
+
export interface NativeViewTagSelection {
|
|
34
|
+
readonly tier: NativeViewSelectionTier;
|
|
35
|
+
readonly gesturePolicy?: NativeViewSelectionGesturePolicy;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface NativeViewTagMetadata {
|
|
39
|
+
readonly moduleName: string;
|
|
40
|
+
readonly propKeys: readonly string[];
|
|
41
|
+
readonly selection?: NativeViewTagSelection;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Tag configuration specifying how a tag behaves.
|
|
46
|
+
*/
|
|
47
|
+
export interface TagConfig {
|
|
48
|
+
/** The canonical internal type */
|
|
49
|
+
readonly canonicalType: CanonicalTagType;
|
|
50
|
+
|
|
51
|
+
/** The protocol NodeType for creating views */
|
|
52
|
+
readonly nodeType: NodeType;
|
|
53
|
+
|
|
54
|
+
/** Whether this tag uses RN semantics */
|
|
55
|
+
readonly isRNStyle: boolean;
|
|
56
|
+
|
|
57
|
+
/** Default flex direction for this tag */
|
|
58
|
+
readonly defaultFlexDirection: FlexDirection;
|
|
59
|
+
|
|
60
|
+
/** Whether this tag is a text container (allows raw text children) */
|
|
61
|
+
readonly isTextContainer: boolean;
|
|
62
|
+
|
|
63
|
+
/** Whether this tag supports press events */
|
|
64
|
+
readonly supportsPressEvents: boolean;
|
|
65
|
+
|
|
66
|
+
/** Whether this tag supports change events (for toggles, inputs) */
|
|
67
|
+
readonly supportsChangeEvents?: boolean;
|
|
68
|
+
|
|
69
|
+
/** Whether this tag supports transition lifecycle events */
|
|
70
|
+
readonly supportsTransitionEvents?: boolean;
|
|
71
|
+
|
|
72
|
+
/** Native-view metadata for dynamically registered module-backed views. */
|
|
73
|
+
readonly nativeView?: NativeViewTagMetadata;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface TagConfigOptions {
|
|
77
|
+
isRNStyle?: boolean;
|
|
78
|
+
supportsPressEvents?: boolean;
|
|
79
|
+
supportsChangeEvents?: boolean;
|
|
80
|
+
nativeView?: NativeViewTagMetadata;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createTagConfig(
|
|
84
|
+
canonicalType: CanonicalTagType,
|
|
85
|
+
nodeType: NodeType,
|
|
86
|
+
options: TagConfigOptions = {},
|
|
87
|
+
): TagConfig {
|
|
88
|
+
const isRNStyle = options.isRNStyle === true;
|
|
89
|
+
return {
|
|
90
|
+
canonicalType,
|
|
91
|
+
nodeType,
|
|
92
|
+
isRNStyle,
|
|
93
|
+
defaultFlexDirection: isRNStyle ? 'column' : 'row',
|
|
94
|
+
isTextContainer: canonicalType === 'text',
|
|
95
|
+
supportsPressEvents: options.supportsPressEvents === true,
|
|
96
|
+
...(options.supportsChangeEvents === true ? { supportsChangeEvents: true } : {}),
|
|
97
|
+
supportsTransitionEvents: true,
|
|
98
|
+
...(options.nativeView ? { nativeView: options.nativeView } : {}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function tagEntries(
|
|
103
|
+
tags: readonly string[],
|
|
104
|
+
canonicalType: CanonicalTagType,
|
|
105
|
+
nodeType: NodeType,
|
|
106
|
+
options: TagConfigOptions = {},
|
|
107
|
+
): Array<[string, TagConfig]> {
|
|
108
|
+
return tags.map((tag) => [
|
|
109
|
+
tag,
|
|
110
|
+
createTagConfig(canonicalType, nodeType, options),
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const WEB_VIEW_TAGS = [
|
|
115
|
+
'div',
|
|
116
|
+
'span',
|
|
117
|
+
'section',
|
|
118
|
+
'main',
|
|
119
|
+
'article',
|
|
120
|
+
'nav',
|
|
121
|
+
'aside',
|
|
122
|
+
'header',
|
|
123
|
+
'footer',
|
|
124
|
+
'form',
|
|
125
|
+
'label',
|
|
126
|
+
] as const;
|
|
127
|
+
const WEB_TEXT_TAGS = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'text'] as const;
|
|
128
|
+
const TOGGLE_NATIVE_VIEW: NativeViewTagMetadata = {
|
|
129
|
+
moduleName: 'exact.toggle',
|
|
130
|
+
propKeys: ['value', 'tintColor', 'disabled', 'glassEffect', 'accessibilityLabel'],
|
|
131
|
+
selection: {
|
|
132
|
+
tier: 'delegated',
|
|
133
|
+
gesturePolicy: 'internal',
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Web tag configurations.
|
|
139
|
+
* These use CSS defaults: flexDirection = 'row'.
|
|
140
|
+
*/
|
|
141
|
+
const webTags: Record<string, TagConfig> = Object.fromEntries([
|
|
142
|
+
...tagEntries(WEB_VIEW_TAGS, 'view', NodeType.View),
|
|
143
|
+
...tagEntries(WEB_TEXT_TAGS, 'text', NodeType.Text),
|
|
144
|
+
['img', createTagConfig('image', NodeType.Image, { supportsChangeEvents: true })],
|
|
145
|
+
['svg', createTagConfig('svg', NodeType.Svg)],
|
|
146
|
+
['input', createTagConfig('input', NodeType.TextInput, { supportsChangeEvents: true })],
|
|
147
|
+
['button', createTagConfig('pressable', NodeType.View, { supportsPressEvents: true })],
|
|
148
|
+
['a', createTagConfig('pressable', NodeType.View, { supportsPressEvents: true })],
|
|
149
|
+
['textarea', createTagConfig('input', NodeType.TextInput, { supportsChangeEvents: true })],
|
|
150
|
+
['toggle', createTagConfig('nativeView', NodeType.NativeView, {
|
|
151
|
+
supportsChangeEvents: true,
|
|
152
|
+
nativeView: TOGGLE_NATIVE_VIEW,
|
|
153
|
+
})],
|
|
154
|
+
]);
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* React Native tag configurations.
|
|
158
|
+
* These use RN defaults: flexDirection = 'column'.
|
|
159
|
+
*/
|
|
160
|
+
const rnTags: Record<string, TagConfig> = Object.fromEntries([
|
|
161
|
+
['View', createTagConfig('view', NodeType.View, { isRNStyle: true })],
|
|
162
|
+
['Text', createTagConfig('text', NodeType.Text, { isRNStyle: true })],
|
|
163
|
+
['Image', createTagConfig('image', NodeType.Image, { isRNStyle: true, supportsChangeEvents: true })],
|
|
164
|
+
['Svg', createTagConfig('svg', NodeType.Svg, { isRNStyle: true })],
|
|
165
|
+
['VideoView', createTagConfig('video', NodeType.Video, { isRNStyle: true })],
|
|
166
|
+
['ScrollView', createTagConfig('scroll', NodeType.ScrollView, { isRNStyle: true })],
|
|
167
|
+
['List', createTagConfig('list', NodeType.List, { isRNStyle: true })],
|
|
168
|
+
['TextInput', createTagConfig('input', NodeType.TextInput, { isRNStyle: true, supportsChangeEvents: true })],
|
|
169
|
+
['Pressable', createTagConfig('pressable', NodeType.Pressable, { isRNStyle: true, supportsPressEvents: true })],
|
|
170
|
+
['Toggle', createTagConfig('nativeView', NodeType.NativeView, {
|
|
171
|
+
isRNStyle: true,
|
|
172
|
+
supportsChangeEvents: true,
|
|
173
|
+
nativeView: TOGGLE_NATIVE_VIEW,
|
|
174
|
+
})],
|
|
175
|
+
['Switch', createTagConfig('nativeView', NodeType.NativeView, {
|
|
176
|
+
isRNStyle: true,
|
|
177
|
+
supportsChangeEvents: true,
|
|
178
|
+
nativeView: TOGGLE_NATIVE_VIEW,
|
|
179
|
+
})],
|
|
180
|
+
]);
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Combined tag map for all supported tags.
|
|
184
|
+
*/
|
|
185
|
+
const tagMap: Record<string, TagConfig> = {
|
|
186
|
+
...webTags,
|
|
187
|
+
...rnTags,
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
const nativeViewTags = new Map<string, TagConfig>();
|
|
191
|
+
const nativeViewTagNamesByModule = new Map<string, string>();
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Get the configuration for a tag.
|
|
195
|
+
* Returns null if the tag is not recognized.
|
|
196
|
+
*
|
|
197
|
+
* @param tag - The tag name (e.g., 'div', 'View', 'text')
|
|
198
|
+
*/
|
|
199
|
+
export function getTagConfig(tag: string): TagConfig | null {
|
|
200
|
+
return nativeViewTags.get(tag) ?? tagMap[tag] ?? null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Check if a tag is a valid Exact tag.
|
|
205
|
+
*
|
|
206
|
+
* @param tag - The tag name to check
|
|
207
|
+
*/
|
|
208
|
+
export function isValidTag(tag: string): boolean {
|
|
209
|
+
return nativeViewTags.has(tag) || tag in tagMap;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Check if a tag is a web tag.
|
|
214
|
+
*
|
|
215
|
+
* @param tag - The tag name to check
|
|
216
|
+
*/
|
|
217
|
+
export function isWebTag(tag: string): boolean {
|
|
218
|
+
return tag in webTags;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function registerNativeViewTag(metadata: NativeViewTagMetadata): string {
|
|
222
|
+
const existingTag = nativeViewTagNamesByModule.get(metadata.moduleName);
|
|
223
|
+
if (existingTag) {
|
|
224
|
+
return existingTag;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const tag = `ExactNativeView:${metadata.moduleName}`;
|
|
228
|
+
nativeViewTagNamesByModule.set(metadata.moduleName, tag);
|
|
229
|
+
nativeViewTags.set(tag, {
|
|
230
|
+
// RFC 0073 routes native control changes back through the existing
|
|
231
|
+
// renderer event binding path, so native-view tags must opt into Change
|
|
232
|
+
// events when callers provide an `onChange` prop.
|
|
233
|
+
...createTagConfig('nativeView', NodeType.NativeView, {
|
|
234
|
+
isRNStyle: true,
|
|
235
|
+
supportsChangeEvents: true,
|
|
236
|
+
nativeView: metadata,
|
|
237
|
+
}),
|
|
238
|
+
});
|
|
239
|
+
return tag;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function _clearNativeViewTagsForTesting(): void {
|
|
243
|
+
nativeViewTags.clear();
|
|
244
|
+
nativeViewTagNamesByModule.clear();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Check if a tag is a React Native tag.
|
|
249
|
+
*
|
|
250
|
+
* @param tag - The tag name to check
|
|
251
|
+
*/
|
|
252
|
+
export function isRNTag(tag: string): boolean {
|
|
253
|
+
return tag in rnTags;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Get all valid web tag names.
|
|
258
|
+
*/
|
|
259
|
+
export function getWebTags(): string[] {
|
|
260
|
+
return Object.keys(webTags);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Get all valid RN tag names.
|
|
265
|
+
*/
|
|
266
|
+
export function getRNTags(): string[] {
|
|
267
|
+
return Object.keys(rnTags);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Get all valid tag names.
|
|
272
|
+
*/
|
|
273
|
+
export function getAllTags(): string[] {
|
|
274
|
+
return [...Object.keys(tagMap), ...nativeViewTags.keys()];
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Default tag config for unknown tags (treated as View).
|
|
279
|
+
* This provides a fallback for development but should warn.
|
|
280
|
+
*/
|
|
281
|
+
export const defaultTagConfig: TagConfig = {
|
|
282
|
+
canonicalType: 'view',
|
|
283
|
+
nodeType: NodeType.View,
|
|
284
|
+
isRNStyle: false,
|
|
285
|
+
defaultFlexDirection: 'row',
|
|
286
|
+
isTextContainer: false,
|
|
287
|
+
supportsPressEvents: false,
|
|
288
|
+
supportsTransitionEvents: true,
|
|
289
|
+
};
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
import { createParagraphKey } from '@exact/text/paragraph-key';
|
|
2
|
+
import type {
|
|
3
|
+
ParagraphDirection,
|
|
4
|
+
ParagraphSpec,
|
|
5
|
+
ParagraphSpan,
|
|
6
|
+
ParagraphTextAlign,
|
|
7
|
+
ParagraphTextStyle,
|
|
8
|
+
ParagraphTruncation,
|
|
9
|
+
ParagraphWritingMode,
|
|
10
|
+
} from '@exact/text/paragraph-spec';
|
|
11
|
+
import { isValidElement } from 'react';
|
|
12
|
+
|
|
13
|
+
interface ParagraphLoweringContext {
|
|
14
|
+
readonly inheritedStyle?: ParagraphTextStyle;
|
|
15
|
+
readonly locale?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface TextParagraphLoweringResult {
|
|
19
|
+
readonly key: string;
|
|
20
|
+
readonly plainText: string;
|
|
21
|
+
readonly spec: ParagraphSpec;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function readNonEmptyString(value: unknown): string | undefined {
|
|
25
|
+
return typeof value === 'string' && value.trim().length > 0 ? value.trim() : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function readPositiveInteger(value: unknown): number | undefined {
|
|
29
|
+
return typeof value === 'number' && Number.isFinite(value) && value > 0
|
|
30
|
+
? Math.trunc(value)
|
|
31
|
+
: undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readFiniteNumber(value: unknown): number | undefined {
|
|
35
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function toParagraphDirection(value: unknown): ParagraphDirection | undefined {
|
|
39
|
+
return value === 'ltr' || value === 'rtl' || value === 'auto' ? value : undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toParagraphWritingMode(value: unknown): ParagraphWritingMode | undefined {
|
|
43
|
+
return value === 'horizontal-tb' || value === 'vertical-rl' || value === 'vertical-lr'
|
|
44
|
+
? value
|
|
45
|
+
: undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function toParagraphTextAlign(value: unknown): ParagraphTextAlign | undefined {
|
|
49
|
+
return value === 'auto' ||
|
|
50
|
+
value === 'left' ||
|
|
51
|
+
value === 'right' ||
|
|
52
|
+
value === 'center' ||
|
|
53
|
+
value === 'justify'
|
|
54
|
+
? value
|
|
55
|
+
: undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function toParagraphTruncation(value: unknown): ParagraphTruncation | undefined {
|
|
59
|
+
return value === 'head' || value === 'middle' || value === 'tail' || value === 'clip'
|
|
60
|
+
? value
|
|
61
|
+
: undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function toParagraphFontStyle(value: unknown): ParagraphTextStyle['fontStyle'] | undefined {
|
|
65
|
+
return value === 'normal' || value === 'italic' ? value : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function toParagraphFontWeight(value: unknown): ParagraphTextStyle['fontWeight'] | undefined {
|
|
69
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
70
|
+
return value;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return value === 'normal' ||
|
|
74
|
+
value === 'bold' ||
|
|
75
|
+
value === '100' ||
|
|
76
|
+
value === '200' ||
|
|
77
|
+
value === '300' ||
|
|
78
|
+
value === '400' ||
|
|
79
|
+
value === '500' ||
|
|
80
|
+
value === '600' ||
|
|
81
|
+
value === '700' ||
|
|
82
|
+
value === '800' ||
|
|
83
|
+
value === '900'
|
|
84
|
+
? value
|
|
85
|
+
: undefined;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function toParagraphTextDecorationLine(
|
|
89
|
+
value: unknown,
|
|
90
|
+
): ParagraphTextStyle['textDecorationLine'] | undefined {
|
|
91
|
+
return value === 'none' ||
|
|
92
|
+
value === 'underline' ||
|
|
93
|
+
value === 'line-through' ||
|
|
94
|
+
value === 'underline line-through'
|
|
95
|
+
? value
|
|
96
|
+
: undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readTextStyleObject(
|
|
100
|
+
props: Record<string, unknown>,
|
|
101
|
+
): Record<string, unknown> | undefined {
|
|
102
|
+
return typeof props.style === 'object' && props.style !== null
|
|
103
|
+
? (props.style as Record<string, unknown>)
|
|
104
|
+
: undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizeParagraphTextStyle(
|
|
108
|
+
style: ParagraphTextStyle | undefined,
|
|
109
|
+
): ParagraphTextStyle | undefined {
|
|
110
|
+
if (!style) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const normalized = {
|
|
115
|
+
...(style.color !== undefined ? { color: style.color } : null),
|
|
116
|
+
...(style.fontFamily !== undefined ? { fontFamily: style.fontFamily } : null),
|
|
117
|
+
...(style.fontSize !== undefined ? { fontSize: style.fontSize } : null),
|
|
118
|
+
...(style.fontWeight !== undefined ? { fontWeight: style.fontWeight } : null),
|
|
119
|
+
...(style.fontStyle !== undefined ? { fontStyle: style.fontStyle } : null),
|
|
120
|
+
...(style.fontVariantNumeric !== undefined
|
|
121
|
+
? { fontVariantNumeric: style.fontVariantNumeric }
|
|
122
|
+
: null),
|
|
123
|
+
...(style.fontFeatureSettings !== undefined
|
|
124
|
+
? { fontFeatureSettings: style.fontFeatureSettings }
|
|
125
|
+
: null),
|
|
126
|
+
...(style.fontVariationSettings !== undefined
|
|
127
|
+
? { fontVariationSettings: style.fontVariationSettings }
|
|
128
|
+
: null),
|
|
129
|
+
...(style.lineHeight !== undefined ? { lineHeight: style.lineHeight } : null),
|
|
130
|
+
...(style.letterSpacing !== undefined ? { letterSpacing: style.letterSpacing } : null),
|
|
131
|
+
...(style.textDecorationLine !== undefined
|
|
132
|
+
? { textDecorationLine: style.textDecorationLine }
|
|
133
|
+
: null),
|
|
134
|
+
...(style.locale !== undefined ? { locale: style.locale } : null),
|
|
135
|
+
} satisfies ParagraphTextStyle;
|
|
136
|
+
|
|
137
|
+
return Object.keys(normalized).length > 0 ? normalized : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function paragraphTextStyleKey(style: ParagraphTextStyle | undefined): string {
|
|
141
|
+
return JSON.stringify(style ?? null);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function mergeParagraphTextStyle(
|
|
145
|
+
inheritedStyle: ParagraphTextStyle | undefined,
|
|
146
|
+
overrideStyle: ParagraphTextStyle | undefined,
|
|
147
|
+
locale: string | undefined,
|
|
148
|
+
): ParagraphTextStyle | undefined {
|
|
149
|
+
const merged = normalizeParagraphTextStyle({
|
|
150
|
+
...inheritedStyle,
|
|
151
|
+
...overrideStyle,
|
|
152
|
+
...(locale !== undefined ? { locale } : null),
|
|
153
|
+
});
|
|
154
|
+
return merged;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function resolveParagraphTextStyleFromProps(
|
|
158
|
+
props: Record<string, unknown>,
|
|
159
|
+
locale: string | undefined,
|
|
160
|
+
): ParagraphTextStyle | undefined {
|
|
161
|
+
const rawStyle = readTextStyleObject(props);
|
|
162
|
+
|
|
163
|
+
return normalizeParagraphTextStyle({
|
|
164
|
+
color: readNonEmptyString(rawStyle?.color),
|
|
165
|
+
fontFamily:
|
|
166
|
+
typeof rawStyle?.fontFamily === 'string' ||
|
|
167
|
+
(Array.isArray(rawStyle?.fontFamily) &&
|
|
168
|
+
rawStyle.fontFamily.every((entry) => typeof entry === 'string'))
|
|
169
|
+
? (rawStyle.fontFamily as string | readonly string[])
|
|
170
|
+
: undefined,
|
|
171
|
+
fontSize: readFiniteNumber(rawStyle?.fontSize),
|
|
172
|
+
fontWeight: toParagraphFontWeight(rawStyle?.fontWeight),
|
|
173
|
+
fontStyle: toParagraphFontStyle(rawStyle?.fontStyle),
|
|
174
|
+
fontVariantNumeric: readNonEmptyString(rawStyle?.fontVariantNumeric),
|
|
175
|
+
fontFeatureSettings: readNonEmptyString(rawStyle?.fontFeatureSettings),
|
|
176
|
+
fontVariationSettings: readNonEmptyString(rawStyle?.fontVariationSettings),
|
|
177
|
+
lineHeight: readFiniteNumber(rawStyle?.lineHeight),
|
|
178
|
+
letterSpacing: readFiniteNumber(rawStyle?.letterSpacing),
|
|
179
|
+
textDecorationLine: toParagraphTextDecorationLine(rawStyle?.textDecorationLine),
|
|
180
|
+
locale,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function pushTextSpan(
|
|
185
|
+
spans: ParagraphSpan[],
|
|
186
|
+
text: string,
|
|
187
|
+
style: ParagraphTextStyle | undefined,
|
|
188
|
+
): void {
|
|
189
|
+
if (text.length === 0) {
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const normalizedStyle = normalizeParagraphTextStyle(style);
|
|
194
|
+
const previous = spans[spans.length - 1];
|
|
195
|
+
|
|
196
|
+
if (
|
|
197
|
+
previous &&
|
|
198
|
+
previous.type === 'text' &&
|
|
199
|
+
paragraphTextStyleKey(previous.style) === paragraphTextStyleKey(normalizedStyle)
|
|
200
|
+
) {
|
|
201
|
+
spans[spans.length - 1] = {
|
|
202
|
+
...previous,
|
|
203
|
+
text: previous.text + text,
|
|
204
|
+
};
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
spans.push({
|
|
209
|
+
type: 'text',
|
|
210
|
+
text,
|
|
211
|
+
style: normalizedStyle,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function appendParagraphSpans(
|
|
216
|
+
spans: ParagraphSpan[],
|
|
217
|
+
children: unknown,
|
|
218
|
+
context: ParagraphLoweringContext,
|
|
219
|
+
): void {
|
|
220
|
+
if (children === null || children === undefined || typeof children === 'boolean') {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (typeof children === 'string' || typeof children === 'number') {
|
|
225
|
+
pushTextSpan(spans, String(children), context.inheritedStyle);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (Array.isArray(children)) {
|
|
230
|
+
for (const child of children) {
|
|
231
|
+
appendParagraphSpans(spans, child, context);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!isValidElement(children)) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const props =
|
|
241
|
+
typeof children.props === 'object' && children.props !== null
|
|
242
|
+
? (children.props as Record<string, unknown>)
|
|
243
|
+
: {};
|
|
244
|
+
const nextLocale = readNonEmptyString(props.lang) ?? context.locale;
|
|
245
|
+
const nextStyle = mergeParagraphTextStyle(
|
|
246
|
+
context.inheritedStyle,
|
|
247
|
+
resolveParagraphTextStyleFromProps(props, nextLocale),
|
|
248
|
+
nextLocale,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
appendParagraphSpans(spans, props.children, {
|
|
252
|
+
inheritedStyle: nextStyle,
|
|
253
|
+
locale: nextLocale,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export function extractPlainTextFromParagraphSpec(
|
|
258
|
+
spec: ParagraphSpec | null | undefined,
|
|
259
|
+
): string | undefined {
|
|
260
|
+
if (!spec) {
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const plainText = spec.spans
|
|
265
|
+
.map((span) => (span.type === 'text' ? span.text : span.altText ?? ''))
|
|
266
|
+
.join('');
|
|
267
|
+
|
|
268
|
+
return plainText.length > 0 ? plainText : undefined;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function lowerTextPropsToParagraphSpec(
|
|
272
|
+
props: Record<string, unknown>,
|
|
273
|
+
): TextParagraphLoweringResult | null {
|
|
274
|
+
const locale = readNonEmptyString(props.lang);
|
|
275
|
+
const paragraphStyle = resolveParagraphTextStyleFromProps(props, locale);
|
|
276
|
+
const rawStyle = readTextStyleObject(props);
|
|
277
|
+
const spans: ParagraphSpan[] = [];
|
|
278
|
+
|
|
279
|
+
appendParagraphSpans(spans, props.children, {
|
|
280
|
+
inheritedStyle: paragraphStyle,
|
|
281
|
+
locale,
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
if (spans.length === 0) {
|
|
285
|
+
return null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const spec: ParagraphSpec = {
|
|
289
|
+
spans,
|
|
290
|
+
locale,
|
|
291
|
+
baseDirection: toParagraphDirection(rawStyle?.direction),
|
|
292
|
+
writingMode: toParagraphWritingMode(rawStyle?.writingMode),
|
|
293
|
+
textAlign: toParagraphTextAlign(rawStyle?.textAlign),
|
|
294
|
+
maxLines: readPositiveInteger(props.numberOfLines),
|
|
295
|
+
truncation: toParagraphTruncation(props.ellipsizeMode),
|
|
296
|
+
};
|
|
297
|
+
const key = createParagraphKey(spec);
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
key,
|
|
301
|
+
plainText: extractPlainTextFromParagraphSpec(spec) ?? '',
|
|
302
|
+
spec,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function getParagraphKeyForTextProps(
|
|
307
|
+
props: Record<string, unknown>,
|
|
308
|
+
): string | null {
|
|
309
|
+
return lowerTextPropsToParagraphSpec(props)?.key ?? null;
|
|
310
|
+
}
|