@markdstage/markdstage 3.1.0 → 3.3.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.
@@ -0,0 +1,312 @@
1
+ import {
2
+ createScene,
3
+ normalizeScene,
4
+ validateScene,
5
+ } from "./scene-graph.mjs";
6
+
7
+ const DEFAULT_SOURCE_PATH = "architecture";
8
+
9
+ function roundedMetric(value) {
10
+ return Math.round(Math.max(0, Number(value) || 0) * 10) / 10;
11
+ }
12
+
13
+ function finiteNumberOr(value, fallback) {
14
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
15
+ }
16
+
17
+ function nonEmptyStringOr(value, fallback) {
18
+ return typeof value === "string" && value ? value : fallback;
19
+ }
20
+
21
+ function transformBounds(bounds, options) {
22
+ return {
23
+ x: roundedMetric(options.originX + finiteNumberOr(bounds?.x, 0) * options.scale),
24
+ y: roundedMetric(options.originY + finiteNumberOr(bounds?.y, 0) * options.scale),
25
+ width: roundedMetric(finiteNumberOr(bounds?.width, 0) * options.scale),
26
+ height: roundedMetric(finiteNumberOr(bounds?.height, 0) * options.scale),
27
+ };
28
+ }
29
+
30
+ function transformPoint(point, options) {
31
+ return {
32
+ x: roundedMetric(options.originX + finiteNumberOr(point?.x, 0) * options.scale),
33
+ y: roundedMetric(options.originY + finiteNumberOr(point?.y, 0) * options.scale),
34
+ };
35
+ }
36
+
37
+ function scaledMetric(value, options) {
38
+ return value === undefined ? undefined : roundedMetric(finiteNumberOr(value, 0) * options.scale);
39
+ }
40
+
41
+ function isSceneColor(value) {
42
+ if (value === null || value === undefined) return true;
43
+ if (typeof value !== "string") return false;
44
+ const text = value.trim();
45
+ if (/^#(?:[0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(text)) return true;
46
+ return /^rgba?\((.*)\)$/i.test(text);
47
+ }
48
+
49
+ function normalizeColor(value, path, options, diagnostics) {
50
+ if (value === undefined) return undefined;
51
+ const resolved = options.resolveColor(value, path);
52
+ if (resolved === undefined) return undefined;
53
+ if (resolved === null || resolved === "" || resolved === "none" || resolved === "transparent") return null;
54
+ if (isSceneColor(resolved)) return resolved;
55
+ diagnostics.push({
56
+ path,
57
+ kind: "color",
58
+ reason: `color is not resolved for scene graph: ${String(value)}`,
59
+ });
60
+ return null;
61
+ }
62
+
63
+ function definedEntries(object) {
64
+ return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined));
65
+ }
66
+
67
+ function mapStyle(object, path, options, diagnostics) {
68
+ const dash = object.dash === undefined ? undefined : options.resolveDash(object.dash, path);
69
+ const sceneDash = (() => {
70
+ if (dash === undefined) return undefined;
71
+ if (dash === "" || dash === "solid" || dash === "dash" || dash === "dashDot" || dash === "dot") return dash;
72
+ if (dash === "dotted") return "dot";
73
+ const values = String(dash)
74
+ .trim()
75
+ .split(/[ ,]+/)
76
+ .map(Number)
77
+ .filter(Number.isFinite);
78
+ if (values.length >= 2 && values[0] <= 2 && values[1] >= values[0] * 2) return "dot";
79
+ if (values.length >= 2) return "dash";
80
+ return "solid";
81
+ })();
82
+ return definedEntries({
83
+ fill: normalizeColor(object.fill, `${path}.fill`, options, diagnostics),
84
+ stroke: normalizeColor(object.stroke, `${path}.stroke`, options, diagnostics),
85
+ strokeWidth: scaledMetric(object.strokeWidth, options),
86
+ dash: sceneDash,
87
+ opacity: object.opacity,
88
+ cornerRadius: scaledMetric(object.cornerRadius, options),
89
+ });
90
+ }
91
+
92
+ function mapText(text, path, options, diagnostics) {
93
+ if (!text?.paragraphs) return text;
94
+ return {
95
+ paragraphs: text.paragraphs.map((paragraph, paragraphIndex) => ({
96
+ ...paragraph,
97
+ runs: paragraph.runs.map((run, runIndex) => {
98
+ const runPath = `${path}.paragraphs[${paragraphIndex}].runs[${runIndex}]`;
99
+ return definedEntries({
100
+ ...run,
101
+ fontFace: options.fontFace || run.fontFace,
102
+ fontSize: scaledMetric(run.fontSize, options),
103
+ bold: Number(run.fontWeight) >= 600,
104
+ color: normalizeColor(run.color, `${runPath}.color`, options, diagnostics),
105
+ });
106
+ }),
107
+ })),
108
+ };
109
+ }
110
+
111
+ function mapTextLayout(object, options) {
112
+ return definedEntries({
113
+ alignment: object.alignment,
114
+ verticalAlignment: object.verticalAlignment,
115
+ textWrap: object.textWrap,
116
+ textInsets: object.textInsets
117
+ ? Object.fromEntries(
118
+ Object.entries(object.textInsets).map(([key, value]) => [key, scaledMetric(value, options)]),
119
+ )
120
+ : undefined,
121
+ });
122
+ }
123
+
124
+ function applyShapeOpacityToText(text, opacity) {
125
+ if (!text?.paragraphs || !Number.isFinite(opacity)) return text;
126
+ return {
127
+ ...text,
128
+ paragraphs: text.paragraphs.map((paragraph) => ({
129
+ ...paragraph,
130
+ runs: paragraph.runs.map((run) => ({
131
+ ...run,
132
+ opacity: (Number.isFinite(run.opacity) ? run.opacity : 1) * opacity,
133
+ })),
134
+ })),
135
+ };
136
+ }
137
+
138
+ function sourcePathFor(object, fallback) {
139
+ return nonEmptyStringOr(object.architecture?.sourcePath, nonEmptyStringOr(object.sourcePath, fallback));
140
+ }
141
+
142
+ function sourceIdFor(object) {
143
+ return nonEmptyStringOr(object.architecture?.id, nonEmptyStringOr(object.id, ""));
144
+ }
145
+
146
+ function shapePreset(object) {
147
+ return nonEmptyStringOr(object.shape, "rect");
148
+ }
149
+
150
+ function arrowEndFor(value) {
151
+ if (value === true) return "triangle";
152
+ if (value === false || value === undefined || value === null) return "none";
153
+ return value;
154
+ }
155
+
156
+ function imageResult(entry, kind, options) {
157
+ const resolved = options.resolveImage(entry, kind);
158
+ if (typeof resolved === "string") return { src: resolved };
159
+ if (resolved && typeof resolved === "object") return resolved;
160
+ return { src: "" };
161
+ }
162
+
163
+ function imageNode(entry, kind, index, options) {
164
+ const image = imageResult(entry, kind, options);
165
+ const bounds = image.bounds || transformBounds(entry, options);
166
+ const architecture = definedEntries({
167
+ ...(entry.architecture || {}),
168
+ kind,
169
+ id: entry.architecture?.id || entry.id || "",
170
+ sourcePath: entry.architecture?.sourcePath || entry.sourcePath,
171
+ order: entry.architecture?.order ?? entry.order,
172
+ z: entry.architecture?.z ?? entry.z,
173
+ });
174
+ if (!image.src) {
175
+ return {
176
+ kind: "fallback",
177
+ ...(sourceIdFor({ ...entry, architecture }) ? { id: sourceIdFor({ ...entry, architecture }) } : {}),
178
+ sourcePath: sourcePathFor({ ...entry, architecture }, `${kind}s[${index}]`),
179
+ z: finiteNumberOr(image.z, index),
180
+ bounds,
181
+ reason: `${kind}-image-unavailable`,
182
+ meta: { architecture },
183
+ };
184
+ }
185
+ return {
186
+ kind: "image",
187
+ ...(sourceIdFor({ ...entry, architecture }) ? { id: sourceIdFor({ ...entry, architecture }) } : {}),
188
+ sourcePath: sourcePathFor({ ...entry, architecture }, `${kind}s[${index}]`),
189
+ z: finiteNumberOr(image.z, index),
190
+ bounds,
191
+ src: image.src,
192
+ alt: nonEmptyStringOr(image.alt, kind === "icon-picture" ? `${entry.icon} icon` : `${architecture.id} image`),
193
+ fit: image.fit || "fill",
194
+ opacity: image.opacity ?? 1,
195
+ meta: { architecture },
196
+ };
197
+ }
198
+
199
+ function objectNode(object, index, options, diagnostics) {
200
+ const path = `objects[${index}]`;
201
+ if (object.type === "connector") {
202
+ return {
203
+ kind: "connector",
204
+ ...(sourceIdFor(object) ? { id: sourceIdFor(object) } : {}),
205
+ sourcePath: sourcePathFor(object, path),
206
+ z: index,
207
+ points: (object.points || []).map((point) => transformPoint(point, options)),
208
+ style: mapStyle(object, path, options, diagnostics),
209
+ arrowStart: "none",
210
+ arrowEnd: arrowEndFor(object.arrowEnd),
211
+ meta: definedEntries({
212
+ architecture: object.architecture,
213
+ from: object.from,
214
+ to: object.to,
215
+ }),
216
+ };
217
+ }
218
+ if (object.type === "image") {
219
+ return imageNode(object, "image-picture", index, options);
220
+ }
221
+ if (object.type === "shape") {
222
+ const text = object.text === undefined
223
+ ? undefined
224
+ : applyShapeOpacityToText(mapText(object.text, `${path}.text`, options, diagnostics), object.opacity);
225
+ return {
226
+ kind: "shape",
227
+ ...(sourceIdFor(object) ? { id: sourceIdFor(object) } : {}),
228
+ sourcePath: sourcePathFor(object, path),
229
+ z: index,
230
+ bounds: transformBounds(object, options),
231
+ preset: shapePreset(object),
232
+ style: mapStyle(object, path, options, diagnostics),
233
+ ...(text !== undefined ? { text } : {}),
234
+ textLayout: mapTextLayout(object, options),
235
+ meta: definedEntries({
236
+ architecture: object.architecture,
237
+ icon: object.icon,
238
+ }),
239
+ };
240
+ }
241
+ return {
242
+ kind: "fallback",
243
+ sourcePath: sourcePathFor(object, path),
244
+ z: index,
245
+ bounds: transformBounds(object, options),
246
+ reason: `unsupported architecture object type: ${String(object.type)}`,
247
+ meta: definedEntries({ architecture: object.architecture }),
248
+ };
249
+ }
250
+
251
+ function fallbackNode(fallback, index, objectCount, options) {
252
+ return {
253
+ kind: "fallback",
254
+ sourcePath: nonEmptyStringOr(fallback.sourcePath, nonEmptyStringOr(fallback.path, `fallbacks[${index}]`)),
255
+ z: objectCount + index,
256
+ bounds: transformBounds(fallback, options),
257
+ reason: nonEmptyStringOr(fallback.reason, "architecture-rendered-as-artwork"),
258
+ };
259
+ }
260
+
261
+ function normalizeOptions(options = {}) {
262
+ return {
263
+ path: nonEmptyStringOr(options.path, DEFAULT_SOURCE_PATH),
264
+ resolveColor: typeof options.resolveColor === "function" ? options.resolveColor : (value) => value,
265
+ resolveImage: typeof options.resolveImage === "function" ? options.resolveImage : (entry) => entry?.src || "",
266
+ resolveDash: typeof options.resolveDash === "function" ? options.resolveDash : (value) => value,
267
+ scale: finiteNumberOr(options.scale, 1),
268
+ originX: finiteNumberOr(options.originX, 0),
269
+ originY: finiteNumberOr(options.originY, 0),
270
+ fontFace: typeof options.fontFace === "string" && options.fontFace ? options.fontFace : undefined,
271
+ };
272
+ }
273
+
274
+ export function architectureSnapshotToScene(snapshot, options = {}) {
275
+ const normalizedOptions = normalizeOptions(options);
276
+ const diagnostics = [];
277
+ const objects = Array.isArray(snapshot?.objects) ? snapshot.objects : [];
278
+ const icons = Array.isArray(snapshot?.icons) ? snapshot.icons : [];
279
+ const fallbacks = Array.isArray(snapshot?.fallbacks) ? snapshot.fallbacks : [];
280
+ const nodes = objects.flatMap((object, index) => {
281
+ const mapped = [objectNode(object, index, normalizedOptions, diagnostics)];
282
+ if (object.type === "shape" && object.icon) {
283
+ const icon = icons.find((candidate) => candidate.id === object.architecture?.id);
284
+ if (icon) mapped.push(imageNode(icon, "icon-picture", index + 1 / 2, normalizedOptions));
285
+ }
286
+ return mapped;
287
+ });
288
+ const iconObjectIds = new Set(
289
+ objects
290
+ .filter((object) => object.type === "shape" && object.icon)
291
+ .map((object) => object.architecture?.id),
292
+ );
293
+ icons
294
+ .filter((icon) => !iconObjectIds.has(icon.id))
295
+ .forEach((icon, index) => nodes.push(imageNode(icon, "icon-picture", objects.length + index, normalizedOptions)));
296
+ fallbacks.forEach((fallback, index) => nodes.push(fallbackNode(fallback, index, nodes.length, normalizedOptions)));
297
+
298
+ const accessibility = definedEntries({
299
+ title: snapshot?.title,
300
+ description: snapshot?.description,
301
+ });
302
+ const result = normalizeScene(createScene({
303
+ width: finiteNumberOr(snapshot?.canvas?.width, 0) * normalizedOptions.scale,
304
+ height: finiteNumberOr(snapshot?.canvas?.height, 0) * normalizedOptions.scale,
305
+ source: { kind: "architecture", path: normalizedOptions.path },
306
+ ...(Object.keys(accessibility).length ? { accessibility } : {}),
307
+ nodes,
308
+ }));
309
+ result.diagnostics.unshift(...diagnostics);
310
+ validateScene(result.scene);
311
+ return result;
312
+ }