sketchmark 0.1.2 → 0.1.4

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.
@@ -25,6 +25,7 @@ var AIDiagram = (function (exports) {
25
25
  "step",
26
26
  "config",
27
27
  "theme",
28
+ "bare",
28
29
  "bar-chart",
29
30
  "line-chart",
30
31
  "pie-chart",
@@ -64,6 +65,7 @@ var AIDiagram = (function (exports) {
64
65
  "dag",
65
66
  "tree",
66
67
  "force",
68
+ "markdown",
67
69
  ]);
68
70
  const ARROW_PATTERNS = ["<-->", "<->", "-->", "<--", "->", "<-", "---", "--"];
69
71
  // Characters that can start an arrow pattern — used to decide whether a '-'
@@ -101,6 +103,23 @@ var AIDiagram = (function (exports) {
101
103
  i++;
102
104
  continue;
103
105
  }
106
+ if (ch === '"' && peek(1) === '"' && peek(2) === '"') {
107
+ i += 3; // skip opening """
108
+ let raw = "";
109
+ while (i < src.length) {
110
+ if (src[i] === '"' && src[i + 1] === '"' && src[i + 2] === '"') {
111
+ i += 3; // skip closing """
112
+ break;
113
+ }
114
+ if (src[i] === "\n") {
115
+ line++;
116
+ lineStart = i + 1;
117
+ }
118
+ raw += src[i++];
119
+ }
120
+ add("STRING_BLOCK", raw);
121
+ continue;
122
+ }
104
123
  // Strings
105
124
  if (ch === '"' || ch === "'") {
106
125
  const q = ch;
@@ -251,14 +270,16 @@ var AIDiagram = (function (exports) {
251
270
  s.fontSize = parseFloat(p["font-size"]);
252
271
  if (p["font-weight"])
253
272
  s.fontWeight = p["font-weight"];
254
- if (p['text-align'])
255
- s.textAlign = p['text-align'];
256
- if (p['vertical-align'])
257
- s.verticalAlign = p['vertical-align'];
258
- if (p['line-height'])
259
- s.lineHeight = parseFloat(p['line-height']);
260
- if (p['letter-spacing'])
261
- s.letterSpacing = parseFloat(p['letter-spacing']);
273
+ if (p["text-align"])
274
+ s.textAlign = p["text-align"];
275
+ if (p.padding)
276
+ s.padding = parseFloat(p.padding);
277
+ if (p["vertical-align"])
278
+ s.verticalAlign = p["vertical-align"];
279
+ if (p["line-height"])
280
+ s.lineHeight = parseFloat(p["line-height"]);
281
+ if (p["letter-spacing"])
282
+ s.letterSpacing = parseFloat(p["letter-spacing"]);
262
283
  if (p.font)
263
284
  s.font = p.font;
264
285
  if (p["dash"]) {
@@ -298,6 +319,7 @@ var AIDiagram = (function (exports) {
298
319
  notes: [],
299
320
  charts: [],
300
321
  tables: [],
322
+ markdowns: [],
301
323
  styles: {},
302
324
  themes: {},
303
325
  config: {},
@@ -308,6 +330,7 @@ var AIDiagram = (function (exports) {
308
330
  const noteIds = new Set();
309
331
  const chartIds = new Set();
310
332
  const groupIds = new Set();
333
+ const markdownIds = new Set();
311
334
  let i = 0;
312
335
  const cur = () => flat[i] ?? flat[flat.length - 1];
313
336
  const peek1 = () => flat[i + 1] ?? flat[flat.length - 1];
@@ -521,7 +544,7 @@ var AIDiagram = (function (exports) {
521
544
  const group = {
522
545
  kind: "group",
523
546
  id,
524
- label: props.label ?? id,
547
+ label: props.label ?? "",
525
548
  children: [],
526
549
  layout: props.layout,
527
550
  columns: props.columns !== undefined ? parseInt(props.columns, 10) : undefined,
@@ -547,8 +570,18 @@ var AIDiagram = (function (exports) {
547
570
  break;
548
571
  const v = cur().value;
549
572
  // ── Nested group ──────────────────────────────────
550
- if (v === "group") {
573
+ if (v === "group" || v === "bare") {
574
+ const isBare = v === "bare";
551
575
  const nested = parseGroup();
576
+ if (isBare) {
577
+ nested.label = "";
578
+ nested.style = {
579
+ ...nested.style,
580
+ fill: nested.style?.fill ?? "none",
581
+ stroke: nested.style?.stroke ?? "none",
582
+ strokeWidth: nested.style?.strokeWidth ?? 0,
583
+ };
584
+ }
552
585
  ast.groups.push(nested);
553
586
  groupIds.add(nested.id);
554
587
  group.children.push({ kind: "group", id: nested.id });
@@ -570,6 +603,21 @@ var AIDiagram = (function (exports) {
570
603
  group.children.push({ kind: "note", id: note.id });
571
604
  continue;
572
605
  }
606
+ // ── Markdown ───────────────────────────────────────
607
+ if (v === "markdown") {
608
+ const md = parseMarkdown(id);
609
+ ast.markdowns.push(md);
610
+ markdownIds.add(md.id);
611
+ group.children.push({ kind: "markdown", id: md.id });
612
+ continue;
613
+ }
614
+ if (v === "bare") {
615
+ // treat exactly like 'group' but inject defaults
616
+ const grp = parseGroup(); // reuse parseGroup
617
+ grp.label = "";
618
+ grp.style = { ...grp.style, stroke: "none", fill: "none" };
619
+ // rest is identical to group handling
620
+ }
573
621
  // ── Chart ──────────────────────────────────────────
574
622
  if (CHART_TYPES.includes(v)) {
575
623
  const chart = parseChart(v);
@@ -613,71 +661,71 @@ var AIDiagram = (function (exports) {
613
661
  function parseStep() {
614
662
  skip();
615
663
  const toks = lineTokens();
616
- const action = (toks[0]?.value ?? 'highlight');
617
- let target = toks[1]?.value ?? '';
618
- if (toks[2]?.type === 'ARROW' && toks[3]) {
664
+ const action = (toks[0]?.value ?? "highlight");
665
+ let target = toks[1]?.value ?? "";
666
+ if (toks[2]?.type === "ARROW" && toks[3]) {
619
667
  target = `${toks[1].value}${toks[2].value}${toks[3].value}`;
620
668
  }
621
- const step = { kind: 'step', action, target };
669
+ const step = { kind: "step", action, target };
622
670
  for (let j = 2; j < toks.length; j++) {
623
671
  const k = toks[j]?.value;
624
672
  const eq = toks[j + 1];
625
673
  const vt = toks[j + 2];
626
674
  // key=value form
627
- if (eq?.type === 'EQUALS' && vt) {
628
- if (k === 'dx') {
675
+ if (eq?.type === "EQUALS" && vt) {
676
+ if (k === "dx") {
629
677
  step.dx = parseFloat(vt.value);
630
678
  j += 2;
631
679
  continue;
632
680
  }
633
- if (k === 'dy') {
681
+ if (k === "dy") {
634
682
  step.dy = parseFloat(vt.value);
635
683
  j += 2;
636
684
  continue;
637
685
  }
638
- if (k === 'duration') {
686
+ if (k === "duration") {
639
687
  step.duration = parseFloat(vt.value);
640
688
  j += 2;
641
689
  continue;
642
690
  }
643
- if (k === 'delay') {
691
+ if (k === "delay") {
644
692
  step.delay = parseFloat(vt.value);
645
693
  j += 2;
646
694
  continue;
647
695
  }
648
- if (k === 'factor') {
696
+ if (k === "factor") {
649
697
  step.factor = parseFloat(vt.value);
650
698
  j += 2;
651
699
  continue;
652
700
  }
653
- if (k === 'deg') {
701
+ if (k === "deg") {
654
702
  step.deg = parseFloat(vt.value);
655
703
  j += 2;
656
704
  continue;
657
705
  }
658
- if (k === 'fill') {
706
+ if (k === "fill") {
659
707
  step.value = vt.value;
660
708
  j += 2;
661
709
  continue;
662
710
  }
663
- if (k === 'color') {
711
+ if (k === "color") {
664
712
  step.value = vt.value;
665
713
  j += 2;
666
714
  continue;
667
715
  }
668
716
  }
669
717
  // bare key value (legacy)
670
- if (k === 'delay' && eq?.type === 'NUMBER') {
718
+ if (k === "delay" && eq?.type === "NUMBER") {
671
719
  step.delay = parseFloat(eq.value);
672
720
  j++;
673
721
  continue;
674
722
  }
675
- if (k === 'duration' && eq?.type === 'NUMBER') {
723
+ if (k === "duration" && eq?.type === "NUMBER") {
676
724
  step.duration = parseFloat(eq.value);
677
725
  j++;
678
726
  continue;
679
727
  }
680
- if (k === 'trigger') {
728
+ if (k === "trigger") {
681
729
  step.trigger = eq?.value;
682
730
  j++;
683
731
  continue;
@@ -877,6 +925,40 @@ var AIDiagram = (function (exports) {
877
925
  skip();
878
926
  return table;
879
927
  }
928
+ // ── parseMarkdown ─────────────────────────────────────────
929
+ function parseMarkdown(groupId) {
930
+ skip(); // 'markdown'
931
+ const toks = lineTokens();
932
+ let id = groupId ? groupId + "_" + uid("md") : uid("md");
933
+ if (toks[0])
934
+ id = toks[0].value;
935
+ const props = {};
936
+ let j = 1;
937
+ while (j < toks.length - 1) {
938
+ const k = toks[j], eq = toks[j + 1];
939
+ if (eq?.type === "EQUALS" && j + 2 < toks.length) {
940
+ props[k.value] = toks[j + 2].value;
941
+ j += 3;
942
+ }
943
+ else
944
+ j++;
945
+ }
946
+ skipNL();
947
+ let content = "";
948
+ if (cur().type === "STRING_BLOCK") {
949
+ content = cur().value;
950
+ skip();
951
+ }
952
+ return {
953
+ kind: "markdown",
954
+ id,
955
+ content: content.trim(),
956
+ width: props.width ? parseFloat(props.width) : undefined,
957
+ height: props.height ? parseFloat(props.height) : undefined,
958
+ theme: props.theme,
959
+ style: propsToStyle(props),
960
+ };
961
+ }
880
962
  // ── Main parse loop ─────────────────────────────────────
881
963
  skipNL();
882
964
  if (cur().value === "diagram")
@@ -990,8 +1072,18 @@ var AIDiagram = (function (exports) {
990
1072
  continue;
991
1073
  }
992
1074
  // group
993
- if (v === "group") {
1075
+ if (v === "group" || v === "bare") {
1076
+ const isBare = v === "bare";
994
1077
  const grp = parseGroup();
1078
+ if (isBare) {
1079
+ grp.label = "";
1080
+ grp.style = {
1081
+ ...grp.style,
1082
+ fill: grp.style?.fill ?? "none",
1083
+ stroke: grp.style?.stroke ?? "none",
1084
+ strokeWidth: grp.style?.strokeWidth ?? 0,
1085
+ };
1086
+ }
995
1087
  ast.groups.push(grp);
996
1088
  groupIds.add(grp.id);
997
1089
  ast.rootOrder.push({ kind: "group", id: grp.id });
@@ -1026,6 +1118,13 @@ var AIDiagram = (function (exports) {
1026
1118
  ast.rootOrder.push({ kind: "chart", id: chart.id }); // ← ADD
1027
1119
  continue;
1028
1120
  }
1121
+ if (v === "markdown") {
1122
+ const md = parseMarkdown();
1123
+ ast.markdowns.push(md);
1124
+ markdownIds.add(md.id);
1125
+ ast.rootOrder.push({ kind: "markdown", id: md.id });
1126
+ continue;
1127
+ }
1029
1128
  // edge: A -> B (MUST come before shape check)
1030
1129
  if (t.type === "IDENT" || t.type === "STRING" || t.type === "KEYWORD") {
1031
1130
  const nextTok = flat[i + 1];
@@ -1078,6 +1177,91 @@ var AIDiagram = (function (exports) {
1078
1177
  return ast;
1079
1178
  }
1080
1179
 
1180
+ // ============================================================
1181
+ // sketchmark — Markdown inline parser
1182
+ // Supports: # h1 ## h2 ### h3 **bold** *italic* blank lines
1183
+ // ============================================================
1184
+ // ── Font sizes per line kind ──────────────────────────────
1185
+ const LINE_FONT_SIZE = {
1186
+ h1: 40,
1187
+ h2: 28,
1188
+ h3: 20,
1189
+ p: 15,
1190
+ blank: 0,
1191
+ };
1192
+ const LINE_FONT_WEIGHT = {
1193
+ h1: 700,
1194
+ h2: 600,
1195
+ h3: 600,
1196
+ p: 400,
1197
+ blank: 400,
1198
+ };
1199
+ // Spacing below each line kind (px)
1200
+ const LINE_SPACING = {
1201
+ h1: 52,
1202
+ h2: 38,
1203
+ h3: 28,
1204
+ p: 22,
1205
+ blank: 10,
1206
+ };
1207
+ // ── Parse a full markdown string into lines ───────────────
1208
+ function parseMarkdownContent(content) {
1209
+ const raw = content.split('\n');
1210
+ const lines = [];
1211
+ for (const line of raw) {
1212
+ const t = line.trim();
1213
+ if (!t) {
1214
+ lines.push({ kind: 'blank', runs: [] });
1215
+ continue;
1216
+ }
1217
+ if (t.startsWith('### ')) {
1218
+ lines.push({ kind: 'h3', runs: parseInline(t.slice(4)) });
1219
+ }
1220
+ else if (t.startsWith('## ')) {
1221
+ lines.push({ kind: 'h2', runs: parseInline(t.slice(3)) });
1222
+ }
1223
+ else if (t.startsWith('# ')) {
1224
+ lines.push({ kind: 'h1', runs: parseInline(t.slice(2)) });
1225
+ }
1226
+ else {
1227
+ lines.push({ kind: 'p', runs: parseInline(t) });
1228
+ }
1229
+ }
1230
+ // strip leading/trailing blank lines
1231
+ while (lines.length && lines[0].kind === 'blank')
1232
+ lines.shift();
1233
+ while (lines.length && lines[lines.length - 1].kind === 'blank')
1234
+ lines.pop();
1235
+ return lines;
1236
+ }
1237
+ // ── Parse inline bold/italic spans ───────────────────────
1238
+ function parseInline(text) {
1239
+ const runs = [];
1240
+ // Order matters: check ** before *
1241
+ const re = /(\*\*(.+?)\*\*|\*(.+?)\*|[^*]+)/g;
1242
+ let m;
1243
+ while ((m = re.exec(text)) !== null) {
1244
+ if (m[0].startsWith('**')) {
1245
+ runs.push({ text: m[2], bold: true });
1246
+ }
1247
+ else if (m[0].startsWith('*')) {
1248
+ runs.push({ text: m[3], italic: true });
1249
+ }
1250
+ else {
1251
+ if (m[0])
1252
+ runs.push({ text: m[0] });
1253
+ }
1254
+ }
1255
+ return runs;
1256
+ }
1257
+ // ── Calculate natural height of a parsed block ────────────
1258
+ function calcMarkdownHeight(lines, pad = 16) {
1259
+ let h = pad * 2; // top + bottom
1260
+ for (const line of lines)
1261
+ h += LINE_SPACING[line.kind];
1262
+ return h;
1263
+ }
1264
+
1081
1265
  // ============================================================
1082
1266
  // sketchmark — Scene Graph
1083
1267
  // ============================================================
@@ -1168,6 +1352,18 @@ var AIDiagram = (function (exports) {
1168
1352
  h: c.height ?? 240,
1169
1353
  };
1170
1354
  });
1355
+ const markdowns = (ast.markdowns ?? []).map(m => {
1356
+ const themeStyle = m.theme ? (ast.themes[m.theme] ?? {}) : {};
1357
+ return {
1358
+ id: m.id,
1359
+ content: m.content,
1360
+ lines: parseMarkdownContent(m.content),
1361
+ style: { ...themeStyle, ...m.style },
1362
+ width: m.width,
1363
+ height: m.height,
1364
+ x: 0, y: 0, w: 0, h: 0,
1365
+ };
1366
+ });
1171
1367
  // Set parentId for nested groups
1172
1368
  for (const g of groups) {
1173
1369
  for (const child of g.children) {
@@ -1198,6 +1394,7 @@ var AIDiagram = (function (exports) {
1198
1394
  tables,
1199
1395
  notes,
1200
1396
  charts,
1397
+ markdowns,
1201
1398
  animation: { steps: ast.steps, currentStep: -1 },
1202
1399
  styles: ast.styles,
1203
1400
  config: ast.config,
@@ -1222,6 +1419,9 @@ var AIDiagram = (function (exports) {
1222
1419
  function chartMap(sg) {
1223
1420
  return new Map(sg.charts.map((c) => [c.id, c]));
1224
1421
  }
1422
+ function markdownMap(sg) {
1423
+ return new Map((sg.markdowns ?? []).map(m => [m.id, m]));
1424
+ }
1225
1425
 
1226
1426
  // ============================================================
1227
1427
  // sketchmark — Layout Engine (Flexbox-style, recursive)
@@ -1262,30 +1462,40 @@ var AIDiagram = (function (exports) {
1262
1462
  n.h = n.height;
1263
1463
  const labelW = Math.round(n.label.length * FONT_PX_PER_CHAR + BASE_PAD);
1264
1464
  switch (n.shape) {
1265
- case 'circle':
1465
+ case "circle":
1266
1466
  n.w = n.w || Math.max(84, Math.min(MAX_W, labelW));
1267
1467
  n.h = n.h || n.w;
1268
1468
  break;
1269
- case 'diamond':
1469
+ case "diamond":
1270
1470
  n.w = n.w || Math.max(130, Math.min(MAX_W, labelW + 30));
1271
1471
  n.h = n.h || Math.max(62, n.w * 0.46);
1272
1472
  break;
1273
- case 'hexagon':
1473
+ case "hexagon":
1274
1474
  n.w = n.w || Math.max(126, Math.min(MAX_W, labelW + 20));
1275
1475
  n.h = n.h || Math.max(54, n.w * 0.44);
1276
1476
  break;
1277
- case 'triangle':
1477
+ case "triangle":
1278
1478
  n.w = n.w || Math.max(108, Math.min(MAX_W, labelW + 10));
1279
- n.h = n.h || Math.max(64, n.w * 0.60);
1479
+ n.h = n.h || Math.max(64, n.w * 0.6);
1280
1480
  break;
1281
- case 'cylinder':
1481
+ case "cylinder":
1282
1482
  n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
1283
1483
  n.h = n.h || 66;
1284
1484
  break;
1285
- case 'parallelogram':
1485
+ case "parallelogram":
1286
1486
  n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW + 28));
1287
1487
  n.h = n.h || 50;
1288
1488
  break;
1489
+ case "text": {
1490
+ // read fontSize from style if set, otherwise use default
1491
+ const fontSize = Number(n.style?.fontSize ?? 13);
1492
+ const charWidth = fontSize * 0.55;
1493
+ const maxW = n.width ?? 400;
1494
+ const approxLines = Math.ceil((n.label.length * charWidth) / (maxW - 16));
1495
+ n.w = maxW;
1496
+ n.h = n.height ?? Math.max(24, approxLines * fontSize * 1.5 + 8);
1497
+ break;
1498
+ }
1289
1499
  default:
1290
1500
  n.w = n.w || Math.max(MIN_W, Math.min(MAX_W, labelW));
1291
1501
  n.h = n.h || 52;
@@ -1293,7 +1503,7 @@ var AIDiagram = (function (exports) {
1293
1503
  }
1294
1504
  }
1295
1505
  function sizeNote(n) {
1296
- const maxChars = Math.max(...n.lines.map(l => l.length));
1506
+ const maxChars = Math.max(...n.lines.map((l) => l.length));
1297
1507
  n.w = Math.max(120, Math.ceil(maxChars * NOTE_FONT) + NOTE_PAD_X * 2);
1298
1508
  n.h = n.lines.length * NOTE_LINE_H + NOTE_PAD_Y * 2;
1299
1509
  if (n.width && n.w < n.width)
@@ -1309,7 +1519,7 @@ var AIDiagram = (function (exports) {
1309
1519
  t.h = labelH + rowH;
1310
1520
  return;
1311
1521
  }
1312
- const numCols = Math.max(...rows.map(r => r.cells.length));
1522
+ const numCols = Math.max(...rows.map((r) => r.cells.length));
1313
1523
  const colW = Array(numCols).fill(MIN_COL_W);
1314
1524
  for (const row of rows) {
1315
1525
  row.cells.forEach((cell, i) => {
@@ -1318,110 +1528,128 @@ var AIDiagram = (function (exports) {
1318
1528
  }
1319
1529
  t.colWidths = colW;
1320
1530
  t.w = colW.reduce((s, w) => s + w, 0);
1321
- const nHeader = rows.filter(r => r.kind === 'header').length;
1322
- const nData = rows.filter(r => r.kind === 'data').length;
1531
+ const nHeader = rows.filter((r) => r.kind === "header").length;
1532
+ const nData = rows.filter((r) => r.kind === "data").length;
1323
1533
  t.h = labelH + nHeader * headerH + nData * rowH;
1324
1534
  }
1325
1535
  function sizeChart(c) {
1326
1536
  c.w = c.w || 320;
1327
1537
  c.h = c.h || 240;
1328
1538
  }
1539
+ function sizeMarkdown(m) {
1540
+ const pad = Number(m.style?.padding ?? 16);
1541
+ m.w = m.width ?? 400;
1542
+ m.h = m.height ?? calcMarkdownHeight(m.lines, pad);
1543
+ }
1329
1544
  // ── Item size helpers ─────────────────────────────────────
1330
- function iW(r, nm, gm, tm, ntm, cm) {
1331
- if (r.kind === 'node')
1545
+ function iW(r, nm, gm, tm, ntm, cm, mdm) {
1546
+ if (r.kind === "node")
1332
1547
  return nm.get(r.id).w;
1333
- if (r.kind === 'table')
1548
+ if (r.kind === "table")
1334
1549
  return tm.get(r.id).w;
1335
- if (r.kind === 'note')
1550
+ if (r.kind === "note")
1336
1551
  return ntm.get(r.id).w;
1337
- if (r.kind === 'chart')
1552
+ if (r.kind === "chart")
1338
1553
  return cm.get(r.id).w;
1554
+ if (r.kind === "markdown")
1555
+ return mdm.get(r.id).w;
1339
1556
  return gm.get(r.id).w;
1340
1557
  }
1341
- function iH(r, nm, gm, tm, ntm, cm) {
1342
- if (r.kind === 'node')
1558
+ function iH(r, nm, gm, tm, ntm, cm, mdm) {
1559
+ if (r.kind === "node")
1343
1560
  return nm.get(r.id).h;
1344
- if (r.kind === 'table')
1561
+ if (r.kind === "table")
1345
1562
  return tm.get(r.id).h;
1346
- if (r.kind === 'note')
1563
+ if (r.kind === "note")
1347
1564
  return ntm.get(r.id).h;
1348
- if (r.kind === 'chart')
1565
+ if (r.kind === "chart")
1349
1566
  return cm.get(r.id).h;
1567
+ if (r.kind === "markdown")
1568
+ return mdm.get(r.id).h;
1350
1569
  return gm.get(r.id).h;
1351
1570
  }
1352
- function setPos(r, x, y, nm, gm, tm, ntm, cm) {
1353
- if (r.kind === 'node') {
1571
+ function setPos(r, x, y, nm, gm, tm, ntm, cm, mdm) {
1572
+ if (r.kind === "node") {
1354
1573
  const n = nm.get(r.id);
1355
1574
  n.x = Math.round(x);
1356
1575
  n.y = Math.round(y);
1357
1576
  return;
1358
1577
  }
1359
- if (r.kind === 'table') {
1578
+ if (r.kind === "table") {
1360
1579
  const t = tm.get(r.id);
1361
1580
  t.x = Math.round(x);
1362
1581
  t.y = Math.round(y);
1363
1582
  return;
1364
1583
  }
1365
- if (r.kind === 'note') {
1584
+ if (r.kind === "note") {
1366
1585
  const nt = ntm.get(r.id);
1367
1586
  nt.x = Math.round(x);
1368
1587
  nt.y = Math.round(y);
1369
1588
  return;
1370
1589
  }
1371
- if (r.kind === 'chart') {
1590
+ if (r.kind === "chart") {
1372
1591
  const c = cm.get(r.id);
1373
1592
  c.x = Math.round(x);
1374
1593
  c.y = Math.round(y);
1375
1594
  return;
1376
1595
  }
1596
+ if (r.kind === "markdown") {
1597
+ const md = mdm.get(r.id);
1598
+ md.x = Math.round(x);
1599
+ md.y = Math.round(y);
1600
+ return;
1601
+ }
1377
1602
  const g = gm.get(r.id);
1378
1603
  g.x = Math.round(x);
1379
1604
  g.y = Math.round(y);
1380
1605
  }
1381
1606
  // ── Pass 1: Measure (bottom-up) ───────────────────────────
1382
1607
  // Recursively computes w, h for a group from its children's sizes.
1383
- function measure(g, nm, gm, tm, ntm, cm) {
1608
+ function measure(g, nm, gm, tm, ntm, cm, mdm) {
1384
1609
  // Recurse into nested groups first; size tables before reading their dims
1385
1610
  for (const r of g.children) {
1386
- if (r.kind === 'group')
1387
- measure(gm.get(r.id), nm, gm, tm, ntm, cm);
1388
- if (r.kind === 'table')
1611
+ if (r.kind === "group")
1612
+ measure(gm.get(r.id), nm, gm, tm, ntm, cm, mdm);
1613
+ if (r.kind === "table")
1389
1614
  sizeTable(tm.get(r.id));
1390
- if (r.kind === 'note')
1615
+ if (r.kind === "note")
1391
1616
  sizeNote(ntm.get(r.id));
1392
- if (r.kind === 'chart')
1617
+ if (r.kind === "chart")
1393
1618
  sizeChart(cm.get(r.id));
1619
+ if (r.kind === "markdown")
1620
+ sizeMarkdown(mdm.get(r.id));
1394
1621
  }
1395
1622
  const { padding: pad, gap, columns, layout } = g;
1396
1623
  const kids = g.children;
1624
+ const labelH = g.label ? GROUP_LABEL_H : 0;
1397
1625
  if (!kids.length) {
1398
1626
  g.w = pad * 2;
1399
- g.h = pad * 2 + GROUP_LABEL_H;
1627
+ g.h = pad * 2 + labelH;
1400
1628
  if (g.width && g.w < g.width)
1401
1629
  g.w = g.width;
1402
1630
  if (g.height && g.h < g.height)
1403
1631
  g.h = g.height;
1404
1632
  return;
1405
1633
  }
1406
- const ws = kids.map(r => iW(r, nm, gm, tm, ntm, cm));
1407
- const hs = kids.map(r => iH(r, nm, gm, tm, ntm, cm));
1634
+ const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
1635
+ const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
1408
1636
  const n = kids.length;
1409
- if (layout === 'row') {
1637
+ if (layout === "row") {
1410
1638
  g.w = ws.reduce((s, w) => s + w, 0) + gap * (n - 1) + pad * 2;
1411
- g.h = Math.max(...hs) + pad * 2 + GROUP_LABEL_H;
1639
+ g.h = Math.max(...hs) + pad * 2 + labelH;
1412
1640
  }
1413
- else if (layout === 'grid') {
1641
+ else if (layout === "grid") {
1414
1642
  const cols = Math.max(1, columns);
1415
1643
  const rows = Math.ceil(n / cols);
1416
1644
  const cellW = Math.max(...ws);
1417
1645
  const cellH = Math.max(...hs);
1418
1646
  g.w = cols * cellW + (cols - 1) * gap + pad * 2;
1419
- g.h = rows * cellH + (rows - 1) * gap + pad * 2 + GROUP_LABEL_H;
1647
+ g.h = rows * cellH + (rows - 1) * gap + pad * 2 + labelH;
1420
1648
  }
1421
1649
  else {
1422
1650
  // column (default)
1423
1651
  g.w = Math.max(...ws) + pad * 2;
1424
- g.h = hs.reduce((s, h) => s + h, 0) + gap * (n - 1) + pad * 2 + GROUP_LABEL_H;
1652
+ g.h = hs.reduce((s, h) => s + h, 0) + gap * (n - 1) + pad * 2 + labelH;
1425
1653
  }
1426
1654
  // Clamp to minWidth / minHeight — this is what gives distribute() free
1427
1655
  // space to work with for justify=center/end/space-between/space-around
@@ -1436,21 +1664,32 @@ var AIDiagram = (function (exports) {
1436
1664
  const totalSize = sizes.reduce((s, v) => s + v, 0);
1437
1665
  const gapCount = n - 1;
1438
1666
  switch (justify) {
1439
- case 'center': {
1667
+ case "center": {
1440
1668
  const total = totalSize + gap * gapCount;
1441
- return { start: Math.max(0, (contentSize - total) / 2), gaps: Array(gapCount).fill(gap) };
1669
+ return {
1670
+ start: Math.max(0, (contentSize - total) / 2),
1671
+ gaps: Array(gapCount).fill(gap),
1672
+ };
1442
1673
  }
1443
- case 'end': {
1674
+ case "end": {
1444
1675
  const total = totalSize + gap * gapCount;
1445
- return { start: Math.max(0, contentSize - total), gaps: Array(gapCount).fill(gap) };
1676
+ return {
1677
+ start: Math.max(0, contentSize - total),
1678
+ gaps: Array(gapCount).fill(gap),
1679
+ };
1446
1680
  }
1447
- case 'space-between': {
1448
- const g2 = gapCount > 0 ? Math.max(gap, (contentSize - totalSize) / gapCount) : gap;
1681
+ case "space-between": {
1682
+ const g2 = gapCount > 0
1683
+ ? Math.max(gap, (contentSize - totalSize) / gapCount)
1684
+ : gap;
1449
1685
  return { start: 0, gaps: Array(gapCount).fill(g2) };
1450
1686
  }
1451
- case 'space-around': {
1687
+ case "space-around": {
1452
1688
  const space = n > 0 ? (contentSize - totalSize) / n : gap;
1453
- return { start: Math.max(0, space / 2), gaps: Array(gapCount).fill(Math.max(gap, space)) };
1689
+ return {
1690
+ start: Math.max(0, space / 2),
1691
+ gaps: Array(gapCount).fill(Math.max(gap, space)),
1692
+ };
1454
1693
  }
1455
1694
  default: // start
1456
1695
  return { start: 0, gaps: Array(gapCount).fill(gap) };
@@ -1458,70 +1697,73 @@ var AIDiagram = (function (exports) {
1458
1697
  }
1459
1698
  // ── Pass 2: Place (top-down) ──────────────────────────────
1460
1699
  // Assigns x, y to each child. Assumes g.x / g.y already set by parent.
1461
- function place(g, nm, gm, tm, ntm, cm) {
1700
+ function place(g, nm, gm, tm, ntm, cm, mdm) {
1462
1701
  const { padding: pad, gap, columns, layout, align, justify } = g;
1702
+ const labelH = g.label ? GROUP_LABEL_H : 0;
1463
1703
  const contentX = g.x + pad;
1464
- const contentY = g.y + GROUP_LABEL_H + pad;
1704
+ const contentY = g.y + labelH + pad;
1465
1705
  const contentW = g.w - pad * 2;
1466
- const contentH = g.h - pad * 2 - GROUP_LABEL_H;
1706
+ const contentH = g.h - pad * 2 - labelH;
1467
1707
  const kids = g.children;
1468
1708
  if (!kids.length)
1469
1709
  return;
1470
- if (layout === 'row') {
1471
- const ws = kids.map(r => iW(r, nm, gm, tm, ntm, cm));
1472
- const hs = kids.map(r => iH(r, nm, gm, tm, ntm, cm));
1710
+ if (layout === "row") {
1711
+ const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
1712
+ const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
1473
1713
  const maxH = Math.max(...hs);
1474
1714
  const { start, gaps } = distribute(ws, contentW, gap, justify);
1475
1715
  let x = contentX + start;
1476
1716
  for (let i = 0; i < kids.length; i++) {
1477
1717
  let y;
1478
1718
  switch (align) {
1479
- case 'center':
1719
+ case "center":
1480
1720
  y = contentY + (maxH - hs[i]) / 2;
1481
1721
  break;
1482
- case 'end':
1722
+ case "end":
1483
1723
  y = contentY + maxH - hs[i];
1484
1724
  break;
1485
- default: y = contentY;
1725
+ default:
1726
+ y = contentY;
1486
1727
  }
1487
- setPos(kids[i], x, y, nm, gm, tm, ntm, cm);
1728
+ setPos(kids[i], x, y, nm, gm, tm, ntm, cm, mdm);
1488
1729
  x += ws[i] + (i < gaps.length ? gaps[i] : 0);
1489
1730
  }
1490
1731
  }
1491
- else if (layout === 'grid') {
1732
+ else if (layout === "grid") {
1492
1733
  const cols = Math.max(1, columns);
1493
- const cellW = Math.max(...kids.map(r => iW(r, nm, gm, tm, ntm, cm)));
1494
- const cellH = Math.max(...kids.map(r => iH(r, nm, gm, tm, ntm, cm)));
1734
+ const cellW = Math.max(...kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm)));
1735
+ const cellH = Math.max(...kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm)));
1495
1736
  kids.forEach((ref, i) => {
1496
- setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap), nm, gm, tm, ntm, cm);
1737
+ setPos(ref, contentX + (i % cols) * (cellW + gap), contentY + Math.floor(i / cols) * (cellH + gap), nm, gm, tm, ntm, cm, mdm);
1497
1738
  });
1498
1739
  }
1499
1740
  else {
1500
1741
  // column (default)
1501
- const ws = kids.map(r => iW(r, nm, gm, tm, ntm, cm));
1502
- const hs = kids.map(r => iH(r, nm, gm, tm, ntm, cm));
1742
+ const ws = kids.map((r) => iW(r, nm, gm, tm, ntm, cm, mdm));
1743
+ const hs = kids.map((r) => iH(r, nm, gm, tm, ntm, cm, mdm));
1503
1744
  const maxW = Math.max(...ws);
1504
1745
  const { start, gaps } = distribute(hs, contentH, gap, justify);
1505
1746
  let y = contentY + start;
1506
1747
  for (let i = 0; i < kids.length; i++) {
1507
1748
  let x;
1508
1749
  switch (align) {
1509
- case 'center':
1750
+ case "center":
1510
1751
  x = contentX + (maxW - ws[i]) / 2;
1511
1752
  break;
1512
- case 'end':
1753
+ case "end":
1513
1754
  x = contentX + maxW - ws[i];
1514
1755
  break;
1515
- default: x = contentX;
1756
+ default:
1757
+ x = contentX;
1516
1758
  }
1517
- setPos(kids[i], x, y, nm, gm, tm, ntm, cm);
1759
+ setPos(kids[i], x, y, nm, gm, tm, ntm, cm, mdm);
1518
1760
  y += hs[i] + (i < gaps.length ? gaps[i] : 0);
1519
1761
  }
1520
1762
  }
1521
1763
  // Recurse into nested groups
1522
1764
  for (const r of kids) {
1523
- if (r.kind === 'group')
1524
- place(gm.get(r.id), nm, gm, tm, ntm, cm);
1765
+ if (r.kind === "group")
1766
+ place(gm.get(r.id), nm, gm, tm, ntm, cm, mdm);
1525
1767
  }
1526
1768
  }
1527
1769
  // ── Edge routing ──────────────────────────────────────────
@@ -1531,9 +1773,9 @@ var AIDiagram = (function (exports) {
1531
1773
  const dx = ox - cx, dy = oy - cy;
1532
1774
  if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01)
1533
1775
  return [cx, cy];
1534
- if (n.shape === 'circle') {
1776
+ if (n.shape === "circle") {
1535
1777
  const r = n.w * 0.44, len = Math.sqrt(dx * dx + dy * dy);
1536
- return [cx + dx / len * r, cy + dy / len * r];
1778
+ return [cx + (dx / len) * r, cy + (dy / len) * r];
1537
1779
  }
1538
1780
  const hw = n.w / 2 - 2, hh = n.h / 2 - 2;
1539
1781
  const tx = Math.abs(dx) > 0.01 ? hw / Math.abs(dx) : 1e9;
@@ -1578,9 +1820,12 @@ var AIDiagram = (function (exports) {
1578
1820
  }
1579
1821
  function connPt(src, dstCX, dstCY) {
1580
1822
  // SceneNode has a .shape field; use the existing connPoint for it
1581
- if ('shape' in src && src.shape) {
1823
+ if ("shape" in src && src.shape) {
1582
1824
  return connPoint(src, {
1583
- x: dstCX - 1, y: dstCY - 1, w: 2, h: 2});
1825
+ x: dstCX - 1,
1826
+ y: dstCY - 1,
1827
+ w: 2,
1828
+ h: 2});
1584
1829
  }
1585
1830
  return rectConnPoint$2(src.x, src.y, src.w, src.h, dstCX, dstCY);
1586
1831
  }
@@ -1593,84 +1838,84 @@ var AIDiagram = (function (exports) {
1593
1838
  }
1594
1839
  const dstCX = dst.x + dst.w / 2, dstCY = dst.y + dst.h / 2;
1595
1840
  const srcCX = src.x + src.w / 2, srcCY = src.y + src.h / 2;
1596
- e.points = [
1597
- connPt(src, dstCX, dstCY),
1598
- connPt(dst, srcCX, srcCY),
1599
- ];
1841
+ e.points = [connPt(src, dstCX, dstCY), connPt(dst, srcCX, srcCY)];
1600
1842
  }
1601
1843
  }
1602
1844
  function computeBounds(sg, margin) {
1603
1845
  const allX = [
1604
- ...sg.nodes.map(n => n.x + n.w),
1605
- ...sg.groups.filter(g => g.w).map(g => g.x + g.w),
1606
- ...sg.tables.map(t => t.x + t.w),
1607
- ...sg.notes.map(n => n.x + n.w),
1608
- ...sg.charts.map(c => c.x + c.w)
1846
+ ...sg.nodes.map((n) => n.x + n.w),
1847
+ ...sg.groups.filter((g) => g.w).map((g) => g.x + g.w),
1848
+ ...sg.tables.map((t) => t.x + t.w),
1849
+ ...sg.notes.map((n) => n.x + n.w),
1850
+ ...sg.charts.map((c) => c.x + c.w),
1851
+ ...sg.markdowns.map((m) => m.x + m.w),
1609
1852
  ];
1610
1853
  const allY = [
1611
- ...sg.nodes.map(n => n.y + n.h),
1612
- ...sg.groups.filter(g => g.h).map(g => g.y + g.h),
1613
- ...sg.tables.map(t => t.y + t.h),
1614
- ...sg.notes.map(n => n.y + n.h),
1615
- ...sg.charts.map(c => c.y + c.h)
1854
+ ...sg.nodes.map((n) => n.y + n.h),
1855
+ ...sg.groups.filter((g) => g.h).map((g) => g.y + g.h),
1856
+ ...sg.tables.map((t) => t.y + t.h),
1857
+ ...sg.notes.map((n) => n.y + n.h),
1858
+ ...sg.charts.map((c) => c.y + c.h),
1859
+ ...sg.markdowns.map((m) => m.y + m.h),
1616
1860
  ];
1617
1861
  sg.width = (allX.length ? Math.max(...allX) : 400) + margin;
1618
1862
  sg.height = (allY.length ? Math.max(...allY) : 300) + margin;
1619
1863
  }
1620
1864
  // ── Public entry point ────────────────────────────────────
1621
1865
  function layout(sg) {
1622
- const GAP_MAIN = Number(sg.config['gap'] ?? DEFAULT_GAP_MAIN);
1623
- const MARGIN = Number(sg.config['margin'] ?? DEFAULT_MARGIN);
1866
+ const GAP_MAIN = Number(sg.config["gap"] ?? DEFAULT_GAP_MAIN);
1867
+ const MARGIN = Number(sg.config["margin"] ?? DEFAULT_MARGIN);
1624
1868
  const nm = nodeMap(sg);
1625
1869
  const gm = groupMap(sg);
1626
1870
  const tm = tableMap(sg);
1627
1871
  const ntm = noteMap(sg);
1628
1872
  const cm = chartMap(sg);
1629
- console.log('[layout] sg.charts:', sg.charts.map(c => c.id));
1630
- console.log('[layout] sg.rootOrder:', sg.rootOrder.map(r => r.kind + ':' + r.id));
1873
+ const mdm = markdownMap(sg);
1631
1874
  // 1. Size all nodes and tables
1632
1875
  sg.nodes.forEach(sizeNode);
1633
1876
  sg.tables.forEach(sizeTable);
1634
1877
  sg.notes.forEach(sizeNote);
1635
1878
  sg.charts.forEach(sizeChart);
1879
+ sg.markdowns.forEach(sizeMarkdown);
1636
1880
  // src/layout/index.ts — after sg.charts.forEach(sizeChart);
1637
1881
  // 2. Identify root vs nested items
1638
- const nestedGroupIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind === 'group').map(c => c.id)));
1639
- const groupedNodeIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind === 'node').map(c => c.id)));
1640
- const groupedTableIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind === 'table').map(c => c.id)));
1641
- const groupedNoteIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind === 'note').map(c => c.id)));
1642
- const groupedChartIds = new Set(sg.groups.flatMap(g => g.children.filter(c => c.kind === 'chart').map(c => c.id)));
1643
- const rootGroups = sg.groups.filter(g => !nestedGroupIds.has(g.id));
1644
- const rootNodes = sg.nodes.filter(n => !groupedNodeIds.has(n.id));
1645
- const rootTables = sg.tables.filter(t => !groupedTableIds.has(t.id));
1646
- const rootNotes = sg.notes.filter(n => !groupedNoteIds.has(n.id));
1647
- const rootCharts = sg.charts.filter(c => !groupedChartIds.has(c.id));
1882
+ const nestedGroupIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "group").map((c) => c.id)));
1883
+ const groupedNodeIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "node").map((c) => c.id)));
1884
+ const groupedTableIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "table").map((c) => c.id)));
1885
+ const groupedNoteIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "note").map((c) => c.id)));
1886
+ const groupedChartIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "chart").map((c) => c.id)));
1887
+ const groupedMarkdownIds = new Set(sg.groups.flatMap((g) => g.children.filter((c) => c.kind === "markdown").map((c) => c.id)));
1888
+ const rootGroups = sg.groups.filter((g) => !nestedGroupIds.has(g.id));
1889
+ const rootNodes = sg.nodes.filter((n) => !groupedNodeIds.has(n.id));
1890
+ const rootTables = sg.tables.filter((t) => !groupedTableIds.has(t.id));
1891
+ const rootNotes = sg.notes.filter((n) => !groupedNoteIds.has(n.id));
1892
+ const rootCharts = sg.charts.filter((c) => !groupedChartIds.has(c.id));
1893
+ const rootMarkdowns = sg.markdowns.filter((m) => !groupedMarkdownIds.has(m.id));
1648
1894
  // 3. Measure root groups bottom-up
1649
1895
  for (const g of rootGroups)
1650
- measure(g, nm, gm, tm, ntm, cm);
1896
+ measure(g, nm, gm, tm, ntm, cm, mdm);
1651
1897
  // 4. Build root order
1652
1898
  // sg.rootOrder preserves DSL declaration order.
1653
1899
  // Fall back: groups, then nodes, then tables.
1654
1900
  const rootOrder = sg.rootOrder?.length
1655
1901
  ? sg.rootOrder
1656
1902
  : [
1657
- ...rootGroups.map(g => ({ kind: 'group', id: g.id })),
1658
- ...rootNodes.map(n => ({ kind: 'node', id: n.id })),
1659
- ...rootTables.map(t => ({ kind: 'table', id: t.id })),
1660
- ...rootNotes.map(n => ({ kind: 'note', id: n.id })),
1661
- ...rootCharts.map(c => ({ kind: 'chart', id: c.id }))
1903
+ ...rootGroups.map((g) => ({ kind: "group", id: g.id })),
1904
+ ...rootNodes.map((n) => ({ kind: "node", id: n.id })),
1905
+ ...rootTables.map((t) => ({ kind: "table", id: t.id })),
1906
+ ...rootNotes.map((n) => ({ kind: "note", id: n.id })),
1907
+ ...rootCharts.map((c) => ({ kind: "chart", id: c.id })),
1908
+ ...rootMarkdowns.map((m) => ({ kind: "markdown", id: m.id })),
1662
1909
  ];
1663
1910
  // 5. Root-level layout
1664
1911
  // sg.layout:
1665
1912
  // 'row' → items flow left to right (default)
1666
1913
  // 'column' → items flow top to bottom
1667
1914
  // 'grid' → config columns=N grid
1668
- const rootLayout = (sg.layout ?? 'row');
1669
- const rootCols = Number(sg.config['columns'] ?? 1);
1670
- const useGrid = rootLayout === 'grid' && rootCols > 0;
1671
- const useColumn = rootLayout === 'column';
1672
- console.log('[layout] sized charts:', sg.charts.map(c => `${c.id} w=${c.w} h=${c.h}`));
1673
- console.log('[layout] rootOrder chart refs:', rootOrder.filter(r => r.kind === 'chart'));
1915
+ const rootLayout = (sg.layout ?? "row");
1916
+ const rootCols = Number(sg.config["columns"] ?? 1);
1917
+ const useGrid = rootLayout === "grid" && rootCols > 0;
1918
+ const useColumn = rootLayout === "column";
1674
1919
  if (useGrid) {
1675
1920
  // ── Grid: per-row heights, per-column widths (no wasted space) ──
1676
1921
  const cols = rootCols;
@@ -1681,22 +1926,26 @@ var AIDiagram = (function (exports) {
1681
1926
  const col = idx % cols;
1682
1927
  const row = Math.floor(idx / cols);
1683
1928
  let w = 0, h = 0;
1684
- if (ref.kind === 'group') {
1929
+ if (ref.kind === "group") {
1685
1930
  w = gm.get(ref.id).w;
1686
1931
  h = gm.get(ref.id).h;
1687
1932
  }
1688
- else if (ref.kind === 'table') {
1933
+ else if (ref.kind === "table") {
1689
1934
  w = tm.get(ref.id).w;
1690
1935
  h = tm.get(ref.id).h;
1691
1936
  }
1692
- else if (ref.kind === 'note') {
1937
+ else if (ref.kind === "note") {
1693
1938
  w = ntm.get(ref.id).w;
1694
1939
  h = ntm.get(ref.id).h;
1695
1940
  }
1696
- else if (ref.kind === 'chart') {
1941
+ else if (ref.kind === "chart") {
1697
1942
  w = cm.get(ref.id).w;
1698
1943
  h = cm.get(ref.id).h;
1699
1944
  }
1945
+ else if (ref.kind === "markdown") {
1946
+ w = mdm.get(ref.id).w;
1947
+ h = mdm.get(ref.id).h;
1948
+ }
1700
1949
  else {
1701
1950
  w = nm.get(ref.id).w;
1702
1951
  h = nm.get(ref.id).h;
@@ -1719,22 +1968,26 @@ var AIDiagram = (function (exports) {
1719
1968
  rootOrder.forEach((ref, idx) => {
1720
1969
  const x = colX[idx % cols];
1721
1970
  const y = rowY[Math.floor(idx / cols)];
1722
- if (ref.kind === 'group') {
1971
+ if (ref.kind === "group") {
1723
1972
  gm.get(ref.id).x = x;
1724
1973
  gm.get(ref.id).y = y;
1725
1974
  }
1726
- else if (ref.kind === 'table') {
1975
+ else if (ref.kind === "table") {
1727
1976
  tm.get(ref.id).x = x;
1728
1977
  tm.get(ref.id).y = y;
1729
1978
  }
1730
- else if (ref.kind === 'note') {
1979
+ else if (ref.kind === "note") {
1731
1980
  ntm.get(ref.id).x = x;
1732
1981
  ntm.get(ref.id).y = y;
1733
1982
  }
1734
- else if (ref.kind === 'chart') {
1983
+ else if (ref.kind === "chart") {
1735
1984
  cm.get(ref.id).x = x;
1736
1985
  cm.get(ref.id).y = y;
1737
1986
  }
1987
+ else if (ref.kind === "markdown") {
1988
+ mdm.get(ref.id).x = x;
1989
+ mdm.get(ref.id).y = y;
1990
+ }
1738
1991
  else {
1739
1992
  nm.get(ref.id).x = x;
1740
1993
  nm.get(ref.id).y = y;
@@ -1746,44 +1999,52 @@ var AIDiagram = (function (exports) {
1746
1999
  let pos = MARGIN;
1747
2000
  for (const ref of rootOrder) {
1748
2001
  let w = 0, h = 0;
1749
- if (ref.kind === 'group') {
2002
+ if (ref.kind === "group") {
1750
2003
  w = gm.get(ref.id).w;
1751
2004
  h = gm.get(ref.id).h;
1752
2005
  }
1753
- else if (ref.kind === 'table') {
2006
+ else if (ref.kind === "table") {
1754
2007
  w = tm.get(ref.id).w;
1755
2008
  h = tm.get(ref.id).h;
1756
2009
  }
1757
- else if (ref.kind === 'note') {
2010
+ else if (ref.kind === "note") {
1758
2011
  w = ntm.get(ref.id).w;
1759
2012
  h = ntm.get(ref.id).h;
1760
2013
  }
1761
- else if (ref.kind === 'chart') {
2014
+ else if (ref.kind === "chart") {
1762
2015
  w = cm.get(ref.id).w;
1763
2016
  h = cm.get(ref.id).h;
1764
2017
  }
2018
+ else if (ref.kind === "markdown") {
2019
+ w = mdm.get(ref.id).w;
2020
+ h = mdm.get(ref.id).h;
2021
+ }
1765
2022
  else {
1766
2023
  w = nm.get(ref.id).w;
1767
2024
  h = nm.get(ref.id).h;
1768
2025
  }
1769
2026
  const x = useColumn ? MARGIN : pos;
1770
2027
  const y = useColumn ? pos : MARGIN;
1771
- if (ref.kind === 'group') {
2028
+ if (ref.kind === "group") {
1772
2029
  gm.get(ref.id).x = x;
1773
2030
  gm.get(ref.id).y = y;
1774
2031
  }
1775
- else if (ref.kind === 'table') {
2032
+ else if (ref.kind === "table") {
1776
2033
  tm.get(ref.id).x = x;
1777
2034
  tm.get(ref.id).y = y;
1778
2035
  }
1779
- else if (ref.kind === 'note') {
2036
+ else if (ref.kind === "note") {
1780
2037
  ntm.get(ref.id).x = x;
1781
2038
  ntm.get(ref.id).y = y;
1782
2039
  }
1783
- else if (ref.kind === 'chart') {
2040
+ else if (ref.kind === "chart") {
1784
2041
  cm.get(ref.id).x = x;
1785
2042
  cm.get(ref.id).y = y;
1786
2043
  }
2044
+ else if (ref.kind === "markdown") {
2045
+ mdm.get(ref.id).x = x;
2046
+ mdm.get(ref.id).y = y;
2047
+ }
1787
2048
  else {
1788
2049
  nm.get(ref.id).x = x;
1789
2050
  nm.get(ref.id).y = y;
@@ -1793,10 +2054,9 @@ var AIDiagram = (function (exports) {
1793
2054
  }
1794
2055
  // 6. Place children within each root group (top-down, recursive)
1795
2056
  for (const g of rootGroups)
1796
- place(g, nm, gm, tm, ntm, cm);
2057
+ place(g, nm, gm, tm, ntm, cm, mdm);
1797
2058
  // 7. Route edges and compute canvas size
1798
2059
  routeEdges(sg);
1799
- console.log('[layout] chart positions:', sg.charts.map(c => `${c.id} x=${c.x} y=${c.y}`));
1800
2060
  computeBounds(sg, MARGIN);
1801
2061
  return sg;
1802
2062
  }
@@ -2525,6 +2785,26 @@ var AIDiagram = (function (exports) {
2525
2785
  loadFont(raw);
2526
2786
  return resolveFont(raw);
2527
2787
  }
2788
+ function wrapText$1(text, maxWidth, fontSize) {
2789
+ const words = text.split(' ');
2790
+ const charsPerPx = fontSize * 0.55; // approximate
2791
+ const maxChars = Math.floor(maxWidth / charsPerPx);
2792
+ const lines = [];
2793
+ let current = '';
2794
+ for (const word of words) {
2795
+ const test = current ? `${current} ${word}` : word;
2796
+ if (test.length > maxChars && current) {
2797
+ lines.push(current);
2798
+ current = word;
2799
+ }
2800
+ else {
2801
+ current = test;
2802
+ }
2803
+ }
2804
+ if (current)
2805
+ lines.push(current);
2806
+ return lines;
2807
+ }
2528
2808
  // ── SVG text helpers ──────────────────────────────────────────────────────
2529
2809
  /**
2530
2810
  * Single-line SVG text element.
@@ -2867,7 +3147,9 @@ var AIDiagram = (function (exports) {
2867
3147
  const gFontSize = Number(gs.fontSize ?? 12);
2868
3148
  const gFont = resolveStyleFont$1(gs, diagramFont);
2869
3149
  const gLetterSpacing = gs.letterSpacing;
2870
- gg.appendChild(mkText(g.label, g.x + 14, g.y + 14, gFontSize, 500, gLabelColor, "start", gFont, gLetterSpacing));
3150
+ if (g.label) {
3151
+ gg.appendChild(mkText(g.label, g.x + 14, g.y + 14, gFontSize, 500, gLabelColor, "start", gFont, gLetterSpacing));
3152
+ }
2871
3153
  GL.appendChild(gg);
2872
3154
  }
2873
3155
  svg.appendChild(GL);
@@ -2960,7 +3242,9 @@ var AIDiagram = (function (exports) {
2960
3242
  : textAlign === "right"
2961
3243
  ? n.x + n.w - 8
2962
3244
  : n.x + n.w / 2;
2963
- const lines = n.label.split("\n");
3245
+ const lines = n.shape === 'text' && !n.label.includes('\n')
3246
+ ? wrapText$1(n.label, n.w - 16, fontSize)
3247
+ : n.label.split('\n');
2964
3248
  const verticalAlign = String(n.style?.verticalAlign ?? "middle");
2965
3249
  const nodeBodyTop = n.y + 6;
2966
3250
  const nodeBodyBottom = n.y + n.h - 6;
@@ -3157,6 +3441,54 @@ var AIDiagram = (function (exports) {
3157
3441
  NoteL.appendChild(ng);
3158
3442
  }
3159
3443
  svg.appendChild(NoteL);
3444
+ markdownMap(sg);
3445
+ const MDL = mkGroup('markdown-layer');
3446
+ for (const m of sg.markdowns) {
3447
+ const mg = mkGroup(`markdown-${m.id}`, 'mdg');
3448
+ const mFont = resolveStyleFont$1(m.style, diagramFont);
3449
+ const baseColor = String(m.style?.color ?? palette.nodeText);
3450
+ const textAlign = String(m.style?.textAlign ?? 'left');
3451
+ const anchor = textAlign === 'right' ? 'end'
3452
+ : textAlign === 'center' ? 'middle'
3453
+ : 'start';
3454
+ const PAD = Number(m.style?.padding ?? 16);
3455
+ const textX = textAlign === 'right' ? m.x + m.w - PAD
3456
+ : textAlign === 'center' ? m.x + m.w / 2
3457
+ : m.x + PAD;
3458
+ let y = m.y + PAD;
3459
+ for (const line of m.lines) {
3460
+ if (line.kind === 'blank') {
3461
+ y += LINE_SPACING.blank;
3462
+ continue;
3463
+ }
3464
+ const fontSize = LINE_FONT_SIZE[line.kind];
3465
+ const fontWeight = LINE_FONT_WEIGHT[line.kind];
3466
+ const t = se('text');
3467
+ t.setAttribute('x', String(textX));
3468
+ t.setAttribute('y', String(y + fontSize / 2));
3469
+ t.setAttribute('text-anchor', anchor);
3470
+ t.setAttribute('dominant-baseline', 'middle');
3471
+ t.setAttribute('font-family', mFont);
3472
+ t.setAttribute('font-size', String(fontSize));
3473
+ t.setAttribute('font-weight', String(fontWeight));
3474
+ t.setAttribute('fill', baseColor);
3475
+ t.setAttribute('pointer-events', 'none');
3476
+ t.setAttribute('user-select', 'none');
3477
+ for (const run of line.runs) {
3478
+ const span = se('tspan');
3479
+ span.textContent = run.text;
3480
+ if (run.bold)
3481
+ span.setAttribute('font-weight', '700');
3482
+ if (run.italic)
3483
+ span.setAttribute('font-style', 'italic');
3484
+ t.appendChild(span);
3485
+ }
3486
+ mg.appendChild(t);
3487
+ y += LINE_SPACING[line.kind];
3488
+ }
3489
+ MDL.appendChild(mg);
3490
+ }
3491
+ svg.appendChild(MDL);
3160
3492
  // ── Charts ────────────────────────────────────────────────
3161
3493
  const CL = mkGroup("chart-layer");
3162
3494
  for (const c of sg.charts) {
@@ -3439,10 +3771,6 @@ var AIDiagram = (function (exports) {
3439
3771
  return resolveFont(raw);
3440
3772
  }
3441
3773
  // ── Canvas text helpers ────────────────────────────────────────────────────
3442
- /**
3443
- * Draw a single line of text.
3444
- * align: 'left' | 'center' | 'right' (maps to ctx.textAlign)
3445
- */
3446
3774
  function drawText(ctx, txt, x, y, sz = 14, wt = 500, col = '#1a1208', align = 'center', font = 'system-ui, sans-serif', letterSpacing) {
3447
3775
  ctx.save();
3448
3776
  ctx.font = `${wt} ${sz}px ${font}`;
@@ -3467,9 +3795,6 @@ var AIDiagram = (function (exports) {
3467
3795
  }
3468
3796
  ctx.restore();
3469
3797
  }
3470
- /**
3471
- * Draw multiple lines of text, vertically centred around cy.
3472
- */
3473
3798
  function drawMultilineText(ctx, lines, x, cy, sz = 14, wt = 500, col = '#1a1208', align = 'center', lineH = 18, font = 'system-ui, sans-serif', letterSpacing) {
3474
3799
  const totalH = (lines.length - 1) * lineH;
3475
3800
  const startY = cy - totalH / 2;
@@ -3477,6 +3802,27 @@ var AIDiagram = (function (exports) {
3477
3802
  drawText(ctx, line, x, startY + i * lineH, sz, wt, col, align, font, letterSpacing);
3478
3803
  });
3479
3804
  }
3805
+ // Soft word-wrap for `text` shape nodes
3806
+ function wrapText(text, maxWidth, fontSize) {
3807
+ const charWidth = fontSize * 0.55;
3808
+ const maxChars = Math.floor(maxWidth / charWidth);
3809
+ const words = text.split(' ');
3810
+ const lines = [];
3811
+ let current = '';
3812
+ for (const word of words) {
3813
+ const test = current ? `${current} ${word}` : word;
3814
+ if (test.length > maxChars && current) {
3815
+ lines.push(current);
3816
+ current = word;
3817
+ }
3818
+ else {
3819
+ current = test;
3820
+ }
3821
+ }
3822
+ if (current)
3823
+ lines.push(current);
3824
+ return lines.length ? lines : [text];
3825
+ }
3480
3826
  // ── Arrow direction ────────────────────────────────────────────────────────
3481
3827
  function connMeta(connector) {
3482
3828
  if (connector === '--')
@@ -3568,7 +3914,7 @@ var AIDiagram = (function (exports) {
3568
3914
  ], opts);
3569
3915
  break;
3570
3916
  case 'text':
3571
- break;
3917
+ break; // no shape drawn
3572
3918
  case 'image': {
3573
3919
  if (n.imageUrl) {
3574
3920
  const img = new Image();
@@ -3658,7 +4004,9 @@ var AIDiagram = (function (exports) {
3658
4004
  // ── Title ────────────────────────────────────────────────
3659
4005
  if (sg.title) {
3660
4006
  const titleSize = Number(sg.config['title-size'] ?? 18);
3661
- drawText(ctx, sg.title, sg.width / 2, 28, titleSize, 600, palette.titleText, 'center', diagramFont);
4007
+ const titleWeight = Number(sg.config['title-weight'] ?? 600);
4008
+ const titleColor = String(sg.config['title-color'] ?? palette.titleText);
4009
+ drawText(ctx, sg.title, sg.width / 2, 28, titleSize, titleWeight, titleColor, 'center', diagramFont);
3662
4010
  }
3663
4011
  // ── Groups (outermost first) ─────────────────────────────
3664
4012
  const sortedGroups = [...sg.groups].sort((a, b) => groupDepth(a, gm) - groupDepth(b, gm));
@@ -3674,13 +4022,16 @@ var AIDiagram = (function (exports) {
3674
4022
  strokeWidth: Number(gs.strokeWidth ?? 1.2),
3675
4023
  strokeLineDash: gs.strokeDash ?? palette.groupDash,
3676
4024
  });
3677
- // ── Group label: font, font-size, letter-spacing ─────
3678
- // always left-anchored (single line)
3679
- const gFontSize = Number(gs.fontSize ?? 12);
3680
- const gFont = resolveStyleFont(gs, diagramFont);
3681
- const gLetterSpacing = gs.letterSpacing;
3682
- const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
3683
- drawText(ctx, g.label, g.x + 14, g.y + 16, gFontSize, 500, gLabelColor, 'left', gFont, gLetterSpacing);
4025
+ // ── Group label ──────────────────────────────────────
4026
+ // Only render when label has content — empty label = no reserved space
4027
+ // supports: font, font-size, letter-spacing (always left-anchored)
4028
+ if (g.label) {
4029
+ const gFontSize = Number(gs.fontSize ?? 12);
4030
+ const gFont = resolveStyleFont(gs, diagramFont);
4031
+ const gLetterSpacing = gs.letterSpacing;
4032
+ const gLabelColor = gs.color ? String(gs.color) : palette.groupLabel;
4033
+ drawText(ctx, g.label, g.x + 14, g.y + 16, gFontSize, 500, gLabelColor, 'left', gFont, gLetterSpacing);
4034
+ }
3684
4035
  }
3685
4036
  // ── Edges ─────────────────────────────────────────────────
3686
4037
  for (const e of sg.edges) {
@@ -3733,7 +4084,8 @@ var AIDiagram = (function (exports) {
3733
4084
  for (const n of sg.nodes) {
3734
4085
  renderShape(rc, ctx, n, palette, R);
3735
4086
  // ── Node / text typography ─────────────────────────
3736
- // supports: font, font-size, letter-spacing, text-align, line-height
4087
+ // supports: font, font-size, letter-spacing, text-align,
4088
+ // vertical-align, line-height, word-wrap (text shape)
3737
4089
  const fontSize = Number(n.style?.fontSize ?? (n.shape === 'text' ? 13 : 14));
3738
4090
  const fontWeight = n.style?.fontWeight ?? (n.shape === 'text' ? 400 : 500);
3739
4091
  const textColor = String(n.style?.color ??
@@ -3742,15 +4094,28 @@ var AIDiagram = (function (exports) {
3742
4094
  const textAlign = String(n.style?.textAlign ?? 'center');
3743
4095
  const lineHeight = Number(n.style?.lineHeight ?? 1.3) * fontSize;
3744
4096
  const letterSpacing = n.style?.letterSpacing;
4097
+ const vertAlign = String(n.style?.verticalAlign ?? 'middle');
4098
+ // x shifts for left/right alignment
3745
4099
  const textX = textAlign === 'left' ? n.x + 8
3746
4100
  : textAlign === 'right' ? n.x + n.w - 8
3747
4101
  : n.x + n.w / 2;
3748
- const lines = n.label.split('\n');
4102
+ // word-wrap for text shape; explicit \n for all others
4103
+ const rawLines = n.label.split('\n');
4104
+ const lines = n.shape === 'text' && rawLines.length === 1
4105
+ ? wrapText(n.label, n.w - 16, fontSize)
4106
+ : rawLines;
4107
+ // vertical-align: compute textCY from top/middle/bottom
4108
+ const nodeBodyTop = n.y + 6;
4109
+ const nodeBodyBottom = n.y + n.h - 6;
4110
+ const blockH = (lines.length - 1) * lineHeight;
4111
+ const textCY = vertAlign === 'top' ? nodeBodyTop + blockH / 2
4112
+ : vertAlign === 'bottom' ? nodeBodyBottom - blockH / 2
4113
+ : n.y + n.h / 2; // middle (default)
3749
4114
  if (lines.length > 1) {
3750
- drawMultilineText(ctx, lines, textX, n.y + n.h / 2, fontSize, fontWeight, textColor, textAlign, lineHeight, nodeFont, letterSpacing);
4115
+ drawMultilineText(ctx, lines, textX, textCY, fontSize, fontWeight, textColor, textAlign, lineHeight, nodeFont, letterSpacing);
3751
4116
  }
3752
4117
  else {
3753
- drawText(ctx, n.label, textX, n.y + n.h / 2, fontSize, fontWeight, textColor, textAlign, nodeFont, letterSpacing);
4118
+ drawText(ctx, lines[0] ?? '', textX, textCY, fontSize, fontWeight, textColor, textAlign, nodeFont, letterSpacing);
3754
4119
  }
3755
4120
  }
3756
4121
  // ── Tables ────────────────────────────────────────────────
@@ -3761,7 +4126,8 @@ var AIDiagram = (function (exports) {
3761
4126
  const textCol = String(gs.color ?? palette.tableText);
3762
4127
  const pad = t.labelH;
3763
4128
  // ── Table-level font ────────────────────────────────
3764
- // supports: font, font-size, letter-spacing (cells also support text-align)
4129
+ // supports: font, font-size, letter-spacing
4130
+ // cells also support text-align
3765
4131
  const tFontSize = Number(gs.fontSize ?? 12);
3766
4132
  const tFont = resolveStyleFont(gs, diagramFont);
3767
4133
  const tLetterSpacing = gs.letterSpacing;
@@ -3772,8 +4138,7 @@ var AIDiagram = (function (exports) {
3772
4138
  rc.line(t.x, t.y + pad, t.x + t.w, t.y + pad, {
3773
4139
  roughness: 0.6, seed: hashStr$1(t.id + 'l'), stroke: strk, strokeWidth: 1,
3774
4140
  });
3775
- // ── Table label: font, font-size, letter-spacing ────
3776
- // always left-anchored
4141
+ // ── Table label: always left-anchored ───────────────
3777
4142
  drawText(ctx, t.label, t.x + 10, t.y + pad / 2, tFontSize, 500, textCol, 'left', tFont, tLetterSpacing);
3778
4143
  let rowY = t.y + pad;
3779
4144
  for (const row of t.rows) {
@@ -3787,7 +4152,7 @@ var AIDiagram = (function (exports) {
3787
4152
  stroke: row.kind === 'header' ? strk : palette.tableDivider,
3788
4153
  strokeWidth: row.kind === 'header' ? 1.2 : 0.6,
3789
4154
  });
3790
- // ── Cell text: font, font-size, letter-spacing, text-align ──
4155
+ // ── Cell text ───────────────────────────────────
3791
4156
  // header always centered; data rows respect gs.textAlign
3792
4157
  const cellAlignProp = (row.kind === 'header'
3793
4158
  ? 'center'
@@ -3835,22 +4200,91 @@ var AIDiagram = (function (exports) {
3835
4200
  ], { roughness: 0.4, seed: hashStr$1(n.id + 'f'),
3836
4201
  fill: palette.noteFold, fillStyle: 'solid', stroke: strk, strokeWidth: 0.8 });
3837
4202
  // ── Note typography ─────────────────────────────────
3838
- // supports: font, font-size, letter-spacing, text-align, line-height
4203
+ // supports: font, font-size, letter-spacing, text-align,
4204
+ // vertical-align, line-height
3839
4205
  const nFontSize = Number(gs.fontSize ?? 12);
3840
4206
  const nFont = resolveStyleFont(gs, diagramFont);
3841
4207
  const nLetterSpacing = gs.letterSpacing;
3842
4208
  const nLineHeight = Number(gs.lineHeight ?? 1.4) * nFontSize;
3843
4209
  const nTextAlign = String(gs.textAlign ?? 'left');
4210
+ const nVertAlign = String(gs.verticalAlign ?? 'top');
3844
4211
  const nColor = String(gs.color ?? palette.noteText);
3845
4212
  const nTextX = nTextAlign === 'right' ? x + w - fold - 6
3846
4213
  : nTextAlign === 'center' ? x + (w - fold) / 2
3847
4214
  : x + 12;
4215
+ // vertical-align inside note body (below fold)
4216
+ const bodyTop = y + fold + 8;
4217
+ const bodyBottom = y + h - 8;
4218
+ const blockH = (n.lines.length - 1) * nLineHeight;
4219
+ const blockCY = nVertAlign === 'bottom' ? bodyBottom - blockH / 2
4220
+ : nVertAlign === 'middle' ? (bodyTop + bodyBottom) / 2
4221
+ : bodyTop + blockH / 2; // top (default)
3848
4222
  if (n.lines.length > 1) {
3849
- const blockCY = y + fold / 2 + (h - fold) / 2;
3850
4223
  drawMultilineText(ctx, n.lines, nTextX, blockCY, nFontSize, 400, nColor, nTextAlign, nLineHeight, nFont, nLetterSpacing);
3851
4224
  }
3852
4225
  else {
3853
- drawText(ctx, n.lines[0] ?? '', nTextX, y + h / 2, nFontSize, 400, nColor, nTextAlign, nFont, nLetterSpacing);
4226
+ drawText(ctx, n.lines[0] ?? '', nTextX, blockCY, nFontSize, 400, nColor, nTextAlign, nFont, nLetterSpacing);
4227
+ }
4228
+ }
4229
+ // ── Markdown blocks ────────────────────────────────────────
4230
+ // Renders prose with Markdown headings and bold/italic inline spans.
4231
+ // Canvas has no native bold-within-a-run, so each run is drawn
4232
+ // individually with its own ctx.font setting.
4233
+ for (const m of (sg.markdowns ?? [])) {
4234
+ const mFont = resolveStyleFont(m.style, diagramFont);
4235
+ const baseColor = String(m.style?.color ?? palette.nodeText);
4236
+ const textAlign = String(m.style?.textAlign ?? 'left');
4237
+ const PAD = Number(m.style?.padding ?? 16);
4238
+ const anchorX = textAlign === 'right' ? m.x + m.w - PAD
4239
+ : textAlign === 'center' ? m.x + m.w / 2
4240
+ : m.x + PAD;
4241
+ let y = m.y + PAD;
4242
+ for (const line of m.lines) {
4243
+ if (line.kind === 'blank') {
4244
+ y += LINE_SPACING.blank;
4245
+ continue;
4246
+ }
4247
+ const fontSize = LINE_FONT_SIZE[line.kind];
4248
+ const fontWeight = LINE_FONT_WEIGHT[line.kind];
4249
+ const lineY = y + fontSize / 2;
4250
+ // Measure total run width for left-offset when runs mix bold/italic
4251
+ // Simple: draw each run consecutively from a computed start x
4252
+ ctx.save();
4253
+ ctx.textBaseline = 'middle';
4254
+ ctx.fillStyle = baseColor;
4255
+ if (textAlign === 'center' || textAlign === 'right') {
4256
+ // Measure full line width first
4257
+ let totalW = 0;
4258
+ for (const run of line.runs) {
4259
+ const runStyle = run.italic ? 'italic ' : '';
4260
+ const runWeight = run.bold ? 700 : fontWeight;
4261
+ ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
4262
+ totalW += ctx.measureText(run.text).width;
4263
+ }
4264
+ let runX = textAlign === 'center' ? anchorX - totalW / 2 : anchorX - totalW;
4265
+ ctx.textAlign = 'left';
4266
+ for (const run of line.runs) {
4267
+ const runStyle = run.italic ? 'italic ' : '';
4268
+ const runWeight = run.bold ? 700 : fontWeight;
4269
+ ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
4270
+ ctx.fillText(run.text, runX, lineY);
4271
+ runX += ctx.measureText(run.text).width;
4272
+ }
4273
+ }
4274
+ else {
4275
+ // left-aligned — draw runs left to right from anchorX
4276
+ let runX = anchorX;
4277
+ ctx.textAlign = 'left';
4278
+ for (const run of line.runs) {
4279
+ const runStyle = run.italic ? 'italic ' : '';
4280
+ const runWeight = run.bold ? 700 : fontWeight;
4281
+ ctx.font = `${runStyle}${runWeight} ${fontSize}px ${mFont}`;
4282
+ ctx.fillText(run.text, runX, lineY);
4283
+ runX += ctx.measureText(run.text).width;
4284
+ }
4285
+ }
4286
+ ctx.restore();
4287
+ y += LINE_SPACING[line.kind];
3854
4288
  }
3855
4289
  }
3856
4290
  // ── Charts ────────────────────────────────────────────────
@@ -4810,6 +5244,89 @@ var AIDiagram = (function (exports) {
4810
5244
  }
4811
5245
  }
4812
5246
 
5247
+ // ============================================================
5248
+ // sketchmark — Encrypted sharing
5249
+ // Diagram DSL is encrypted in the browser.
5250
+ // The server stores an opaque blob it cannot read.
5251
+ // The decryption key lives only in the URL fragment (#key=...).
5252
+ // ============================================================
5253
+ const WORKER_URL = 'https://sketchmark.anmism.workers.dev';
5254
+ // ── Crypto helpers ────────────────────────────────────────
5255
+ async function generateKey() {
5256
+ return crypto.subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, // extractable so we can export to URL
5257
+ ['encrypt', 'decrypt']);
5258
+ }
5259
+ async function keyToBase64(key) {
5260
+ const raw = await crypto.subtle.exportKey('raw', key);
5261
+ return btoa(String.fromCharCode(...new Uint8Array(raw)));
5262
+ }
5263
+ async function base64ToKey(b64) {
5264
+ const raw = Uint8Array.from(atob(b64), c => c.charCodeAt(0));
5265
+ return crypto.subtle.importKey('raw', raw, { name: 'AES-GCM' }, false, // not extractable on the receiving end
5266
+ ['decrypt']);
5267
+ }
5268
+ // ── Encrypt ───────────────────────────────────────────────
5269
+ async function encryptDSL(dsl, key) {
5270
+ const iv = crypto.getRandomValues(new Uint8Array(12));
5271
+ const encoded = new TextEncoder().encode(dsl);
5272
+ const encrypted = await crypto.subtle.encrypt({ name: 'AES-GCM', iv }, key, encoded);
5273
+ // prepend iv to the blob: [ iv (12 bytes) | ciphertext ]
5274
+ const result = new Uint8Array(12 + encrypted.byteLength);
5275
+ result.set(iv, 0);
5276
+ result.set(new Uint8Array(encrypted), 12);
5277
+ return result;
5278
+ }
5279
+ // ── Decrypt ───────────────────────────────────────────────
5280
+ async function decryptBlob(blob, key) {
5281
+ const iv = blob.slice(0, 12);
5282
+ const ciphertext = blob.slice(12);
5283
+ const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv }, key, ciphertext);
5284
+ return new TextDecoder().decode(decrypted);
5285
+ }
5286
+ // ── Public API ────────────────────────────────────────────
5287
+ /**
5288
+ * Encrypt DSL, upload to worker, return shareable URL.
5289
+ * The URL fragment (#key=...) never reaches the server.
5290
+ */
5291
+ async function shareDiagram(dsl) {
5292
+ const key = await generateKey();
5293
+ const blob = await encryptDSL(dsl, key);
5294
+ const keyB64 = await keyToBase64(key);
5295
+ const res = await fetch(`${WORKER_URL}/api/blob`, {
5296
+ method: 'POST',
5297
+ headers: { 'Content-Type': 'application/octet-stream' },
5298
+ body: blob.buffer,
5299
+ });
5300
+ if (!res.ok)
5301
+ throw new Error(`Upload failed: ${res.status}`);
5302
+ const { id } = await res.json();
5303
+ // key goes into the fragment — browser never sends this to any server
5304
+ return `${window.location.origin}/playground.html?s=${id}#key=${keyB64}`;
5305
+ }
5306
+ /**
5307
+ * Read ?s= and #key= from the current URL, fetch + decrypt the diagram.
5308
+ * Returns null if no share params found.
5309
+ */
5310
+ async function loadSharedDiagram() {
5311
+ const params = new URLSearchParams(window.location.search);
5312
+ const id = params.get('s');
5313
+ if (!id)
5314
+ return null;
5315
+ // key is in the fragment — parse manually, not via URLSearchParams
5316
+ // (URLSearchParams on hash strips the #)
5317
+ const fragment = window.location.hash.slice(1);
5318
+ const keyMatch = fragment.match(/key=([^&]+)/);
5319
+ if (!keyMatch)
5320
+ return null;
5321
+ const keyB64 = keyMatch[1];
5322
+ const res = await fetch(`${WORKER_URL}/api/blob/${id}`);
5323
+ if (!res.ok)
5324
+ throw new Error('Diagram not found or expired');
5325
+ const blob = await res.arrayBuffer();
5326
+ const key = await base64ToKey(keyB64);
5327
+ return decryptBlob(blob, key);
5328
+ }
5329
+
4813
5330
  // ============================================================
4814
5331
  // sketchmark — Public API
4815
5332
  // ============================================================
@@ -4901,6 +5418,8 @@ var AIDiagram = (function (exports) {
4901
5418
  exports.lerp = lerp;
4902
5419
  exports.listThemes = listThemes;
4903
5420
  exports.loadFont = loadFont;
5421
+ exports.loadSharedDiagram = loadSharedDiagram;
5422
+ exports.markdownMap = markdownMap;
4904
5423
  exports.nodeMap = nodeMap;
4905
5424
  exports.parse = parse;
4906
5425
  exports.parseHex = parseHex;
@@ -4910,6 +5429,7 @@ var AIDiagram = (function (exports) {
4910
5429
  exports.renderToSVG = renderToSVG;
4911
5430
  exports.resolveFont = resolveFont;
4912
5431
  exports.resolvePalette = resolvePalette;
5432
+ exports.shareDiagram = shareDiagram;
4913
5433
  exports.sleep = sleep;
4914
5434
  exports.svgToPNGDataURL = svgToPNGDataURL;
4915
5435
  exports.svgToString = svgToString;