@milehimikey/em 1.4.0 → 1.6.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/.claude/skills/event-modeling/SKILL.md +121 -30
- package/.claude/skills/event-modeling/reference/conform.md +36 -7
- package/.claude/skills/event-modeling/reference/em-dsl.md +210 -48
- package/.claude/skills/event-modeling/reference/extract.md +24 -7
- package/.claude/skills/event-modeling/reference/methodology.md +129 -32
- package/.claude/skills/event-modeling/templates/conformance-report.md +9 -7
- package/.claude/skills/event-modeling/templates/model-readme.md +9 -0
- package/.claude/skills/event-modeling/templates/slice.md +20 -1
- package/.claude/skills/event-modeling/templates/state.md +8 -0
- package/README.md +1 -0
- package/dist/catalog/build.d.ts +33 -0
- package/dist/catalog/build.js +117 -0
- package/dist/catalog/build.js.map +1 -0
- package/dist/catalog/classify.d.ts +14 -0
- package/dist/catalog/classify.js +44 -0
- package/dist/catalog/classify.js.map +1 -0
- package/dist/catalog/pages.d.ts +45 -0
- package/dist/catalog/pages.js +174 -0
- package/dist/catalog/pages.js.map +1 -0
- package/dist/catalog/sliceDoc.d.ts +10 -0
- package/dist/catalog/sliceDoc.js +20 -0
- package/dist/catalog/sliceDoc.js.map +1 -0
- package/dist/cli/catalog-inputs.d.ts +16 -0
- package/dist/cli/catalog-inputs.js +18 -0
- package/dist/cli/catalog-inputs.js.map +1 -0
- package/dist/cli/glossary-inputs.d.ts +15 -0
- package/dist/cli/glossary-inputs.js +17 -0
- package/dist/cli/glossary-inputs.js.map +1 -0
- package/dist/cli/render-inputs.d.ts +27 -0
- package/dist/cli/render-inputs.js +41 -0
- package/dist/cli/render-inputs.js.map +1 -0
- package/dist/cli.js +188 -10
- package/dist/cli.js.map +1 -1
- package/dist/emit/diffJson.d.ts +1 -1
- package/dist/emit/diffJson.js +8 -1
- package/dist/emit/diffJson.js.map +1 -1
- package/dist/emit/dot.d.ts +7 -0
- package/dist/emit/dot.js +10 -4
- package/dist/emit/dot.js.map +1 -1
- package/dist/emit/glossaryJson.d.ts +13 -0
- package/dist/emit/glossaryJson.js +36 -0
- package/dist/emit/glossaryJson.js.map +1 -0
- package/dist/emit/json.d.ts +16 -2
- package/dist/emit/json.js +57 -6
- package/dist/emit/json.js.map +1 -1
- package/dist/emit/theme.d.ts +3 -0
- package/dist/emit/theme.js +19 -0
- package/dist/emit/theme.js.map +1 -1
- package/dist/model/diff.d.ts +36 -1
- package/dist/model/diff.js +198 -35
- package/dist/model/diff.js.map +1 -1
- package/dist/model/edges.d.ts +12 -1
- package/dist/model/edges.js +26 -24
- package/dist/model/edges.js.map +1 -1
- package/dist/model/glossary.d.ts +68 -0
- package/dist/model/glossary.js +138 -0
- package/dist/model/glossary.js.map +1 -0
- package/dist/model/model.d.ts +35 -0
- package/dist/model/model.js +41 -0
- package/dist/model/model.js.map +1 -1
- package/dist/model/validate.js +252 -1
- package/dist/model/validate.js.map +1 -1
- package/dist/parser/ast.d.ts +20 -0
- package/dist/parser/parser.js +158 -29
- package/dist/parser/parser.js.map +1 -1
- package/dist/render/drawEdges.d.ts +3 -1
- package/dist/render/drawEdges.js +119 -7
- package/dist/render/drawEdges.js.map +1 -1
- package/dist/render/drawNotes.d.ts +7 -3
- package/dist/render/drawNotes.js +75 -28
- package/dist/render/drawNotes.js.map +1 -1
- package/dist/render/render.d.ts +20 -1
- package/dist/render/render.js +57 -17
- package/dist/render/render.js.map +1 -1
- package/dist/render/serve.js +155 -0
- package/dist/render/serve.js.map +1 -1
- package/dist/render/sliceDiagram.d.ts +11 -0
- package/dist/render/sliceDiagram.js +198 -0
- package/dist/render/sliceDiagram.js.map +1 -0
- package/dist/render/sliceOverlay.d.ts +21 -0
- package/dist/render/sliceOverlay.js +111 -0
- package/dist/render/sliceOverlay.js.map +1 -0
- package/dist/render/sliceStatus.d.ts +5 -0
- package/dist/render/sliceStatus.js +26 -0
- package/dist/render/sliceStatus.js.map +1 -0
- package/dist/render/statusOverlay.d.ts +16 -0
- package/dist/render/statusOverlay.js +110 -0
- package/dist/render/statusOverlay.js.map +1 -0
- package/dist/render/svgGeometry.d.ts +43 -0
- package/dist/render/svgGeometry.js +69 -3
- package/dist/render/svgGeometry.js.map +1 -1
- package/dist/render/watch.d.ts +1 -1
- package/dist/render/watch.js +5 -2
- package/dist/render/watch.js.map +1 -1
- package/package.json +3 -2
package/dist/render/drawNotes.js
CHANGED
|
@@ -1,21 +1,28 @@
|
|
|
1
1
|
// SPDX-License-Identifier: MIT
|
|
2
|
-
// Draws "has notes" and "has
|
|
2
|
+
// Draws "has notes", "has issues", and "has an accepted divergence" affordances on top of
|
|
3
|
+
// a Graphviz-rendered SVG:
|
|
3
4
|
// - a small folded-corner (dog-ear) marker per noted box, top-right, amber, carrying
|
|
4
5
|
// a footnote number, linked to the note file;
|
|
5
6
|
// - a small folded-corner marker per element with an open `issue`, top-left, red,
|
|
6
7
|
// carrying its own footnote number — no link (there's nothing to open), just a
|
|
7
8
|
// `<title>` tooltip, since the full text lives in the legend;
|
|
9
|
+
// - a small folded-corner marker per element with a `divergence`, bottom-right, teal,
|
|
10
|
+
// same tooltip-only treatment as `issue` (inline text, no file to link to) — the
|
|
11
|
+
// *resolved* counterpart to issue's red: right side reads "documented" (note,
|
|
12
|
+
// divergence), left reads "open" (issue);
|
|
8
13
|
// - a legend appended below the diagram: a "Notes" section mapping each amber number
|
|
9
|
-
// to its element and note file,
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
14
|
+
// to its element and note file, an "Issues" section mapping each red number to its
|
|
15
|
+
// element and issue text, and an "Accepted Divergences" section mapping each teal
|
|
16
|
+
// number to its element and divergence text — each present only when non-empty.
|
|
17
|
+
// An element may carry a note, an issue, and a divergence all at once: note/issue sit on
|
|
18
|
+
// the top corners, divergence sits at bottom-right, so none of them overlap, and each
|
|
19
|
+
// keeps its own independent numbering (a "1" note, a "1" issue, and a "1" divergence on
|
|
20
|
+
// the same box are unambiguous — distinct color, distinct corner, distinct legend
|
|
21
|
+
// section).
|
|
22
|
+
// In SVG the note markers/rows are anchors (click to open the markdown); issue and
|
|
23
|
+
// divergence markers/rows never are, since inline text has no file to link to. Raster
|
|
24
|
+
// output (PNG/PDF) can't carry links or tooltips either way, so the legend is what tells
|
|
25
|
+
// you which note, issue, or divergence belongs to which element there.
|
|
19
26
|
//
|
|
20
27
|
// Marker coordinates come from parseNodeRects (the box coordinate space, inside
|
|
21
28
|
// Graphviz's transform group). The legend is laid out in the SVG's root/viewBox
|
|
@@ -26,6 +33,8 @@ const FILL = "#F4C430"; // sticky-note amber (notes)
|
|
|
26
33
|
const STROKE = "#7A5200"; // dark amber — reads on light and orange fills alike
|
|
27
34
|
const ISSUE_FILL = "#E53935"; // red sticky (open issues)
|
|
28
35
|
const ISSUE_STROKE = "#8B0000"; // dark red
|
|
36
|
+
const DIVERGENCE_FILL = "#26A69A"; // muted teal (accepted divergences — settled, not open)
|
|
37
|
+
const DIVERGENCE_STROKE = "#00695C"; // dark teal
|
|
29
38
|
/** Elements carrying a note, in document order. Index + 1 is the footnote number. */
|
|
30
39
|
export function notedElements(model) {
|
|
31
40
|
return model.elements.filter((el) => el.note);
|
|
@@ -34,6 +43,10 @@ export function notedElements(model) {
|
|
|
34
43
|
export function issuedElements(model) {
|
|
35
44
|
return model.elements.filter((el) => el.issue);
|
|
36
45
|
}
|
|
46
|
+
/** Elements carrying an accepted divergence, in document order. Index + 1 is the footnote number. */
|
|
47
|
+
export function divergedElements(model) {
|
|
48
|
+
return model.elements.filter((el) => el.divergence);
|
|
49
|
+
}
|
|
37
50
|
const rawNote = (el) => el.note ?? "";
|
|
38
51
|
/** SVG group with a numbered corner marker per noted element (inside the graph group). */
|
|
39
52
|
export function buildNoteMarkers(model, rects, hrefOf = rawNote) {
|
|
@@ -57,15 +70,29 @@ export function buildIssueMarkers(model, rects) {
|
|
|
57
70
|
});
|
|
58
71
|
return `<g class="em-issues">${markers.join("")}</g>`;
|
|
59
72
|
}
|
|
60
|
-
/**
|
|
61
|
-
function
|
|
62
|
-
const
|
|
73
|
+
/** SVG group with a numbered corner marker per element with an accepted divergence. */
|
|
74
|
+
export function buildDivergenceMarkers(model, rects) {
|
|
75
|
+
const markers = [];
|
|
76
|
+
divergedElements(model).forEach((el, i) => {
|
|
77
|
+
const r = rects.get(el.id);
|
|
78
|
+
if (!r)
|
|
79
|
+
return;
|
|
80
|
+
markers.push(divergenceMarker(r, el.divergence ?? "", i + 1));
|
|
81
|
+
});
|
|
82
|
+
return `<g class="em-divergences">${markers.join("")}</g>`;
|
|
83
|
+
}
|
|
84
|
+
/** The dog-ear glyph + footnote number, anchored at one corner of the box. */
|
|
85
|
+
function cornerGlyph(r, num, corner, vAlign, fill, stroke) {
|
|
63
86
|
const x = corner === "right" ? r.right - INSET : r.left + INSET;
|
|
64
87
|
const foldX = corner === "right" ? x - FOLD : x + FOLD;
|
|
65
|
-
|
|
66
|
-
|
|
88
|
+
// y grows downward: a top marker nudges down from the top edge and folds further down;
|
|
89
|
+
// a bottom marker nudges up from the bottom edge and folds further up.
|
|
90
|
+
const y = vAlign === "top" ? r.top + INSET : r.bottom - INSET;
|
|
91
|
+
const foldY = vAlign === "top" ? y + FOLD : y - FOLD;
|
|
92
|
+
const tri = `M${n(foldX)},${n(y)} L${n(x)},${n(y)} L${n(x)},${n(foldY)} Z`;
|
|
93
|
+
const crease = `M${n(foldX)},${n(y)} L${n(x)},${n(foldY)}`;
|
|
67
94
|
const numX = corner === "right" ? foldX - 2 : foldX + 2;
|
|
68
|
-
const numY = y + 9;
|
|
95
|
+
const numY = vAlign === "top" ? y + 9 : y - 5;
|
|
69
96
|
const anchor = corner === "right" ? "end" : "start";
|
|
70
97
|
return (`<path d="${tri}" fill="${fill}" stroke="${stroke}" stroke-width="1" stroke-linejoin="round"/>` +
|
|
71
98
|
`<path d="${crease}" fill="none" stroke="${stroke}" stroke-width="0.7"/>` +
|
|
@@ -77,7 +104,7 @@ function noteMarker(r, note, num) {
|
|
|
77
104
|
const href = esc(note);
|
|
78
105
|
return (`<a xlink:href="${href}" href="${href}" target="_blank">` +
|
|
79
106
|
`<title>${num}. ${href}</title>` +
|
|
80
|
-
cornerGlyph(r, num, "right", FILL, STROKE) +
|
|
107
|
+
cornerGlyph(r, num, "right", "top", FILL, STROKE) +
|
|
81
108
|
`</a>`);
|
|
82
109
|
}
|
|
83
110
|
/**
|
|
@@ -87,7 +114,16 @@ function noteMarker(r, note, num) {
|
|
|
87
114
|
* read the full issue text (tooltips don't survive rasterization either).
|
|
88
115
|
*/
|
|
89
116
|
function issueMarker(r, issue, num) {
|
|
90
|
-
return `<g><title>${num}. ${esc(issue)}</title>${cornerGlyph(r, num, "left", ISSUE_FILL, ISSUE_STROKE)}</g>`;
|
|
117
|
+
return `<g><title>${num}. ${esc(issue)}</title>${cornerGlyph(r, num, "left", "top", ISSUE_FILL, ISSUE_STROKE)}</g>`;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A folded-corner glyph + footnote number at the box's bottom-right, marking an
|
|
121
|
+
* accepted divergence — the resolved counterpart to `issueMarker`'s open question.
|
|
122
|
+
* Same tooltip-only treatment (no link target); the legend is the reliable read.
|
|
123
|
+
*/
|
|
124
|
+
function divergenceMarker(r, divergence, num) {
|
|
125
|
+
return (`<g><title>${num}. ${esc(divergence)}</title>` +
|
|
126
|
+
`${cornerGlyph(r, num, "right", "bottom", DIVERGENCE_FILL, DIVERGENCE_STROKE)}</g>`);
|
|
91
127
|
}
|
|
92
128
|
// ---- legend ----
|
|
93
129
|
const PAD_X = 16;
|
|
@@ -97,14 +133,15 @@ const LINE_H = 18;
|
|
|
97
133
|
const PAD_BOTTOM = 14;
|
|
98
134
|
/**
|
|
99
135
|
* Grow the canvas and append a legend below the diagram: a "Notes" section (if any
|
|
100
|
-
* element has a note)
|
|
101
|
-
*
|
|
102
|
-
* parsed.
|
|
136
|
+
* element has a note), an "Issues" section (if any element has an open issue), and an
|
|
137
|
+
* "Accepted Divergences" section (if any element has one). Returns the SVG unchanged
|
|
138
|
+
* if there's nothing to show or the dimensions can't be parsed.
|
|
103
139
|
*/
|
|
104
140
|
export function appendNoteLegend(svg, model, hrefOf = rawNote) {
|
|
105
141
|
const noted = notedElements(model);
|
|
106
142
|
const issued = issuedElements(model);
|
|
107
|
-
|
|
143
|
+
const diverged = divergedElements(model);
|
|
144
|
+
if (noted.length === 0 && issued.length === 0 && diverged.length === 0)
|
|
108
145
|
return svg;
|
|
109
146
|
const vb = /viewBox="([\d.eE+-]+)\s+([\d.eE+-]+)\s+([\d.eE+-]+)\s+([\d.eE+-]+)"/.exec(svg);
|
|
110
147
|
const hpt = /height="([\d.eE+-]+)pt"/.exec(svg);
|
|
@@ -116,7 +153,8 @@ export function appendNoteLegend(svg, model, hrefOf = rawNote) {
|
|
|
116
153
|
const vh = +vb[4];
|
|
117
154
|
const notesH = noted.length > 0 ? HEAD_H + noted.length * LINE_H : 0;
|
|
118
155
|
const issuesH = issued.length > 0 ? HEAD_H + issued.length * LINE_H : 0;
|
|
119
|
-
const
|
|
156
|
+
const divergencesH = diverged.length > 0 ? HEAD_H + diverged.length * LINE_H : 0;
|
|
157
|
+
const legendH = PAD_TOP + notesH + issuesH + divergencesH + PAD_BOTTOM;
|
|
120
158
|
const newVh = vh + legendH;
|
|
121
159
|
const newHpt = (+hpt[1]) * (newVh / vh); // keep the pt:viewBox ratio (handles scaling)
|
|
122
160
|
const top = minY + vh; // legend sits just below the diagram, in root coords
|
|
@@ -143,6 +181,15 @@ export function appendNoteLegend(svg, model, hrefOf = rawNote) {
|
|
|
143
181
|
.join("");
|
|
144
182
|
sectionY += issuesH;
|
|
145
183
|
}
|
|
184
|
+
if (diverged.length > 0) {
|
|
185
|
+
sections +=
|
|
186
|
+
`<text x="${n(minX + PAD_X)}" y="${n(sectionY + 14)}" font-family="Helvetica" ` +
|
|
187
|
+
`font-weight="bold" font-size="12" fill="${DIVERGENCE_STROKE}">Accepted Divergences</text>`;
|
|
188
|
+
sections += diverged
|
|
189
|
+
.map((el, i) => row(el, el.divergence ?? "", i + 1, minX + PAD_X, sectionY + HEAD_H + i * LINE_H + 13, "divergence"))
|
|
190
|
+
.join("");
|
|
191
|
+
sectionY += divergencesH;
|
|
192
|
+
}
|
|
146
193
|
const legend = `<g class="em-note-legend">` +
|
|
147
194
|
`<rect x="${n(minX)}" y="${n(top)}" width="${n(vw)}" height="${n(legendH)}" fill="#FFFFFF"/>` +
|
|
148
195
|
`<line x1="${n(minX)}" y1="${n(top)}" x2="${n(minX + vw)}" y2="${n(top)}" stroke="#D0D0D0" stroke-width="1"/>` +
|
|
@@ -152,18 +199,18 @@ export function appendNoteLegend(svg, model, hrefOf = rawNote) {
|
|
|
152
199
|
}
|
|
153
200
|
/**
|
|
154
201
|
* One legend row: "N. Element name — text". Notes rows link to the note file;
|
|
155
|
-
* issue rows carry
|
|
202
|
+
* issue and divergence rows carry their full text and have no link target.
|
|
156
203
|
*/
|
|
157
204
|
function row(el, hrefOrText, num, x, y, kind = "note") {
|
|
158
|
-
const color = kind === "issue" ? ISSUE_STROKE : STROKE;
|
|
159
|
-
const label = esc(kind === "issue" ? el.issue ?? "" : el.note ?? ""); // readable text
|
|
205
|
+
const color = kind === "issue" ? ISSUE_STROKE : kind === "divergence" ? DIVERGENCE_STROKE : STROKE;
|
|
206
|
+
const label = esc(kind === "issue" ? el.issue ?? "" : kind === "divergence" ? el.divergence ?? "" : el.note ?? ""); // readable text
|
|
160
207
|
const name = esc(el.name);
|
|
161
208
|
const text = `<text x="${n(x)}" y="${n(y)}" font-family="Helvetica" font-size="11" fill="#202124">` +
|
|
162
209
|
`<tspan font-weight="bold" fill="${color}">${num}.</tspan>` +
|
|
163
210
|
`<tspan dx="6">${name}</tspan>` +
|
|
164
211
|
`<tspan dx="6" fill="#5F6368">— ${label}</tspan>` +
|
|
165
212
|
`</text>`;
|
|
166
|
-
if (kind === "issue")
|
|
213
|
+
if (kind === "issue" || kind === "divergence")
|
|
167
214
|
return text; // nothing to link to
|
|
168
215
|
const href = esc(hrefOrText); // resolved link target
|
|
169
216
|
return `<a xlink:href="${href}" href="${href}" target="_blank">${text}</a>`;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"drawNotes.js","sourceRoot":"","sources":["../../src/render/drawNotes.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,
|
|
1
|
+
{"version":3,"file":"drawNotes.js","sourceRoot":"","sources":["../../src/render/drawNotes.ts"],"names":[],"mappings":"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"}
|
package/dist/render/render.d.ts
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
import { NormalizedModel } from "../model/model.js";
|
|
2
|
+
import { Grid } from "../layout/grid.js";
|
|
2
3
|
export declare function formatFromPath(outPath: string, fallback?: string): string;
|
|
3
|
-
|
|
4
|
+
/** Run the Graphviz WASM layout pass only — the part shared by every caller that
|
|
5
|
+
* needs to compose the same layout for more than one output location (a full
|
|
6
|
+
* diagram and a slice diagram of it, note hrefs recomputed for each — see
|
|
7
|
+
* buildCatalog and `em render --slice`), so it isn't paid for twice. */
|
|
8
|
+
export declare function layoutDot(dot: string): Promise<string>;
|
|
9
|
+
/**
|
|
10
|
+
* Compose a laid-out SVG with edges, notes, slice tags/metadata, and the legend —
|
|
11
|
+
* the pure/string half of rendering, split out from layoutDot's WASM call. `outDir`
|
|
12
|
+
* is where the composed SVG will ultimately be written; note links are authored
|
|
13
|
+
* relative to `baseDir` (the .em file) and get rewritten relative to `outDir` here,
|
|
14
|
+
* so a caller composing the *same* layout for two different output locations (e.g.
|
|
15
|
+
* a full diagram and a slice diagram one directory deeper) must call this twice,
|
|
16
|
+
* once per real outDir, rather than reuse one composed string for both.
|
|
17
|
+
*/
|
|
18
|
+
export declare function composeSvg(rawSvg: string, model: NormalizedModel, grid: Grid, baseDir: string, outDir: string): string;
|
|
19
|
+
/** Serialize a composed SVG string to `outPath` in the requested format — split out
|
|
20
|
+
* so a caller can transform the composed string in between composing and writing it. */
|
|
21
|
+
export declare function writeRendered(svg: string, outPath: string, format?: string): Promise<void>;
|
|
22
|
+
export declare function renderDot(dot: string, model: NormalizedModel, grid: Grid, outPath: string, format?: string, baseDir?: string): Promise<void>;
|
|
4
23
|
/**
|
|
5
24
|
* Resolve a note path (authored relative to the .em file) into an href relative
|
|
6
25
|
* to the output SVG's directory, so links stay valid wherever the SVG is
|
package/dist/render/render.js
CHANGED
|
@@ -12,9 +12,12 @@ import { writeFile } from "node:fs/promises";
|
|
|
12
12
|
import { dirname, extname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
13
13
|
import { Graphviz } from "@hpcc-js/wasm-graphviz";
|
|
14
14
|
import { Resvg } from "@resvg/resvg-js";
|
|
15
|
-
import { parseNodeRects } from "./svgGeometry.js";
|
|
15
|
+
import { fitCanvas, parseNodeRects } from "./svgGeometry.js";
|
|
16
16
|
import { buildEdgeOverlay } from "./drawEdges.js";
|
|
17
|
-
import { buildNoteMarkers, buildIssueMarkers, appendNoteLegend } from "./drawNotes.js";
|
|
17
|
+
import { buildNoteMarkers, buildIssueMarkers, buildDivergenceMarkers, appendNoteLegend } from "./drawNotes.js";
|
|
18
|
+
import { sliceOverlayIds, tagSliceAttrs, buildSliceOverlay } from "./sliceOverlay.js";
|
|
19
|
+
import { readSliceStatuses } from "./sliceStatus.js";
|
|
20
|
+
import { applyStatusColors, appendStatusLegend } from "./statusOverlay.js";
|
|
18
21
|
const RSVG_BIN = process.env.EM_RSVG || "rsvg-convert";
|
|
19
22
|
// The WASM Graphviz module is loaded once and reused across renders.
|
|
20
23
|
let graphvizModule;
|
|
@@ -25,13 +28,31 @@ export function formatFromPath(outPath, fallback = "svg") {
|
|
|
25
28
|
const ext = extname(outPath).replace(/^\./, "").toLowerCase();
|
|
26
29
|
return ext || fallback;
|
|
27
30
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
/** Run the Graphviz WASM layout pass only — the part shared by every caller that
|
|
32
|
+
* needs to compose the same layout for more than one output location (a full
|
|
33
|
+
* diagram and a slice diagram of it, note hrefs recomputed for each — see
|
|
34
|
+
* buildCatalog and `em render --slice`), so it isn't paid for twice. */
|
|
35
|
+
export async function layoutDot(dot) {
|
|
33
36
|
const gv = await graphviz();
|
|
34
|
-
|
|
37
|
+
return gv.layout(dot, "svg", "dot");
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Compose a laid-out SVG with edges, notes, slice tags/metadata, and the legend —
|
|
41
|
+
* the pure/string half of rendering, split out from layoutDot's WASM call. `outDir`
|
|
42
|
+
* is where the composed SVG will ultimately be written; note links are authored
|
|
43
|
+
* relative to `baseDir` (the .em file) and get rewritten relative to `outDir` here,
|
|
44
|
+
* so a caller composing the *same* layout for two different output locations (e.g.
|
|
45
|
+
* a full diagram and a slice diagram one directory deeper) must call this twice,
|
|
46
|
+
* once per real outDir, rather than reuse one composed string for both.
|
|
47
|
+
*/
|
|
48
|
+
export function composeSvg(rawSvg, model, grid, baseDir, outDir) {
|
|
49
|
+
const hrefOf = (el) => noteHref(el.note ?? "", baseDir, outDir);
|
|
50
|
+
const statuses = readSliceStatuses(model, baseDir);
|
|
51
|
+
return withOverlays(rawSvg, model, grid, hrefOf, statuses);
|
|
52
|
+
}
|
|
53
|
+
/** Serialize a composed SVG string to `outPath` in the requested format — split out
|
|
54
|
+
* so a caller can transform the composed string in between composing and writing it. */
|
|
55
|
+
export async function writeRendered(svg, outPath, format = formatFromPath(outPath)) {
|
|
35
56
|
if (format === "svg") {
|
|
36
57
|
await writeFile(outPath, svg, "utf8");
|
|
37
58
|
return;
|
|
@@ -49,6 +70,12 @@ export async function renderDot(dot, model, outPath, format = formatFromPath(out
|
|
|
49
70
|
throw new Error(`format '${format}' is not built in (svg and png are). Install librsvg ` +
|
|
50
71
|
`(provides '${RSVG_BIN}') to render '${format}', or use -o file.svg / -o file.png.`);
|
|
51
72
|
}
|
|
73
|
+
export async function renderDot(dot, model, grid, outPath, format = formatFromPath(outPath), baseDir = process.cwd()) {
|
|
74
|
+
const outDir = dirname(outPath);
|
|
75
|
+
const raw = await layoutDot(dot);
|
|
76
|
+
const svg = composeSvg(raw, model, grid, baseDir, outDir);
|
|
77
|
+
await writeRendered(svg, outPath, format);
|
|
78
|
+
}
|
|
52
79
|
/**
|
|
53
80
|
* Resolve a note path (authored relative to the .em file) into an href relative
|
|
54
81
|
* to the output SVG's directory, so links stay valid wherever the SVG is
|
|
@@ -63,26 +90,39 @@ export function noteHref(note, baseDir, outDir) {
|
|
|
63
90
|
return rel.split(sep).join("/"); // posix separators for URLs
|
|
64
91
|
}
|
|
65
92
|
/** Draw the semantic edges and note markers into a Graphviz-rendered SVG. */
|
|
66
|
-
function withOverlays(svg, model, hrefOf) {
|
|
67
|
-
const rects = parseNodeRects(svg, new Set(model.byId.keys()));
|
|
68
|
-
const { defs, group } = buildEdgeOverlay(model, rects);
|
|
93
|
+
function withOverlays(svg, model, grid, hrefOf, statuses) {
|
|
94
|
+
const rects = parseNodeRects(svg, new Set([...model.byId.keys(), ...sliceOverlayIds(grid)]));
|
|
95
|
+
const { defs, group, bbox } = buildEdgeOverlay(model, rects);
|
|
69
96
|
const notes = buildNoteMarkers(model, rects, hrefOf);
|
|
70
97
|
const issues = buildIssueMarkers(model, rects);
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
|
|
98
|
+
const divergences = buildDivergenceMarkers(model, rects);
|
|
99
|
+
// an edge detour can run outside the box grid Graphviz sized the canvas for, so make
|
|
100
|
+
// room before anything else measures or appends to the viewBox
|
|
101
|
+
let out = fitCanvas(svg, bbox);
|
|
102
|
+
// recolor each slice's header cell per its doc status (src/render/sliceStatus.ts) —
|
|
103
|
+
// a scoped string replace keyed on each header's own <title> id, independent of
|
|
104
|
+
// tagSliceAttrs/buildSliceOverlay below, so the ordering here isn't load-bearing.
|
|
105
|
+
out = applyStatusColors(out, grid, statuses);
|
|
106
|
+
// tag each element's node group with data-slice="<index>" so the storyboard
|
|
107
|
+
// viewer (`em watch --serve`) can highlight/zoom to one slice at a time
|
|
108
|
+
// client-side, purely from what's already in the SVG.
|
|
109
|
+
out = tagSliceAttrs(out, model);
|
|
110
|
+
const sliceOverlay = buildSliceOverlay(out, grid, rects);
|
|
111
|
+
// arrowhead markers + slice metadata/style go just inside <svg …>
|
|
112
|
+
out = out.replace(/(<svg\b[^>]*>)/, `$1${defs}${sliceOverlay}`);
|
|
74
113
|
// edges go under the boxes: just before the first node group
|
|
75
114
|
const nodeAt = out.search(/<g\b[^>]*class="node"[^>]*>/);
|
|
76
115
|
if (nodeAt >= 0)
|
|
77
116
|
out = out.slice(0, nodeAt) + group + out.slice(nodeAt);
|
|
78
117
|
else
|
|
79
118
|
out = out.replace(/<\/svg>/, `${group}</svg>`);
|
|
80
|
-
// note/issue markers go on top of the boxes — inside the graph transform
|
|
81
|
-
// (so they share the box coordinate space) but after every node, as the last
|
|
119
|
+
// note/issue/divergence markers go on top of the boxes — inside the graph transform
|
|
120
|
+
// group (so they share the box coordinate space) but after every node, as the last
|
|
82
121
|
// children of that group, making them the topmost clickable layer.
|
|
83
|
-
out = out.replace(/(<\/g>\s*)(<\/svg>)/, `${notes}${issues}$1$2`);
|
|
122
|
+
out = out.replace(/(<\/g>\s*)(<\/svg>)/, `${notes}${issues}${divergences}$1$2`);
|
|
84
123
|
// grow the canvas and append the legend below the diagram (root coords)
|
|
85
124
|
out = appendNoteLegend(out, model, hrefOf);
|
|
125
|
+
out = appendStatusLegend(out, grid, statuses);
|
|
86
126
|
return out;
|
|
87
127
|
}
|
|
88
128
|
/** Convert an SVG string to the requested format with an optional system rsvg-convert. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/render/render.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,+BAA+B;AAC/B,EAAE;AACF,+EAA+E;AAC/E,iFAAiF;AACjF,6EAA6E;AAC7E,+EAA+E;AAC/E,gFAAgF;AAChF,sEAAsE;AAEtE,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,WAAW,CAAC;AACjF,OAAO,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../../src/render/render.ts"],"names":[],"mappings":"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"}
|
package/dist/render/serve.js
CHANGED
|
@@ -135,6 +135,14 @@ async function serveStatic(pathname, root, res) {
|
|
|
135
135
|
// Served viewer page. Same double-buffered, no-flicker swap as the static
|
|
136
136
|
// live.html fallback, but driven by SSE push instead of a poll loop — the
|
|
137
137
|
// browser reloads only when the server says the SVG actually changed.
|
|
138
|
+
//
|
|
139
|
+
// Also carries stakeholder review mode: a Prev/Next storyboard that highlights
|
|
140
|
+
// and pans/zooms to one slice at a time, reading the `data-slice`
|
|
141
|
+
// attributes/`<metadata id="em-slices">` that src/render/sliceOverlay.ts
|
|
142
|
+
// embeds in every rendered SVG. Review-mode state (`reviewMode`/`activeSlice`)
|
|
143
|
+
// deliberately lives outside reload()'s closure and is re-applied after every
|
|
144
|
+
// SSE-triggered swap — a facilitator hand-editing an `issue "..."` clause live
|
|
145
|
+
// during a session should see it appear without losing their place.
|
|
138
146
|
const VIEWER_HTML = `<!doctype html>
|
|
139
147
|
<html lang="en">
|
|
140
148
|
<head>
|
|
@@ -150,6 +158,26 @@ const VIEWER_HTML = `<!doctype html>
|
|
|
150
158
|
header .dot { width: 9px; height: 9px; border-radius: 50%; background: #34c759; transition: background .3s; }
|
|
151
159
|
header .dot.stale { background: #f0a020; }
|
|
152
160
|
header .stamp { margin-left: auto; opacity: .7; font-variant-numeric: tabular-nums; }
|
|
161
|
+
.btn {
|
|
162
|
+
background: #3b4754; color: #fff; border: 0; border-radius: 4px;
|
|
163
|
+
padding: .3rem .7rem; font-size: 13px; cursor: pointer; font-family: inherit;
|
|
164
|
+
}
|
|
165
|
+
.btn:hover { background: #4b5763; }
|
|
166
|
+
.btn:disabled { opacity: .4; cursor: default; }
|
|
167
|
+
.btn.active { background: #2f6fed; }
|
|
168
|
+
#storyboard {
|
|
169
|
+
display: flex; align-items: center; gap: .5rem; padding: .4rem .9rem;
|
|
170
|
+
background: #e8eaed; border-bottom: 1px solid #d0d3d7;
|
|
171
|
+
}
|
|
172
|
+
#storyboard[hidden] { display: none; }
|
|
173
|
+
#storyboard .nav { background: none; border: 0; font-size: 18px; line-height: 1; cursor: pointer; color: #3b4754; padding: 0 .3rem; }
|
|
174
|
+
#storyboard .nav:disabled { opacity: .3; cursor: default; }
|
|
175
|
+
#filmstrip { display: flex; gap: .35rem; overflow-x: auto; }
|
|
176
|
+
#filmstrip .slice {
|
|
177
|
+
padding: .25rem .6rem; border-radius: 12px; background: #fff; border: 1px solid #d0d3d7;
|
|
178
|
+
font-size: 12px; white-space: nowrap; cursor: pointer; color: #3b4754; font-family: inherit;
|
|
179
|
+
}
|
|
180
|
+
#filmstrip .slice.active { background: #2f6fed; border-color: #2f6fed; color: #fff; font-weight: 600; }
|
|
153
181
|
#stage { padding: 1rem; }
|
|
154
182
|
object { width: 100%; height: calc(100vh - 60px); border: 0; }
|
|
155
183
|
</style>
|
|
@@ -158,8 +186,14 @@ const VIEWER_HTML = `<!doctype html>
|
|
|
158
186
|
<header>
|
|
159
187
|
<span class="dot" id="dot"></span>
|
|
160
188
|
<span>Event Model — live (push)</span>
|
|
189
|
+
<button class="btn" id="reviewToggle" title="Step through one slice at a time">Review mode</button>
|
|
161
190
|
<span class="stamp" id="stamp">connecting…</span>
|
|
162
191
|
</header>
|
|
192
|
+
<nav id="storyboard" hidden>
|
|
193
|
+
<button class="nav" id="prevSlice" aria-label="Previous slice">‹</button>
|
|
194
|
+
<div id="filmstrip"></div>
|
|
195
|
+
<button class="nav" id="nextSlice" aria-label="Next slice">›</button>
|
|
196
|
+
</nav>
|
|
163
197
|
<div id="stage">
|
|
164
198
|
<object id="svg" type="image/svg+xml"></object>
|
|
165
199
|
</div>
|
|
@@ -170,8 +204,21 @@ const VIEWER_HTML = `<!doctype html>
|
|
|
170
204
|
const stage = document.getElementById("stage");
|
|
171
205
|
const stamp = document.getElementById("stamp");
|
|
172
206
|
const dot = document.getElementById("dot");
|
|
207
|
+
const reviewToggle = document.getElementById("reviewToggle");
|
|
208
|
+
const storyboard = document.getElementById("storyboard");
|
|
209
|
+
const filmstrip = document.getElementById("filmstrip");
|
|
210
|
+
const prevBtn = document.getElementById("prevSlice");
|
|
211
|
+
const nextBtn = document.getElementById("nextSlice");
|
|
173
212
|
let current = document.getElementById("svg");
|
|
174
213
|
|
|
214
|
+
// --- storyboard state: outside reload()'s closure on purpose, see the
|
|
215
|
+
// file-level comment above — an SSE reload must not reset these. ---
|
|
216
|
+
let reviewMode = false;
|
|
217
|
+
let activeSlice = null; // slice index, or null for "show all"
|
|
218
|
+
let slices = []; // [{index, name, x0, x1}], from this load's <metadata>
|
|
219
|
+
let rowLabels = null; // {x0, x1} | null, the swimlane label column's range
|
|
220
|
+
let naturalViewBox = null; // this load's pre-zoom viewBox, cached once per load
|
|
221
|
+
|
|
175
222
|
// Double-buffer: load into a hidden <object>, swap on load, so the shared
|
|
176
223
|
// screen never flashes white during a reload. Keep <object> (not <img>) so
|
|
177
224
|
// note "..." links inside the SVG stay clickable.
|
|
@@ -185,10 +232,118 @@ const VIEWER_HTML = `<!doctype html>
|
|
|
185
232
|
current = next;
|
|
186
233
|
stamp.textContent = new Date().toLocaleTimeString();
|
|
187
234
|
dot.classList.remove("stale");
|
|
235
|
+
onSvgLoaded();
|
|
188
236
|
}, { once: true });
|
|
189
237
|
next.setAttribute("data", SVG_FILE + "?t=" + Date.now());
|
|
190
238
|
}
|
|
191
239
|
|
|
240
|
+
function readSliceMeta(doc) {
|
|
241
|
+
const el = doc.getElementById("em-slices");
|
|
242
|
+
if (!el) return null;
|
|
243
|
+
try { return JSON.parse(el.textContent); } catch { return null; }
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The currently-loaded SVG's own document + root element, or null before
|
|
247
|
+
// the first load / if the served file isn't a well-formed SVG.
|
|
248
|
+
function currentSvgDoc() {
|
|
249
|
+
const doc = current.contentDocument;
|
|
250
|
+
const svgRoot = doc && doc.querySelector("svg");
|
|
251
|
+
return doc && svgRoot ? { doc, svgRoot } : null;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Re-read this load's slice metadata + natural viewBox, then reapply
|
|
255
|
+
// whatever storyboard state was already in effect (a fresh SSE push mid-
|
|
256
|
+
// review shouldn't reset the facilitator's position).
|
|
257
|
+
function onSvgLoaded() {
|
|
258
|
+
const loaded = currentSvgDoc();
|
|
259
|
+
if (!loaded) return;
|
|
260
|
+
|
|
261
|
+
naturalViewBox = loaded.svgRoot.getAttribute("viewBox");
|
|
262
|
+
const meta = readSliceMeta(loaded.doc);
|
|
263
|
+
slices = (meta && meta.slices) || [];
|
|
264
|
+
rowLabels = (meta && meta.rowLabels) || null;
|
|
265
|
+
// The model may have changed shape (slice added/removed) between loads.
|
|
266
|
+
if (activeSlice !== null && !slices.some((s) => s.index === activeSlice)) {
|
|
267
|
+
activeSlice = slices.length ? slices[0].index : null;
|
|
268
|
+
}
|
|
269
|
+
render();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Apply reviewMode/activeSlice to the currently-loaded SVG + the control bar.
|
|
273
|
+
// Safe to call directly from UI handlers (Prev/Next/toggle/arrow keys) — it
|
|
274
|
+
// never re-reads metadata, just re-applies the cached state.
|
|
275
|
+
function render() {
|
|
276
|
+
storyboard.hidden = slices.length === 0;
|
|
277
|
+
reviewToggle.classList.toggle("active", reviewMode);
|
|
278
|
+
reviewToggle.disabled = slices.length === 0;
|
|
279
|
+
renderFilmstrip();
|
|
280
|
+
|
|
281
|
+
const loaded = currentSvgDoc();
|
|
282
|
+
if (!loaded) return;
|
|
283
|
+
const { doc, svgRoot } = loaded;
|
|
284
|
+
|
|
285
|
+
doc.querySelectorAll("[data-slice]").forEach((node) => {
|
|
286
|
+
const dim = reviewMode && activeSlice !== null &&
|
|
287
|
+
Number(node.getAttribute("data-slice")) !== activeSlice;
|
|
288
|
+
node.classList.toggle("em-slice-dim", dim);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
if (!naturalViewBox) return;
|
|
292
|
+
if (!reviewMode || activeSlice === null) {
|
|
293
|
+
svgRoot.setAttribute("viewBox", naturalViewBox);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const slice = slices.find((s) => s.index === activeSlice);
|
|
297
|
+
if (!slice) return;
|
|
298
|
+
const [nx, ny, nw, nh] = naturalViewBox.split(/\\s+/).map(Number);
|
|
299
|
+
const PAD = 24; // breathing room around the framed slice
|
|
300
|
+
let x0 = rowLabels ? Math.min(rowLabels.x0, slice.x0) : slice.x0;
|
|
301
|
+
let x1 = slice.x1;
|
|
302
|
+
x0 = Math.max(nx, x0 - PAD);
|
|
303
|
+
x1 = Math.min(nx + nw, x1 + PAD);
|
|
304
|
+
svgRoot.setAttribute("viewBox", x0 + " " + ny + " " + Math.max(1, x1 - x0) + " " + nh);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function renderFilmstrip() {
|
|
308
|
+
filmstrip.innerHTML = "";
|
|
309
|
+
for (const s of slices) {
|
|
310
|
+
const b = document.createElement("button");
|
|
311
|
+
b.className = "slice" + (s.index === activeSlice ? " active" : "");
|
|
312
|
+
b.textContent = s.name;
|
|
313
|
+
b.addEventListener("click", () => goTo(s.index));
|
|
314
|
+
filmstrip.appendChild(b);
|
|
315
|
+
}
|
|
316
|
+
const first = slices[0], last = slices[slices.length - 1];
|
|
317
|
+
prevBtn.disabled = activeSlice === null || !first || activeSlice <= first.index;
|
|
318
|
+
nextBtn.disabled = activeSlice === null || !last || activeSlice >= last.index;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function goTo(index) {
|
|
322
|
+
activeSlice = index;
|
|
323
|
+
render();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function step(delta) {
|
|
327
|
+
if (!slices.length) return;
|
|
328
|
+
if (activeSlice === null) { goTo(slices[0].index); return; }
|
|
329
|
+
const i = slices.findIndex((s) => s.index === activeSlice);
|
|
330
|
+
const next = slices[Math.min(slices.length - 1, Math.max(0, i + delta))];
|
|
331
|
+
if (next) goTo(next.index);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
reviewToggle.addEventListener("click", () => {
|
|
335
|
+
reviewMode = !reviewMode;
|
|
336
|
+
if (reviewMode && activeSlice === null && slices.length) activeSlice = slices[0].index;
|
|
337
|
+
render();
|
|
338
|
+
});
|
|
339
|
+
prevBtn.addEventListener("click", () => step(-1));
|
|
340
|
+
nextBtn.addEventListener("click", () => step(1));
|
|
341
|
+
document.addEventListener("keydown", (e) => {
|
|
342
|
+
if (!reviewMode) return;
|
|
343
|
+
if (e.key === "ArrowLeft") step(-1);
|
|
344
|
+
else if (e.key === "ArrowRight") step(1);
|
|
345
|
+
});
|
|
346
|
+
|
|
192
347
|
reload();
|
|
193
348
|
|
|
194
349
|
const es = new EventSource("/__events");
|