@formicoidea/labre-core 0.50.0 → 0.51.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/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 +2 -0
- package/dist/_pkgs/affine-shared/services/index.js +2 -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/commands.js +5 -2
- package/dist/host-panels.d.ts +21 -1
- package/dist/host-panels.js +50 -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 +8 -1
- package/package.json +13 -1
|
@@ -0,0 +1,732 @@
|
|
|
1
|
+
import { PointStyle } from '../../../affine-model/index.js';
|
|
2
|
+
import { drawioColor } from './colors.js';
|
|
3
|
+
/**
|
|
4
|
+
* The canvas as a draw.io drawing — the GENERIC writer of `canvas:drawio:export`
|
|
5
|
+
* (`docs/adr/0019`, amendment of 2026-10-14; `docs/adr/0012` P2's visual tier).
|
|
6
|
+
*
|
|
7
|
+
* ## What it writes, and what it promises
|
|
8
|
+
*
|
|
9
|
+
* One uncompressed `<mxfile>` holding one page: a `<mxGraphModel>` whose cells
|
|
10
|
+
* are what the canvas PAINTS — a shape as a draw.io shape, a connector as an
|
|
11
|
+
* edge, a text as a text, a group as a draw.io group, and the canvas blocks a
|
|
12
|
+
* reader needs (a note, a frame, an edgeless text, an image as a placeholder
|
|
13
|
+
* naming its blob). A framework artefact is written by its generic rendering —
|
|
14
|
+
* the shape it extends and the words grouped with it — and carries its `role`
|
|
15
|
+
* as a `labre-role` attribute, so a reader that knows the vocabulary can
|
|
16
|
+
* recognise it (`importDrawio`, UML, does for the class and use case
|
|
17
|
+
* families). That is a courtesy to a reader, not a model: this is the VISUAL
|
|
18
|
+
* tier, and what a framework means is written by its own semantic exporter
|
|
19
|
+
* (PlantUML, XMI, `.bpmn`, OWM), never here.
|
|
20
|
+
*
|
|
21
|
+
* ## Pure, and deterministic
|
|
22
|
+
*
|
|
23
|
+
* No `std`, no DOM, no clock: element records in, text out (`docs/adr/0012`
|
|
24
|
+
* P3), so labre-mcp can call it and a unit suite can prove it with plain
|
|
25
|
+
* objects. The same canvas writes the same bytes — cells in z-order (frames
|
|
26
|
+
* first, then the fractional `index`, the id breaking ties), numbers rounded to
|
|
27
|
+
* two decimals, no timestamp, no generated id: a cell's id IS the element's id
|
|
28
|
+
* (`docs/adr/0012` D3), so a re-import can name what it read.
|
|
29
|
+
*
|
|
30
|
+
* ## What is never written
|
|
31
|
+
*
|
|
32
|
+
* No link, no URL, no script: a label is the element's plain text,
|
|
33
|
+
* HTML-escaped (draw.io reads `html=1` labels as markup) and then XML-escaped,
|
|
34
|
+
* so a `<script>` somebody typed arrives as the seven characters they typed.
|
|
35
|
+
* An image is a placeholder carrying its blob id, never the bytes nor an
|
|
36
|
+
* address. Nothing here reads `pivotDocId`, tags or `interchange` payloads.
|
|
37
|
+
*
|
|
38
|
+
* ## Where it is known to lose
|
|
39
|
+
*
|
|
40
|
+
* A freehand stroke, a highlighter and a polygon's outline have no draw.io
|
|
41
|
+
* shape: the first two are left out, the third is drawn as its bounding box,
|
|
42
|
+
* and each costs one warning per kind. A curved connector is drawn
|
|
43
|
+
* `curved=1` through no waypoint (draw.io routes its own curve). A block other
|
|
44
|
+
* than the four above (an embed, an attachment, a LaTeX block) is left out with
|
|
45
|
+
* a warning naming its flavour. Colours are printed in their LIGHT variant.
|
|
46
|
+
*/
|
|
47
|
+
/* ── The format ───────────────────────────────────────────────────────── */
|
|
48
|
+
export const DRAWIO_EXTENSION = '.drawio';
|
|
49
|
+
export const DRAWIO_MIME = 'application/xml';
|
|
50
|
+
/** The name a canvas with no title is given, as a file and as a page. */
|
|
51
|
+
const FALLBACK_NAME = 'canvas';
|
|
52
|
+
const str = (value) => typeof value === 'string' ? value : undefined;
|
|
53
|
+
const num = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
54
|
+
/** A `Y.Text`, a `Text`, a string — whatever says `toString()` with its words. */
|
|
55
|
+
function textOf(value) {
|
|
56
|
+
if (value === undefined || value === null)
|
|
57
|
+
return '';
|
|
58
|
+
if (typeof value === 'string')
|
|
59
|
+
return value;
|
|
60
|
+
if (typeof value === 'object' && 'toString' in value) {
|
|
61
|
+
const out = String(value);
|
|
62
|
+
return out === '[object Object]' ? '' : out;
|
|
63
|
+
}
|
|
64
|
+
return '';
|
|
65
|
+
}
|
|
66
|
+
/** The stored `xywh` string, or the `[x, y, w, h]` a live model derives. */
|
|
67
|
+
function boxOf(xywh) {
|
|
68
|
+
try {
|
|
69
|
+
const parsed = typeof xywh === 'string' ? JSON.parse(xywh) : xywh;
|
|
70
|
+
if (!Array.isArray(parsed) || parsed.length < 4)
|
|
71
|
+
return null;
|
|
72
|
+
const [x = NaN, y = NaN, w = NaN, h = NaN] = parsed.map(Number);
|
|
73
|
+
if (![x, y, w, h].every(Number.isFinite))
|
|
74
|
+
return null;
|
|
75
|
+
return { x, y, w, h };
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** The ids a group-like element holds: `childIds`, else its `children` map. */
|
|
82
|
+
function childIdsOf(rec) {
|
|
83
|
+
if (Array.isArray(rec.childIds)) {
|
|
84
|
+
return rec.childIds.filter((id) => typeof id === 'string');
|
|
85
|
+
}
|
|
86
|
+
const children = rec.children;
|
|
87
|
+
if (children && typeof children.keys === 'function') {
|
|
88
|
+
return [...children.keys()];
|
|
89
|
+
}
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
/** A block's words: its own text, then every descendant's, one per line. */
|
|
93
|
+
function blockText(block) {
|
|
94
|
+
const lines = [];
|
|
95
|
+
const walk = (node, depth) => {
|
|
96
|
+
const props = node.props ?? {};
|
|
97
|
+
const own = textOf(props.text ?? node.text).trim();
|
|
98
|
+
if (own)
|
|
99
|
+
lines.push(own);
|
|
100
|
+
if (depth > 32)
|
|
101
|
+
return;
|
|
102
|
+
const children = node.children;
|
|
103
|
+
if (Array.isArray(children)) {
|
|
104
|
+
for (const child of children)
|
|
105
|
+
walk(child, depth + 1);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
walk(block, 0);
|
|
109
|
+
return lines.join('\n');
|
|
110
|
+
}
|
|
111
|
+
const propsOf = (entry) => (entry.block ? entry.rec.props : entry.rec) ?? {};
|
|
112
|
+
/* ── Escaping ─────────────────────────────────────────────────────────── */
|
|
113
|
+
/**
|
|
114
|
+
* Characters XML 1.0 forbids outright. A label holding one would make the whole
|
|
115
|
+
* file unreadable, so it is dropped rather than escaped (there is no escape
|
|
116
|
+
* for them in XML 1.0).
|
|
117
|
+
*/
|
|
118
|
+
const XML_FORBIDDEN = /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g;
|
|
119
|
+
/** An attribute value: XML-escaped, line breaks kept as character references. */
|
|
120
|
+
function attr(value) {
|
|
121
|
+
return value
|
|
122
|
+
.replaceAll(XML_FORBIDDEN, '')
|
|
123
|
+
.replaceAll('&', '&')
|
|
124
|
+
.replaceAll('<', '<')
|
|
125
|
+
.replaceAll('>', '>')
|
|
126
|
+
.replaceAll('"', '"')
|
|
127
|
+
.replaceAll('\n', ' ')
|
|
128
|
+
.replaceAll('\r', ' ')
|
|
129
|
+
.replaceAll('\t', '	');
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* A label for an `html=1` cell: the plain text HTML-escaped first, so draw.io
|
|
133
|
+
* renders what was typed and never interprets it, then line breaks as `<br>`.
|
|
134
|
+
* {@link attr} escapes the result a second time for the attribute.
|
|
135
|
+
*/
|
|
136
|
+
function htmlLabel(text) {
|
|
137
|
+
return text
|
|
138
|
+
.replaceAll('&', '&')
|
|
139
|
+
.replaceAll('<', '<')
|
|
140
|
+
.replaceAll('>', '>')
|
|
141
|
+
.replaceAll('"', '"')
|
|
142
|
+
.replaceAll("'", ''')
|
|
143
|
+
.replaceAll(/\r\n|\r|\n/g, '<br>');
|
|
144
|
+
}
|
|
145
|
+
/** Two decimals, no `-0`: the same canvas writes the same bytes. */
|
|
146
|
+
function fmt(value) {
|
|
147
|
+
const rounded = Math.round(value * 100) / 100;
|
|
148
|
+
return String(rounded === 0 ? 0 : rounded);
|
|
149
|
+
}
|
|
150
|
+
function styleString(lead, style) {
|
|
151
|
+
return [
|
|
152
|
+
...lead,
|
|
153
|
+
...style.map(([key, value]) =>
|
|
154
|
+
// A value never carries the separators the style string is made of.
|
|
155
|
+
`${key}=${String(value).replaceAll(/[;=]/g, '')}`),
|
|
156
|
+
]
|
|
157
|
+
.map(part => `${part};`)
|
|
158
|
+
.join('');
|
|
159
|
+
}
|
|
160
|
+
function pushColor(style, key, opacityKey, color) {
|
|
161
|
+
if (!color)
|
|
162
|
+
return;
|
|
163
|
+
style.push([key, color.value]);
|
|
164
|
+
if (opacityKey && color.opacity !== undefined) {
|
|
165
|
+
style.push([opacityKey, color.opacity]);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const FONT_PREFIX = 'blocksuite:surface:';
|
|
169
|
+
/** Font family, size, colour, weight, italics and decoration. */
|
|
170
|
+
function pushFont(style, props) {
|
|
171
|
+
const family = str(props.fontFamily);
|
|
172
|
+
if (family)
|
|
173
|
+
style.push(['fontFamily', family.replace(FONT_PREFIX, '')]);
|
|
174
|
+
const size = num(props.fontSize);
|
|
175
|
+
if (size !== undefined)
|
|
176
|
+
style.push(['fontSize', fmt(size)]);
|
|
177
|
+
pushColor(style, 'fontColor', null, drawioColor(props.color));
|
|
178
|
+
// draw.io's `fontStyle` is a bit mask: 1 bold, 2 italic, 4 underline, 8
|
|
179
|
+
// strike-through. The decoration is a CSS token list (ADR 0030), read token
|
|
180
|
+
// by token.
|
|
181
|
+
let mask = 0;
|
|
182
|
+
const weight = Number(props.fontWeight);
|
|
183
|
+
if (Number.isFinite(weight) && weight >= 600)
|
|
184
|
+
mask |= 1;
|
|
185
|
+
if (props.fontStyle === 'italic')
|
|
186
|
+
mask |= 2;
|
|
187
|
+
const decoration = str(props.textDecoration)?.split(/\s+/) ?? [];
|
|
188
|
+
if (decoration.includes('underline'))
|
|
189
|
+
mask |= 4;
|
|
190
|
+
if (decoration.includes('line-through'))
|
|
191
|
+
mask |= 8;
|
|
192
|
+
if (mask)
|
|
193
|
+
style.push(['fontStyle', mask]);
|
|
194
|
+
}
|
|
195
|
+
const ALIGN = Object.assign(Object.create(null), {
|
|
196
|
+
left: 'left',
|
|
197
|
+
center: 'center',
|
|
198
|
+
right: 'right',
|
|
199
|
+
});
|
|
200
|
+
const VERTICAL_ALIGN = Object.assign(Object.create(null), { top: 'top', center: 'middle', bottom: 'bottom' });
|
|
201
|
+
function pushStroke(style, props, colorKey) {
|
|
202
|
+
const strokeStyle = str(props.strokeStyle);
|
|
203
|
+
if (strokeStyle === 'none') {
|
|
204
|
+
style.push(['strokeColor', 'none']);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
pushColor(style, 'strokeColor', 'strokeOpacity', drawioColor(props[colorKey]));
|
|
208
|
+
const width = num(props.strokeWidth);
|
|
209
|
+
if (width !== undefined)
|
|
210
|
+
style.push(['strokeWidth', fmt(width)]);
|
|
211
|
+
if (strokeStyle === 'dash')
|
|
212
|
+
style.push(['dashed', 1]);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* A Labre end marker as draw.io's `[arrow, fill]` pair (`PointStyle`, ADR
|
|
216
|
+
* 0016). The enum is append-only, so the default answers for a marker
|
|
217
|
+
* appended after this was written: draw.io's plain open arrow, never a
|
|
218
|
+
* failure.
|
|
219
|
+
*/
|
|
220
|
+
function arrowOf(pointStyle) {
|
|
221
|
+
switch (pointStyle) {
|
|
222
|
+
case PointStyle.None:
|
|
223
|
+
return ['none', 0];
|
|
224
|
+
case PointStyle.Triangle:
|
|
225
|
+
return ['block', 1];
|
|
226
|
+
case PointStyle.TriangleHollow:
|
|
227
|
+
return ['block', 0];
|
|
228
|
+
case PointStyle.Diamond:
|
|
229
|
+
return ['diamond', 1];
|
|
230
|
+
case PointStyle.DiamondHollow:
|
|
231
|
+
return ['diamond', 0];
|
|
232
|
+
case PointStyle.Circle:
|
|
233
|
+
return ['oval', 1];
|
|
234
|
+
default:
|
|
235
|
+
return ['open', 0];
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/* ── The writer ───────────────────────────────────────────────────────── */
|
|
239
|
+
const ROOT = '0';
|
|
240
|
+
const LAYER = '1';
|
|
241
|
+
/** The elements (and `context.blocks`) handed over, as one draw.io page. */
|
|
242
|
+
export function exportCanvasDrawio(elements, context = {}) {
|
|
243
|
+
const entries = collect(elements, context.blocks ?? []);
|
|
244
|
+
const byId = new Map(entries.map(entry => [entry.id, entry]));
|
|
245
|
+
// The exported group holding each entry. A group lists its DIRECT children
|
|
246
|
+
// (a nested group lists the inner group, not what it holds), so one claim
|
|
247
|
+
// per child is the whole of the nesting.
|
|
248
|
+
const parentOf = new Map();
|
|
249
|
+
for (const entry of entries) {
|
|
250
|
+
if (entry.type !== 'group' && entry.type !== 'mindmap')
|
|
251
|
+
continue;
|
|
252
|
+
for (const childId of childIdsOf(entry.rec)) {
|
|
253
|
+
if (childId === entry.id || !byId.has(childId))
|
|
254
|
+
continue;
|
|
255
|
+
if (!parentOf.has(childId))
|
|
256
|
+
parentOf.set(childId, entry.id);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// A group listed as its own ancestor is a broken record, not a drawing: the
|
|
260
|
+
// chain is cut where it loops rather than walked forever.
|
|
261
|
+
for (const [child] of parentOf) {
|
|
262
|
+
const seen = new Set([child]);
|
|
263
|
+
let at = parentOf.get(child);
|
|
264
|
+
while (at !== undefined) {
|
|
265
|
+
if (seen.has(at)) {
|
|
266
|
+
parentOf.delete(child);
|
|
267
|
+
break;
|
|
268
|
+
}
|
|
269
|
+
seen.add(at);
|
|
270
|
+
at = parentOf.get(at);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
const warnings = new Map();
|
|
274
|
+
const warn = (sentence) => warnings.set(sentence, (warnings.get(sentence) ?? 0) + 1);
|
|
275
|
+
const cells = [];
|
|
276
|
+
const emitted = new Set();
|
|
277
|
+
const vertexIds = new Set();
|
|
278
|
+
const originOf = (id) => {
|
|
279
|
+
const parent = id === undefined ? undefined : byId.get(id);
|
|
280
|
+
return parent?.bound ? [parent.bound.x, parent.bound.y] : [0, 0];
|
|
281
|
+
};
|
|
282
|
+
const emit = (entry) => {
|
|
283
|
+
if (emitted.has(entry.id))
|
|
284
|
+
return;
|
|
285
|
+
emitted.add(entry.id);
|
|
286
|
+
const parentId = parentOf.get(entry.id);
|
|
287
|
+
const parent = parentId === undefined ? undefined : byId.get(parentId);
|
|
288
|
+
// A parent cell comes before its children: draw.io paints a group's
|
|
289
|
+
// children at the group's own place in the stack.
|
|
290
|
+
if (parent)
|
|
291
|
+
emit(parent);
|
|
292
|
+
const cell = cellOf(entry, parent?.id ?? LAYER, originOf(parent?.id), warn);
|
|
293
|
+
if (!cell)
|
|
294
|
+
return;
|
|
295
|
+
cells.push(...cell.xml);
|
|
296
|
+
if (cell.vertex)
|
|
297
|
+
vertexIds.add(entry.id);
|
|
298
|
+
};
|
|
299
|
+
for (const entry of entries) {
|
|
300
|
+
if (entry.type === 'connector')
|
|
301
|
+
continue;
|
|
302
|
+
emit(entry);
|
|
303
|
+
}
|
|
304
|
+
// Edges after every vertex, in their own z-order: an edge names its ends
|
|
305
|
+
// by id, so they must be known by then, and above the shapes is where
|
|
306
|
+
// draw.io itself puts a newly drawn edge.
|
|
307
|
+
for (const entry of entries) {
|
|
308
|
+
if (entry.type !== 'connector')
|
|
309
|
+
continue;
|
|
310
|
+
const parentId = parentOf.get(entry.id);
|
|
311
|
+
const parent = parentId === undefined ? undefined : byId.get(parentId);
|
|
312
|
+
if (parent)
|
|
313
|
+
emit(parent);
|
|
314
|
+
cells.push(...edgeOf(entry, parent?.id ?? LAYER, originOf(parent?.id), vertexIds));
|
|
315
|
+
emitted.add(entry.id);
|
|
316
|
+
}
|
|
317
|
+
for (const entry of entries) {
|
|
318
|
+
if (entry.type !== 'mindmap')
|
|
319
|
+
continue;
|
|
320
|
+
cells.push(...mindmapEdges(entry, vertexIds));
|
|
321
|
+
}
|
|
322
|
+
const rawName = (context.name ?? '').trim();
|
|
323
|
+
const name = rawName || FALLBACK_NAME;
|
|
324
|
+
const text = [
|
|
325
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
326
|
+
'<mxfile host="Labre">',
|
|
327
|
+
` <diagram id="labre-canvas" name="${attr(name)}">`,
|
|
328
|
+
' <mxGraphModel grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="0" pageScale="1" math="0" shadow="0">',
|
|
329
|
+
' <root>',
|
|
330
|
+
` <mxCell id="${ROOT}" />`,
|
|
331
|
+
` <mxCell id="${LAYER}" parent="${ROOT}" />`,
|
|
332
|
+
...cells.map(cell => ` ${cell}`),
|
|
333
|
+
' </root>',
|
|
334
|
+
' </mxGraphModel>',
|
|
335
|
+
' </diagram>',
|
|
336
|
+
'</mxfile>',
|
|
337
|
+
'',
|
|
338
|
+
].join('\n');
|
|
339
|
+
const sentences = [...warnings].map(([sentence, count]) => sentence.replace('{{count}}', String(count)));
|
|
340
|
+
return {
|
|
341
|
+
text,
|
|
342
|
+
filename: `${safeName(rawName)}${DRAWIO_EXTENSION}`,
|
|
343
|
+
mime: DRAWIO_MIME,
|
|
344
|
+
...(sentences.length > 0 ? { warnings: sentences } : {}),
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
/** A file name a disk accepts: the shape `safeFilename` gives every export. */
|
|
348
|
+
function safeName(raw) {
|
|
349
|
+
const safe = raw
|
|
350
|
+
.replaceAll(/[\\/:*?"<>|]/g, '-')
|
|
351
|
+
.replaceAll(/\s+/g, ' ')
|
|
352
|
+
.trim()
|
|
353
|
+
.slice(0, 120)
|
|
354
|
+
.replace(/[. ]+$/, '');
|
|
355
|
+
return safe || FALLBACK_NAME;
|
|
356
|
+
}
|
|
357
|
+
/* ── Ordering ─────────────────────────────────────────────────────────── */
|
|
358
|
+
function collect(elements, blocks) {
|
|
359
|
+
const entries = [];
|
|
360
|
+
const seen = new Set();
|
|
361
|
+
const add = (rec, block) => {
|
|
362
|
+
const id = str(rec.id);
|
|
363
|
+
if (!id || seen.has(id))
|
|
364
|
+
return;
|
|
365
|
+
// Mindmap collapse (`hidden`) is painted as absence; so is a note shown in
|
|
366
|
+
// the page view only. The other two visibilities (ADR 0031) are the
|
|
367
|
+
// caller's to apply, because only the caller knows this viewer.
|
|
368
|
+
if (rec.hidden === true)
|
|
369
|
+
return;
|
|
370
|
+
const type = block ? str(rec.flavour) : str(rec.type);
|
|
371
|
+
if (!type)
|
|
372
|
+
return;
|
|
373
|
+
if (block &&
|
|
374
|
+
type === 'affine:note' &&
|
|
375
|
+
rec.props?.displayMode === 'doc') {
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
seen.add(id);
|
|
379
|
+
const props = block ? (rec.props ?? {}) : rec;
|
|
380
|
+
entries.push({
|
|
381
|
+
id,
|
|
382
|
+
type,
|
|
383
|
+
block,
|
|
384
|
+
rec,
|
|
385
|
+
bound: boxOf(rec.xywh ?? props.xywh ?? rec.deserializedXYWH),
|
|
386
|
+
index: str(rec.index) ?? str(props.index) ?? '',
|
|
387
|
+
});
|
|
388
|
+
};
|
|
389
|
+
for (const element of elements)
|
|
390
|
+
add(element, false);
|
|
391
|
+
for (const block of blocks)
|
|
392
|
+
add(block, true);
|
|
393
|
+
// Frames are the floor of the canvas (they are painted under everything),
|
|
394
|
+
// then the fractional index, then the id: one order for one canvas.
|
|
395
|
+
const floor = (entry) => (entry.type === 'affine:frame' ? 0 : 1);
|
|
396
|
+
return entries.sort((a, b) => floor(a) - floor(b) ||
|
|
397
|
+
(a.index < b.index ? -1 : a.index > b.index ? 1 : 0) ||
|
|
398
|
+
(a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
399
|
+
}
|
|
400
|
+
/* ── Cells ────────────────────────────────────────────────────────────── */
|
|
401
|
+
/** `<object>` attributes every cell carries: its id, its kind and its role. */
|
|
402
|
+
function objectAttrs(entry, label) {
|
|
403
|
+
const role = str(entry.rec.role);
|
|
404
|
+
return [
|
|
405
|
+
`id="${attr(entry.id)}"`,
|
|
406
|
+
`label="${attr(label)}"`,
|
|
407
|
+
`labre-type="${attr(entry.type)}"`,
|
|
408
|
+
...(role ? [`labre-role="${attr(role)}"`] : []),
|
|
409
|
+
].join(' ');
|
|
410
|
+
}
|
|
411
|
+
function vertexXml(entry, parent, origin, label, style, extra = {}) {
|
|
412
|
+
const box = entry.bound ?? { x: 0, y: 0, w: 0, h: 0 };
|
|
413
|
+
return [
|
|
414
|
+
`<object ${objectAttrs(entry, label)}${extra.object ?? ''}>`,
|
|
415
|
+
` <mxCell style="${attr(style)}" vertex="1"${extra.cell ?? ''} parent="${attr(parent)}">`,
|
|
416
|
+
` <mxGeometry x="${fmt(box.x - origin[0])}" y="${fmt(box.y - origin[1])}" width="${fmt(box.w)}" height="${fmt(box.h)}" as="geometry" />`,
|
|
417
|
+
' </mxCell>',
|
|
418
|
+
'</object>',
|
|
419
|
+
];
|
|
420
|
+
}
|
|
421
|
+
const SKIPPED = (kind) => `Left out {{count}} × ${kind}: draw.io has no shape for it.`;
|
|
422
|
+
function cellOf(entry, parent, origin, warn) {
|
|
423
|
+
const props = propsOf(entry);
|
|
424
|
+
const rotate = num(props.rotate ?? entry.rec.rotate);
|
|
425
|
+
const rotation = rotate ? [['rotation', fmt(rotate)]] : [];
|
|
426
|
+
if (entry.block) {
|
|
427
|
+
switch (entry.type) {
|
|
428
|
+
case 'affine:note': {
|
|
429
|
+
const style = [
|
|
430
|
+
['align', 'left'],
|
|
431
|
+
['verticalAlign', 'top'],
|
|
432
|
+
];
|
|
433
|
+
pushColor(style, 'fillColor', 'fillOpacity', drawioColor(props.background));
|
|
434
|
+
style.push(['strokeColor', 'none'], ['spacing', 12]);
|
|
435
|
+
return {
|
|
436
|
+
xml: vertexXml(entry, parent, origin, htmlLabel(blockText(entry.rec)), styleString(['rounded=1', 'whiteSpace=wrap', 'html=1'], style)),
|
|
437
|
+
vertex: true,
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
case 'affine:frame': {
|
|
441
|
+
const style = [
|
|
442
|
+
['align', 'left'],
|
|
443
|
+
['verticalAlign', 'top'],
|
|
444
|
+
['spacingLeft', 8],
|
|
445
|
+
['fontStyle', 1],
|
|
446
|
+
];
|
|
447
|
+
const fill = drawioColor(props.background);
|
|
448
|
+
pushColor(style, 'fillColor', 'fillOpacity', fill ?? { value: 'none' });
|
|
449
|
+
return {
|
|
450
|
+
xml: vertexXml(entry, parent, origin, htmlLabel(textOf(props.title).trim()), styleString(['rounded=0', 'whiteSpace=wrap', 'html=1'], style)),
|
|
451
|
+
vertex: true,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
case 'affine:edgeless-text': {
|
|
455
|
+
const style = [
|
|
456
|
+
['strokeColor', 'none'],
|
|
457
|
+
['fillColor', 'none'],
|
|
458
|
+
['align', ALIGN[str(props.textAlign) ?? ''] ?? 'left'],
|
|
459
|
+
['verticalAlign', 'top'],
|
|
460
|
+
...rotation,
|
|
461
|
+
];
|
|
462
|
+
pushFont(style, props);
|
|
463
|
+
return {
|
|
464
|
+
xml: vertexXml(entry, parent, origin, htmlLabel(blockText(entry.rec)), styleString(['text', 'whiteSpace=wrap', 'html=1'], style)),
|
|
465
|
+
vertex: true,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
case 'affine:image': {
|
|
469
|
+
// A REFERENCE, never the bytes and never an address: the cell names
|
|
470
|
+
// the blob (`labre-image`) and shows its caption, so a reader that
|
|
471
|
+
// can resolve the blob may, and draw.io shows a labelled box.
|
|
472
|
+
const sourceId = str(props.sourceId) ?? '';
|
|
473
|
+
return {
|
|
474
|
+
xml: vertexXml(entry, parent, origin, htmlLabel(str(props.caption)?.trim() ?? ''), styleString(['rounded=0', 'whiteSpace=wrap', 'html=1'], [['dashed', 1], ['fillColor', 'none'], ...rotation]), sourceId ? { object: ` labre-image="${attr(sourceId)}"` } : {}),
|
|
475
|
+
vertex: true,
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
default:
|
|
479
|
+
warn(SKIPPED(entry.type));
|
|
480
|
+
return null;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
switch (entry.type) {
|
|
484
|
+
case 'group':
|
|
485
|
+
case 'mindmap': {
|
|
486
|
+
const title = textOf(props.title).trim();
|
|
487
|
+
const style = title
|
|
488
|
+
? [
|
|
489
|
+
['verticalLabelPosition', 'top'],
|
|
490
|
+
['verticalAlign', 'bottom'],
|
|
491
|
+
['align', 'left'],
|
|
492
|
+
]
|
|
493
|
+
: [];
|
|
494
|
+
return {
|
|
495
|
+
xml: vertexXml(entry, parent, origin, htmlLabel(title), styleString(['group', 'html=1'], style), { cell: ' connectable="0"' }),
|
|
496
|
+
vertex: false,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
case 'text': {
|
|
500
|
+
const style = [
|
|
501
|
+
['strokeColor', 'none'],
|
|
502
|
+
['fillColor', 'none'],
|
|
503
|
+
['align', ALIGN[str(props.textAlign) ?? ''] ?? 'center'],
|
|
504
|
+
['verticalAlign', 'top'],
|
|
505
|
+
...rotation,
|
|
506
|
+
];
|
|
507
|
+
pushFont(style, props);
|
|
508
|
+
return {
|
|
509
|
+
xml: vertexXml(entry, parent, origin, htmlLabel(textOf(props.text)), styleString(['text', 'whiteSpace=wrap', 'html=1'], style)),
|
|
510
|
+
vertex: true,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
case 'brush':
|
|
514
|
+
case 'highlighter':
|
|
515
|
+
warn(SKIPPED(entry.type));
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
// Every other element is a box: a shape (and every framework node, which
|
|
519
|
+
// extends one) by its `shapeType`, a framework board by its bound.
|
|
520
|
+
const shapeType = str(props.shapeType);
|
|
521
|
+
const lead = ['whiteSpace=wrap', 'html=1'];
|
|
522
|
+
const style = [];
|
|
523
|
+
if (shapeType === 'ellipse')
|
|
524
|
+
lead.unshift('ellipse');
|
|
525
|
+
else if (shapeType === 'diamond')
|
|
526
|
+
lead.unshift('rhombus');
|
|
527
|
+
else if (shapeType === 'triangle') {
|
|
528
|
+
lead.unshift('triangle');
|
|
529
|
+
style.push(['direction', 'north']);
|
|
530
|
+
}
|
|
531
|
+
else {
|
|
532
|
+
if (shapeType === 'polygon') {
|
|
533
|
+
warn('Drew {{count}} × polygon as its bounding box: draw.io has no free polygon.');
|
|
534
|
+
}
|
|
535
|
+
const radius = num(props.radius) ?? 0;
|
|
536
|
+
const box = entry.bound;
|
|
537
|
+
if (radius > 0 && box) {
|
|
538
|
+
const px = radius < 1 ? Math.min(box.w, box.h) * radius : radius;
|
|
539
|
+
lead.unshift('rounded=1');
|
|
540
|
+
style.push(['absoluteArcSize', 1], ['arcSize', fmt(px * 2)]);
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
lead.unshift('rounded=0');
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
const isShape = shapeType !== undefined;
|
|
547
|
+
if (isShape) {
|
|
548
|
+
// `filled` is the shape's own switch; an unfilled shape paints no fill
|
|
549
|
+
// whatever colour it remembers.
|
|
550
|
+
if (props.filled === false)
|
|
551
|
+
style.push(['fillColor', 'none']);
|
|
552
|
+
else
|
|
553
|
+
pushColor(style, 'fillColor', 'fillOpacity', drawioColor(props.fillColor));
|
|
554
|
+
pushStroke(style, props, 'strokeColor');
|
|
555
|
+
style.push(['align', ALIGN[str(props.textAlign) ?? ''] ?? 'center'], [
|
|
556
|
+
'verticalAlign',
|
|
557
|
+
VERTICAL_ALIGN[str(props.textVerticalAlign) ?? ''] ?? 'middle',
|
|
558
|
+
]);
|
|
559
|
+
pushFont(style, props);
|
|
560
|
+
}
|
|
561
|
+
else {
|
|
562
|
+
// A board: a floor with its name in the corner. Its fill, when it
|
|
563
|
+
// declares one under a name this writer knows; never a guess.
|
|
564
|
+
const fill = drawioColor(props.fillColor ?? props.background);
|
|
565
|
+
pushColor(style, 'fillColor', 'fillOpacity', fill ?? { value: 'none' });
|
|
566
|
+
pushStroke(style, props, props.strokeColor === undefined ? 'stroke' : 'strokeColor');
|
|
567
|
+
style.push(['align', 'left'], ['verticalAlign', 'top'], ['spacingLeft', 8]);
|
|
568
|
+
}
|
|
569
|
+
style.push(...rotation);
|
|
570
|
+
const label = isShape && props.textDisplay === false
|
|
571
|
+
? ''
|
|
572
|
+
: textOf(props.text ??
|
|
573
|
+
props.title ??
|
|
574
|
+
props.name ??
|
|
575
|
+
props.heading ??
|
|
576
|
+
props.label).trim();
|
|
577
|
+
return {
|
|
578
|
+
xml: vertexXml(entry, parent, origin, htmlLabel(label), styleString(lead, style)),
|
|
579
|
+
vertex: true,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function pointOf(value) {
|
|
583
|
+
if (!value || typeof value !== 'object')
|
|
584
|
+
return null;
|
|
585
|
+
const x = Number(value[0]);
|
|
586
|
+
const y = Number(value[1]);
|
|
587
|
+
return Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null;
|
|
588
|
+
}
|
|
589
|
+
/** The routed path, absolute: `absolutePath`, else `path` off the bound. */
|
|
590
|
+
function pathOf(entry) {
|
|
591
|
+
const absolute = entry.rec.absolutePath;
|
|
592
|
+
if (Array.isArray(absolute) && absolute.length >= 2) {
|
|
593
|
+
return absolute.map(pointOf).filter((p) => p !== null);
|
|
594
|
+
}
|
|
595
|
+
const relative = entry.rec.path;
|
|
596
|
+
const box = entry.bound;
|
|
597
|
+
if (Array.isArray(relative) && relative.length >= 2) {
|
|
598
|
+
const [dx, dy] = box ? [box.x, box.y] : [0, 0];
|
|
599
|
+
return relative
|
|
600
|
+
.map(pointOf)
|
|
601
|
+
.filter((p) => p !== null)
|
|
602
|
+
.map(([x, y]) => [x + dx, y + dy]);
|
|
603
|
+
}
|
|
604
|
+
return [];
|
|
605
|
+
}
|
|
606
|
+
function edgeLabelCell(id, parent, text, at) {
|
|
607
|
+
return [
|
|
608
|
+
`<mxCell id="${attr(id)}" value="${attr(htmlLabel(text))}" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" vertex="1" connectable="0" parent="${attr(parent)}">`,
|
|
609
|
+
` <mxGeometry x="${at}" relative="1" as="geometry">`,
|
|
610
|
+
' <mxPoint as="offset" />',
|
|
611
|
+
' </mxGeometry>',
|
|
612
|
+
'</mxCell>',
|
|
613
|
+
].join('\n ');
|
|
614
|
+
}
|
|
615
|
+
function edgeOf(entry, parent, origin, vertexIds) {
|
|
616
|
+
const rec = entry.rec;
|
|
617
|
+
const path = pathOf(entry);
|
|
618
|
+
const source = rec.source ?? {};
|
|
619
|
+
const target = rec.target ?? {};
|
|
620
|
+
const sourceId = str(source.id);
|
|
621
|
+
const targetId = str(target.id);
|
|
622
|
+
const sourceCell = sourceId && vertexIds.has(sourceId) ? sourceId : null;
|
|
623
|
+
const targetCell = targetId && vertexIds.has(targetId) ? targetId : null;
|
|
624
|
+
const mode = num(rec.mode);
|
|
625
|
+
const lead = ['html=1'];
|
|
626
|
+
const style = [];
|
|
627
|
+
if (mode === 1)
|
|
628
|
+
style.push(['edgeStyle', 'orthogonalEdgeStyle'], ['rounded', 0]);
|
|
629
|
+
else if (mode === 0)
|
|
630
|
+
style.push(['edgeStyle', 'none']);
|
|
631
|
+
else
|
|
632
|
+
style.push(['curved', 1]);
|
|
633
|
+
const [startArrow, startFill] = arrowOf(rec.frontEndpointStyle ?? PointStyle.None);
|
|
634
|
+
const [endArrow, endFill] = arrowOf(rec.rearEndpointStyle ?? PointStyle.Arrow);
|
|
635
|
+
style.push(['startArrow', startArrow], ['startFill', startFill], ['endArrow', endArrow], ['endFill', endFill]);
|
|
636
|
+
pushStroke(style, rec, 'stroke');
|
|
637
|
+
// A `position` on a CONNECTED end is where on the shape it attaches, as a
|
|
638
|
+
// fraction of the box — draw.io's own exit / entry constraint.
|
|
639
|
+
const anchor = (end, prefix) => {
|
|
640
|
+
const at = pointOf(end.position);
|
|
641
|
+
if (!at)
|
|
642
|
+
return;
|
|
643
|
+
style.push([`${prefix}X`, fmt(at[0])], [`${prefix}Y`, fmt(at[1])], [`${prefix}Dx`, 0], [`${prefix}Dy`, 0]);
|
|
644
|
+
};
|
|
645
|
+
if (sourceCell)
|
|
646
|
+
anchor(source, 'exit');
|
|
647
|
+
if (targetCell)
|
|
648
|
+
anchor(target, 'entry');
|
|
649
|
+
const local = ([x, y]) => [x - origin[0], y - origin[1]];
|
|
650
|
+
const freeEnd = (end, fallback) => {
|
|
651
|
+
if (!str(end.id)) {
|
|
652
|
+
const at = pointOf(end.position);
|
|
653
|
+
if (at)
|
|
654
|
+
return at;
|
|
655
|
+
}
|
|
656
|
+
return fallback ?? null;
|
|
657
|
+
};
|
|
658
|
+
const start = freeEnd(source, path[0]);
|
|
659
|
+
const finish = freeEnd(target, path[path.length - 1]);
|
|
660
|
+
const geometry = [];
|
|
661
|
+
if (start) {
|
|
662
|
+
const [x, y] = local(start);
|
|
663
|
+
geometry.push(`<mxPoint x="${fmt(x)}" y="${fmt(y)}" as="sourcePoint" />`);
|
|
664
|
+
}
|
|
665
|
+
if (finish) {
|
|
666
|
+
const [x, y] = local(finish);
|
|
667
|
+
geometry.push(`<mxPoint x="${fmt(x)}" y="${fmt(y)}" as="targetPoint" />`);
|
|
668
|
+
}
|
|
669
|
+
// Waypoints only where draw.io would route the same way: the corners of an
|
|
670
|
+
// elbowed line. A straight line has none, and a curve's control polygon is
|
|
671
|
+
// not draw.io's (it curves through waypoints, Labre bends towards them).
|
|
672
|
+
if (mode === 1 && path.length > 2) {
|
|
673
|
+
geometry.push('<Array as="points">', ...path.slice(1, -1).map(point => {
|
|
674
|
+
const [x, y] = local(point);
|
|
675
|
+
return ` <mxPoint x="${fmt(x)}" y="${fmt(y)}" />`;
|
|
676
|
+
}), '</Array>');
|
|
677
|
+
}
|
|
678
|
+
const label = textOf(rec.text).trim();
|
|
679
|
+
const labelPosition = num(rec.labelOffset?.distance);
|
|
680
|
+
const geometryX = labelPosition !== undefined && labelPosition !== 0.5
|
|
681
|
+
? ` x="${fmt(labelPosition * 2 - 1)}"`
|
|
682
|
+
: '';
|
|
683
|
+
const ends = [
|
|
684
|
+
...(sourceCell ? [`source="${attr(sourceCell)}"`] : []),
|
|
685
|
+
...(targetCell ? [`target="${attr(targetCell)}"`] : []),
|
|
686
|
+
].join(' ');
|
|
687
|
+
const xml = [
|
|
688
|
+
`<object ${objectAttrs(entry, htmlLabel(label))}>`,
|
|
689
|
+
` <mxCell style="${attr(styleString(lead, style))}" edge="1" parent="${attr(parent)}"${ends ? ` ${ends}` : ''}>`,
|
|
690
|
+
` <mxGeometry${geometryX} relative="1" as="geometry">`,
|
|
691
|
+
...geometry.map(line => ` ${line}`),
|
|
692
|
+
' </mxGeometry>',
|
|
693
|
+
' </mxCell>',
|
|
694
|
+
'</object>',
|
|
695
|
+
];
|
|
696
|
+
// The per-end labels (ADR 0020) as draw.io's own end-label cells: children
|
|
697
|
+
// of the edge, pinned at -1 (source) and 1 (target) along it.
|
|
698
|
+
const sourceLabel = textOf(rec.sourceLabel).trim();
|
|
699
|
+
const targetLabel = textOf(rec.targetLabel).trim();
|
|
700
|
+
if (sourceLabel) {
|
|
701
|
+
xml.push(edgeLabelCell(`${entry.id}-source-label`, entry.id, sourceLabel, -1));
|
|
702
|
+
}
|
|
703
|
+
if (targetLabel) {
|
|
704
|
+
xml.push(edgeLabelCell(`${entry.id}-target-label`, entry.id, targetLabel, 1));
|
|
705
|
+
}
|
|
706
|
+
return xml;
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* A mindmap's branches. They are painted by the mindmap from its `children`
|
|
710
|
+
* map, never stored as connectors, so they are written here from the same
|
|
711
|
+
* map: one plain line from each node to its parent node.
|
|
712
|
+
*/
|
|
713
|
+
function mindmapEdges(entry, vertexIds) {
|
|
714
|
+
const children = entry.rec.children;
|
|
715
|
+
const tree = (children && typeof children.toJSON === 'function'
|
|
716
|
+
? children.toJSON()
|
|
717
|
+
: children);
|
|
718
|
+
if (!tree)
|
|
719
|
+
return [];
|
|
720
|
+
const xml = [];
|
|
721
|
+
for (const id of Object.keys(tree).sort()) {
|
|
722
|
+
const parentId = str(tree[id]?.parent);
|
|
723
|
+
if (!parentId || !vertexIds.has(id) || !vertexIds.has(parentId))
|
|
724
|
+
continue;
|
|
725
|
+
xml.push([
|
|
726
|
+
`<mxCell id="${attr(`${entry.id}-${id}`)}" style="html=1;curved=1;endArrow=none;startArrow=none;" edge="1" parent="${attr(entry.id)}" source="${attr(parentId)}" target="${attr(id)}">`,
|
|
727
|
+
` <mxGeometry relative="1" as="geometry" />`,
|
|
728
|
+
'</mxCell>',
|
|
729
|
+
].join('\n '));
|
|
730
|
+
}
|
|
731
|
+
return xml;
|
|
732
|
+
}
|