@apollohg/react-native-rich-text-editor 2.0.0 → 2.0.2

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 (180) hide show
  1. package/README.md +8 -116
  2. package/THIRD_PARTY_NOTICES.md +1 -1
  3. package/android/build.gradle +3 -0
  4. package/android/src/main/java/com/apollohg/editor/EditorEditTextImages.kt +25 -0
  5. package/android/src/main/java/com/apollohg/editor/EditorV2RenderValidation.kt +1 -0
  6. package/android/src/main/java/com/apollohg/editor/ImageResizeOverlayView.kt +101 -33
  7. package/android/src/main/java/com/apollohg/editor/RenderMediaSpans.kt +15 -3
  8. package/android/src/main/java/com/apollohg/editor/RichTextEditorView.kt +14 -0
  9. package/android/src/main/java/com/apollohg/editor/viewer/AndroidProseLayoutEngine.kt +1 -1
  10. package/app.plugin.js +42 -39
  11. package/dist/AtomHost.js +18 -11
  12. package/dist/EditorAddon.js +4 -2
  13. package/dist/EditorAddonNormalization.js +17 -9
  14. package/dist/EditorMentionThemeNormalization.js +6 -3
  15. package/dist/EditorMentionThemeValidation.js +28 -11
  16. package/dist/EditorStyleSheetNormalization.js +85 -38
  17. package/dist/EditorStyleSheetTypes.d.ts +1 -1
  18. package/dist/EditorTheme.d.ts +4 -0
  19. package/dist/EditorTheme.js +5 -3
  20. package/dist/EditorToolbarComponent.d.ts +1 -1
  21. package/dist/EditorToolbarComponent.js +1 -1
  22. package/dist/EditorToolbarRegistry.js +5 -5
  23. package/dist/EditorToolbarTypes.d.ts +12 -12
  24. package/dist/EditorToolbarVisuals.js +1 -1
  25. package/dist/ExternalTextComposition.js +89 -43
  26. package/dist/ImageLoadingPolicy.d.ts +1 -1
  27. package/dist/NativeEditorBoundaryError.d.ts +7 -7
  28. package/dist/NativeEditorBoundaryError.js +9 -5
  29. package/dist/NativeEditorCollaborationTransport.d.ts +7 -7
  30. package/dist/NativeEditorCollaborationTransport.js +28 -15
  31. package/dist/NativeEditorCreateConfig.js +11 -6
  32. package/dist/NativeEditorCreateJson.js +51 -25
  33. package/dist/NativeEditorCreateValidation.js +11 -6
  34. package/dist/NativeEditorDocumentBridge.d.ts +1 -1
  35. package/dist/NativeEditorDocumentBridge.js +32 -18
  36. package/dist/NativeEditorDocumentHandle.d.ts +4 -4
  37. package/dist/NativeEditorDocumentHandle.js +1 -1
  38. package/dist/NativeEditorLocalAwareness.js +25 -13
  39. package/dist/NativeEditorNativeModule.d.ts +8 -1
  40. package/dist/NativeEditorNativeModule.js +7 -5
  41. package/dist/NativeEditorRenderNormalization.js +66 -25
  42. package/dist/NativeEditorResultNormalization.js +53 -29
  43. package/dist/NativeEditorTypes.d.ts +10 -10
  44. package/dist/NativeEditorV2Decimal.js +6 -3
  45. package/dist/NativeProseViewer.js +11 -3
  46. package/dist/ResourceLimits.d.ts +2 -2
  47. package/dist/ResourceLimits.js +7 -2
  48. package/dist/RichTextEditor.js +1 -1
  49. package/dist/RichTextEditorNativeTypes.d.ts +1 -1
  50. package/dist/RichTextEditorNativeView.d.ts +1 -3
  51. package/dist/RichTextEditorSerialization.js +24 -14
  52. package/dist/RichTextEditorTypes.d.ts +1 -1
  53. package/dist/YjsCollaboration.d.ts +7 -7
  54. package/dist/YjsCollaboration.js +42 -26
  55. package/dist/addons.d.ts +4 -4
  56. package/dist/addons.js +3 -3
  57. package/dist/atomInstances.js +6 -3
  58. package/dist/atomUpdates.js +7 -4
  59. package/dist/atoms.js +20 -12
  60. package/dist/attributeValidation.js +55 -24
  61. package/dist/contentExpression.js +80 -42
  62. package/dist/listMarkers.d.ts +1 -1
  63. package/dist/listMarkers.js +1 -1
  64. package/dist/schemaDefinition.d.ts +1 -1
  65. package/dist/schemaDefinition.js +23 -14
  66. package/dist/schemaDocument.js +1 -1
  67. package/dist/schemaDocumentConstruction.js +25 -14
  68. package/dist/schemaNormalization.js +51 -29
  69. package/dist/schemaPresets.d.ts +1 -1
  70. package/dist/schemaPresets.js +9 -4
  71. package/dist/schemaResolution.js +46 -25
  72. package/dist/specs/PreparedProseViewerNativeComponent.d.ts +4 -0
  73. package/dist/useEditorToolbarInteractions.d.ts +2 -2
  74. package/dist/useEditorToolbarInteractions.js +45 -23
  75. package/dist/useEditorToolbarItems.d.ts +1 -1
  76. package/dist/useEditorToolbarItems.js +4 -3
  77. package/dist/useEditorToolbarPresentation.d.ts +3 -3
  78. package/dist/useEditorToolbarPresentation.js +4 -4
  79. package/dist/useEditorToolbarState.d.ts +1 -1
  80. package/dist/useEditorToolbarState.js +1 -1
  81. package/dist/useFocusPreservingFrames.js +11 -8
  82. package/dist/useNativeEditor.d.ts +1 -1
  83. package/dist/useNativeEditor.js +21 -11
  84. package/dist/useRichTextEditorCommands.d.ts +2 -2
  85. package/dist/useRichTextEditorCommands.js +59 -29
  86. package/dist/useRichTextEditorEvents.d.ts +3 -3
  87. package/dist/useRichTextEditorEvents.js +75 -32
  88. package/dist/useRichTextEditorMentions.d.ts +4 -4
  89. package/dist/useRichTextEditorMentions.js +48 -29
  90. package/dist/useRichTextEditorPresentation.d.ts +4 -4
  91. package/dist/useRichTextEditorPresentation.js +25 -23
  92. package/dist/useRichTextEditorState.d.ts +3 -3
  93. package/dist/useRichTextEditorState.js +20 -15
  94. package/dist/useRichTextEditorUpdates.d.ts +1 -1
  95. package/dist/useRichTextEditorUpdates.js +99 -41
  96. package/dist/useViewerAtoms.js +63 -36
  97. package/ios/EditorCore.xcframework/ios-arm64/libeditor_core.a +0 -0
  98. package/ios/EditorCore.xcframework/ios-arm64_x86_64-simulator/libeditor_core.a +0 -0
  99. package/ios/EditorInteractionOverlays.swift +114 -122
  100. package/ios/EditorLayoutManager.swift +13 -3
  101. package/ios/EditorStyleSheet.swift +12 -1
  102. package/ios/EditorTextView+Images.swift +32 -10
  103. package/ios/EditorTextView.swift +1 -1
  104. package/ios/EditorV2Adapter+RenderValidation.swift +2 -2
  105. package/ios/NativeToolbar+ButtonStyle.swift +17 -5
  106. package/ios/NativeToolbar+Diagnostics.swift +15 -0
  107. package/ios/RenderBridge+Atoms.swift +0 -1
  108. package/ios/RenderBridge+Attachments.swift +0 -29
  109. package/ios/RichTextEditorView.swift +4 -12
  110. package/ios/Viewer/CoreTextProseLayoutEngine+AttributedText.swift +3 -2
  111. package/package.json +11 -3
  112. package/react-native.config.js +1 -1
  113. package/rust/android/arm64-v8a/libeditor_core.so +0 -0
  114. package/rust/android/armeabi-v7a/libeditor_core.so +0 -0
  115. package/rust/android/x86/libeditor_core.so +0 -0
  116. package/rust/android/x86_64/libeditor_core.so +0 -0
  117. package/src/AtomHost.tsx +58 -28
  118. package/src/EditorAddon.ts +18 -8
  119. package/src/EditorAddonNormalization.ts +40 -11
  120. package/src/EditorMentionThemeNormalization.ts +15 -4
  121. package/src/EditorMentionThemeValidation.ts +51 -14
  122. package/src/EditorStyleSheet.ts +1 -0
  123. package/src/EditorStyleSheetNormalization.ts +195 -64
  124. package/src/EditorStyleSheetTypes.ts +1 -1
  125. package/src/EditorTheme.ts +13 -5
  126. package/src/EditorToolbarComponent.tsx +2 -1
  127. package/src/EditorToolbarRegistry.tsx +23 -13
  128. package/src/EditorToolbarTypes.tsx +12 -12
  129. package/src/EditorToolbarVisuals.tsx +6 -3
  130. package/src/EditorUpdateRevision.ts +1 -0
  131. package/src/ExternalTextComposition.ts +259 -49
  132. package/src/ImageLoadingPolicy.ts +3 -1
  133. package/src/NativeEditorBoundaryError.ts +38 -11
  134. package/src/NativeEditorCollaborationTransport.ts +108 -27
  135. package/src/NativeEditorCreateConfig.ts +92 -19
  136. package/src/NativeEditorCreateJson.ts +163 -26
  137. package/src/NativeEditorCreateValidation.ts +33 -12
  138. package/src/NativeEditorDocumentBridge.ts +111 -19
  139. package/src/NativeEditorDocumentHandle.ts +9 -4
  140. package/src/NativeEditorLocalAwareness.ts +70 -15
  141. package/src/NativeEditorNativeModule.ts +13 -9
  142. package/src/NativeEditorRenderNormalization.ts +128 -29
  143. package/src/NativeEditorResultNormalization.ts +143 -32
  144. package/src/NativeEditorTypes.ts +12 -11
  145. package/src/NativeEditorV2Decimal.ts +12 -3
  146. package/src/NativeProseViewer.tsx +46 -16
  147. package/src/ResourceLimits.ts +15 -8
  148. package/src/RichTextEditor.tsx +3 -1
  149. package/src/RichTextEditorNativeTypes.tsx +1 -1
  150. package/src/RichTextEditorNativeView.tsx +2 -8
  151. package/src/RichTextEditorSerialization.tsx +70 -24
  152. package/src/RichTextEditorTypes.tsx +1 -1
  153. package/src/YjsCollaboration.ts +143 -47
  154. package/src/addons.ts +12 -8
  155. package/src/atomInstances.ts +25 -5
  156. package/src/atomUpdates.ts +23 -6
  157. package/src/atoms.ts +85 -18
  158. package/src/attributeValidation.ts +97 -33
  159. package/src/contentExpression.ts +218 -63
  160. package/src/listMarkers.ts +1 -1
  161. package/src/schemaDefinition.ts +82 -20
  162. package/src/schemaDocument.ts +15 -1
  163. package/src/schemaDocumentConstruction.ts +80 -16
  164. package/src/schemaNormalization.ts +155 -41
  165. package/src/schemaPresets.ts +37 -31
  166. package/src/schemaResolution.ts +126 -27
  167. package/src/specs/PreparedProseViewerNativeComponent.ts +4 -2
  168. package/src/useEditorToolbarInteractions.tsx +85 -36
  169. package/src/useEditorToolbarItems.tsx +50 -22
  170. package/src/useEditorToolbarPresentation.tsx +81 -58
  171. package/src/useEditorToolbarState.tsx +8 -6
  172. package/src/useFocusPreservingFrames.ts +43 -15
  173. package/src/useNativeEditor.ts +111 -51
  174. package/src/useRichTextEditorCommands.tsx +127 -54
  175. package/src/useRichTextEditorEvents.tsx +144 -42
  176. package/src/useRichTextEditorMentions.tsx +118 -37
  177. package/src/useRichTextEditorPresentation.tsx +128 -114
  178. package/src/useRichTextEditorState.tsx +76 -40
  179. package/src/useRichTextEditorUpdates.tsx +207 -56
  180. package/src/useViewerAtoms.tsx +162 -66
@@ -71,7 +71,7 @@ export interface NodeSpec {
71
71
  * `insert_content_json`) admits attrs on this node that are not declared
72
72
  * in `attrs`, instead of filtering them out. Default `false`. Intended
73
73
  * for node types with an intentional pass-through-metadata contract
74
- * (e.g. the mention node see `mentionNodeSpec()` in addons.ts).
74
+ * (e.g. the mention node : see `mentionNodeSpec()` in addons.ts).
75
75
  */
76
76
  allowUndeclaredAttrs?: boolean;
77
77
  /** Public JSON representation when it differs from the native node name. */
@@ -165,18 +165,35 @@ export function outputTag(spec: DOMOutputSpec): string {
165
165
 
166
166
  export function appendGroup(group: string | undefined, name: string): string {
167
167
  const groups = group?.split(/\s+/).filter(Boolean) ?? [];
168
- if (!groups.includes(name)) groups.push(name);
168
+
169
+ if (!groups.includes(name)) {
170
+ groups.push(name);
171
+ }
172
+
169
173
  return groups.join(' ');
170
174
  }
171
175
 
172
176
  export function schemaNodeRole(name: string, node: SchemaNodeSpec): string {
173
- if (node.role != null) return node.role === 'heading' ? 'textBlock' : node.role;
174
- if (name === 'doc') return 'doc';
175
- if (name === 'text') return 'text';
177
+ if (node.role != null) {
178
+ return node.role === 'heading' ? 'textBlock' : node.role;
179
+ }
180
+
181
+ if (name === 'doc') {
182
+ return 'doc';
183
+ }
184
+
185
+ if (name === 'text') {
186
+ return 'text';
187
+ }
188
+
176
189
  if (node.content === 'inline*' && node.group?.split(/\s+/).includes('block')) {
177
190
  return 'textBlock';
178
191
  }
179
- if (node.group?.split(/\s+/).includes('inline')) return 'inline';
192
+
193
+ if (node.group?.split(/\s+/).includes('inline')) {
194
+ return 'inline';
195
+ }
196
+
180
197
  return 'block';
181
198
  }
182
199
 
@@ -188,16 +205,24 @@ export function caseAttributeValue(
188
205
  defaultValue: unknown
189
206
  ): unknown {
190
207
  const rule = parseDOM?.find(
191
- (candidate) =>
208
+ candidate =>
192
209
  candidate.tag === tag &&
193
210
  candidate.attrs != null &&
194
211
  String(candidate.attrs[attribute]) === key
195
212
  );
213
+
196
214
  if (rule?.attrs && Object.prototype.hasOwnProperty.call(rule.attrs, attribute)) {
197
215
  return rule.attrs[attribute];
198
216
  }
199
- if (typeof defaultValue === 'number') return Number(key);
200
- if (typeof defaultValue === 'boolean') return key === 'true';
217
+
218
+ if (typeof defaultValue === 'number') {
219
+ return Number(key);
220
+ }
221
+
222
+ if (typeof defaultValue === 'boolean') {
223
+ return key === 'true';
224
+ }
225
+
201
226
  return key;
202
227
  }
203
228
 
@@ -208,62 +233,75 @@ export function validateAttributeDOMRules(
208
233
  defaultValue: unknown
209
234
  ): void {
210
235
  const cases = Object.entries(switched.cases);
236
+
211
237
  let discriminatorType =
212
238
  typeof defaultValue === 'string' ||
213
239
  typeof defaultValue === 'number' ||
214
240
  typeof defaultValue === 'boolean'
215
241
  ? typeof defaultValue
216
242
  : undefined;
243
+
217
244
  if (discriminatorType == null && parseDOM != null) {
218
- const parsedTypes = parseDOM.map((rule) => {
245
+ const parsedTypes = parseDOM.map(rule => {
219
246
  const value = rule.attrs?.[switched.switchOn];
247
+
220
248
  return typeof value === 'string' ||
221
249
  typeof value === 'number' ||
222
250
  typeof value === 'boolean'
223
251
  ? typeof value
224
252
  : undefined;
225
253
  });
254
+
226
255
  const firstType = parsedTypes[0];
227
- if (firstType != null && parsedTypes.every((type) => type === firstType)) {
256
+
257
+ if (firstType != null && parsedTypes.every(type => type === firstType)) {
228
258
  discriminatorType = firstType;
229
259
  }
230
260
  }
261
+
231
262
  if (discriminatorType == null) {
232
263
  throw new Error(
233
264
  `node '${name}' DOM discriminator '${switched.switchOn}' must have a scalar type`
234
265
  );
235
266
  }
267
+
236
268
  if (parseDOM == null) {
237
- const validCases = cases.every(([caseKey]) => {
269
+ const validCases = cases.every(([ caseKey ]) => {
238
270
  if (discriminatorType === 'number') {
239
271
  return /^-?(?:0|[1-9]\d*)(?:\.\d+)?$/.test(caseKey);
240
272
  }
273
+
241
274
  return discriminatorType !== 'boolean' || caseKey === 'true' || caseKey === 'false';
242
275
  });
276
+
243
277
  if (!validCases) {
244
278
  throw new Error(
245
279
  `node '${name}' DOM discriminator '${switched.switchOn}' must use ${discriminatorType} values`
246
280
  );
247
281
  }
282
+
248
283
  return;
249
284
  }
250
- const matchesCase = ([caseKey, output]: [string, DOMOutputSpec]) =>
285
+
286
+ const matchesCase = ([ caseKey, output ]: [string, DOMOutputSpec]) =>
251
287
  parseDOM.filter(
252
- (rule) =>
288
+ rule =>
253
289
  rule.tag === outputTag(output) &&
254
290
  rule.attrs != null &&
255
291
  Object.prototype.hasOwnProperty.call(rule.attrs, switched.switchOn) &&
256
292
  String(rule.attrs[switched.switchOn]) === caseKey
257
293
  ).length === 1;
294
+
258
295
  if (parseDOM.length !== cases.length || !cases.every(matchesCase)) {
259
296
  throw new Error(
260
297
  `node '${name}' DOM parse rules must map one-to-one with '${switched.switchOn}' output cases`
261
298
  );
262
299
  }
300
+
263
301
  if (
264
302
  discriminatorType != null &&
265
303
  parseDOM.some(
266
- (rule) =>
304
+ rule =>
267
305
  rule.attrs != null && typeof rule.attrs[switched.switchOn] !== discriminatorType
268
306
  )
269
307
  ) {
@@ -282,11 +320,14 @@ export function staticDOMTag(
282
320
  if ((parseDOM?.length ?? 0) > 1) {
283
321
  throw new Error(`${kind} '${name}' has multiple static DOM parse rules`);
284
322
  }
323
+
285
324
  const parsed = parseDOM?.[0]?.tag;
286
325
  const serialized = toDOM == null ? undefined : outputTag(toDOM);
326
+
287
327
  if (parsed != null && serialized != null && parsed !== serialized) {
288
328
  throw new Error(`${kind} '${name}' parses '${parsed}' but serializes '${serialized}'`);
289
329
  }
330
+
290
331
  return serialized ?? parsed;
291
332
  }
292
333
 
@@ -294,7 +335,8 @@ export function staticDOMTag(
294
335
  export function defineSchema(spec: SchemaSpec): SchemaDefinition {
295
336
  const nodes: NodeSpec[] = [];
296
337
  const names = new Set<string>();
297
- for (const [name, node] of Object.entries(spec.nodes)) {
338
+
339
+ for (const [ name, node ] of Object.entries(spec.nodes)) {
298
340
  const common = {
299
341
  content: node.content ?? '',
300
342
  ...(node.group == null ? {} : { group: node.group }),
@@ -309,11 +351,14 @@ export function defineSchema(spec: SchemaSpec): SchemaDefinition {
309
351
  ? {}
310
352
  : { allowUndeclaredAttrs: node.allowUndeclaredAttrs }),
311
353
  };
354
+
312
355
  if (node.toDOM != null && !Array.isArray(node.toDOM)) {
313
356
  const switched = node.toDOM as AttributeDOMOutputSpec;
357
+
314
358
  if (RESERVED_WIRE_NODE_TYPES.has(name)) {
315
359
  throw new Error(`node '${name}' uses a reserved wire projection type`);
316
360
  }
361
+
317
362
  if (
318
363
  node.attrs == null ||
319
364
  !Object.prototype.hasOwnProperty.call(node.attrs, switched.switchOn)
@@ -322,18 +367,25 @@ export function defineSchema(spec: SchemaSpec): SchemaDefinition {
322
367
  `node '${name}' switches on undeclared attribute '${switched.switchOn}'`
323
368
  );
324
369
  }
370
+
325
371
  if (Object.keys(switched.cases).length === 0) {
326
372
  throw new Error(`node '${name}' has no DOM output cases`);
327
373
  }
374
+
328
375
  const discriminatorDefault = node.attrs[switched.switchOn]?.default;
329
376
  validateAttributeDOMRules(name, switched, node.parseDOM, discriminatorDefault);
330
377
  const { attrs: _attrs, ...variantCommon } = common;
331
- for (const [caseKey, output] of Object.entries(switched.cases)) {
378
+
379
+ for (const [ caseKey, output ] of Object.entries(switched.cases)) {
332
380
  const tag = outputTag(output);
333
- if (names.has(tag))
381
+
382
+ if (names.has(tag)) {
334
383
  throw new Error(`schema produces duplicate native node '${tag}'`);
384
+ }
385
+
335
386
  names.add(tag);
336
387
  const { [switched.switchOn]: _discriminator, ...variantAttrs } = node.attrs ?? {};
388
+
337
389
  nodes.push({
338
390
  name: tag,
339
391
  ...variantCommon,
@@ -354,17 +406,23 @@ export function defineSchema(spec: SchemaSpec): SchemaDefinition {
354
406
  },
355
407
  });
356
408
  }
409
+
357
410
  continue;
358
411
  }
359
412
 
360
- if (names.has(name)) throw new Error(`schema produces duplicate native node '${name}'`);
413
+ if (names.has(name)) {
414
+ throw new Error(`schema produces duplicate native node '${name}'`);
415
+ }
416
+
361
417
  names.add(name);
418
+
362
419
  const tag = staticDOMTag(
363
420
  'node',
364
421
  name,
365
422
  node.parseDOM,
366
423
  node.toDOM as DOMOutputSpec | undefined
367
424
  );
425
+
368
426
  nodes.push({
369
427
  name,
370
428
  ...common,
@@ -372,12 +430,14 @@ export function defineSchema(spec: SchemaSpec): SchemaDefinition {
372
430
  });
373
431
  }
374
432
 
375
- const marks: MarkSpec[] = Object.entries(spec.marks ?? {}).map(([name, mark]) => {
433
+ const marks: MarkSpec[] = Object.entries(spec.marks ?? {}).map(([ name, mark ]) => {
376
434
  const tag = staticDOMTag('mark', name, mark.parseDOM, mark.toDOM);
377
435
  const normalizedTag = tag?.toLowerCase();
436
+
378
437
  if (normalizedTag != null && !ALLOWED_MARK_HTML_TAGS.has(normalizedTag)) {
379
438
  throw new Error(`mark '${name}' has disallowed HTML tag '${tag}'`);
380
439
  }
440
+
381
441
  return {
382
442
  name,
383
443
  ...(mark.attrs == null ? {} : { attrs: mark.attrs }),
@@ -388,7 +448,9 @@ export function defineSchema(spec: SchemaSpec): SchemaDefinition {
388
448
  : { allowUndeclaredAttrs: mark.allowUndeclaredAttrs }),
389
449
  };
390
450
  });
451
+
391
452
  const schema = { nodes, marks };
453
+
392
454
  return spec.atoms == null || spec.atoms.length === 0
393
455
  ? schema
394
456
  : withAtomsSchema(schema, spec.atoms);
@@ -20,26 +20,33 @@ export function defaultEmptyDocument(
20
20
  limits?: DocumentDescriptorLimits
21
21
  ): DocumentJSON {
22
22
  const resolvedLimits = resolveDescriptorLimits(limits);
23
+
23
24
  if (schema.nodes.length > resolvedLimits.maxSchemaNodes) {
24
25
  throw schemaBoundaryError(resolvedLimits.maxSchemaNodes, schema.nodes.length);
25
26
  }
27
+
26
28
  let expressionBytes = 0;
29
+
27
30
  for (const node of schema.nodes) {
28
31
  if (node != null && typeof node.content === 'string') {
29
32
  expressionBytes += utf8ByteLengthUpTo(
30
33
  node.content,
31
34
  resolvedLimits.maxSchemaExpressionBytes - expressionBytes
32
35
  );
36
+
33
37
  if (expressionBytes > resolvedLimits.maxSchemaExpressionBytes) {
34
38
  throw schemaBoundaryError(resolvedLimits.maxSchemaExpressionBytes, expressionBytes);
35
39
  }
36
40
  }
37
41
  }
42
+
38
43
  const admissionBudget = createSchemaWorkBudget(resolvedLimits);
39
44
  const admittedCollections = admitSchemaCollections(schema, admissionBudget);
45
+
40
46
  if (admittedCollections == null) {
41
47
  throw new Error('schema cannot construct a default document: invalid schema collections');
42
48
  }
49
+
43
50
  return constructDefaultEmptyDocument(schema, resolvedLimits, admittedCollections);
44
51
  }
45
52
 
@@ -60,10 +67,12 @@ export function resolveDocumentDescriptor(
60
67
  ): ResolvedDocumentSchema {
61
68
  const resolvedLimits = resolveDescriptorLimits(limits);
62
69
  const resolvedSchema = resolveDocumentSchema(schema, resolvedLimits);
63
- const documentNode = resolvedSchema.nodes.find((node) => node.role === 'doc');
70
+ const documentNode = resolvedSchema.nodes.find(node => node.role === 'doc');
71
+
64
72
  if (!documentNode) {
65
73
  throw new NativeEditorBoundaryError('SCHEMA_INVALID', 'schema has no document-role node');
66
74
  }
75
+
67
76
  return {
68
77
  schema: resolvedSchema,
69
78
  documentNodeName: documentNode.name,
@@ -80,13 +89,18 @@ export function normalizeDocumentJson(
80
89
  'documentNodeName' in schemaOrDescriptor
81
90
  ? schemaOrDescriptor
82
91
  : resolveDocumentDescriptor(schemaOrDescriptor, limits);
92
+
83
93
  const root = doc as { type?: unknown; content?: unknown } | null;
94
+
84
95
  if (root?.type !== descriptor.documentNodeName) {
85
96
  return doc;
86
97
  }
98
+
87
99
  const content = root?.content;
100
+
88
101
  if (Array.isArray(content) && content.length > 0) {
89
102
  return doc;
90
103
  }
104
+
91
105
  return descriptor.emptyDocument;
92
106
  }
@@ -13,22 +13,31 @@ export function constructDefaultEmptyDocument(
13
13
  resolvedLimits: ResolvedEditorResourceLimits,
14
14
  admittedCollections: AdmittedSchemaCollections
15
15
  ): DocumentJSON {
16
- const docNode = schema.nodes.find((node) => node.role === 'doc');
17
- if (!docNode) throw new Error('schema cannot construct a default document: missing doc role');
16
+ const docNode = schema.nodes.find(node => node.role === 'doc');
17
+
18
+ if (!docNode) {
19
+ throw new Error('schema cannot construct a default document: missing doc role');
20
+ }
18
21
 
19
22
  const budget = { nodes: 0, work: 0, exhausted: false };
23
+
20
24
  const workLimit = Math.min(
21
25
  DEFAULT_CONTENT_MAX_NODES,
22
26
  resolvedLimits.maxSchemaNodes * 64 + resolvedLimits.maxSchemaExpressionBytes * 32
23
27
  );
28
+
24
29
  const consumeWork = (): boolean => {
25
30
  if (budget.work >= workLimit) {
26
31
  budget.exhausted = true;
32
+
27
33
  return false;
28
34
  }
35
+
29
36
  budget.work += 1;
37
+
30
38
  return true;
31
39
  };
40
+
32
41
  const candidatePriority = (candidate: NodeSpec): number => {
33
42
  if (
34
43
  candidate.role === 'textBlock' &&
@@ -36,51 +45,75 @@ export function constructDefaultEmptyDocument(
36
45
  ) {
37
46
  return 0;
38
47
  }
48
+
39
49
  return candidate.role === 'textBlock' ? 1 : 2;
40
50
  };
51
+
41
52
  const candidatesBySymbol = new Map<string, NodeSpec[]>();
42
53
  let candidateIndexAdmitted = true;
54
+
43
55
  const addCandidate = (symbol: string, candidate: NodeSpec): boolean => {
44
- if (!consumeWork()) return false;
56
+ if (!consumeWork()) {
57
+ return false;
58
+ }
59
+
45
60
  const candidates = candidatesBySymbol.get(symbol);
46
- if (candidates) candidates.push(candidate);
47
- else candidatesBySymbol.set(symbol, [candidate]);
61
+
62
+ if (candidates) {
63
+ candidates.push(candidate);
64
+ } else {
65
+ candidatesBySymbol.set(symbol, [ candidate ]);
66
+ }
67
+
48
68
  return true;
49
69
  };
70
+
50
71
  for (const candidate of schema.nodes) {
51
72
  if (!addCandidate(candidate.name, candidate)) {
52
73
  candidateIndexAdmitted = false;
53
74
  break;
54
75
  }
76
+
55
77
  for (const group of admittedCollections.groupsByNode.get(candidate) ?? []) {
56
78
  if (group !== candidate.name && !addCandidate(group, candidate)) {
57
79
  candidateIndexAdmitted = false;
58
80
  break;
59
81
  }
60
82
  }
61
- if (!candidateIndexAdmitted) break;
83
+
84
+ if (!candidateIndexAdmitted) {
85
+ break;
86
+ }
62
87
  }
88
+
63
89
  for (const candidates of candidatesBySymbol.values()) {
64
90
  const sortingWork = Math.max(
65
91
  candidates.length,
66
92
  Math.ceil(candidates.length * Math.log2(Math.max(2, candidates.length)))
67
93
  );
94
+
68
95
  for (let index = 0; index < sortingWork; index += 1) {
69
96
  if (!consumeWork()) {
70
97
  candidateIndexAdmitted = false;
71
98
  break;
72
99
  }
73
100
  }
74
- if (!candidateIndexAdmitted) break;
101
+
102
+ if (!candidateIndexAdmitted) {
103
+ break;
104
+ }
105
+
75
106
  candidates.sort(
76
107
  (left, right) =>
77
108
  candidatePriority(left) - candidatePriority(right) ||
78
109
  left.name.localeCompare(right.name)
79
110
  );
80
111
  }
112
+
81
113
  if (!candidateIndexAdmitted) {
82
114
  throw schemaBoundaryError(workLimit, workLimit + 1);
83
115
  }
116
+
84
117
  const constructNode = (
85
118
  node: NodeSpec,
86
119
  visiting: Set<string>,
@@ -92,57 +125,88 @@ export function constructDefaultEmptyDocument(
92
125
  ) {
93
126
  return undefined;
94
127
  }
95
- if (node.role === 'text' || visiting.has(node.name)) return undefined;
128
+
129
+ if (node.role === 'text' || visiting.has(node.name)) {
130
+ return undefined;
131
+ }
132
+
96
133
  const attrs = admittedCollections.attrsByNode.get(node) ?? [];
97
- for (const _attr of attrs) {
134
+
135
+ for (let index = 0; index < attrs.length; index += 1) {
98
136
  if (!consumeWork()) {
99
137
  budget.exhausted = true;
138
+
100
139
  return undefined;
101
140
  }
102
141
  }
103
- if (attrs.some(([, spec]) => spec.default === undefined)) {
142
+
143
+ if (attrs.some(([ , spec ]) => spec.default === undefined)) {
104
144
  return undefined;
105
145
  }
106
146
 
107
147
  visiting.add(node.name);
148
+
108
149
  const children = minimalContentMatch(
109
150
  node.content ?? '',
110
- (symbol) => {
151
+ symbol => {
111
152
  for (const candidate of candidatesBySymbol.get(symbol) ?? []) {
112
- if (!consumeWork()) return undefined;
153
+ if (!consumeWork()) {
154
+ return undefined;
155
+ }
156
+
113
157
  const constructed = constructNode(candidate, visiting, depth + 1);
114
- if (constructed) return constructed;
158
+
159
+ if (constructed) {
160
+ return constructed;
161
+ }
115
162
  }
163
+
116
164
  return undefined;
117
165
  },
118
166
  consumeWork
119
167
  );
168
+
120
169
  visiting.delete(node.name);
121
- if (!children) return undefined;
170
+
171
+ if (!children) {
172
+ return undefined;
173
+ }
174
+
122
175
  if (budget.nodes >= resolvedLimits.maxDocumentNodes) {
123
176
  budget.exhausted = true;
177
+
124
178
  return undefined;
125
179
  }
180
+
126
181
  budget.nodes += 1;
127
182
 
128
183
  const result: DocumentJSON = { type: node.json?.type ?? node.name };
129
- if (children.length > 0) result.content = children;
184
+
185
+ if (children.length > 0) {
186
+ result.content = children;
187
+ }
188
+
130
189
  const projectedAttrs = Object.entries(node.json?.attrs ?? {});
190
+
131
191
  if (attrs.length > 0 || projectedAttrs.length > 0) {
132
192
  result.attrs = Object.fromEntries([
133
- ...attrs.map(([name, spec]) => [name, spec.default] as const),
193
+ ...attrs.map(([ name, spec ]) => [ name, spec.default ] as const),
134
194
  ...projectedAttrs,
135
195
  ]);
136
196
  }
197
+
137
198
  return result;
138
199
  };
139
200
 
140
201
  const document = constructNode(docNode, new Set(), 0);
202
+
141
203
  if (!document) {
142
204
  if (budget.exhausted || budget.work >= workLimit) {
143
205
  throw schemaBoundaryError(workLimit, workLimit + 1);
144
206
  }
207
+
145
208
  throw new Error(`schema cannot construct a default document for '${docNode.name}'`);
146
209
  }
210
+
147
211
  return document;
148
212
  }