@apollohg/react-native-rich-text-editor 2.0.0 → 2.0.1
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/README.md +1 -1
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/android/build.gradle +3 -0
- package/app.plugin.js +42 -39
- package/dist/AtomHost.js +18 -11
- package/dist/EditorAddon.js +4 -2
- package/dist/EditorAddonNormalization.js +17 -9
- package/dist/EditorMentionThemeNormalization.js +6 -3
- package/dist/EditorMentionThemeValidation.js +28 -11
- package/dist/EditorStyleSheetNormalization.js +84 -37
- package/dist/EditorTheme.d.ts +4 -0
- package/dist/EditorTheme.js +5 -3
- package/dist/EditorToolbarComponent.d.ts +1 -1
- package/dist/EditorToolbarComponent.js +1 -1
- package/dist/EditorToolbarRegistry.js +5 -5
- package/dist/EditorToolbarTypes.d.ts +12 -12
- package/dist/EditorToolbarVisuals.js +1 -1
- package/dist/ExternalTextComposition.js +89 -43
- package/dist/ImageLoadingPolicy.d.ts +1 -1
- package/dist/NativeEditorBoundaryError.d.ts +7 -7
- package/dist/NativeEditorBoundaryError.js +9 -5
- package/dist/NativeEditorCollaborationTransport.d.ts +7 -7
- package/dist/NativeEditorCollaborationTransport.js +28 -15
- package/dist/NativeEditorCreateConfig.js +11 -6
- package/dist/NativeEditorCreateJson.js +51 -25
- package/dist/NativeEditorCreateValidation.js +11 -6
- package/dist/NativeEditorDocumentBridge.d.ts +1 -1
- package/dist/NativeEditorDocumentBridge.js +32 -18
- package/dist/NativeEditorDocumentHandle.d.ts +4 -4
- package/dist/NativeEditorDocumentHandle.js +1 -1
- package/dist/NativeEditorLocalAwareness.js +25 -13
- package/dist/NativeEditorNativeModule.d.ts +8 -1
- package/dist/NativeEditorNativeModule.js +7 -5
- package/dist/NativeEditorRenderNormalization.js +66 -25
- package/dist/NativeEditorResultNormalization.js +53 -29
- package/dist/NativeEditorTypes.d.ts +10 -10
- package/dist/NativeEditorV2Decimal.js +6 -3
- package/dist/NativeProseViewer.js +11 -3
- package/dist/ResourceLimits.d.ts +2 -2
- package/dist/ResourceLimits.js +7 -2
- package/dist/RichTextEditor.js +1 -1
- package/dist/RichTextEditorNativeTypes.d.ts +1 -1
- package/dist/RichTextEditorNativeView.d.ts +1 -3
- package/dist/RichTextEditorSerialization.js +24 -14
- package/dist/RichTextEditorTypes.d.ts +1 -1
- package/dist/YjsCollaboration.d.ts +7 -7
- package/dist/YjsCollaboration.js +42 -26
- package/dist/addons.d.ts +4 -4
- package/dist/addons.js +3 -3
- package/dist/atomInstances.js +6 -3
- package/dist/atomUpdates.js +7 -4
- package/dist/atoms.js +20 -12
- package/dist/attributeValidation.js +55 -24
- package/dist/contentExpression.js +80 -42
- package/dist/listMarkers.d.ts +1 -1
- package/dist/listMarkers.js +1 -1
- package/dist/schemaDefinition.d.ts +1 -1
- package/dist/schemaDefinition.js +23 -14
- package/dist/schemaDocument.js +1 -1
- package/dist/schemaDocumentConstruction.js +25 -14
- package/dist/schemaNormalization.js +51 -29
- package/dist/schemaPresets.d.ts +1 -1
- package/dist/schemaPresets.js +9 -4
- package/dist/schemaResolution.js +46 -25
- package/dist/specs/PreparedProseViewerNativeComponent.d.ts +4 -0
- package/dist/useEditorToolbarInteractions.d.ts +2 -2
- package/dist/useEditorToolbarInteractions.js +45 -23
- package/dist/useEditorToolbarItems.d.ts +1 -1
- package/dist/useEditorToolbarItems.js +4 -3
- package/dist/useEditorToolbarPresentation.d.ts +3 -3
- package/dist/useEditorToolbarPresentation.js +4 -4
- package/dist/useEditorToolbarState.d.ts +1 -1
- package/dist/useEditorToolbarState.js +1 -1
- package/dist/useFocusPreservingFrames.js +11 -8
- package/dist/useNativeEditor.d.ts +1 -1
- package/dist/useNativeEditor.js +21 -11
- package/dist/useRichTextEditorCommands.d.ts +2 -2
- package/dist/useRichTextEditorCommands.js +59 -29
- package/dist/useRichTextEditorEvents.d.ts +3 -3
- package/dist/useRichTextEditorEvents.js +75 -32
- package/dist/useRichTextEditorMentions.d.ts +4 -4
- package/dist/useRichTextEditorMentions.js +48 -29
- package/dist/useRichTextEditorPresentation.d.ts +4 -4
- package/dist/useRichTextEditorPresentation.js +25 -23
- package/dist/useRichTextEditorState.d.ts +3 -3
- package/dist/useRichTextEditorState.js +20 -15
- package/dist/useRichTextEditorUpdates.d.ts +1 -1
- package/dist/useRichTextEditorUpdates.js +99 -41
- package/dist/useViewerAtoms.js +63 -36
- package/ios/EditorCore.xcframework/Info.plist +5 -5
- package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
- package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
- package/package.json +11 -3
- package/react-native.config.js +1 -1
- package/rust/android/arm64-v8a/libeditor_core.so +0 -0
- package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
- package/rust/android/x86/libeditor_core.so +0 -0
- package/rust/android/x86_64/libeditor_core.so +0 -0
- package/src/AtomHost.tsx +58 -28
- package/src/EditorAddon.ts +18 -8
- package/src/EditorAddonNormalization.ts +40 -11
- package/src/EditorMentionThemeNormalization.ts +15 -4
- package/src/EditorMentionThemeValidation.ts +51 -14
- package/src/EditorStyleSheet.ts +1 -0
- package/src/EditorStyleSheetNormalization.ts +195 -64
- package/src/EditorTheme.ts +13 -5
- package/src/EditorToolbarComponent.tsx +2 -1
- package/src/EditorToolbarRegistry.tsx +23 -13
- package/src/EditorToolbarTypes.tsx +12 -12
- package/src/EditorToolbarVisuals.tsx +6 -3
- package/src/EditorUpdateRevision.ts +1 -0
- package/src/ExternalTextComposition.ts +259 -49
- package/src/ImageLoadingPolicy.ts +3 -1
- package/src/NativeEditorBoundaryError.ts +38 -11
- package/src/NativeEditorCollaborationTransport.ts +108 -27
- package/src/NativeEditorCreateConfig.ts +92 -19
- package/src/NativeEditorCreateJson.ts +163 -26
- package/src/NativeEditorCreateValidation.ts +33 -12
- package/src/NativeEditorDocumentBridge.ts +111 -19
- package/src/NativeEditorDocumentHandle.ts +9 -4
- package/src/NativeEditorLocalAwareness.ts +70 -15
- package/src/NativeEditorNativeModule.ts +13 -9
- package/src/NativeEditorRenderNormalization.ts +128 -29
- package/src/NativeEditorResultNormalization.ts +143 -32
- package/src/NativeEditorTypes.ts +12 -11
- package/src/NativeEditorV2Decimal.ts +12 -3
- package/src/NativeProseViewer.tsx +46 -16
- package/src/ResourceLimits.ts +15 -8
- package/src/RichTextEditor.tsx +3 -1
- package/src/RichTextEditorNativeTypes.tsx +1 -1
- package/src/RichTextEditorNativeView.tsx +2 -8
- package/src/RichTextEditorSerialization.tsx +70 -24
- package/src/RichTextEditorTypes.tsx +1 -1
- package/src/YjsCollaboration.ts +143 -47
- package/src/addons.ts +12 -8
- package/src/atomInstances.ts +25 -5
- package/src/atomUpdates.ts +23 -6
- package/src/atoms.ts +85 -18
- package/src/attributeValidation.ts +97 -33
- package/src/contentExpression.ts +218 -63
- package/src/listMarkers.ts +1 -1
- package/src/schemaDefinition.ts +82 -20
- package/src/schemaDocument.ts +15 -1
- package/src/schemaDocumentConstruction.ts +80 -16
- package/src/schemaNormalization.ts +155 -41
- package/src/schemaPresets.ts +37 -31
- package/src/schemaResolution.ts +126 -27
- package/src/specs/PreparedProseViewerNativeComponent.ts +4 -2
- package/src/useEditorToolbarInteractions.tsx +85 -36
- package/src/useEditorToolbarItems.tsx +50 -22
- package/src/useEditorToolbarPresentation.tsx +81 -58
- package/src/useEditorToolbarState.tsx +8 -6
- package/src/useFocusPreservingFrames.ts +43 -15
- package/src/useNativeEditor.ts +111 -51
- package/src/useRichTextEditorCommands.tsx +127 -54
- package/src/useRichTextEditorEvents.tsx +144 -42
- package/src/useRichTextEditorMentions.tsx +118 -37
- package/src/useRichTextEditorPresentation.tsx +128 -114
- package/src/useRichTextEditorState.tsx +76 -40
- package/src/useRichTextEditorUpdates.tsx +207 -56
- package/src/useViewerAtoms.tsx +162 -66
package/src/atomUpdates.ts
CHANGED
|
@@ -2,14 +2,21 @@ import type { AtomAttrsUpdate } from './atoms';
|
|
|
2
2
|
import { AtomUpdateAttrsError } from './atomInstances';
|
|
3
3
|
|
|
4
4
|
function copyJson(value: unknown, ancestors = new Set<object>()): unknown {
|
|
5
|
-
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
6
|
-
|
|
5
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') {
|
|
6
|
+
return value;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
|
|
7
13
|
if (typeof value !== 'object' || ancestors.has(value) || ancestors.size > 128) {
|
|
8
14
|
throw new AtomUpdateAttrsError(
|
|
9
15
|
'not-applicable',
|
|
10
16
|
'Atom attributes must contain finite JSON values.'
|
|
11
17
|
);
|
|
12
18
|
}
|
|
19
|
+
|
|
13
20
|
if (
|
|
14
21
|
!Array.isArray(value) &&
|
|
15
22
|
Object.getPrototypeOf(value) !== Object.prototype &&
|
|
@@ -20,13 +27,17 @@ function copyJson(value: unknown, ancestors = new Set<object>()): unknown {
|
|
|
20
27
|
'Atom attributes must contain plain JSON objects.'
|
|
21
28
|
);
|
|
22
29
|
}
|
|
30
|
+
|
|
23
31
|
ancestors.add(value);
|
|
32
|
+
|
|
24
33
|
const result = Array.isArray(value)
|
|
25
|
-
? Array.from(value,
|
|
34
|
+
? Array.from(value, entry => copyJson(entry, ancestors))
|
|
26
35
|
: Object.fromEntries(
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
Object.entries(value).map(([ key, entry ]) => [ key, copyJson(entry, ancestors) ])
|
|
37
|
+
);
|
|
38
|
+
|
|
29
39
|
ancestors.delete(value);
|
|
40
|
+
|
|
30
41
|
return Object.freeze(result);
|
|
31
42
|
}
|
|
32
43
|
|
|
@@ -36,17 +47,23 @@ export function resolveAtomAttrsUpdate(
|
|
|
36
47
|
): Record<string, unknown> {
|
|
37
48
|
let current = copyJson(attrs) as Record<string, unknown>;
|
|
38
49
|
let patch: Record<string, unknown> = {};
|
|
39
|
-
|
|
50
|
+
|
|
51
|
+
const updates: readonly (Record<string, unknown> | ((current: Readonly<Record<string, unknown>>) => Record<string, unknown>))[] = Array.isArray(update) ? update : [ update ];
|
|
52
|
+
|
|
53
|
+
for (const entry of updates) {
|
|
40
54
|
const value: unknown = typeof entry === 'function' ? entry(current) : entry;
|
|
55
|
+
|
|
41
56
|
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
42
57
|
throw new AtomUpdateAttrsError(
|
|
43
58
|
'not-applicable',
|
|
44
59
|
'Atom updates must return an attribute object.'
|
|
45
60
|
);
|
|
46
61
|
}
|
|
62
|
+
|
|
47
63
|
const partial = copyJson(value) as Record<string, unknown>;
|
|
48
64
|
patch = { ...patch, ...partial };
|
|
49
65
|
current = Object.freeze({ ...current, ...partial });
|
|
50
66
|
}
|
|
67
|
+
|
|
51
68
|
return patch;
|
|
52
69
|
}
|
package/src/atoms.ts
CHANGED
|
@@ -128,8 +128,14 @@ export interface SerializedEditorAtoms {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
function isAtomComponent(value: unknown): value is AtomComponent {
|
|
131
|
-
if (typeof value === 'function')
|
|
132
|
-
|
|
131
|
+
if (typeof value === 'function') {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (value == null || typeof value !== 'object' || !('$$typeof' in value)) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
|
|
133
139
|
return (
|
|
134
140
|
value.$$typeof === Symbol.for('react.memo') ||
|
|
135
141
|
value.$$typeof === Symbol.for('react.forward_ref') ||
|
|
@@ -153,13 +159,17 @@ function stringRecord(value: unknown, label: string): Record<string, string> {
|
|
|
153
159
|
if (value == null || typeof value !== 'object' || Array.isArray(value)) {
|
|
154
160
|
throw new Error(`${label} must be an object`);
|
|
155
161
|
}
|
|
162
|
+
|
|
156
163
|
const result: Record<string, string> = {};
|
|
157
|
-
|
|
164
|
+
|
|
165
|
+
for (const [ key, entry ] of Object.entries(value)) {
|
|
158
166
|
if (typeof entry !== 'string') {
|
|
159
167
|
throw new Error(`${label} '${key}' must be a string`);
|
|
160
168
|
}
|
|
169
|
+
|
|
161
170
|
result[key] = entry;
|
|
162
171
|
}
|
|
172
|
+
|
|
163
173
|
return result;
|
|
164
174
|
}
|
|
165
175
|
|
|
@@ -175,17 +185,23 @@ function normalizedHtmlRules(config: AtomNodeConfig<any>): NodeHtmlRules {
|
|
|
175
185
|
if (config.html == null || typeof config.html !== 'object' || Array.isArray(config.html)) {
|
|
176
186
|
throw new Error(`atom '${config.name}' requires HTML rules`);
|
|
177
187
|
}
|
|
188
|
+
|
|
178
189
|
const raw = config.html as NodeHtmlRules & Record<string, unknown>;
|
|
179
|
-
|
|
190
|
+
|
|
191
|
+
if (Object.keys(raw).some(key => ![ 'tag', 'staticAttrs', 'attrMap' ].includes(key))) {
|
|
180
192
|
throw new Error(`atom '${config.name}' has an unknown HTML rule field`);
|
|
181
193
|
}
|
|
194
|
+
|
|
182
195
|
if (typeof raw.tag !== 'string' || !isSafeTag(raw.tag)) {
|
|
183
196
|
throw new Error(`atom '${config.name}' has an invalid HTML tag '${String(raw.tag)}'`);
|
|
184
197
|
}
|
|
198
|
+
|
|
185
199
|
const staticAttrs = stringRecord(raw.staticAttrs, `atom '${config.name}' staticAttrs`);
|
|
200
|
+
|
|
186
201
|
if (Object.keys(staticAttrs).length === 0) {
|
|
187
202
|
throw new Error(`atom '${config.name}' requires a static attribute discriminator`);
|
|
188
203
|
}
|
|
204
|
+
|
|
189
205
|
for (const name of Object.keys(staticAttrs)) {
|
|
190
206
|
if (!isSafeAttr(name)) {
|
|
191
207
|
throw new Error(`atom '${config.name}' has an invalid HTML attribute '${name}'`);
|
|
@@ -193,35 +209,44 @@ function normalizedHtmlRules(config: AtomNodeConfig<any>): NodeHtmlRules {
|
|
|
193
209
|
}
|
|
194
210
|
|
|
195
211
|
const attrs = config.attrs ?? {};
|
|
212
|
+
|
|
196
213
|
const attrMap =
|
|
197
214
|
raw.attrMap == null
|
|
198
215
|
? Object.fromEntries(
|
|
199
|
-
|
|
200
|
-
|
|
216
|
+
Object.keys(attrs).map(name => [ name, `data-${kebabCase(name)}` ])
|
|
217
|
+
)
|
|
201
218
|
: stringRecord(raw.attrMap, `atom '${config.name}' attrMap`);
|
|
219
|
+
|
|
202
220
|
for (const name of Object.keys(attrMap)) {
|
|
203
221
|
if (!Object.prototype.hasOwnProperty.call(attrs, name)) {
|
|
204
222
|
throw new Error(`atom '${config.name}' maps undeclared attribute '${name}'`);
|
|
205
223
|
}
|
|
206
224
|
}
|
|
225
|
+
|
|
207
226
|
for (const name of Object.keys(attrs)) {
|
|
208
227
|
if (!Object.prototype.hasOwnProperty.call(attrMap, name)) {
|
|
209
228
|
throw new Error(`atom '${config.name}' attrMap is missing '${name}'`);
|
|
210
229
|
}
|
|
211
230
|
}
|
|
231
|
+
|
|
212
232
|
const targets = new Set<string>();
|
|
233
|
+
|
|
213
234
|
for (const target of Object.values(attrMap)) {
|
|
214
235
|
if (!isSafeAttr(target)) {
|
|
215
236
|
throw new Error(`atom '${config.name}' has an invalid HTML attribute '${target}'`);
|
|
216
237
|
}
|
|
238
|
+
|
|
217
239
|
if (targets.has(target)) {
|
|
218
240
|
throw new Error(`atom '${config.name}' repeats HTML attribute '${target}'`);
|
|
219
241
|
}
|
|
242
|
+
|
|
220
243
|
if (Object.prototype.hasOwnProperty.call(staticAttrs, target)) {
|
|
221
244
|
throw new Error(`atom '${config.name}' collides on HTML attribute '${target}'`);
|
|
222
245
|
}
|
|
246
|
+
|
|
223
247
|
targets.add(target);
|
|
224
248
|
}
|
|
249
|
+
|
|
225
250
|
return { tag: raw.tag, staticAttrs, attrMap };
|
|
226
251
|
}
|
|
227
252
|
|
|
@@ -231,37 +256,49 @@ export function defineAtomNode<const S extends Record<string, AttrSpec>>(
|
|
|
231
256
|
export function defineAtomNode<A = Record<string, unknown>>(
|
|
232
257
|
config: AtomNodeConfig<A> & (string extends keyof A ? {} : { attrs: AtomAttributeSpecs<A> })
|
|
233
258
|
): AtomNodeDefinition<A, Required<A>>;
|
|
259
|
+
|
|
234
260
|
export function defineAtomNode(config: AtomNodeConfig<any>): AtomNodeDefinition<any> {
|
|
235
261
|
if (config == null || typeof config !== 'object') {
|
|
236
262
|
throw new Error('atom config must be an object');
|
|
237
263
|
}
|
|
264
|
+
|
|
238
265
|
if (typeof config.name !== 'string' || config.name.length === 0) {
|
|
239
266
|
throw new Error('atom name must be a non-empty string');
|
|
240
267
|
}
|
|
268
|
+
|
|
241
269
|
if (RESERVED_WIRE_NODE_TYPES.has(config.name)) {
|
|
242
270
|
throw new Error(`atom name '${config.name}' is reserved`);
|
|
243
271
|
}
|
|
272
|
+
|
|
244
273
|
if (!isAtomComponent(config.component)) {
|
|
245
274
|
throw new Error(`atom '${config.name}' requires a component`);
|
|
246
275
|
}
|
|
276
|
+
|
|
247
277
|
if (
|
|
248
278
|
config.estimatedHeight != null &&
|
|
249
279
|
(!Number.isFinite(config.estimatedHeight) || config.estimatedHeight < 0)
|
|
250
280
|
) {
|
|
251
281
|
throw new Error(`atom '${config.name}' estimatedHeight must be non-negative`);
|
|
252
282
|
}
|
|
253
|
-
|
|
283
|
+
|
|
284
|
+
if ('allowUndeclaredAttrs' in config) {
|
|
254
285
|
throw new Error(`atom '${config.name}' cannot allow undeclared attributes`);
|
|
255
286
|
}
|
|
287
|
+
|
|
256
288
|
if (config.attrs != null && (typeof config.attrs !== 'object' || Array.isArray(config.attrs))) {
|
|
257
289
|
throw new Error(`atom '${config.name}' attrs must be an object`);
|
|
258
290
|
}
|
|
259
291
|
|
|
260
|
-
for (const [name, spec] of Object.entries(config.attrs ?? {}))
|
|
292
|
+
for (const [ name, spec ] of Object.entries(config.attrs ?? {})) {
|
|
261
293
|
validateAttributeSpec(spec, name);
|
|
262
|
-
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (config.idAttribute !== undefined && config.attrs?.[config.idAttribute]?.type !== 'string') {
|
|
263
297
|
throw new Error(`atom '${config.name}' identifier attribute must declare type string`);
|
|
298
|
+
}
|
|
299
|
+
|
|
264
300
|
const html = normalizedHtmlRules(config);
|
|
301
|
+
|
|
265
302
|
const nodeSpec: NodeSpec = {
|
|
266
303
|
name: config.name,
|
|
267
304
|
content: '',
|
|
@@ -271,6 +308,7 @@ export function defineAtomNode(config: AtomNodeConfig<any>): AtomNodeDefinition<
|
|
|
271
308
|
...(config.attrs == null ? {} : { attrs: config.attrs }),
|
|
272
309
|
html,
|
|
273
310
|
};
|
|
311
|
+
|
|
274
312
|
return {
|
|
275
313
|
name: config.name,
|
|
276
314
|
...(config.attrs == null ? {} : { attrs: config.attrs }),
|
|
@@ -279,8 +317,9 @@ export function defineAtomNode(config: AtomNodeConfig<any>): AtomNodeDefinition<
|
|
|
279
317
|
...(config.idAttribute === undefined ? {} : { idAttribute: config.idAttribute }),
|
|
280
318
|
estimatedHeight: config.estimatedHeight ?? DEFAULT_ATOM_CHIP_HEIGHT,
|
|
281
319
|
nodeSpec,
|
|
282
|
-
buildFragmentJson(attrs, descriptor) {
|
|
320
|
+
buildFragmentJson(attrs: Record<string, unknown> | undefined, descriptor) {
|
|
283
321
|
validateAttributes(attrs ?? {}, config.attrs ?? {});
|
|
322
|
+
|
|
284
323
|
return {
|
|
285
324
|
type: descriptor?.documentNodeName ?? 'doc',
|
|
286
325
|
content: [
|
|
@@ -295,10 +334,14 @@ export function defineAtomNode(config: AtomNodeConfig<any>): AtomNodeDefinition<
|
|
|
295
334
|
}
|
|
296
335
|
|
|
297
336
|
function schemaValuesEqual(left: unknown, right: unknown): boolean {
|
|
298
|
-
if (Object.is(left, right))
|
|
337
|
+
if (Object.is(left, right)) {
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
|
|
299
341
|
if (left == null || right == null || typeof left !== 'object' || typeof right !== 'object') {
|
|
300
342
|
return false;
|
|
301
343
|
}
|
|
344
|
+
|
|
302
345
|
if (Array.isArray(left) || Array.isArray(right)) {
|
|
303
346
|
return (
|
|
304
347
|
Array.isArray(left) &&
|
|
@@ -307,10 +350,12 @@ function schemaValuesEqual(left: unknown, right: unknown): boolean {
|
|
|
307
350
|
left.every((value, index) => schemaValuesEqual(value, right[index]))
|
|
308
351
|
);
|
|
309
352
|
}
|
|
353
|
+
|
|
310
354
|
const leftRecord = left as Record<string, unknown>;
|
|
311
355
|
const rightRecord = right as Record<string, unknown>;
|
|
312
356
|
const leftKeys = Object.keys(leftRecord).sort();
|
|
313
357
|
const rightKeys = Object.keys(rightRecord).sort();
|
|
358
|
+
|
|
314
359
|
return (
|
|
315
360
|
leftKeys.length === rightKeys.length &&
|
|
316
361
|
leftKeys.every(
|
|
@@ -321,19 +366,27 @@ function schemaValuesEqual(left: unknown, right: unknown): boolean {
|
|
|
321
366
|
}
|
|
322
367
|
|
|
323
368
|
function assertUnambiguousHtmlRules(nodes: readonly NodeSpec[]): void {
|
|
324
|
-
const ruled = nodes.filter(
|
|
369
|
+
const ruled = nodes.filter(node => node.html != null);
|
|
370
|
+
|
|
325
371
|
for (let rightIndex = 1; rightIndex < ruled.length; rightIndex += 1) {
|
|
326
372
|
const right = ruled[rightIndex];
|
|
373
|
+
|
|
327
374
|
for (let leftIndex = 0; leftIndex < rightIndex; leftIndex += 1) {
|
|
328
375
|
const left = ruled[leftIndex];
|
|
329
|
-
|
|
376
|
+
|
|
377
|
+
if (left.html?.tag !== right.html?.tag) {
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
|
|
330
381
|
const leftStatic = left.html?.staticAttrs ?? {};
|
|
331
382
|
const rightStatic = right.html?.staticAttrs ?? {};
|
|
383
|
+
|
|
332
384
|
const conflicts = Object.entries(leftStatic).some(
|
|
333
|
-
([name, value]) =>
|
|
385
|
+
([ name, value ]) =>
|
|
334
386
|
Object.prototype.hasOwnProperty.call(rightStatic, name) &&
|
|
335
387
|
rightStatic[name] !== value
|
|
336
388
|
);
|
|
389
|
+
|
|
337
390
|
if (!conflicts) {
|
|
338
391
|
throw new Error(`ambiguous atom HTML rules for '${left.name}' and '${right.name}'`);
|
|
339
392
|
}
|
|
@@ -345,35 +398,49 @@ export function withAtomsSchema(
|
|
|
345
398
|
schema: SchemaDefinition,
|
|
346
399
|
atoms: readonly AtomNodeDefinition<any>[]
|
|
347
400
|
): SchemaDefinition {
|
|
348
|
-
const nodes = [...schema.nodes];
|
|
401
|
+
const nodes = [ ...schema.nodes ];
|
|
349
402
|
let changed = false;
|
|
403
|
+
|
|
350
404
|
for (const atom of atoms) {
|
|
351
|
-
const existing = nodes.find(
|
|
405
|
+
const existing = nodes.find(node => node.name === atom.name);
|
|
406
|
+
|
|
352
407
|
if (existing != null) {
|
|
353
408
|
if (!schemaValuesEqual(existing, atom.nodeSpec)) {
|
|
354
409
|
throw new Error(`schema already declares conflicting node '${atom.name}'`);
|
|
355
410
|
}
|
|
411
|
+
|
|
356
412
|
continue;
|
|
357
413
|
}
|
|
414
|
+
|
|
358
415
|
nodes.push(atom.nodeSpec);
|
|
359
416
|
changed = true;
|
|
360
417
|
}
|
|
418
|
+
|
|
361
419
|
assertUnambiguousHtmlRules(nodes);
|
|
420
|
+
|
|
362
421
|
return changed ? { ...schema, nodes } : schema;
|
|
363
422
|
}
|
|
364
423
|
|
|
365
424
|
export function serializeEditorAtoms(
|
|
366
425
|
atoms?: readonly AtomNodeDefinition<any>[]
|
|
367
426
|
): string | undefined {
|
|
368
|
-
if (atoms == null || atoms.length === 0)
|
|
427
|
+
if (atoms == null || atoms.length === 0) {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
|
|
369
431
|
const serialized: SerializedEditorAtoms = {
|
|
370
432
|
nodeTypes: [],
|
|
371
433
|
estimatedHeights: {},
|
|
372
434
|
};
|
|
435
|
+
|
|
373
436
|
for (const atom of atoms) {
|
|
374
|
-
if (Object.prototype.hasOwnProperty.call(serialized.estimatedHeights, atom.name))
|
|
437
|
+
if (Object.prototype.hasOwnProperty.call(serialized.estimatedHeights, atom.name)) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
|
|
375
441
|
serialized.nodeTypes.push(atom.name);
|
|
376
442
|
serialized.estimatedHeights[atom.name] = atom.estimatedHeight ?? DEFAULT_ATOM_CHIP_HEIGHT;
|
|
377
443
|
}
|
|
444
|
+
|
|
378
445
|
return JSON.stringify(serialized);
|
|
379
446
|
}
|
|
@@ -5,33 +5,56 @@ function jsonType(value: unknown): string {
|
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
function validateJson(value: unknown, ancestors = new Set<object>()): void {
|
|
8
|
-
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean') {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (typeof value !== 'object' || ancestors.has(value) || ancestors.size > 128) {
|
|
11
17
|
throw new Error('Attribute values must be finite, acyclic JSON.');
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
if (
|
|
13
21
|
!Array.isArray(value) &&
|
|
14
22
|
Object.getPrototypeOf(value) !== Object.prototype &&
|
|
15
23
|
Object.getPrototypeOf(value) !== null
|
|
16
|
-
)
|
|
24
|
+
) {
|
|
17
25
|
throw new Error('Attribute values must be plain JSON objects.');
|
|
26
|
+
}
|
|
27
|
+
|
|
18
28
|
ancestors.add(value);
|
|
19
29
|
const entries = Array.isArray(value) ? value : Object.values(value);
|
|
20
|
-
|
|
30
|
+
|
|
31
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
32
|
+
validateJson(entries[index], ancestors);
|
|
33
|
+
}
|
|
34
|
+
|
|
21
35
|
ancestors.delete(value);
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
function equal(left: unknown, right: unknown): boolean {
|
|
25
|
-
if (left === right)
|
|
26
|
-
|
|
39
|
+
if (left === right) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (left == null || right == null || typeof left !== 'object' || typeof right !== 'object') {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (Array.isArray(left) !== Array.isArray(right)) {
|
|
27
48
|
return false;
|
|
28
|
-
|
|
49
|
+
}
|
|
50
|
+
|
|
29
51
|
const a = Object.keys(left),
|
|
30
52
|
b = Object.keys(right);
|
|
53
|
+
|
|
31
54
|
return (
|
|
32
55
|
a.length === b.length &&
|
|
33
56
|
a.every(
|
|
34
|
-
|
|
57
|
+
key =>
|
|
35
58
|
Object.prototype.hasOwnProperty.call(right, key) &&
|
|
36
59
|
equal(
|
|
37
60
|
(left as Record<string, unknown>)[key],
|
|
@@ -44,73 +67,114 @@ function equal(left: unknown, right: unknown): boolean {
|
|
|
44
67
|
export function validateAttribute(value: unknown, spec: AttrSpec, name: string): void {
|
|
45
68
|
validateJson(value);
|
|
46
69
|
const type = jsonType(value);
|
|
47
|
-
|
|
70
|
+
|
|
71
|
+
if ((spec.type != null && type !== spec.type) || (type === 'number' && !Number.isFinite(value))) {
|
|
48
72
|
throw new Error(`invalid attribute '${name}': expected ${spec.type ?? 'finite number'}`);
|
|
49
|
-
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (spec.enum != null && !spec.enum.some(entry => equal(entry, value))) {
|
|
50
76
|
throw new Error(`invalid attribute '${name}': outside enum`);
|
|
77
|
+
}
|
|
78
|
+
|
|
51
79
|
const size =
|
|
52
80
|
typeof value === 'number'
|
|
53
81
|
? value
|
|
54
82
|
: typeof value === 'string'
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
83
|
+
? [ ...value ].length
|
|
84
|
+
: Array.isArray(value)
|
|
85
|
+
? value.length
|
|
86
|
+
: undefined;
|
|
87
|
+
|
|
59
88
|
if (
|
|
60
89
|
(spec.min != null && (size == null || size < spec.min)) ||
|
|
61
90
|
(spec.max != null && (size == null || size > spec.max))
|
|
62
|
-
)
|
|
91
|
+
) {
|
|
63
92
|
throw new Error(`invalid attribute '${name}': outside bounds`);
|
|
93
|
+
}
|
|
64
94
|
}
|
|
65
95
|
|
|
66
96
|
export function validateAttributeSpec(spec: AttrSpec, name: string): void {
|
|
67
|
-
if (spec == null || typeof spec !== 'object' || Array.isArray(spec))
|
|
97
|
+
if (spec == null || typeof spec !== 'object' || Array.isArray(spec)) {
|
|
68
98
|
throw new Error(`invalid attribute '${name}' declaration`);
|
|
69
|
-
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (Object.keys(spec).some(key => ![ 'default',
|
|
102
|
+
'type',
|
|
103
|
+
'enum',
|
|
104
|
+
'min',
|
|
105
|
+
'max' ].includes(key))) {
|
|
70
106
|
throw new Error(`invalid attribute '${name}' declaration field`);
|
|
107
|
+
}
|
|
108
|
+
|
|
71
109
|
if (
|
|
72
110
|
spec.type !== undefined &&
|
|
73
|
-
!['string',
|
|
74
|
-
|
|
111
|
+
![ 'string',
|
|
112
|
+
'number',
|
|
113
|
+
'boolean',
|
|
114
|
+
'object',
|
|
115
|
+
'array' ].includes(spec.type)
|
|
116
|
+
) {
|
|
75
117
|
throw new Error(`invalid attribute '${name}' type`);
|
|
118
|
+
}
|
|
119
|
+
|
|
76
120
|
if (spec.min !== undefined || spec.max !== undefined) {
|
|
77
121
|
if (
|
|
78
|
-
!['number', 'string', 'array'].includes(spec.type ?? '') ||
|
|
122
|
+
![ 'number', 'string', 'array' ].includes(spec.type ?? '') ||
|
|
79
123
|
(spec.min !== undefined && !Number.isFinite(spec.min)) ||
|
|
80
124
|
(spec.max !== undefined && !Number.isFinite(spec.max)) ||
|
|
81
125
|
(spec.min !== undefined && spec.max !== undefined && spec.min > spec.max)
|
|
82
|
-
)
|
|
126
|
+
) {
|
|
83
127
|
throw new Error(`invalid attribute '${name}' bounds`);
|
|
128
|
+
}
|
|
129
|
+
|
|
84
130
|
if (
|
|
85
131
|
spec.type !== 'number' &&
|
|
86
|
-
[spec.min, spec.max].some(
|
|
87
|
-
|
|
132
|
+
[ spec.min, spec.max ].some(
|
|
133
|
+
bound => bound !== undefined && (!Number.isInteger(bound) || bound < 0)
|
|
88
134
|
)
|
|
89
|
-
)
|
|
135
|
+
) {
|
|
90
136
|
throw new Error(`invalid attribute '${name}' length bounds`);
|
|
137
|
+
}
|
|
91
138
|
}
|
|
139
|
+
|
|
92
140
|
if (spec.enum !== undefined) {
|
|
93
|
-
if (!Array.isArray(spec.enum) || spec.enum.length === 0)
|
|
141
|
+
if (!Array.isArray(spec.enum) || spec.enum.length === 0) {
|
|
94
142
|
throw new Error(`invalid attribute '${name}' enum`);
|
|
95
|
-
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (new Set(spec.enum.map(jsonType)).size !== 1) {
|
|
96
146
|
throw new Error(`invalid attribute '${name}': enum values must share one JSON type`);
|
|
97
|
-
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
for (const entry of spec.enum) {
|
|
150
|
+
validateAttribute(entry, { ...spec, enum: undefined }, name);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (spec.default !== undefined) {
|
|
155
|
+
validateAttribute(spec.default, spec, name);
|
|
98
156
|
}
|
|
99
|
-
if (spec.default !== undefined) validateAttribute(spec.default, spec, name);
|
|
100
157
|
}
|
|
101
158
|
|
|
102
159
|
export function validateAttributes(
|
|
103
160
|
attrs: Record<string, unknown>,
|
|
104
161
|
specs: Record<string, AttrSpec>
|
|
105
162
|
): void {
|
|
106
|
-
for (const name of Object.keys(attrs))
|
|
107
|
-
if (!Object.prototype.hasOwnProperty.call(specs, name))
|
|
163
|
+
for (const name of Object.keys(attrs)) {
|
|
164
|
+
if (!Object.prototype.hasOwnProperty.call(specs, name)) {
|
|
108
165
|
throw new Error(`undeclared attribute '${name}'`);
|
|
109
|
-
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
for (const [ name, spec ] of Object.entries(specs)) {
|
|
110
170
|
const value = Object.prototype.hasOwnProperty.call(attrs, name)
|
|
111
171
|
? attrs[name]
|
|
112
172
|
: spec.default;
|
|
113
|
-
|
|
173
|
+
|
|
174
|
+
if (value === undefined) {
|
|
175
|
+
throw new Error(`required attribute '${name}' missing`);
|
|
176
|
+
}
|
|
177
|
+
|
|
114
178
|
validateAttribute(value, spec, name);
|
|
115
179
|
}
|
|
116
180
|
}
|