@the-package-labs/idocs-core 0.3.1 → 0.5.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/index.js CHANGED
@@ -1,6 +1,12 @@
1
+ import {
2
+ attachNested,
3
+ linkDiagrams,
4
+ nestedDiagrams
5
+ } from "./chunk-P2X2XSBM.js";
1
6
  import {
2
7
  BUILTIN_EDGE_KINDS,
3
8
  BUILTIN_KINDS,
9
+ DEFAULT_ENTITY_WIDTH,
4
10
  DEFAULT_NODE_WIDTH,
5
11
  STEP_KIND_COLORS,
6
12
  dist,
@@ -8,7 +14,7 @@ import {
8
14
  pointAt,
9
15
  polylineLength,
10
16
  roundedPath
11
- } from "./chunk-FRVCGXIG.js";
17
+ } from "./chunk-ZYFFV7Y7.js";
12
18
 
13
19
  // src/layout.ts
14
20
  import ELK from "elkjs/lib/elk.bundled.js";
@@ -951,6 +957,312 @@ function overlap(a, b, pad) {
951
957
  return a.x < b.x + b.w + pad && a.x + a.w + pad > b.x && a.y < b.y + b.h + pad && a.y + a.h + pad > b.y;
952
958
  }
953
959
 
960
+ // src/nested-layout.ts
961
+ var INLINE_PAD = { top: 40, left: 4, right: 4, bottom: 4 };
962
+ function openingView(diagram, ref) {
963
+ const has = (id) => diagram.views.some((v) => v.id === id);
964
+ const of = (scenario) => diagram.scenarios.find((s) => s.id === scenario)?.view;
965
+ return [ref.view, of(ref.scenario), of(diagram.defaults?.scenario), diagram.defaults?.view].find(
966
+ has
967
+ ) ?? diagram.views[0].id;
968
+ }
969
+ var canExpand = (ref, diagrams) => !!ref && ref.mode !== "open" && !!diagrams[ref.name];
970
+ var canOpen = (ref, diagrams) => !!ref && ref.mode !== "expand" && !!diagrams[ref.name];
971
+ var pathJoin = (prefix, id) => `${prefix}${id}`;
972
+ function defaultExpanded(diagram, diagrams, prefix = "", seen = []) {
973
+ const out = [];
974
+ for (const node of Object.values(diagram.nodes)) {
975
+ const ref = node.diagram;
976
+ const child = ref && diagrams[ref.name];
977
+ if (!ref || !child || seen.includes(ref.name) || !ref.expanded || !canExpand(ref, diagrams))
978
+ continue;
979
+ const path = pathJoin(prefix, node.id);
980
+ out.push(path, ...defaultExpanded(child, diagrams, `${path}/`, [...seen, diagram.name]));
981
+ }
982
+ return out;
983
+ }
984
+ function measurable(diagram, viewId, expanded, diagrams, prefix = "") {
985
+ const view = diagram.views.find((v) => v.id === viewId);
986
+ if (!view) return [];
987
+ const out = [];
988
+ for (const id of view.nodeIds) {
989
+ const node = diagram.nodes[id];
990
+ const path = pathJoin(prefix, id);
991
+ out.push({ key: path, node });
992
+ const ref = node.diagram;
993
+ const child = ref && expanded.has(path) && canExpand(ref, diagrams) ? diagrams[ref.name] : void 0;
994
+ if (ref && child) {
995
+ out.push(...measurable(child, openingView(child, ref), expanded, diagrams, `${path}/`));
996
+ }
997
+ }
998
+ return out;
999
+ }
1000
+ async function layoutNested(diagram, viewId, { sizes = {}, expanded, diagrams = {}, prefix = "" } = {}) {
1001
+ const view = diagram.views.find((v) => v.id === viewId);
1002
+ if (!view) throw new Error(`Unknown view "${viewId}"`);
1003
+ const own = {};
1004
+ for (const id of view.nodeIds) {
1005
+ const size = sizes[pathJoin(prefix, id)];
1006
+ if (size) own[id] = size;
1007
+ }
1008
+ const inner = {};
1009
+ await Promise.all(
1010
+ view.nodeIds.map(async (id) => {
1011
+ const ref = diagram.nodes[id].diagram;
1012
+ const path = pathJoin(prefix, id);
1013
+ const child = ref && expanded?.has(path) && canExpand(ref, diagrams) ? diagrams[ref.name] : void 0;
1014
+ if (!ref || !child) return;
1015
+ const childView = openingView(child, ref);
1016
+ const layout = await layoutNested(child, childView, {
1017
+ sizes,
1018
+ expanded,
1019
+ diagrams,
1020
+ prefix: `${path}/`
1021
+ });
1022
+ inner[id] = {
1023
+ name: ref.name,
1024
+ viewId: childView,
1025
+ offset: { x: INLINE_PAD.left, y: INLINE_PAD.top },
1026
+ layout
1027
+ };
1028
+ own[id] = {
1029
+ w: layout.layout.width + INLINE_PAD.left + INLINE_PAD.right,
1030
+ h: layout.layout.height + INLINE_PAD.top + INLINE_PAD.bottom
1031
+ };
1032
+ })
1033
+ );
1034
+ return { layout: await layoutView(diagram, viewId, own), inner };
1035
+ }
1036
+ function rectOfPath(nested, path) {
1037
+ let level = nested;
1038
+ let at = { x: 0, y: 0 };
1039
+ const ids = path.split("/");
1040
+ for (const [i, id] of ids.entries()) {
1041
+ const r = level?.layout.nodes[id];
1042
+ if (!level || !r) return void 0;
1043
+ if (i === ids.length - 1) return { x: at.x + r.x, y: at.y + r.y, w: r.w, h: r.h };
1044
+ const inner = level.inner[id];
1045
+ if (!inner) return void 0;
1046
+ at = { x: at.x + r.x + inner.offset.x, y: at.y + r.y + inner.offset.y };
1047
+ level = inner.layout;
1048
+ }
1049
+ return void 0;
1050
+ }
1051
+
1052
+ // src/outline.ts
1053
+ function buildOutline(diagram, view, { diagrams = {}, expanded = /* @__PURE__ */ new Set(), prefix = "", seen = [] } = {}) {
1054
+ const inView = new Set(view.nodeIds);
1055
+ const groups = new Set(view.groupIds);
1056
+ const drawn = view.edgeIds.map((id) => diagram.edges[id]).filter((e) => !e.hidden);
1057
+ const connectionsOf = (id) => {
1058
+ const out = [];
1059
+ for (const e of drawn) {
1060
+ const sends = e.from === id;
1061
+ const gets = e.to === id;
1062
+ if (!sends && !gets) continue;
1063
+ const other = sends ? e.to : e.from;
1064
+ out.push({
1065
+ edge: e.id,
1066
+ dir: e.both ? "both" : sends ? "out" : "in",
1067
+ other,
1068
+ otherTitle: diagram.nodes[other]?.title ?? other,
1069
+ label: e.label,
1070
+ kind: e.kind,
1071
+ status: e.status
1072
+ });
1073
+ }
1074
+ const rank = { out: 0, both: 1, in: 2 };
1075
+ return out.sort((a, b) => rank[a.dir] - rank[b.dir]);
1076
+ };
1077
+ const nodeItem = (id) => {
1078
+ const node = diagram.nodes[id];
1079
+ const path = pathJoin(prefix, id);
1080
+ const item = {
1081
+ type: "node",
1082
+ path,
1083
+ id,
1084
+ title: node.title,
1085
+ sub: node.sub,
1086
+ icon: node.icon,
1087
+ status: node.status,
1088
+ connections: connectionsOf(id)
1089
+ };
1090
+ const ref = node.diagram;
1091
+ const child = ref && diagrams[ref.name];
1092
+ if (ref && child && !seen.includes(ref.name) && (canExpand(ref, diagrams) || canOpen(ref, diagrams))) {
1093
+ const shown = canExpand(ref, diagrams) && expanded.has(path);
1094
+ const childView = child.views.find((v) => v.id === openingView(child, ref));
1095
+ item.inside = {
1096
+ title: child.title,
1097
+ expand: canExpand(ref, diagrams),
1098
+ open: canOpen(ref, diagrams),
1099
+ expanded: shown,
1100
+ items: shown && childView ? buildOutline(child, childView, {
1101
+ diagrams,
1102
+ expanded,
1103
+ prefix: `${path}/`,
1104
+ seen: [...seen, diagram.name]
1105
+ }) : []
1106
+ };
1107
+ }
1108
+ return item;
1109
+ };
1110
+ const childGroups = /* @__PURE__ */ new Map();
1111
+ for (const gid of view.groupIds) {
1112
+ const parent = diagram.groups[gid]?.parent;
1113
+ const key = parent && groups.has(parent) ? parent : "";
1114
+ childGroups.set(key, [...childGroups.get(key) ?? [], gid]);
1115
+ }
1116
+ const childNodes = /* @__PURE__ */ new Map();
1117
+ for (const id of view.nodeIds) {
1118
+ if (!inView.has(id)) continue;
1119
+ const group = diagram.nodes[id].group;
1120
+ const key = group && groups.has(group) ? group : "";
1121
+ childNodes.set(key, [...childNodes.get(key) ?? [], id]);
1122
+ }
1123
+ const position = new Map(view.nodeIds.map((id, i) => [id, i]));
1124
+ const firstNode = (gid) => Math.min(
1125
+ ...(childNodes.get(gid) ?? []).map((n) => position.get(n)),
1126
+ ...(childGroups.get(gid) ?? []).map(firstNode)
1127
+ );
1128
+ const groupItem = (gid) => {
1129
+ const g = diagram.groups[gid];
1130
+ const items = fill(gid);
1131
+ return {
1132
+ type: "group",
1133
+ path: pathJoin(prefix, `#${gid}`),
1134
+ id: gid,
1135
+ title: g.label,
1136
+ caption: g.caption,
1137
+ icon: g.icon,
1138
+ status: g.status,
1139
+ items,
1140
+ count: items.reduce((n, i) => n + (i.type === "group" ? i.count : 1), 0)
1141
+ };
1142
+ };
1143
+ function fill(key) {
1144
+ const ranked = [
1145
+ ...(childGroups.get(key) ?? []).map((gid) => ({
1146
+ at: firstNode(gid),
1147
+ make: () => groupItem(gid)
1148
+ })),
1149
+ ...(childNodes.get(key) ?? []).map((id) => ({
1150
+ at: position.get(id),
1151
+ make: () => nodeItem(id)
1152
+ }))
1153
+ ];
1154
+ return ranked.sort((a, b) => a.at - b.at).map((r) => r.make());
1155
+ }
1156
+ return fill("");
1157
+ }
1158
+ function outlineNodes(items) {
1159
+ const out = [];
1160
+ for (const item of items) {
1161
+ if (item.type === "group") out.push(...outlineNodes(item.items));
1162
+ else out.push(item, ...outlineNodes(item.inside?.items ?? []));
1163
+ }
1164
+ return out;
1165
+ }
1166
+
1167
+ // src/phases.ts
1168
+ var hasPhases = (scenario) => scenario.phases.some((p) => p.title);
1169
+ function phaseSpans(scenario) {
1170
+ const out = [];
1171
+ scenario.steps.forEach((step, i) => {
1172
+ const last = out[out.length - 1];
1173
+ if (last && last.index === step.phase) {
1174
+ last.to = i;
1175
+ last.count++;
1176
+ return;
1177
+ }
1178
+ const phase = scenario.phases[step.phase];
1179
+ out.push({
1180
+ index: step.phase,
1181
+ title: phase?.title ?? "",
1182
+ caption: phase?.caption,
1183
+ from: i,
1184
+ to: i,
1185
+ count: 1
1186
+ });
1187
+ });
1188
+ return out;
1189
+ }
1190
+
1191
+ // src/scrubber.ts
1192
+ function buildScrubber(tl) {
1193
+ const steps = tl.scenario.steps;
1194
+ const slots = [];
1195
+ let i = 0;
1196
+ while (i < steps.length) {
1197
+ let j = i + 1;
1198
+ const par = steps[i].par;
1199
+ if (par !== void 0) while (j < steps.length && steps[j].par === par) j++;
1200
+ const run = tl.steps.slice(i, j);
1201
+ slots.push({ from: i, to: j - 1, t0: Math.min(...run.map((s) => s.start)), t1: 0 });
1202
+ i = j;
1203
+ }
1204
+ slots.forEach((slot, k) => {
1205
+ slot.t1 = slots[k + 1]?.t0 ?? tl.duration;
1206
+ });
1207
+ if (slots[0]) slots[0].t0 = 0;
1208
+ const slotOf = (step) => slots.findIndex((s) => s.from <= step && step <= s.to);
1209
+ const sections = phaseSpans(tl.scenario).map((span) => ({
1210
+ phase: span.index,
1211
+ title: span.title,
1212
+ caption: span.caption,
1213
+ from: slotOf(span.from),
1214
+ to: slotOf(span.to)
1215
+ }));
1216
+ return { slots, sections, duration: tl.duration };
1217
+ }
1218
+ var clamp01 = (n) => Math.min(1, Math.max(0, n));
1219
+ function positionAt(s, t) {
1220
+ const n = s.slots.length;
1221
+ if (!n) return 0;
1222
+ let k = 0;
1223
+ s.slots.forEach((slot2, i) => {
1224
+ if (slot2.t0 <= t) k = i;
1225
+ });
1226
+ const slot = s.slots[k];
1227
+ const length = slot.t1 - slot.t0;
1228
+ return (k + (length > 0 ? clamp01((t - slot.t0) / length) : 0)) / n;
1229
+ }
1230
+ function timeAt(s, position) {
1231
+ const n = s.slots.length;
1232
+ if (!n) return 0;
1233
+ const x = clamp01(position) * n;
1234
+ const k = Math.min(n - 1, Math.floor(x));
1235
+ const slot = s.slots[k];
1236
+ return slot.t0 + (x - k) * (slot.t1 - slot.t0);
1237
+ }
1238
+ function slotAt(s, position) {
1239
+ const n = s.slots.length;
1240
+ return n ? Math.min(n - 1, Math.floor(clamp01(position) * n)) : -1;
1241
+ }
1242
+ function slotOfStep(s, index) {
1243
+ return s.slots.findIndex((slot) => slot.from <= index && index <= slot.to);
1244
+ }
1245
+
1246
+ // src/start.ts
1247
+ var defaultMode = (diagram) => diagram.defaults?.mode ?? "flow";
1248
+ function startOf(diagram, want = {}) {
1249
+ const d = diagram.defaults ?? {};
1250
+ const scenarioById = (id) => diagram.scenarios.find((s) => s.id === id);
1251
+ const hasView = (id) => diagram.views.some((v) => v.id === id);
1252
+ const first = diagram.views[0].id;
1253
+ let view;
1254
+ let scenario;
1255
+ if (want.view !== void 0 || want.scenario !== void 0) {
1256
+ view = [want.view, scenarioById(want.scenario)?.view].find(hasView) ?? first;
1257
+ scenario = scenarioById(want.scenario)?.id ?? (scenarioById(d.scenario)?.view === view ? d.scenario ?? null : null);
1258
+ } else {
1259
+ scenario = scenarioById(d.scenario)?.id ?? null;
1260
+ view = [scenarioById(scenario ?? void 0)?.view, d.view].find(hasView) ?? first;
1261
+ }
1262
+ const mode = want.mode ?? scenarioById(scenario ?? void 0)?.mode ?? defaultMode(diagram);
1263
+ return { view, scenario, mode };
1264
+ }
1265
+
954
1266
  // src/story.ts
955
1267
  function buildStory(diagram, scenario) {
956
1268
  const steps = scenario.steps;
@@ -1249,23 +1561,47 @@ function runBounds(tl, index) {
1249
1561
  export {
1250
1562
  BUILTIN_EDGE_KINDS,
1251
1563
  BUILTIN_KINDS,
1564
+ DEFAULT_ENTITY_WIDTH,
1252
1565
  DEFAULT_NODE_WIDTH,
1566
+ INLINE_PAD,
1253
1567
  STEP_KIND_COLORS,
1568
+ attachNested,
1569
+ buildOutline,
1570
+ buildScrubber,
1254
1571
  buildStory,
1255
1572
  buildTimeline,
1573
+ canExpand,
1574
+ canOpen,
1575
+ defaultExpanded,
1576
+ defaultMode,
1256
1577
  dist,
1257
1578
  frameAt,
1579
+ hasPhases,
1258
1580
  labelWidth,
1581
+ layoutNested,
1259
1582
  layoutView,
1260
1583
  letterKey,
1584
+ linkDiagrams,
1585
+ measurable,
1586
+ nestedDiagrams,
1587
+ openingView,
1588
+ outlineNodes,
1261
1589
  packGrid,
1590
+ pathJoin,
1591
+ phaseSpans,
1262
1592
  pointAt,
1263
1593
  polylineLength,
1594
+ positionAt,
1595
+ rectOfPath,
1264
1596
  roundedPath,
1265
1597
  routeEdges,
1266
1598
  runBounds,
1267
1599
  scopeSvgIds,
1600
+ slotAt,
1601
+ slotOfStep,
1602
+ startOf,
1268
1603
  stepAt,
1269
1604
  stepEnd,
1270
- stepStart
1605
+ stepStart,
1606
+ timeAt
1271
1607
  };
@@ -0,0 +1,69 @@
1
+ import { type NodeSize } from './layout';
2
+ import type { CompiledDiagram, CompiledNode, Layout, NodeDiagram, Pt, Rect } from './types';
3
+ /**
4
+ * What an expanded node adds around the inner diagram: the header that names it, and a margin.
5
+ * The inner layout already keeps a margin of its own, so the sides stay thin.
6
+ */
7
+ export declare const INLINE_PAD: {
8
+ top: number;
9
+ left: number;
10
+ right: number;
11
+ bottom: number;
12
+ };
13
+ /** A diagram laid out together with the diagrams expanded inside it, at any depth. */
14
+ export interface NestedLayout {
15
+ layout: Layout;
16
+ /** By node id: the layout of the diagram that node shows in place. */
17
+ inner: Record<string, InnerLayout>;
18
+ }
19
+ export interface InnerLayout {
20
+ /** Name of the diagram shown. */
21
+ name: string;
22
+ viewId: string;
23
+ /** Where the inner diagram's origin sits inside the expanded node's rectangle. */
24
+ offset: Pt;
25
+ layout: NestedLayout;
26
+ }
27
+ export type Diagrams = Record<string, CompiledDiagram | undefined>;
28
+ /**
29
+ * The view a node's diagram shows when opened: the one the node names, its scenario's, the one the
30
+ * diagram opens on by itself, or the first.
31
+ */
32
+ export declare function openingView(diagram: CompiledDiagram, ref: NodeDiagram): string;
33
+ /** Whether a reader may show this node's diagram in place: it is at hand and not limited to `open`. */
34
+ export declare const canExpand: (ref: NodeDiagram | undefined, diagrams: Diagrams) => boolean;
35
+ /** Whether a reader may move into this node's diagram: it is at hand and not limited to `expand`. */
36
+ export declare const canOpen: (ref: NodeDiagram | undefined, diagrams: Diagrams) => boolean;
37
+ /** Paths are node ids joined with `/` (ids cannot contain it): `orders`, `orders/stock`. */
38
+ export declare const pathJoin: (prefix: string, id: string) => string;
39
+ /**
40
+ * The paths the author asked to start expanded (`expanded: true`), at every depth. A node counts
41
+ * wherever it sits in its diagram, in whichever view, so switching views loses nothing.
42
+ */
43
+ export declare function defaultExpanded(diagram: CompiledDiagram, diagrams: Diagrams, prefix?: string, seen?: readonly string[]): string[];
44
+ /**
45
+ * Every node whose card has to be measured to lay `viewId` out with these paths expanded: the
46
+ * view's own, and those of each diagram shown in place. `key` is the path, which is what the
47
+ * layout reads sizes by.
48
+ */
49
+ export declare function measurable(diagram: CompiledDiagram, viewId: string, expanded: ReadonlySet<string>, diagrams: Diagrams, prefix?: string): {
50
+ key: string;
51
+ node: CompiledNode;
52
+ }[];
53
+ export interface NestedOptions {
54
+ /** Measured card sizes by path. A plain node id for the diagram itself, `orders/api` inside. */
55
+ sizes?: Record<string, Partial<NodeSize>>;
56
+ /** Paths shown in place. */
57
+ expanded?: ReadonlySet<string>;
58
+ /** Where the diagrams a node opens are found, by name. */
59
+ diagrams?: Diagrams;
60
+ prefix?: string;
61
+ }
62
+ /** Lay out `viewId` with the nodes in `expanded` shown in place, each as a group around its diagram. */
63
+ export declare function layoutNested(diagram: CompiledDiagram, viewId: string, { sizes, expanded, diagrams, prefix }?: NestedOptions): Promise<NestedLayout>;
64
+ /**
65
+ * Where a node sits in the world of the diagram laid out first, however deep it is: the rectangle
66
+ * of `orders/stock` is `stock`'s inside `orders`, moved to where `orders` is. Undefined when a
67
+ * step of the path is not laid out (it is not expanded, or not in the view).
68
+ */
69
+ export declare function rectOfPath(nested: NestedLayout, path: string): Rect | undefined;
@@ -0,0 +1,24 @@
1
+ import type { Diagnostic, Where } from './compiler';
2
+ import type { CompiledDiagram } from './types';
3
+ /** What the linker needs to know about one diagram folder. `diagram` is missing when it failed to compile. */
4
+ export interface LinkEntry {
5
+ diagram?: CompiledDiagram;
6
+ where?: Where;
7
+ }
8
+ /**
9
+ * Every diagram that `name` can reach by opening nodes, directly or through other diagrams, but
10
+ * not `name` itself.
11
+ */
12
+ export declare function nestedDiagrams(all: Record<string, CompiledDiagram>, name: string): Record<string, CompiledDiagram>;
13
+ /**
14
+ * Give every diagram that opens others the diagrams it can reach, as `nested`, so wherever a
15
+ * compiled diagram goes its insides go with it and the viewer is told nothing. Diagrams that open
16
+ * nothing are returned as they are. Safe to call twice.
17
+ */
18
+ export declare function attachNested(all: Record<string, CompiledDiagram>): Record<string, CompiledDiagram>;
19
+ /**
20
+ * Check what no single diagram can: that a node opens a diagram which exists, that the view and
21
+ * scenario it asks for exist there, and that diagrams do not open each other in a circle (a node
22
+ * cannot contain itself).
23
+ */
24
+ export declare function linkDiagrams(entries: Record<string, LinkEntry>): Diagnostic[];
package/dist/node.d.ts CHANGED
@@ -3,6 +3,7 @@ import type { CompiledDiagram } from './types';
3
3
  export * from './compile';
4
4
  export { loadIconSets, resolveIcon, searchIcons } from './icons';
5
5
  export { renderMarkdown } from './markdown';
6
+ export { attachNested, type LinkEntry, linkDiagrams, nestedDiagrams } from './nesting';
6
7
  export { authoringJsonSchema, FamilySchema, KNOWN_KEYS, RootSchema } from './schema';
7
8
  export interface LoadedProject {
8
9
  diagrams: Record<string, CompiledDiagram>;
package/dist/node.js CHANGED
@@ -1,3 +1,8 @@
1
+ import {
2
+ attachNested,
3
+ linkDiagrams,
4
+ nestedDiagrams
5
+ } from "./chunk-P2X2XSBM.js";
1
6
  import {
2
7
  FamilySchema,
3
8
  KNOWN_KEYS,
@@ -6,8 +11,8 @@ import {
6
11
  compileDiagram,
7
12
  createIconResolver,
8
13
  renderMarkdown
9
- } from "./chunk-5BMICZRC.js";
10
- import "./chunk-FRVCGXIG.js";
14
+ } from "./chunk-Q2NHZ6TV.js";
15
+ import "./chunk-ZYFFV7Y7.js";
11
16
 
12
17
  // src/node.ts
13
18
  import { existsSync, readdirSync, readFileSync as readFileSync2, statSync } from "node:fs";
@@ -48,6 +53,7 @@ function loadProject(root, dir = "diagrams") {
48
53
  const base = diagramsDir(root, dir);
49
54
  const result = { diagrams: {}, diagnostics: [], watch: [base] };
50
55
  if (!existsSync(base)) return result;
56
+ const entries = {};
51
57
  for (const name of readdirSync(base).sort()) {
52
58
  const folder = join(base, name);
53
59
  if (!statSync(folder).isDirectory()) continue;
@@ -59,7 +65,7 @@ function loadProject(root, dir = "diagrams") {
59
65
  file: join(scenarioDir, f),
60
66
  text: readFileSync2(join(scenarioDir, f), "utf8")
61
67
  })) : [];
62
- const { diagram, diagnostics } = compileDiagram2({
68
+ const { diagram, diagnostics, where } = compileDiagram2({
63
69
  name,
64
70
  file: relative(root, file),
65
71
  text: readFileSync2(file, "utf8"),
@@ -68,8 +74,11 @@ function loadProject(root, dir = "diagrams") {
68
74
  });
69
75
  result.diagnostics.push(...diagnostics);
70
76
  if (diagram) result.diagrams[name] = diagram;
77
+ entries[name] = { diagram, where };
71
78
  result.watch.push(folder);
72
79
  }
80
+ result.diagnostics.push(...linkDiagrams(entries));
81
+ result.diagrams = attachNested(result.diagrams);
73
82
  return result;
74
83
  }
75
84
  function readInside(folder, rel) {
@@ -91,12 +100,15 @@ export {
91
100
  FamilySchema,
92
101
  KNOWN_KEYS,
93
102
  RootSchema,
103
+ attachNested,
94
104
  authoringJsonSchema,
95
105
  compileDiagram2 as compileDiagram,
96
106
  diagramsDir,
97
107
  formatDiagnostic,
108
+ linkDiagrams,
98
109
  loadIconSets,
99
110
  loadProject,
111
+ nestedDiagrams,
100
112
  renderMarkdown,
101
113
  resolveIcon,
102
114
  searchIcons
@@ -0,0 +1,63 @@
1
+ import { type Diagrams } from './nested-layout';
2
+ import type { CompiledDiagram, CompiledView, IconData, Status } from './types';
3
+ /** One edge of a node, seen from that node. */
4
+ export interface OutlineConnection {
5
+ edge: string;
6
+ /** `out`: this node sends to `other`; `in`: it receives from `other`; `both`: edge goes either way. */
7
+ dir: 'out' | 'in' | 'both';
8
+ /** Id of the node at the other end, in the same diagram. */
9
+ other: string;
10
+ /** Its title, so a row needs nothing else to be read. */
11
+ otherTitle: string;
12
+ label?: string;
13
+ kind: string;
14
+ status: Status;
15
+ }
16
+ export interface OutlineNode {
17
+ type: 'node';
18
+ /** The node id, or ids joined with `/` for a node inside one shown in place (`orders/stock`). */
19
+ path: string;
20
+ id: string;
21
+ title: string;
22
+ sub?: string;
23
+ icon?: IconData;
24
+ status: Status;
25
+ connections: OutlineConnection[];
26
+ /** Present when the node holds a diagram the reader can reach. */
27
+ inside?: {
28
+ title: string;
29
+ /** The reader may show it in place, and may move into it. */
30
+ expand: boolean;
31
+ open: boolean;
32
+ /** Shown in place right now; `items` are then the nodes of that diagram. */
33
+ expanded: boolean;
34
+ items: OutlineItem[];
35
+ };
36
+ }
37
+ export interface OutlineGroup {
38
+ type: 'group';
39
+ /** Group id, prefixed like a node path so it is unique across the levels shown in place. */
40
+ path: string;
41
+ id: string;
42
+ title: string;
43
+ caption?: string;
44
+ icon?: IconData;
45
+ status: Status;
46
+ items: OutlineItem[];
47
+ /** How many nodes it holds, however deep. */
48
+ count: number;
49
+ }
50
+ export type OutlineItem = OutlineNode | OutlineGroup;
51
+ export interface OutlineOptions {
52
+ /** Where the diagrams a node holds are found, by name. */
53
+ diagrams?: Diagrams;
54
+ /** Paths shown in place. */
55
+ expanded?: ReadonlySet<string>;
56
+ prefix?: string;
57
+ /** Diagrams already on the way down, so a loop cannot run forever. */
58
+ seen?: readonly string[];
59
+ }
60
+ /** The nodes and groups of `view` as a tree, in the order the author wrote them. */
61
+ export declare function buildOutline(diagram: CompiledDiagram, view: CompiledView, { diagrams, expanded, prefix, seen }?: OutlineOptions): OutlineItem[];
62
+ /** Every node of an outline, in reading order, at any depth. */
63
+ export declare function outlineNodes(items: readonly OutlineItem[]): OutlineNode[];
@@ -0,0 +1,16 @@
1
+ import type { CompiledScenario } from './types';
2
+ export interface PhaseSpan {
3
+ /** Index in `scenario.phases`. */
4
+ index: number;
5
+ title: string;
6
+ caption?: string;
7
+ /** Index of its first and last step in `scenario.steps`. */
8
+ from: number;
9
+ to: number;
10
+ /** How many steps it holds. */
11
+ count: number;
12
+ }
13
+ /** Whether the author named the phases. A scenario written as plain `steps` has one nameless phase. */
14
+ export declare const hasPhases: (scenario: CompiledScenario) => boolean;
15
+ /** The phases that hold steps, in order. Steps follow their phase, so each phase is one run. */
16
+ export declare function phaseSpans(scenario: CompiledScenario): PhaseSpan[];