@formicoidea/labre-core 0.50.0 → 0.52.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. package/dist/_pkgs/affine-block-attachment/asset-picker.d.ts +24 -0
  2. package/dist/_pkgs/affine-block-attachment/asset-picker.js +87 -0
  3. package/dist/_pkgs/affine-block-attachment/configs/slash-menu.js +3 -20
  4. package/dist/_pkgs/affine-block-attachment/index.d.ts +1 -0
  5. package/dist/_pkgs/affine-block-attachment/index.js +1 -0
  6. package/dist/_pkgs/affine-block-attachment/utils.d.ts +9 -1
  7. package/dist/_pkgs/affine-block-attachment/utils.js +9 -0
  8. package/dist/_pkgs/affine-block-bookmark/adapters/markdown/markdown.js +14 -1
  9. package/dist/_pkgs/affine-block-bookmark/adapters/notion-html.js +5 -1
  10. package/dist/_pkgs/affine-block-database/adapters/markdown.js +5 -3
  11. package/dist/_pkgs/affine-block-embed/common/adapters/html.js +5 -3
  12. package/dist/_pkgs/affine-block-embed/common/adapters/markdown.js +11 -0
  13. package/dist/_pkgs/affine-block-frame/frame-template.d.ts +58 -0
  14. package/dist/_pkgs/affine-block-frame/frame-template.js +207 -0
  15. package/dist/_pkgs/affine-block-frame/frame-toolbar.js +19 -3
  16. package/dist/_pkgs/affine-block-frame/index.d.ts +1 -0
  17. package/dist/_pkgs/affine-block-frame/index.js +1 -0
  18. package/dist/_pkgs/affine-block-frame/translations.d.ts +7 -0
  19. package/dist/_pkgs/affine-block-frame/translations.js +16 -1
  20. package/dist/_pkgs/affine-block-frame/view.js +6 -0
  21. package/dist/_pkgs/affine-block-image/asset-picker.d.ts +63 -0
  22. package/dist/_pkgs/affine-block-image/asset-picker.js +183 -0
  23. package/dist/_pkgs/affine-block-image/commands/insert-images.d.ts +5 -0
  24. package/dist/_pkgs/affine-block-image/commands/insert-images.js +8 -8
  25. package/dist/_pkgs/affine-block-image/components/page-image-block.d.ts +11 -0
  26. package/dist/_pkgs/affine-block-image/components/page-image-block.js +81 -12
  27. package/dist/_pkgs/affine-block-image/configs/replace-action.d.ts +9 -0
  28. package/dist/_pkgs/affine-block-image/configs/replace-action.js +27 -0
  29. package/dist/_pkgs/affine-block-image/configs/toolbar.js +6 -0
  30. package/dist/_pkgs/affine-block-image/crop/crop-controller.d.ts +46 -0
  31. package/dist/_pkgs/affine-block-image/crop/crop-controller.js +104 -0
  32. package/dist/_pkgs/affine-block-image/crop/crop-math.d.ts +70 -0
  33. package/dist/_pkgs/affine-block-image/crop/crop-math.js +141 -0
  34. package/dist/_pkgs/affine-block-image/crop/crop-overlay.d.ts +57 -0
  35. package/dist/_pkgs/affine-block-image/crop/crop-overlay.js +343 -0
  36. package/dist/_pkgs/affine-block-image/crop/crop-toolbar.d.ts +12 -0
  37. package/dist/_pkgs/affine-block-image/crop/crop-toolbar.js +78 -0
  38. package/dist/_pkgs/affine-block-image/crop/write-crop.d.ts +16 -0
  39. package/dist/_pkgs/affine-block-image/crop/write-crop.js +57 -0
  40. package/dist/_pkgs/affine-block-image/edgeless-clipboard-config.js +4 -1
  41. package/dist/_pkgs/affine-block-image/effects.js +2 -0
  42. package/dist/_pkgs/affine-block-image/image-block.d.ts +2 -0
  43. package/dist/_pkgs/affine-block-image/image-block.js +2 -0
  44. package/dist/_pkgs/affine-block-image/image-edgeless-block.d.ts +2 -0
  45. package/dist/_pkgs/affine-block-image/image-edgeless-block.js +36 -8
  46. package/dist/_pkgs/affine-block-image/image-resize-manager.js +15 -0
  47. package/dist/_pkgs/affine-block-image/index.d.ts +1 -0
  48. package/dist/_pkgs/affine-block-image/index.js +1 -0
  49. package/dist/_pkgs/affine-block-image/translations.d.ts +15 -0
  50. package/dist/_pkgs/affine-block-image/translations.js +43 -0
  51. package/dist/_pkgs/affine-block-image/utils.d.ts +13 -0
  52. package/dist/_pkgs/affine-block-image/utils.js +10 -1
  53. package/dist/_pkgs/affine-block-root/clipboard/page-clipboard.js +5 -0
  54. package/dist/_pkgs/affine-block-root/commands/page-export-commands.d.ts +2 -0
  55. package/dist/_pkgs/affine-block-root/commands/page-export-commands.js +71 -0
  56. package/dist/_pkgs/affine-block-root/index.d.ts +1 -0
  57. package/dist/_pkgs/affine-block-root/index.js +1 -0
  58. package/dist/_pkgs/affine-block-root/view.js +8 -0
  59. package/dist/_pkgs/affine-block-surface/extensions/drawio-export/capability.d.ts +30 -0
  60. package/dist/_pkgs/affine-block-surface/extensions/drawio-export/capability.js +44 -0
  61. package/dist/_pkgs/affine-block-surface/extensions/drawio-export/colors.d.ts +26 -0
  62. package/dist/_pkgs/affine-block-surface/extensions/drawio-export/colors.js +82 -0
  63. package/dist/_pkgs/affine-block-surface/extensions/drawio-export/command.d.ts +18 -0
  64. package/dist/_pkgs/affine-block-surface/extensions/drawio-export/command.js +91 -0
  65. package/dist/_pkgs/affine-block-surface/extensions/drawio-export/index.d.ts +3 -0
  66. package/dist/_pkgs/affine-block-surface/extensions/drawio-export/index.js +3 -0
  67. package/dist/_pkgs/affine-block-surface/extensions/drawio-export/writer.d.ts +50 -0
  68. package/dist/_pkgs/affine-block-surface/extensions/drawio-export/writer.js +732 -0
  69. package/dist/_pkgs/affine-block-surface/extensions/export-manager/draw-image-block.d.ts +12 -0
  70. package/dist/_pkgs/affine-block-surface/extensions/export-manager/draw-image-block.js +15 -0
  71. package/dist/_pkgs/affine-block-surface/extensions/export-manager/export-manager.js +7 -1
  72. package/dist/_pkgs/affine-block-surface/extensions/export-svg/command.d.ts +7 -1
  73. package/dist/_pkgs/affine-block-surface/extensions/export-svg/command.js +60 -1
  74. package/dist/_pkgs/affine-block-surface/extensions/export-svg/index.d.ts +2 -2
  75. package/dist/_pkgs/affine-block-surface/extensions/export-svg/index.js +2 -2
  76. package/dist/_pkgs/affine-block-surface/extensions/export-svg/marker-version.d.ts +6 -0
  77. package/dist/_pkgs/affine-block-surface/extensions/export-svg/marker-version.js +8 -0
  78. package/dist/_pkgs/affine-block-surface/extensions/export-svg/render.d.ts +28 -1
  79. package/dist/_pkgs/affine-block-surface/extensions/export-svg/render.js +66 -6
  80. package/dist/_pkgs/affine-block-surface/extensions/export-svg/svg-context.d.ts +2 -7
  81. package/dist/_pkgs/affine-block-surface/extensions/export-svg/svg-context.js +2 -6
  82. package/dist/_pkgs/affine-block-surface/extensions/index.d.ts +2 -0
  83. package/dist/_pkgs/affine-block-surface/extensions/index.js +2 -0
  84. package/dist/_pkgs/affine-block-surface/extensions/interchange-export.d.ts +84 -0
  85. package/dist/_pkgs/affine-block-surface/extensions/interchange-export.js +150 -0
  86. package/dist/_pkgs/affine-block-surface/extensions/interchange-import.js +24 -2
  87. package/dist/_pkgs/affine-block-surface/extensions/interchange.d.ts +23 -2
  88. package/dist/_pkgs/affine-block-surface/extensions/interchange.js +4 -1
  89. package/dist/_pkgs/affine-block-surface/extensions/legend-role.d.ts +18 -0
  90. package/dist/_pkgs/affine-block-surface/extensions/legend-role.js +20 -0
  91. package/dist/_pkgs/affine-block-surface/extensions/legend.d.ts +2 -19
  92. package/dist/_pkgs/affine-block-surface/extensions/legend.js +2 -18
  93. package/dist/_pkgs/affine-block-surface/extensions/svg-sanitize.d.ts +67 -0
  94. package/dist/_pkgs/affine-block-surface/extensions/svg-sanitize.js +235 -0
  95. package/dist/_pkgs/affine-block-surface/extensions/svg-sketch-translations.d.ts +1 -0
  96. package/dist/_pkgs/affine-block-surface/extensions/svg-sketch-translations.js +9 -0
  97. package/dist/_pkgs/affine-block-surface/extensions/svg-sketch.d.ts +17 -13
  98. package/dist/_pkgs/affine-block-surface/extensions/svg-sketch.js +52 -80
  99. package/dist/_pkgs/affine-block-surface/extensions/xml-element.d.ts +98 -0
  100. package/dist/_pkgs/affine-block-surface/extensions/xml-element.js +261 -0
  101. package/dist/_pkgs/affine-block-surface/extensions/xml-reader.d.ts +238 -0
  102. package/dist/_pkgs/affine-block-surface/extensions/xml-reader.js +570 -0
  103. package/dist/_pkgs/affine-block-surface/framework-background/def.js +1 -1
  104. package/dist/_pkgs/affine-block-surface/index.d.ts +1 -0
  105. package/dist/_pkgs/affine-block-surface/index.js +1 -0
  106. package/dist/_pkgs/affine-block-surface/interchange-headless.d.ts +29 -0
  107. package/dist/_pkgs/affine-block-surface/interchange-headless.js +32 -0
  108. package/dist/_pkgs/affine-block-surface/view.js +22 -2
  109. package/dist/_pkgs/affine-block-table/adapters/utils.js +24 -1
  110. package/dist/_pkgs/affine-gfx-note/toolbar/note-menu.js +5 -8
  111. package/dist/_pkgs/affine-gfx-template/contribute.d.ts +14 -4
  112. package/dist/_pkgs/affine-gfx-template/contribute.js +47 -6
  113. package/dist/_pkgs/affine-gfx-template/toolbar/builtin-templates.d.ts +7 -2
  114. package/dist/_pkgs/affine-gfx-template/toolbar/builtin-templates.js +14 -40
  115. package/dist/_pkgs/affine-gfx-template/toolbar/resolve-name.d.ts +1 -1
  116. package/dist/_pkgs/affine-gfx-template/toolbar/resolve-name.js +1 -1
  117. package/dist/_pkgs/affine-gfx-template/toolbar/template-panel.d.ts +4 -4
  118. package/dist/_pkgs/affine-gfx-template/toolbar/template-panel.js +9 -9
  119. package/dist/_pkgs/affine-gfx-template/toolbar/template-type.d.ts +2 -4
  120. package/dist/_pkgs/affine-gfx-template/translations.d.ts +5 -0
  121. package/dist/_pkgs/affine-gfx-template/translations.js +9 -0
  122. package/dist/_pkgs/affine-inline-footnote/adapters/markdown/inline-delta.js +6 -0
  123. package/dist/_pkgs/affine-inline-footnote/adapters/markdown/markdown-inline.js +7 -0
  124. package/dist/_pkgs/affine-inline-link/adapters/html/html-inline.js +4 -1
  125. package/dist/_pkgs/affine-inline-link/adapters/html/inline-delta.js +3 -1
  126. package/dist/_pkgs/affine-inline-link/adapters/markdown/inline-delta.js +4 -1
  127. package/dist/_pkgs/affine-inline-link/adapters/notion-html/html-inline.js +2 -1
  128. package/dist/_pkgs/affine-inline-link/link-node/affine-link.d.ts +7 -0
  129. package/dist/_pkgs/affine-inline-link/link-node/affine-link.js +37 -0
  130. package/dist/_pkgs/affine-inline-link/link-node/index.d.ts +1 -0
  131. package/dist/_pkgs/affine-inline-link/link-node/index.js +1 -0
  132. package/dist/_pkgs/affine-inline-link/link-node/propose-views.d.ts +22 -0
  133. package/dist/_pkgs/affine-inline-link/link-node/propose-views.js +59 -0
  134. package/dist/_pkgs/affine-model/blocks/image/image-crop.d.ts +48 -0
  135. package/dist/_pkgs/affine-model/blocks/image/image-crop.js +78 -0
  136. package/dist/_pkgs/affine-model/blocks/image/image-model.d.ts +6 -0
  137. package/dist/_pkgs/affine-model/blocks/image/image-model.js +2 -0
  138. package/dist/_pkgs/affine-model/blocks/image/index.d.ts +1 -0
  139. package/dist/_pkgs/affine-model/blocks/image/index.js +1 -0
  140. package/dist/_pkgs/affine-model/consts/shape.d.ts +2 -0
  141. package/dist/_pkgs/affine-model/consts/shape.js +5 -0
  142. package/dist/_pkgs/affine-model/elements/bpmn/consts.d.ts +31 -0
  143. package/dist/_pkgs/affine-model/elements/bpmn/consts.js +31 -0
  144. package/dist/_pkgs/affine-model/elements/bpmn/index.d.ts +1 -0
  145. package/dist/_pkgs/affine-model/elements/bpmn/index.js +1 -0
  146. package/dist/_pkgs/affine-model/elements/bpmn/pool.d.ts +0 -25
  147. package/dist/_pkgs/affine-model/elements/bpmn/pool.js +1 -25
  148. package/dist/_pkgs/affine-model/elements/shape/shape.d.ts +0 -2
  149. package/dist/_pkgs/affine-model/elements/shape/shape.js +1 -2
  150. package/dist/_pkgs/affine-model/elements/uml/consts.d.ts +133 -0
  151. package/dist/_pkgs/affine-model/elements/uml/consts.js +107 -0
  152. package/dist/_pkgs/affine-model/elements/uml/diagram.d.ts +1 -79
  153. package/dist/_pkgs/affine-model/elements/uml/diagram.js +1 -53
  154. package/dist/_pkgs/affine-model/elements/uml/fragment.d.ts +0 -15
  155. package/dist/_pkgs/affine-model/elements/uml/fragment.js +1 -15
  156. package/dist/_pkgs/affine-model/elements/uml/index.d.ts +1 -0
  157. package/dist/_pkgs/affine-model/elements/uml/index.js +1 -0
  158. package/dist/_pkgs/affine-model/elements/uml/partition.d.ts +0 -20
  159. package/dist/_pkgs/affine-model/elements/uml/partition.js +1 -20
  160. package/dist/_pkgs/affine-model/elements/uml/region.d.ts +0 -11
  161. package/dist/_pkgs/affine-model/elements/uml/region.js +1 -11
  162. package/dist/_pkgs/affine-model/headless.d.ts +19 -0
  163. package/dist/_pkgs/affine-model/headless.js +19 -0
  164. package/dist/_pkgs/affine-shared/headless.d.ts +10 -0
  165. package/dist/_pkgs/affine-shared/headless.js +10 -0
  166. package/dist/_pkgs/affine-shared/services/asset-picker-service.d.ts +63 -0
  167. package/dist/_pkgs/affine-shared/services/asset-picker-service.js +20 -0
  168. package/dist/_pkgs/affine-shared/services/frame-template-service.d.ts +83 -0
  169. package/dist/_pkgs/affine-shared/services/frame-template-service.js +17 -0
  170. package/dist/_pkgs/affine-shared/services/index.d.ts +3 -0
  171. package/dist/_pkgs/affine-shared/services/index.js +3 -0
  172. package/dist/_pkgs/affine-shared/services/thread-marks-service.d.ts +69 -0
  173. package/dist/_pkgs/affine-shared/services/thread-marks-service.js +20 -0
  174. package/dist/_pkgs/affine-shared/services/translation-service/chrome.d.ts +6 -0
  175. package/dist/_pkgs/affine-shared/services/translation-service/chrome.js +10 -0
  176. package/dist/_pkgs/affine-widget-drag-handle/watchers/drag-event-watcher.js +8 -3
  177. package/dist/_pkgs/affine-widget-keyboard-toolbar/config.js +4 -16
  178. package/dist/_pkgs/affine-widget-linked-doc/transformers/html.d.ts +7 -0
  179. package/dist/_pkgs/affine-widget-linked-doc/transformers/html.js +25 -24
  180. package/dist/_pkgs/affine-widget-linked-doc/transformers/index.d.ts +1 -1
  181. package/dist/_pkgs/affine-widget-linked-doc/transformers/markdown.d.ts +7 -0
  182. package/dist/_pkgs/affine-widget-linked-doc/transformers/markdown.js +26 -25
  183. package/dist/_pkgs/affine-widget-linked-doc/transformers/utils.d.ts +23 -0
  184. package/dist/_pkgs/affine-widget-linked-doc/transformers/utils.js +14 -0
  185. package/dist/_pkgs/affine-widget-thread-marks/effects.d.ts +1 -0
  186. package/dist/_pkgs/affine-widget-thread-marks/effects.js +4 -0
  187. package/dist/_pkgs/affine-widget-thread-marks/excerpt.d.ts +19 -0
  188. package/dist/_pkgs/affine-widget-thread-marks/excerpt.js +68 -0
  189. package/dist/_pkgs/affine-widget-thread-marks/index.d.ts +5 -0
  190. package/dist/_pkgs/affine-widget-thread-marks/index.js +8 -0
  191. package/dist/_pkgs/affine-widget-thread-marks/toolbar.d.ts +49 -0
  192. package/dist/_pkgs/affine-widget-thread-marks/toolbar.js +108 -0
  193. package/dist/_pkgs/affine-widget-thread-marks/translations.d.ts +9 -0
  194. package/dist/_pkgs/affine-widget-thread-marks/translations.js +11 -0
  195. package/dist/_pkgs/affine-widget-thread-marks/view.d.ts +12 -0
  196. package/dist/_pkgs/affine-widget-thread-marks/view.js +25 -0
  197. package/dist/_pkgs/affine-widget-thread-marks/widget.d.ts +51 -0
  198. package/dist/_pkgs/affine-widget-thread-marks/widget.js +214 -0
  199. package/dist/commands.js +5 -2
  200. package/dist/extensions/view.js +2 -0
  201. package/dist/host-panels.d.ts +24 -1
  202. package/dist/host-panels.js +61 -10
  203. package/dist/interchange.d.ts +1 -0
  204. package/dist/interchange.js +1 -0
  205. package/dist/model/headless.d.ts +1 -0
  206. package/dist/model/headless.js +1 -0
  207. package/dist/shared/headless.d.ts +1 -0
  208. package/dist/shared/headless.js +1 -0
  209. package/dist/translations.js +10 -1
  210. package/package.json +13 -1
@@ -1,6 +1,6 @@
1
1
  import { ConnectorElementModel } from '../../affine-model/index.js';
2
- import { fillPlaceholders, NotificationProvider, translateKey, } from '../../affine-shared/services/index.js';
3
- import { openSingleFileWithSpec } from '../../affine-shared/utils/index.js';
2
+ import { FileSizeLimitProvider, fillPlaceholders, NotificationProvider, translateKey, } from '../../affine-shared/services/index.js';
3
+ import { formatSize, openSingleFileWithSpec } from '../../affine-shared/utils/index.js';
4
4
  import { Bound, getCommonBound } from '../../global/gfx/index.js';
5
5
  import { GfxControllerIdentifier } from '../../std/gfx/index.js';
6
6
  import { DefaultTool } from '../tool/default-tool.js';
@@ -293,6 +293,13 @@ const IMPORT_DEFAULT_NAME_FALLBACK = 'Imported diagram';
293
293
  */
294
294
  const IMPORT_READONLY_KEY = 'com.labre.interchange.import.readonly';
295
295
  const IMPORT_READONLY_FALLBACK = 'This page became read-only while the file was being read, so nothing was imported.';
296
+ /**
297
+ * The file is larger than the host's `FileSizeLimitProvider` allows — the same
298
+ * ceiling an attachment or an image upload obeys, refused BEFORE a byte of it
299
+ * is read. `{{size}}` is the formatted ceiling, never the file's own size.
300
+ */
301
+ const IMPORT_TOO_LARGE_KEY = 'com.labre.interchange.import.too-large';
302
+ const IMPORT_TOO_LARGE_FALLBACK = 'This file is larger than {{size}}, the most this editor imports.';
296
303
  const IMPORT_REMARKS_KEY = 'com.labre.interchange.import.remarks';
297
304
  const IMPORT_REMARKS_FALLBACK = 'What the import could not keep as it was';
298
305
  const IMPORT_CONSOLE_KEY = 'com.labre.interchange.import.console';
@@ -532,6 +539,21 @@ export async function importInterchangeFile(std, capability, file, options = {})
532
539
  if (!gfx.surface || std.store.readonly)
533
540
  return;
534
541
  const { format } = capability;
542
+ // The host's upload ceiling (`FileSizeLimitProvider`, the seam an attachment
543
+ // and an image already obey), checked on the SIZE before the text is read: a
544
+ // reader holds the whole file as a string, and the decode step may multiply
545
+ // it. Optional like every seam — absent, nothing is refused here.
546
+ const maxFileSize = std.getOptional(FileSizeLimitProvider)?.maxFileSize;
547
+ if (maxFileSize !== undefined && file.size > maxFileSize) {
548
+ notifyImport(std, {
549
+ title: translateKey(std, IMPORT_FAILED_KEY, IMPORT_FAILED_FALLBACK),
550
+ message: translateKey(std, IMPORT_TOO_LARGE_KEY, IMPORT_TOO_LARGE_FALLBACK, {
551
+ size: formatSize(maxFileSize) ?? String(maxFileSize),
552
+ }),
553
+ accent: 'error',
554
+ });
555
+ return;
556
+ }
535
557
  let result;
536
558
  try {
537
559
  const text = await file.text();
@@ -1,5 +1,5 @@
1
1
  import { type ServiceProvider } from '../../global/di/index.js';
2
- import type { GfxPrimitiveElementModel } from '../../std/gfx/index.js';
2
+ import type { GfxBlockElementModel, GfxPrimitiveElementModel } from '../../std/gfx/index.js';
3
3
  import type { ExtensionType } from '../../store/index.js';
4
4
  /**
5
5
  * The interchange registry (`docs/adr/0012`, P1).
@@ -33,6 +33,11 @@ import type { ExtensionType } from '../../store/index.js';
33
33
  * beside `ValidationRuleExtension`, because offering to read a file is tooling.
34
34
  * Turning a framework's flag off removes its import and export commands; it
35
35
  * does not touch one element a past import created. See `docs/adr/0009`.
36
+ *
37
+ * One owner is not a framework: `'canvas'`, which writes what the canvas
38
+ * PAINTS rather than what a framework means (`canvas:drawio:export`,
39
+ * `docs/adr/0019` amendment of 2026-10-14). It speaks no vocabulary, so no
40
+ * flag gates it, and the surface registers it unconditionally.
36
41
  */
37
42
  /**
38
43
  * A format Labre can read or write, and what it PROMISES (ADR 0012, P2).
@@ -226,6 +231,15 @@ export interface InterchangeExportContext {
226
231
  * named `Order-to cash` in the XML as well as on the download.
227
232
  */
228
233
  name?: string;
234
+ /**
235
+ * The canvas BLOCKS that go with the elements — notes, frames, edgeless
236
+ * texts, images — in document order. Optional, and only a writer of what the
237
+ * canvas paints reads it (`canvas:drawio:export`): a framework's exporter
238
+ * speaks about its own elements and ignores it, and a caller that passes
239
+ * none gets exactly what it got before this field existed. Models, never
240
+ * DI-only types, so labre-mcp can hand over what its store holds.
241
+ */
242
+ blocks?: readonly GfxBlockElementModel[];
229
243
  }
230
244
  /** What the caller tells an importer about the source it is handing over. */
231
245
  export interface InterchangeImportContext {
@@ -273,6 +287,10 @@ export interface InterchangeImportResult {
273
287
  * speaks about itself. Document order is the caller's, and it matters: it is
274
288
  * the tie-break the audit and the validation rules already break on, so an
275
289
  * export cannot disagree with a badge the user can see.
290
+ *
291
+ * A RENDERING is the one exception, and it is the visual tier's: part of a
292
+ * picture is a smaller picture, so the canvas's draw.io writer is handed the
293
+ * selection when there is one (`canvas.export.drawio`).
276
294
  */
277
295
  export type InterchangeExporter = (elements: readonly GfxPrimitiveElementModel[], context: InterchangeExportContext) => InterchangeExportResult;
278
296
  /**
@@ -321,7 +339,10 @@ export type InterchangeImportCapability = Extract<InterchangeCapability, {
321
339
  }>;
322
340
  /** The one way to spell a capability's id. */
323
341
  export declare function interchangeCapabilityId(framework: string, formatId: string, direction: InterchangeDirection): string;
324
- /** A framework registers its interchange here; nothing else registers any. */
342
+ /**
343
+ * A framework registers its interchange here, and so does the canvas (its
344
+ * generic draw.io writer); nothing else registers any.
345
+ */
325
346
  export declare const InterchangeIdentifier: import("../../global/di/types.js").ServiceIdentifier<InterchangeCapability> & (<U extends InterchangeCapability = InterchangeCapability>(variant: import("../../global/di/types.js").ServiceVariant) => import("../../global/di/types.js").ServiceIdentifier<U>);
326
347
  /**
327
348
  * Register a framework's interchange capabilities. Call it from the FLAG-GATED
@@ -32,7 +32,10 @@ const ID_SEPARATOR = ':';
32
32
  export function interchangeCapabilityId(framework, formatId, direction) {
33
33
  return `${framework}${ID_SEPARATOR}${formatId}${ID_SEPARATOR}${direction}`;
34
34
  }
35
- /** A framework registers its interchange here; nothing else registers any. */
35
+ /**
36
+ * A framework registers its interchange here, and so does the canvas (its
37
+ * generic draw.io writer); nothing else registers any.
38
+ */
36
39
  export const InterchangeIdentifier = createIdentifier('Interchange');
37
40
  /**
38
41
  * Register a framework's interchange capabilities. Call it from the FLAG-GATED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * The role the legend's GROUP carries — the one thing on a generated legend
3
+ * that says what it is.
4
+ *
5
+ * Its glyphs deliberately carry none (see {@link addShape}), which is what
6
+ * keeps a legend out of the scan and out of every validation rule. But "made of
7
+ * nothing in particular" is unreadable to anything that has to tell a legend
8
+ * from a drawing, and something does: the `.bpmn` export counts what it left
9
+ * behind inside a pool, and a legend's fourteen glyphs would be reported as
10
+ * fourteen losses. The wrapper says so instead, once, structurally — no title
11
+ * to match on, no framework name, no geometry.
12
+ *
13
+ * `core:` because it belongs to no framework: no `RoleDefs` declares it, so
14
+ * `roleIsA` matches it against nothing, no rule evaluates it and no legend row
15
+ * lights because of it. Nothing is backfilled, as ever — a legend drawn before
16
+ * this shipped carries no role and is not recognised.
17
+ */
18
+ export declare const LEGEND_ROLE = "core:legend";
@@ -0,0 +1,20 @@
1
+ // Its own module so the headless interchange entry (`./interchange`) can read
2
+ // it without the legend's editor graph (ADR 0012, P3 as amended 2026-10-14).
3
+ /**
4
+ * The role the legend's GROUP carries — the one thing on a generated legend
5
+ * that says what it is.
6
+ *
7
+ * Its glyphs deliberately carry none (see {@link addShape}), which is what
8
+ * keeps a legend out of the scan and out of every validation rule. But "made of
9
+ * nothing in particular" is unreadable to anything that has to tell a legend
10
+ * from a drawing, and something does: the `.bpmn` export counts what it left
11
+ * behind inside a pool, and a legend's fourteen glyphs would be reported as
12
+ * fourteen losses. The wrapper says so instead, once, structurally — no title
13
+ * to match on, no framework name, no geometry.
14
+ *
15
+ * `core:` because it belongs to no framework: no `RoleDefs` declares it, so
16
+ * `roleIsA` matches it against nothing, no rule evaluates it and no legend row
17
+ * lights because of it. Nothing is backfilled, as ever — a legend drawn before
18
+ * this shipped carries no role and is not recognised.
19
+ */
20
+ export const LEGEND_ROLE = 'core:legend';
@@ -1,6 +1,7 @@
1
1
  import { Bound } from '../../global/gfx/index.js';
2
2
  import { type BlockStdScope, type CommandLegendExtra, type CommandLegendRow, type CommandOwner } from '../../std/index.js';
3
3
  import { type GfxController, type RoleId } from '../../std/gfx/index.js';
4
+ import { LEGEND_ROLE } from './legend-role.js';
4
5
  /**
5
6
  * The legend PLATFORM: one engine, one box, for every framework that documents
6
7
  * itself.
@@ -56,24 +57,7 @@ export type LegendExtra = CommandLegendExtra;
56
57
  export declare const LABEL_COLOR: string;
57
58
  export declare const LABEL_FONT = "blocksuite:surface:Inter";
58
59
  export declare const LABEL_FONT_SIZE = 14;
59
- /**
60
- * The role the legend's GROUP carries — the one thing on a generated legend
61
- * that says what it is.
62
- *
63
- * Its glyphs deliberately carry none (see {@link addShape}), which is what
64
- * keeps a legend out of the scan and out of every validation rule. But "made of
65
- * nothing in particular" is unreadable to anything that has to tell a legend
66
- * from a drawing, and something does: the `.bpmn` export counts what it left
67
- * behind inside a pool, and a legend's fourteen glyphs would be reported as
68
- * fourteen losses. The wrapper says so instead, once, structurally — no title
69
- * to match on, no framework name, no geometry.
70
- *
71
- * `core:` because it belongs to no framework: no `RoleDefs` declares it, so
72
- * `roleIsA` matches it against nothing, no rule evaluates it and no legend row
73
- * lights because of it. Nothing is backfilled, as ever — a legend drawn before
74
- * this shipped carries no role and is not recognised.
75
- */
76
- export declare const LEGEND_ROLE = "core:legend";
60
+ export { LEGEND_ROLE };
77
61
  /**
78
62
  * How big the box {@link addLegend} would draw for these sections is — without
79
63
  * drawing it. The engine needs the height to drop the box bottom-left of a
@@ -141,4 +125,3 @@ export declare function legendFromCommands(std: BlockStdScope, owner: CommandOwn
141
125
  export declare function createBoardLegend(std: BlockStdScope, board: {
142
126
  xywh: string;
143
127
  }, owner: CommandOwner): string | undefined;
144
- export {};
@@ -5,6 +5,7 @@ import { Bound } from '../../global/gfx/index.js';
5
5
  import { CATALOGUE_CATEGORY_KEY_PREFIX, getRegisteredCommands, humanizeCategory, } from '../../std/index.js';
6
6
  import { GfxControllerIdentifier, roleIsA, RoleVocabularyIdentifier, } from '../../std/gfx/index.js';
7
7
  import { EdgelessCRUDIdentifier } from './crud-extension.js';
8
+ import { LEGEND_ROLE } from './legend-role.js';
8
9
  /**
9
10
  * The ink and the type a notation writes its labels in: this box's rows, and
10
11
  * the words the DDD prefabs set beside a dot or a marker.
@@ -81,24 +82,7 @@ function addSwatchElement(surface, props) {
81
82
  const { role: _role, ...neutral } = props;
82
83
  return surface.addElement(neutral);
83
84
  }
84
- /**
85
- * The role the legend's GROUP carries — the one thing on a generated legend
86
- * that says what it is.
87
- *
88
- * Its glyphs deliberately carry none (see {@link addShape}), which is what
89
- * keeps a legend out of the scan and out of every validation rule. But "made of
90
- * nothing in particular" is unreadable to anything that has to tell a legend
91
- * from a drawing, and something does: the `.bpmn` export counts what it left
92
- * behind inside a pool, and a legend's fourteen glyphs would be reported as
93
- * fourteen losses. The wrapper says so instead, once, structurally — no title
94
- * to match on, no framework name, no geometry.
95
- *
96
- * `core:` because it belongs to no framework: no `RoleDefs` declares it, so
97
- * `roleIsA` matches it against nothing, no rule evaluates it and no legend row
98
- * lights because of it. Nothing is backfilled, as ever — a legend drawn before
99
- * this shipped carries no role and is not recognised.
100
- */
101
- export const LEGEND_ROLE = 'core:legend';
85
+ export { LEGEND_ROLE };
102
86
  /**
103
87
  * Group the box's elements under one titled group, without
104
88
  * `@labre/affine-gfx-group`'s `createGroupCommand`: that package depends on
@@ -0,0 +1,67 @@
1
+ import { type XmlElement } from './xml-element.js';
2
+ import type { XmlNode } from './xml-reader.js';
3
+ /**
4
+ * The SVG sanitizer: an ALLOW-LIST applied to the tree `readXml` read, which
5
+ * replaced DOMPurify on a DOM (ADR 0012, P3 as amended 2026-10-14).
6
+ *
7
+ * Security code, written to be read in one sitting. An `.svg` is an executable
8
+ * document handed over by whoever sent the file, and this is the only gate
9
+ * between it and every reader that walks it (the sketch, Wardley's
10
+ * recognisers). The rule is one sentence: **what is not on a list below does
11
+ * not come through.** No deny-list, no repair, no attempt to keep the harmless
12
+ * half of something suspicious.
13
+ *
14
+ * - An element survives only in the SVG namespace (or none, for a file that
15
+ * forgot its `xmlns`) and only if its local name is in
16
+ * {@link ALLOWED_ELEMENTS}. Anything else goes WITH its subtree: `<script>`,
17
+ * `<foreignObject>` and its XHTML, `<use>` (which can pull in another
18
+ * document), the animation elements (which can rewrite an attribute after
19
+ * this gate), every vendor namespace.
20
+ * - An attribute survives only if it is unprefixed and in
21
+ * {@link ALLOWED_ATTRIBUTES} (or `data-*` / `aria-*`), or one of the two
22
+ * namespaced ones a drawing needs (`xml:space`, the `xmlns` declarations).
23
+ * So no `on*` handler is ever on a list.
24
+ * - A surviving value is then checked: no `javascript:`, `vbscript:` or
25
+ * `data:` anywhere, no `url(…)` that is not a same-document `#ref`, no
26
+ * `image-set(…)` (whose bare strings are URLs), no CSS expression, binding
27
+ * or import, and no backslash (a CSS escape is how the first three hide).
28
+ * - `href` / `xlink:href` keep only a same-document `#ref`, except on
29
+ * `<image>`, which also keeps an inline PNG, JPEG, GIF or WebP. No external
30
+ * resource survives, so nothing a file names is ever fetched.
31
+ * - A `<style>` sheet that fails the value check is removed whole.
32
+ * - Nothing nested deeper than {@link MAX_SVG_DEPTH} comes through, because
33
+ * the walks after this one recurse. The cut happens during the walk: what
34
+ * lies below it, or below a removed element, is never resolved or copied.
35
+ *
36
+ * It reports what it took, by kind, and the sketch turns that into its notes.
37
+ */
38
+ export declare const SVG_NAMESPACE = "http://www.w3.org/2000/svg";
39
+ /** Deeper than this is cut: no drawing nests 256 groups, and walks recurse. */
40
+ export declare const MAX_SVG_DEPTH = 256;
41
+ /**
42
+ * Is this value inert? Read with every whitespace and control character
43
+ * removed and lower-cased, because `java\tscript:` and `JaVaScRiPt:` are the
44
+ * same scheme to a browser.
45
+ */
46
+ export declare function isSafeSvgValue(value: string): boolean;
47
+ /** One thing the sanitizer took: an element (and its subtree) or an attribute. */
48
+ export type SvgRemoval = {
49
+ element: string;
50
+ } | {
51
+ attribute: string;
52
+ };
53
+ /** What the sanitizer took, for the sketch to say. */
54
+ export interface SvgSanitizeReport {
55
+ /** In document order, names as written, repeats included. */
56
+ removed: SvgRemoval[];
57
+ /** Something sat deeper than {@link MAX_SVG_DEPTH} and was cut. */
58
+ tooDeep: boolean;
59
+ }
60
+ /**
61
+ * The root, sanitized: a NEW tree holding only what the lists allow, or
62
+ * `undefined` when the root itself is not an allowed `<svg>`.
63
+ */
64
+ export declare function sanitizeSvgNode(source: XmlNode): {
65
+ root: XmlElement | undefined;
66
+ report: SvgSanitizeReport;
67
+ };
@@ -0,0 +1,235 @@
1
+ import { xmlElementOf, XML_NAMESPACE, XmlNamespaceScope, XMLNS_NAMESPACE, } from './xml-element.js';
2
+ /**
3
+ * The SVG sanitizer: an ALLOW-LIST applied to the tree `readXml` read, which
4
+ * replaced DOMPurify on a DOM (ADR 0012, P3 as amended 2026-10-14).
5
+ *
6
+ * Security code, written to be read in one sitting. An `.svg` is an executable
7
+ * document handed over by whoever sent the file, and this is the only gate
8
+ * between it and every reader that walks it (the sketch, Wardley's
9
+ * recognisers). The rule is one sentence: **what is not on a list below does
10
+ * not come through.** No deny-list, no repair, no attempt to keep the harmless
11
+ * half of something suspicious.
12
+ *
13
+ * - An element survives only in the SVG namespace (or none, for a file that
14
+ * forgot its `xmlns`) and only if its local name is in
15
+ * {@link ALLOWED_ELEMENTS}. Anything else goes WITH its subtree: `<script>`,
16
+ * `<foreignObject>` and its XHTML, `<use>` (which can pull in another
17
+ * document), the animation elements (which can rewrite an attribute after
18
+ * this gate), every vendor namespace.
19
+ * - An attribute survives only if it is unprefixed and in
20
+ * {@link ALLOWED_ATTRIBUTES} (or `data-*` / `aria-*`), or one of the two
21
+ * namespaced ones a drawing needs (`xml:space`, the `xmlns` declarations).
22
+ * So no `on*` handler is ever on a list.
23
+ * - A surviving value is then checked: no `javascript:`, `vbscript:` or
24
+ * `data:` anywhere, no `url(…)` that is not a same-document `#ref`, no
25
+ * `image-set(…)` (whose bare strings are URLs), no CSS expression, binding
26
+ * or import, and no backslash (a CSS escape is how the first three hide).
27
+ * - `href` / `xlink:href` keep only a same-document `#ref`, except on
28
+ * `<image>`, which also keeps an inline PNG, JPEG, GIF or WebP. No external
29
+ * resource survives, so nothing a file names is ever fetched.
30
+ * - A `<style>` sheet that fails the value check is removed whole.
31
+ * - Nothing nested deeper than {@link MAX_SVG_DEPTH} comes through, because
32
+ * the walks after this one recurse. The cut happens during the walk: what
33
+ * lies below it, or below a removed element, is never resolved or copied.
34
+ *
35
+ * It reports what it took, by kind, and the sketch turns that into its notes.
36
+ */
37
+ export const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
38
+ const XLINK_NAMESPACE = 'http://www.w3.org/1999/xlink';
39
+ /** Deeper than this is cut: no drawing nests 256 groups, and walks recurse. */
40
+ export const MAX_SVG_DEPTH = 256;
41
+ /** Lower-cased local names, as the sketch compares them. */
42
+ const ALLOWED_ELEMENTS = new Set([
43
+ 'svg', 'g', 'defs', 'symbol', 'switch', 'a', 'view',
44
+ 'title', 'desc', 'metadata', 'style',
45
+ 'rect', 'circle', 'ellipse', 'line', 'polyline', 'polygon', 'path',
46
+ 'text', 'tspan', 'textpath', 'image',
47
+ 'clippath', 'mask', 'marker', 'pattern',
48
+ 'lineargradient', 'radialgradient', 'stop',
49
+ 'filter', 'feblend', 'fecolormatrix', 'fecomponenttransfer', 'fecomposite',
50
+ 'feconvolvematrix', 'fediffuselighting', 'fedisplacementmap',
51
+ 'fedistantlight', 'fedropshadow', 'feflood', 'fefunca', 'fefuncb',
52
+ 'fefuncg', 'fefuncr', 'fegaussianblur', 'femerge', 'femergenode',
53
+ 'femorphology', 'feoffset', 'fepointlight', 'fespecularlighting',
54
+ 'fespotlight', 'fetile', 'feturbulence',
55
+ ]); // prettier-ignore
56
+ /** Lower-cased, unprefixed. `href` is not here: it has its own rule. */
57
+ const ALLOWED_ATTRIBUTES = new Set([
58
+ 'id', 'class', 'style', 'lang', 'version', 'type', 'media',
59
+ 'x', 'y', 'z', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry',
60
+ 'fx', 'fy', 'fr', 'dx', 'dy', 'width', 'height', 'd', 'points',
61
+ 'pathlength', 'viewbox', 'preserveaspectratio', 'transform',
62
+ 'transform-origin', 'rotate', 'textlength', 'lengthadjust',
63
+ 'fill', 'fill-opacity', 'fill-rule', 'stroke', 'stroke-width',
64
+ 'stroke-opacity', 'stroke-dasharray', 'stroke-dashoffset', 'stroke-linecap',
65
+ 'stroke-linejoin', 'stroke-miterlimit', 'opacity', 'color', 'display',
66
+ 'visibility', 'overflow', 'clip', 'clip-path', 'clip-rule', 'mask',
67
+ 'mask-type', 'filter', 'paint-order', 'vector-effect', 'pointer-events',
68
+ 'shape-rendering', 'text-rendering', 'image-rendering', 'color-rendering',
69
+ 'color-interpolation', 'color-interpolation-filters',
70
+ 'font-family', 'font-size', 'font-size-adjust', 'font-stretch',
71
+ 'font-style', 'font-variant', 'font-weight', 'letter-spacing',
72
+ 'word-spacing', 'text-anchor', 'text-decoration', 'dominant-baseline',
73
+ 'alignment-baseline', 'baseline-shift', 'writing-mode', 'direction',
74
+ 'unicode-bidi', 'systemlanguage',
75
+ 'marker-start', 'marker-mid', 'marker-end', 'markerwidth', 'markerheight',
76
+ 'markerunits', 'refx', 'refy', 'orient',
77
+ 'clippathunits', 'maskunits', 'maskcontentunits', 'patternunits',
78
+ 'patterncontentunits', 'patterntransform', 'gradientunits',
79
+ 'gradienttransform', 'spreadmethod', 'offset', 'stop-color', 'stop-opacity',
80
+ 'filterunits', 'primitiveunits', 'in', 'in2', 'result', 'mode', 'operator',
81
+ 'values', 'k1', 'k2', 'k3', 'k4', 'stddeviation', 'flood-color',
82
+ 'flood-opacity', 'lighting-color', 'edgemode', 'kernelmatrix', 'order',
83
+ 'scale', 'xchannelselector', 'ychannelselector', 'basefrequency',
84
+ 'numoctaves', 'seed', 'stitchtiles', 'surfacescale', 'specularconstant',
85
+ 'specularexponent', 'diffuseconstant', 'azimuth', 'elevation',
86
+ 'limitingconeangle', 'pointsatx', 'pointsaty', 'pointsatz', 'tablevalues',
87
+ 'slope', 'intercept', 'amplitude', 'exponent', 'bias', 'divisor',
88
+ 'targetx', 'targety', 'preservealpha', 'radius', 'kernelunitlength',
89
+ ]); // prettier-ignore
90
+ /** An inline raster, the only `data:` that survives, and only on `<image>`. */
91
+ const SAFE_IMAGE_DATA = /^data:image\/(?:png|jpeg|jpg|gif|webp);base64,[a-z0-9+/=\s]*$/i;
92
+ /**
93
+ * Is this value inert? Read with every whitespace and control character
94
+ * removed and lower-cased, because `java\tscript:` and `JaVaScRiPt:` are the
95
+ * same scheme to a browser.
96
+ */
97
+ export function isSafeSvgValue(value) {
98
+ const folded = value.replaceAll(/[\u0000- \u007f-\u009f]/g, '').toLowerCase();
99
+ if (folded.includes('\\'))
100
+ return false;
101
+ if (/javascript:|vbscript:|data:|expression\(|@import|-moz-binding|behavior:|image-set\(/.test(folded)) {
102
+ return false;
103
+ }
104
+ for (const match of folded.matchAll(/url\(/g)) {
105
+ const rest = folded.slice(match.index + 4).replace(/^['"]/, '');
106
+ if (!rest.startsWith('#'))
107
+ return false;
108
+ }
109
+ return true;
110
+ }
111
+ function isAllowedAttribute(attr, element) {
112
+ if (attr.namespaceURI === XMLNS_NAMESPACE)
113
+ return true;
114
+ if (attr.namespaceURI === XML_NAMESPACE)
115
+ return attr.localName === 'space';
116
+ const isHref = attr.localName === 'href' &&
117
+ (attr.prefix === null || attr.namespaceURI === XLINK_NAMESPACE);
118
+ if (isHref) {
119
+ const value = attr.value.trim();
120
+ return ((value.startsWith('#') && isSafeSvgValue(value)) ||
121
+ (element === 'image' && SAFE_IMAGE_DATA.test(value)));
122
+ }
123
+ if (attr.prefix !== null)
124
+ return false;
125
+ const name = attr.localName.toLowerCase();
126
+ const listed = ALLOWED_ATTRIBUTES.has(name) || /^(?:data|aria)-[\w.-]+$/.test(name);
127
+ return listed && isSafeSvgValue(attr.value);
128
+ }
129
+ /** A sheet's own character data, which is all of it a sanitized `<style>` keeps. */
130
+ function styleEntriesOf(node) {
131
+ if (!node.content) {
132
+ return node.text === undefined ? [] : [{ kind: 'text', value: node.text }];
133
+ }
134
+ return node.content.filter(entry => 'kind' in entry && entry.kind === 'text');
135
+ }
136
+ function styleTextOf(node) {
137
+ return styleEntriesOf(node)
138
+ .map(entry => ('kind' in entry && entry.kind === 'text' ? entry.value : ''))
139
+ .join('');
140
+ }
141
+ /**
142
+ * The root, sanitized: a NEW tree holding only what the lists allow, or
143
+ * `undefined` when the root itself is not an allowed `<svg>`.
144
+ */
145
+ export function sanitizeSvgNode(source) {
146
+ const report = { removed: [], tooDeep: false };
147
+ const scope = new XmlNamespaceScope();
148
+ /** Entered in `scope` by the caller; exited by the caller too. */
149
+ const allowed = (node) => {
150
+ const name = node.local.toLowerCase();
151
+ const namespaceURI = scope.resolve(node.prefix);
152
+ if (namespaceURI !== SVG_NAMESPACE && namespaceURI !== null)
153
+ return false;
154
+ if (namespaceURI === null && node.prefix !== '')
155
+ return false;
156
+ if (!ALLOWED_ELEMENTS.has(name))
157
+ return false;
158
+ return name !== 'style' || isSafeSvgValue(styleTextOf(node));
159
+ };
160
+ const copy = (node) => {
161
+ const name = node.local.toLowerCase();
162
+ const attrs = Object.create(null);
163
+ for (const attr of scope.attributesOf(node)) {
164
+ if (isAllowedAttribute(attr, name))
165
+ attrs[attr.name] = attr.value;
166
+ else
167
+ report.removed.push({ attribute: attr.name });
168
+ }
169
+ return {
170
+ name: node.name,
171
+ prefix: node.prefix,
172
+ local: node.local,
173
+ attrs,
174
+ children: [],
175
+ content: [],
176
+ range: node.range,
177
+ };
178
+ };
179
+ scope.enter(source);
180
+ if (!allowed(source) || source.local.toLowerCase() !== 'svg') {
181
+ return { root: undefined, report };
182
+ }
183
+ // Depth-first with an exit step, so the namespace scope and the depth are
184
+ // those of the element being read, and the report is in document order.
185
+ const frameOf = (from, to) => {
186
+ const name = from.local.toLowerCase();
187
+ // `<metadata>` is RDF and vendor bookkeeping the walk drops in silence;
188
+ // its content is not read, so it is not reported either. A `<style>`
189
+ // keeps its own character data and nothing else: that is the text the
190
+ // value check read, and checking a subtree per nested sheet was quadratic.
191
+ const entries = name === 'metadata'
192
+ ? []
193
+ : name === 'style'
194
+ ? styleEntriesOf(from)
195
+ : (from.content ?? from.children);
196
+ return { from, to, entries, index: 0 };
197
+ };
198
+ const clean = copy(source);
199
+ const stack = [frameOf(source, clean)];
200
+ while (stack.length > 0) {
201
+ const frame = stack[stack.length - 1];
202
+ if (frame.index >= frame.entries.length) {
203
+ scope.exit(frame.from);
204
+ stack.pop();
205
+ continue;
206
+ }
207
+ const entry = frame.entries[frame.index];
208
+ frame.index += 1;
209
+ if ('kind' in entry) {
210
+ // Character data stays (it is what a `<text>` says); a comment or an
211
+ // instruction renders nothing and is not carried.
212
+ if (entry.kind === 'text') {
213
+ const text = { kind: 'text', value: entry.value };
214
+ frame.to.content.push(text);
215
+ }
216
+ continue;
217
+ }
218
+ // `stack.length` is the depth of the element about to be entered.
219
+ if (stack.length > MAX_SVG_DEPTH) {
220
+ report.tooDeep = true;
221
+ continue;
222
+ }
223
+ scope.enter(entry);
224
+ if (!allowed(entry)) {
225
+ scope.exit(entry);
226
+ report.removed.push({ element: entry.name });
227
+ continue;
228
+ }
229
+ const child = copy(entry);
230
+ frame.to.children.push(child);
231
+ frame.to.content.push(child);
232
+ stack.push(frameOf(entry, child));
233
+ }
234
+ return { root: xmlElementOf(clean), report };
235
+ }
@@ -38,6 +38,7 @@ export declare const SVG_SKETCH_SWITCH: ChromeWording;
38
38
  export declare const SVG_SKETCH_SKIPPED: ChromeWording;
39
39
  export declare const SVG_SKETCH_REMOVED: ChromeWording;
40
40
  export declare const SVG_SKETCH_REMOVED_ATTRIBUTE: ChromeWording;
41
+ export declare const SVG_SKETCH_TOO_DEEP: ChromeWording;
41
42
  export declare const SVG_SKETCH_EMPTY: ChromeWording;
42
43
  export declare const SVG_SKETCH_ERROR_MALFORMED_XML: ChromeWording;
43
44
  export declare const SVG_SKETCH_ERROR_NOT_SVG: ChromeWording;
@@ -1,3 +1,4 @@
1
+ import { XML_READ_TOO_LARGE } from './xml-reader.js';
1
2
  /**
2
3
  * The SVG importer's own remarks (`svg-sketch.ts`) — one key per SENTENCE
3
4
  * SHAPE, not per exact string: a `{{name}}` hole is filled by
@@ -88,6 +89,10 @@ export const SVG_SKETCH_REMOVED_ATTRIBUTE = [
88
89
  'com.labre.interchange.svg.removed-attribute',
89
90
  'Attributes outside the safe SVG drawing vocabulary — event handlers, script URLs, and anything else the sanitizer does not know — were removed before the file was read.',
90
91
  ];
92
+ export const SVG_SKETCH_TOO_DEEP = [
93
+ 'com.labre.interchange.svg.too-deep',
94
+ 'Elements nested more than {{depth}} levels deep were not read.',
95
+ ];
91
96
  export const SVG_SKETCH_EMPTY = [
92
97
  'com.labre.interchange.svg.empty',
93
98
  'No shape or text was recognised in this SVG, so nothing was drawn.',
@@ -136,8 +141,12 @@ export const SVG_SKETCH_WORDINGS = [
136
141
  SVG_SKETCH_SKIPPED,
137
142
  SVG_SKETCH_REMOVED,
138
143
  SVG_SKETCH_REMOVED_ATTRIBUTE,
144
+ SVG_SKETCH_TOO_DEEP,
139
145
  SVG_SKETCH_EMPTY,
140
146
  SVG_SKETCH_ERROR_MALFORMED_XML,
141
147
  SVG_SKETCH_ERROR_NOT_SVG,
142
148
  SVG_SKETCH_ERROR_SANITIZED_AWAY,
149
+ // The shared XML reader's one refusal: no package of its own to list it,
150
+ // and the SVG sketch is the reader every host already registers.
151
+ XML_READ_TOO_LARGE,
143
152
  ];
@@ -1,5 +1,6 @@
1
1
  import type { TranslationParams } from '../../affine-shared/services/index.js';
2
2
  import { type InterchangeImportContext, type InterchangeImportResult, type InterchangeNote, type SerializedElementProps } from './interchange.js';
3
+ import type { XmlElement } from './xml-element.js';
3
4
  /**
4
5
  * **SVG → a canvas sketch.** The platform's VISUAL-tier importer, shared by
5
6
  * every framework that declares it (`docs/adr/0012`, P2).
@@ -162,8 +163,8 @@ export type SvgSketchFrame = Frame;
162
163
  * A framework that claims nodes owes its own heuristics statement for what it
163
164
  * claims; this module's statement covers only what reaches this walk.
164
165
  */
165
- export declare function sketchSvgTree(root: Element, notes: SvgSketchNotebook, options?: {
166
- skip?: ReadonlySet<Element>;
166
+ export declare function sketchSvgTree(root: XmlElement, notes: SvgSketchNotebook, options?: {
167
+ skip?: ReadonlySet<XmlElement>;
167
168
  place?: SvgSketchFrame;
168
169
  }): SerializedElementProps[];
169
170
  /**
@@ -176,28 +177,31 @@ export declare function sketchSvgTree(root: Element, notes: SvgSketchNotebook, o
176
177
  * The transforms the walk ignores are ignored here too, silently: the walk
177
178
  * already says so once per kind when it meets them.
178
179
  */
179
- export declare function svgFrameOf(element: Element): SvgSketchFrame;
180
+ export declare function svgFrameOf(element: XmlElement): SvgSketchFrame;
180
181
  /**
181
182
  * The document, sanitized, or an exception naming what is wrong with it.
182
183
  *
183
- * Well-formedness FIRST, because `DOMParser` reports a malformed document as a
184
- * `parsererror` element rather than by throwing — a verdict in the shape of a
185
- * document — and because DOMPurify's parser is the forgiving HTML one, which
186
- * would quietly repair the very thing the caller needs to be told about. So:
187
- * parse for the verdict, reading nothing out of the result but that; sanitize
188
- * the SOURCE; and walk only what came back from the sanitizer.
184
+ * Read with `readXml` in its conformant mode, then filtered by the allow-list
185
+ * in `svg-sanitize.ts`; the walk and every recogniser see only what came back.
186
+ * No DOM is involved at any step, so the same function runs in the editor, in
187
+ * a test and on a server (ADR 0012, P3 as amended 2026-10-14). It replaced
188
+ * `DOMParser` + DOMPurify, which needed a DOM installed before the library was
189
+ * even imported (the P0 import spike measured that trap).
190
+ *
191
+ * Well-formedness first: anything the reader had to recover from is refused,
192
+ * with the reader's first remark as the detail, as `DOMParser` used to refuse
193
+ * it. A `DOCTYPE` is not an error: it is skipped and no entity it declares is
194
+ * ever expanded, so a `&name;` it declares stays those characters.
189
195
  *
190
196
  * The sanitizer is not optional and it is not defensive programming. An `.svg`
191
197
  * is an executable document — `<script>`, `<foreignObject>` with markup, event
192
- * handler attributes — handed over by whoever sent the file. The same
193
- * `USE_PROFILES: { svg: true }` guard is what the edgeless clipboard already
194
- * puts in front of a pasted SVG.
198
+ * handler attributes — handed over by whoever sent the file.
195
199
  *
196
200
  * The first of the two passes {@link parseSvgSketch} is made of, exported so a
197
201
  * framework that recognises part of a picture reads the SAME sanitised tree
198
202
  * the sketch walks (ADR 0032 §2, §7) — never the source.
199
203
  */
200
- export declare function sanitizeSvg(source: string, notes: SvgSketchNotebook): Element;
204
+ export declare function sanitizeSvg(source: string, notes: SvgSketchNotebook): XmlElement;
201
205
  /**
202
206
  * An SVG file as a canvas sketch — the shared VISUAL-tier importer.
203
207
  *