@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.
- package/dist/_pkgs/affine-block-attachment/asset-picker.d.ts +24 -0
- package/dist/_pkgs/affine-block-attachment/asset-picker.js +87 -0
- package/dist/_pkgs/affine-block-attachment/configs/slash-menu.js +3 -20
- package/dist/_pkgs/affine-block-attachment/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-attachment/index.js +1 -0
- package/dist/_pkgs/affine-block-attachment/utils.d.ts +9 -1
- package/dist/_pkgs/affine-block-attachment/utils.js +9 -0
- package/dist/_pkgs/affine-block-bookmark/adapters/markdown/markdown.js +14 -1
- package/dist/_pkgs/affine-block-bookmark/adapters/notion-html.js +5 -1
- package/dist/_pkgs/affine-block-database/adapters/markdown.js +5 -3
- package/dist/_pkgs/affine-block-embed/common/adapters/html.js +5 -3
- package/dist/_pkgs/affine-block-embed/common/adapters/markdown.js +11 -0
- package/dist/_pkgs/affine-block-frame/frame-template.d.ts +58 -0
- package/dist/_pkgs/affine-block-frame/frame-template.js +207 -0
- package/dist/_pkgs/affine-block-frame/frame-toolbar.js +19 -3
- package/dist/_pkgs/affine-block-frame/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-frame/index.js +1 -0
- package/dist/_pkgs/affine-block-frame/translations.d.ts +7 -0
- package/dist/_pkgs/affine-block-frame/translations.js +16 -1
- package/dist/_pkgs/affine-block-frame/view.js +6 -0
- package/dist/_pkgs/affine-block-image/asset-picker.d.ts +63 -0
- package/dist/_pkgs/affine-block-image/asset-picker.js +183 -0
- package/dist/_pkgs/affine-block-image/commands/insert-images.d.ts +5 -0
- package/dist/_pkgs/affine-block-image/commands/insert-images.js +8 -8
- package/dist/_pkgs/affine-block-image/components/page-image-block.d.ts +11 -0
- package/dist/_pkgs/affine-block-image/components/page-image-block.js +81 -12
- package/dist/_pkgs/affine-block-image/configs/replace-action.d.ts +9 -0
- package/dist/_pkgs/affine-block-image/configs/replace-action.js +27 -0
- package/dist/_pkgs/affine-block-image/configs/toolbar.js +6 -0
- package/dist/_pkgs/affine-block-image/crop/crop-controller.d.ts +46 -0
- package/dist/_pkgs/affine-block-image/crop/crop-controller.js +104 -0
- package/dist/_pkgs/affine-block-image/crop/crop-math.d.ts +70 -0
- package/dist/_pkgs/affine-block-image/crop/crop-math.js +141 -0
- package/dist/_pkgs/affine-block-image/crop/crop-overlay.d.ts +57 -0
- package/dist/_pkgs/affine-block-image/crop/crop-overlay.js +343 -0
- package/dist/_pkgs/affine-block-image/crop/crop-toolbar.d.ts +12 -0
- package/dist/_pkgs/affine-block-image/crop/crop-toolbar.js +78 -0
- package/dist/_pkgs/affine-block-image/crop/write-crop.d.ts +16 -0
- package/dist/_pkgs/affine-block-image/crop/write-crop.js +57 -0
- package/dist/_pkgs/affine-block-image/edgeless-clipboard-config.js +4 -1
- package/dist/_pkgs/affine-block-image/effects.js +2 -0
- package/dist/_pkgs/affine-block-image/image-block.d.ts +2 -0
- package/dist/_pkgs/affine-block-image/image-block.js +2 -0
- package/dist/_pkgs/affine-block-image/image-edgeless-block.d.ts +2 -0
- package/dist/_pkgs/affine-block-image/image-edgeless-block.js +36 -8
- package/dist/_pkgs/affine-block-image/image-resize-manager.js +15 -0
- package/dist/_pkgs/affine-block-image/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-image/index.js +1 -0
- package/dist/_pkgs/affine-block-image/translations.d.ts +15 -0
- package/dist/_pkgs/affine-block-image/translations.js +43 -0
- package/dist/_pkgs/affine-block-image/utils.d.ts +13 -0
- package/dist/_pkgs/affine-block-image/utils.js +10 -1
- package/dist/_pkgs/affine-block-root/clipboard/page-clipboard.js +5 -0
- package/dist/_pkgs/affine-block-root/commands/page-export-commands.d.ts +2 -0
- package/dist/_pkgs/affine-block-root/commands/page-export-commands.js +71 -0
- package/dist/_pkgs/affine-block-root/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-root/index.js +1 -0
- package/dist/_pkgs/affine-block-root/view.js +8 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/capability.d.ts +30 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/capability.js +44 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/colors.d.ts +26 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/colors.js +82 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/command.d.ts +18 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/command.js +91 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/index.d.ts +3 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/index.js +3 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/writer.d.ts +50 -0
- package/dist/_pkgs/affine-block-surface/extensions/drawio-export/writer.js +732 -0
- package/dist/_pkgs/affine-block-surface/extensions/export-manager/draw-image-block.d.ts +12 -0
- package/dist/_pkgs/affine-block-surface/extensions/export-manager/draw-image-block.js +15 -0
- package/dist/_pkgs/affine-block-surface/extensions/export-manager/export-manager.js +7 -1
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/command.d.ts +7 -1
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/command.js +60 -1
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/index.d.ts +2 -2
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/index.js +2 -2
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/marker-version.d.ts +6 -0
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/marker-version.js +8 -0
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/render.d.ts +28 -1
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/render.js +66 -6
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/svg-context.d.ts +2 -7
- package/dist/_pkgs/affine-block-surface/extensions/export-svg/svg-context.js +2 -6
- package/dist/_pkgs/affine-block-surface/extensions/index.d.ts +2 -0
- package/dist/_pkgs/affine-block-surface/extensions/index.js +2 -0
- package/dist/_pkgs/affine-block-surface/extensions/interchange-export.d.ts +84 -0
- package/dist/_pkgs/affine-block-surface/extensions/interchange-export.js +150 -0
- package/dist/_pkgs/affine-block-surface/extensions/interchange-import.js +24 -2
- package/dist/_pkgs/affine-block-surface/extensions/interchange.d.ts +23 -2
- package/dist/_pkgs/affine-block-surface/extensions/interchange.js +4 -1
- package/dist/_pkgs/affine-block-surface/extensions/legend-role.d.ts +18 -0
- package/dist/_pkgs/affine-block-surface/extensions/legend-role.js +20 -0
- package/dist/_pkgs/affine-block-surface/extensions/legend.d.ts +2 -19
- package/dist/_pkgs/affine-block-surface/extensions/legend.js +2 -18
- package/dist/_pkgs/affine-block-surface/extensions/svg-sanitize.d.ts +67 -0
- package/dist/_pkgs/affine-block-surface/extensions/svg-sanitize.js +235 -0
- package/dist/_pkgs/affine-block-surface/extensions/svg-sketch-translations.d.ts +1 -0
- package/dist/_pkgs/affine-block-surface/extensions/svg-sketch-translations.js +9 -0
- package/dist/_pkgs/affine-block-surface/extensions/svg-sketch.d.ts +17 -13
- package/dist/_pkgs/affine-block-surface/extensions/svg-sketch.js +52 -80
- package/dist/_pkgs/affine-block-surface/extensions/xml-element.d.ts +98 -0
- package/dist/_pkgs/affine-block-surface/extensions/xml-element.js +261 -0
- package/dist/_pkgs/affine-block-surface/extensions/xml-reader.d.ts +238 -0
- package/dist/_pkgs/affine-block-surface/extensions/xml-reader.js +570 -0
- package/dist/_pkgs/affine-block-surface/framework-background/def.js +1 -1
- package/dist/_pkgs/affine-block-surface/index.d.ts +1 -0
- package/dist/_pkgs/affine-block-surface/index.js +1 -0
- package/dist/_pkgs/affine-block-surface/interchange-headless.d.ts +29 -0
- package/dist/_pkgs/affine-block-surface/interchange-headless.js +32 -0
- package/dist/_pkgs/affine-block-surface/view.js +22 -2
- package/dist/_pkgs/affine-block-table/adapters/utils.js +24 -1
- package/dist/_pkgs/affine-gfx-note/toolbar/note-menu.js +5 -8
- package/dist/_pkgs/affine-gfx-template/contribute.d.ts +14 -4
- package/dist/_pkgs/affine-gfx-template/contribute.js +47 -6
- package/dist/_pkgs/affine-gfx-template/toolbar/builtin-templates.d.ts +7 -2
- package/dist/_pkgs/affine-gfx-template/toolbar/builtin-templates.js +14 -40
- package/dist/_pkgs/affine-gfx-template/toolbar/resolve-name.d.ts +1 -1
- package/dist/_pkgs/affine-gfx-template/toolbar/resolve-name.js +1 -1
- package/dist/_pkgs/affine-gfx-template/toolbar/template-panel.d.ts +4 -4
- package/dist/_pkgs/affine-gfx-template/toolbar/template-panel.js +9 -9
- package/dist/_pkgs/affine-gfx-template/toolbar/template-type.d.ts +2 -4
- package/dist/_pkgs/affine-gfx-template/translations.d.ts +5 -0
- package/dist/_pkgs/affine-gfx-template/translations.js +9 -0
- package/dist/_pkgs/affine-inline-footnote/adapters/markdown/inline-delta.js +6 -0
- package/dist/_pkgs/affine-inline-footnote/adapters/markdown/markdown-inline.js +7 -0
- package/dist/_pkgs/affine-inline-link/adapters/html/html-inline.js +4 -1
- package/dist/_pkgs/affine-inline-link/adapters/html/inline-delta.js +3 -1
- package/dist/_pkgs/affine-inline-link/adapters/markdown/inline-delta.js +4 -1
- package/dist/_pkgs/affine-inline-link/adapters/notion-html/html-inline.js +2 -1
- package/dist/_pkgs/affine-inline-link/link-node/affine-link.d.ts +7 -0
- package/dist/_pkgs/affine-inline-link/link-node/affine-link.js +37 -0
- package/dist/_pkgs/affine-inline-link/link-node/index.d.ts +1 -0
- package/dist/_pkgs/affine-inline-link/link-node/index.js +1 -0
- package/dist/_pkgs/affine-inline-link/link-node/propose-views.d.ts +22 -0
- package/dist/_pkgs/affine-inline-link/link-node/propose-views.js +59 -0
- package/dist/_pkgs/affine-model/blocks/image/image-crop.d.ts +48 -0
- package/dist/_pkgs/affine-model/blocks/image/image-crop.js +78 -0
- package/dist/_pkgs/affine-model/blocks/image/image-model.d.ts +6 -0
- package/dist/_pkgs/affine-model/blocks/image/image-model.js +2 -0
- package/dist/_pkgs/affine-model/blocks/image/index.d.ts +1 -0
- package/dist/_pkgs/affine-model/blocks/image/index.js +1 -0
- package/dist/_pkgs/affine-model/consts/shape.d.ts +2 -0
- package/dist/_pkgs/affine-model/consts/shape.js +5 -0
- package/dist/_pkgs/affine-model/elements/bpmn/consts.d.ts +31 -0
- package/dist/_pkgs/affine-model/elements/bpmn/consts.js +31 -0
- package/dist/_pkgs/affine-model/elements/bpmn/index.d.ts +1 -0
- package/dist/_pkgs/affine-model/elements/bpmn/index.js +1 -0
- package/dist/_pkgs/affine-model/elements/bpmn/pool.d.ts +0 -25
- package/dist/_pkgs/affine-model/elements/bpmn/pool.js +1 -25
- package/dist/_pkgs/affine-model/elements/shape/shape.d.ts +0 -2
- package/dist/_pkgs/affine-model/elements/shape/shape.js +1 -2
- package/dist/_pkgs/affine-model/elements/uml/consts.d.ts +133 -0
- package/dist/_pkgs/affine-model/elements/uml/consts.js +107 -0
- package/dist/_pkgs/affine-model/elements/uml/diagram.d.ts +1 -79
- package/dist/_pkgs/affine-model/elements/uml/diagram.js +1 -53
- package/dist/_pkgs/affine-model/elements/uml/fragment.d.ts +0 -15
- package/dist/_pkgs/affine-model/elements/uml/fragment.js +1 -15
- package/dist/_pkgs/affine-model/elements/uml/index.d.ts +1 -0
- package/dist/_pkgs/affine-model/elements/uml/index.js +1 -0
- package/dist/_pkgs/affine-model/elements/uml/partition.d.ts +0 -20
- package/dist/_pkgs/affine-model/elements/uml/partition.js +1 -20
- package/dist/_pkgs/affine-model/elements/uml/region.d.ts +0 -11
- package/dist/_pkgs/affine-model/elements/uml/region.js +1 -11
- package/dist/_pkgs/affine-model/headless.d.ts +19 -0
- package/dist/_pkgs/affine-model/headless.js +19 -0
- package/dist/_pkgs/affine-shared/headless.d.ts +10 -0
- package/dist/_pkgs/affine-shared/headless.js +10 -0
- package/dist/_pkgs/affine-shared/services/asset-picker-service.d.ts +63 -0
- package/dist/_pkgs/affine-shared/services/asset-picker-service.js +20 -0
- package/dist/_pkgs/affine-shared/services/frame-template-service.d.ts +83 -0
- package/dist/_pkgs/affine-shared/services/frame-template-service.js +17 -0
- package/dist/_pkgs/affine-shared/services/index.d.ts +3 -0
- package/dist/_pkgs/affine-shared/services/index.js +3 -0
- package/dist/_pkgs/affine-shared/services/thread-marks-service.d.ts +69 -0
- package/dist/_pkgs/affine-shared/services/thread-marks-service.js +20 -0
- package/dist/_pkgs/affine-shared/services/translation-service/chrome.d.ts +6 -0
- package/dist/_pkgs/affine-shared/services/translation-service/chrome.js +10 -0
- package/dist/_pkgs/affine-widget-drag-handle/watchers/drag-event-watcher.js +8 -3
- package/dist/_pkgs/affine-widget-keyboard-toolbar/config.js +4 -16
- package/dist/_pkgs/affine-widget-linked-doc/transformers/html.d.ts +7 -0
- package/dist/_pkgs/affine-widget-linked-doc/transformers/html.js +25 -24
- package/dist/_pkgs/affine-widget-linked-doc/transformers/index.d.ts +1 -1
- package/dist/_pkgs/affine-widget-linked-doc/transformers/markdown.d.ts +7 -0
- package/dist/_pkgs/affine-widget-linked-doc/transformers/markdown.js +26 -25
- package/dist/_pkgs/affine-widget-linked-doc/transformers/utils.d.ts +23 -0
- package/dist/_pkgs/affine-widget-linked-doc/transformers/utils.js +14 -0
- package/dist/_pkgs/affine-widget-thread-marks/effects.d.ts +1 -0
- package/dist/_pkgs/affine-widget-thread-marks/effects.js +4 -0
- package/dist/_pkgs/affine-widget-thread-marks/excerpt.d.ts +19 -0
- package/dist/_pkgs/affine-widget-thread-marks/excerpt.js +68 -0
- package/dist/_pkgs/affine-widget-thread-marks/index.d.ts +5 -0
- package/dist/_pkgs/affine-widget-thread-marks/index.js +8 -0
- package/dist/_pkgs/affine-widget-thread-marks/toolbar.d.ts +49 -0
- package/dist/_pkgs/affine-widget-thread-marks/toolbar.js +108 -0
- package/dist/_pkgs/affine-widget-thread-marks/translations.d.ts +9 -0
- package/dist/_pkgs/affine-widget-thread-marks/translations.js +11 -0
- package/dist/_pkgs/affine-widget-thread-marks/view.d.ts +12 -0
- package/dist/_pkgs/affine-widget-thread-marks/view.js +25 -0
- package/dist/_pkgs/affine-widget-thread-marks/widget.d.ts +51 -0
- package/dist/_pkgs/affine-widget-thread-marks/widget.js +214 -0
- package/dist/commands.js +5 -2
- package/dist/extensions/view.js +2 -0
- package/dist/host-panels.d.ts +24 -1
- package/dist/host-panels.js +61 -10
- package/dist/interchange.d.ts +1 -0
- package/dist/interchange.js +1 -0
- package/dist/model/headless.d.ts +1 -0
- package/dist/model/headless.js +1 -0
- package/dist/shared/headless.d.ts +1 -0
- package/dist/shared/headless.js +1 -0
- package/dist/translations.js +10 -1
- 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
|
-
/**
|
|
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
|
-
/**
|
|
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:
|
|
166
|
-
skip?: ReadonlySet<
|
|
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:
|
|
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
|
-
*
|
|
184
|
-
* `
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
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.
|
|
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):
|
|
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
|
*
|