@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.
Files changed (161) hide show
  1. package/README.md +1 -1
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/android/build.gradle +3 -0
  4. package/app.plugin.js +42 -39
  5. package/dist/AtomHost.js +18 -11
  6. package/dist/EditorAddon.js +4 -2
  7. package/dist/EditorAddonNormalization.js +17 -9
  8. package/dist/EditorMentionThemeNormalization.js +6 -3
  9. package/dist/EditorMentionThemeValidation.js +28 -11
  10. package/dist/EditorStyleSheetNormalization.js +84 -37
  11. package/dist/EditorTheme.d.ts +4 -0
  12. package/dist/EditorTheme.js +5 -3
  13. package/dist/EditorToolbarComponent.d.ts +1 -1
  14. package/dist/EditorToolbarComponent.js +1 -1
  15. package/dist/EditorToolbarRegistry.js +5 -5
  16. package/dist/EditorToolbarTypes.d.ts +12 -12
  17. package/dist/EditorToolbarVisuals.js +1 -1
  18. package/dist/ExternalTextComposition.js +89 -43
  19. package/dist/ImageLoadingPolicy.d.ts +1 -1
  20. package/dist/NativeEditorBoundaryError.d.ts +7 -7
  21. package/dist/NativeEditorBoundaryError.js +9 -5
  22. package/dist/NativeEditorCollaborationTransport.d.ts +7 -7
  23. package/dist/NativeEditorCollaborationTransport.js +28 -15
  24. package/dist/NativeEditorCreateConfig.js +11 -6
  25. package/dist/NativeEditorCreateJson.js +51 -25
  26. package/dist/NativeEditorCreateValidation.js +11 -6
  27. package/dist/NativeEditorDocumentBridge.d.ts +1 -1
  28. package/dist/NativeEditorDocumentBridge.js +32 -18
  29. package/dist/NativeEditorDocumentHandle.d.ts +4 -4
  30. package/dist/NativeEditorDocumentHandle.js +1 -1
  31. package/dist/NativeEditorLocalAwareness.js +25 -13
  32. package/dist/NativeEditorNativeModule.d.ts +8 -1
  33. package/dist/NativeEditorNativeModule.js +7 -5
  34. package/dist/NativeEditorRenderNormalization.js +66 -25
  35. package/dist/NativeEditorResultNormalization.js +53 -29
  36. package/dist/NativeEditorTypes.d.ts +10 -10
  37. package/dist/NativeEditorV2Decimal.js +6 -3
  38. package/dist/NativeProseViewer.js +11 -3
  39. package/dist/ResourceLimits.d.ts +2 -2
  40. package/dist/ResourceLimits.js +7 -2
  41. package/dist/RichTextEditor.js +1 -1
  42. package/dist/RichTextEditorNativeTypes.d.ts +1 -1
  43. package/dist/RichTextEditorNativeView.d.ts +1 -3
  44. package/dist/RichTextEditorSerialization.js +24 -14
  45. package/dist/RichTextEditorTypes.d.ts +1 -1
  46. package/dist/YjsCollaboration.d.ts +7 -7
  47. package/dist/YjsCollaboration.js +42 -26
  48. package/dist/addons.d.ts +4 -4
  49. package/dist/addons.js +3 -3
  50. package/dist/atomInstances.js +6 -3
  51. package/dist/atomUpdates.js +7 -4
  52. package/dist/atoms.js +20 -12
  53. package/dist/attributeValidation.js +55 -24
  54. package/dist/contentExpression.js +80 -42
  55. package/dist/listMarkers.d.ts +1 -1
  56. package/dist/listMarkers.js +1 -1
  57. package/dist/schemaDefinition.d.ts +1 -1
  58. package/dist/schemaDefinition.js +23 -14
  59. package/dist/schemaDocument.js +1 -1
  60. package/dist/schemaDocumentConstruction.js +25 -14
  61. package/dist/schemaNormalization.js +51 -29
  62. package/dist/schemaPresets.d.ts +1 -1
  63. package/dist/schemaPresets.js +9 -4
  64. package/dist/schemaResolution.js +46 -25
  65. package/dist/specs/PreparedProseViewerNativeComponent.d.ts +4 -0
  66. package/dist/useEditorToolbarInteractions.d.ts +2 -2
  67. package/dist/useEditorToolbarInteractions.js +45 -23
  68. package/dist/useEditorToolbarItems.d.ts +1 -1
  69. package/dist/useEditorToolbarItems.js +4 -3
  70. package/dist/useEditorToolbarPresentation.d.ts +3 -3
  71. package/dist/useEditorToolbarPresentation.js +4 -4
  72. package/dist/useEditorToolbarState.d.ts +1 -1
  73. package/dist/useEditorToolbarState.js +1 -1
  74. package/dist/useFocusPreservingFrames.js +11 -8
  75. package/dist/useNativeEditor.d.ts +1 -1
  76. package/dist/useNativeEditor.js +21 -11
  77. package/dist/useRichTextEditorCommands.d.ts +2 -2
  78. package/dist/useRichTextEditorCommands.js +59 -29
  79. package/dist/useRichTextEditorEvents.d.ts +3 -3
  80. package/dist/useRichTextEditorEvents.js +75 -32
  81. package/dist/useRichTextEditorMentions.d.ts +4 -4
  82. package/dist/useRichTextEditorMentions.js +48 -29
  83. package/dist/useRichTextEditorPresentation.d.ts +4 -4
  84. package/dist/useRichTextEditorPresentation.js +25 -23
  85. package/dist/useRichTextEditorState.d.ts +3 -3
  86. package/dist/useRichTextEditorState.js +20 -15
  87. package/dist/useRichTextEditorUpdates.d.ts +1 -1
  88. package/dist/useRichTextEditorUpdates.js +99 -41
  89. package/dist/useViewerAtoms.js +63 -36
  90. package/ios/EditorCore.xcframework/Info.plist +5 -5
  91. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  92. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  93. package/package.json +11 -3
  94. package/react-native.config.js +1 -1
  95. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  96. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  97. package/rust/android/x86/libeditor_core.so +0 -0
  98. package/rust/android/x86_64/libeditor_core.so +0 -0
  99. package/src/AtomHost.tsx +58 -28
  100. package/src/EditorAddon.ts +18 -8
  101. package/src/EditorAddonNormalization.ts +40 -11
  102. package/src/EditorMentionThemeNormalization.ts +15 -4
  103. package/src/EditorMentionThemeValidation.ts +51 -14
  104. package/src/EditorStyleSheet.ts +1 -0
  105. package/src/EditorStyleSheetNormalization.ts +195 -64
  106. package/src/EditorTheme.ts +13 -5
  107. package/src/EditorToolbarComponent.tsx +2 -1
  108. package/src/EditorToolbarRegistry.tsx +23 -13
  109. package/src/EditorToolbarTypes.tsx +12 -12
  110. package/src/EditorToolbarVisuals.tsx +6 -3
  111. package/src/EditorUpdateRevision.ts +1 -0
  112. package/src/ExternalTextComposition.ts +259 -49
  113. package/src/ImageLoadingPolicy.ts +3 -1
  114. package/src/NativeEditorBoundaryError.ts +38 -11
  115. package/src/NativeEditorCollaborationTransport.ts +108 -27
  116. package/src/NativeEditorCreateConfig.ts +92 -19
  117. package/src/NativeEditorCreateJson.ts +163 -26
  118. package/src/NativeEditorCreateValidation.ts +33 -12
  119. package/src/NativeEditorDocumentBridge.ts +111 -19
  120. package/src/NativeEditorDocumentHandle.ts +9 -4
  121. package/src/NativeEditorLocalAwareness.ts +70 -15
  122. package/src/NativeEditorNativeModule.ts +13 -9
  123. package/src/NativeEditorRenderNormalization.ts +128 -29
  124. package/src/NativeEditorResultNormalization.ts +143 -32
  125. package/src/NativeEditorTypes.ts +12 -11
  126. package/src/NativeEditorV2Decimal.ts +12 -3
  127. package/src/NativeProseViewer.tsx +46 -16
  128. package/src/ResourceLimits.ts +15 -8
  129. package/src/RichTextEditor.tsx +3 -1
  130. package/src/RichTextEditorNativeTypes.tsx +1 -1
  131. package/src/RichTextEditorNativeView.tsx +2 -8
  132. package/src/RichTextEditorSerialization.tsx +70 -24
  133. package/src/RichTextEditorTypes.tsx +1 -1
  134. package/src/YjsCollaboration.ts +143 -47
  135. package/src/addons.ts +12 -8
  136. package/src/atomInstances.ts +25 -5
  137. package/src/atomUpdates.ts +23 -6
  138. package/src/atoms.ts +85 -18
  139. package/src/attributeValidation.ts +97 -33
  140. package/src/contentExpression.ts +218 -63
  141. package/src/listMarkers.ts +1 -1
  142. package/src/schemaDefinition.ts +82 -20
  143. package/src/schemaDocument.ts +15 -1
  144. package/src/schemaDocumentConstruction.ts +80 -16
  145. package/src/schemaNormalization.ts +155 -41
  146. package/src/schemaPresets.ts +37 -31
  147. package/src/schemaResolution.ts +126 -27
  148. package/src/specs/PreparedProseViewerNativeComponent.ts +4 -2
  149. package/src/useEditorToolbarInteractions.tsx +85 -36
  150. package/src/useEditorToolbarItems.tsx +50 -22
  151. package/src/useEditorToolbarPresentation.tsx +81 -58
  152. package/src/useEditorToolbarState.tsx +8 -6
  153. package/src/useFocusPreservingFrames.ts +43 -15
  154. package/src/useNativeEditor.ts +111 -51
  155. package/src/useRichTextEditorCommands.tsx +127 -54
  156. package/src/useRichTextEditorEvents.tsx +144 -42
  157. package/src/useRichTextEditorMentions.tsx +118 -37
  158. package/src/useRichTextEditorPresentation.tsx +128 -114
  159. package/src/useRichTextEditorState.tsx +76 -40
  160. package/src/useRichTextEditorUpdates.tsx +207 -56
  161. package/src/useViewerAtoms.tsx +162 -66
@@ -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') return value;
6
- if (typeof value === 'number' && Number.isFinite(value)) return value;
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, (entry) => copyJson(entry, ancestors))
34
+ ? Array.from(value, entry => copyJson(entry, ancestors))
26
35
  : Object.fromEntries(
27
- Object.entries(value).map(([key, entry]) => [key, copyJson(entry, ancestors)])
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
- for (const entry of Array.isArray(update) ? update : [update]) {
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') return true;
132
- if (value == null || typeof value !== 'object' || !('$$typeof' in value)) return false;
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
- for (const [key, entry] of Object.entries(value)) {
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
- if (Object.keys(raw).some((key) => !['tag', 'staticAttrs', 'attrMap'].includes(key))) {
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
- Object.keys(attrs).map((name) => [name, `data-${kebabCase(name)}`])
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
- if ('allowUndeclaredAttrs' in (config as unknown as Record<string, unknown>)) {
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
- if (config.idAttribute !== undefined && config.attrs?.[config.idAttribute]?.type !== 'string')
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)) return true;
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((node) => node.html != null);
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
- if (left.html?.tag !== right.html?.tag) continue;
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((node) => node.name === atom.name);
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) return undefined;
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)) continue;
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') return;
9
- if (typeof value === 'number' && Number.isFinite(value)) return;
10
- if (typeof value !== 'object' || ancestors.has(value) || ancestors.size > 128)
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
- for (let index = 0; index < entries.length; index += 1) validateJson(entries[index], ancestors);
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) return true;
26
- if (left == null || right == null || typeof left !== 'object' || typeof right !== 'object')
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
- if (Array.isArray(left) !== Array.isArray(right)) return false;
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
- (key) =>
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
- if ((spec.type != null && type !== spec.type) || (type === 'number' && !Number.isFinite(value)))
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
- if (spec.enum != null && !spec.enum.some((entry) => equal(entry, value)))
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
- ? [...value].length
56
- : Array.isArray(value)
57
- ? value.length
58
- : undefined;
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
- if (Object.keys(spec).some((key) => !['default', 'type', 'enum', 'min', 'max'].includes(key)))
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', 'number', 'boolean', 'object', 'array'].includes(spec.type)
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
- (bound) => bound !== undefined && (!Number.isInteger(bound) || bound < 0)
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
- if (new Set(spec.enum.map(jsonType)).size !== 1)
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
- for (const entry of spec.enum) validateAttribute(entry, { ...spec, enum: undefined }, name);
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
- for (const [name, spec] of Object.entries(specs)) {
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
- if (value === undefined) throw new Error(`required attribute '${name}' missing`);
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
  }