rapidoc 11.0.1 → 12.1.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/README.md +171 -54
- package/dist/rapidoc-flow-min.js +580 -0
- package/dist/rapidoc-flow-min.js.LICENSE.txt +171 -0
- package/dist/rapidoc-min.js +645 -202
- package/dist/rapidoc-min.js.LICENSE.txt +381 -0
- package/package.json +57 -42
- package/src/README.md +150 -0
- package/src/components/README.md +91 -0
- package/src/components/api-request.ts +912 -0
- package/src/components/api-response.ts +514 -0
- package/src/components/json-tree.ts +171 -0
- package/src/components/schema-base-element.ts +329 -0
- package/src/components/schema-table.ts +475 -0
- package/src/components/schema-tree.ts +393 -0
- package/src/components/tag-input.ts +180 -0
- package/src/flow.js +3 -0
- package/src/index.js +6 -0
- package/src/oauth-receiver.ts +29 -0
- package/src/rapi-flow.ts +1178 -0
- package/src/rapidoc-mini.ts +603 -0
- package/src/rapidoc.ts +1854 -0
- package/src/styles/README.md +96 -0
- package/src/styles/auth-styles.js +318 -0
- package/src/styles/border-styles.js +24 -0
- package/src/styles/css-bundle.js +129 -0
- package/src/styles/custom-styles.js +8 -0
- package/src/styles/dialog-box-styles.js +102 -0
- package/src/styles/endpoint-styles.js +564 -0
- package/src/styles/flex-styles.js +16 -0
- package/src/styles/flow-styles.js +248 -0
- package/src/styles/font-styles.js +432 -0
- package/src/styles/info-styles.js +36 -0
- package/src/styles/input-styles.js +555 -0
- package/src/styles/layout-styles.js +396 -0
- package/src/styles/microlighter-styles.js +99 -0
- package/src/styles/nav-styles.js +633 -0
- package/src/styles/param-list-styles.js +261 -0
- package/src/styles/schema-styles.js +296 -0
- package/src/styles/tab-styles.js +106 -0
- package/src/styles/theme-tokens.js +582 -0
- package/src/templates/README.md +89 -0
- package/src/templates/advanced-search-dialog-template.js +94 -0
- package/src/templates/api-response-template.js +223 -0
- package/src/templates/callback-template.js +86 -0
- package/src/templates/code-samples-template.js +52 -0
- package/src/templates/components-template.js +86 -0
- package/src/templates/downloads-template.js +70 -0
- package/src/templates/endpoint-template.js +377 -0
- package/src/templates/expanded-endpoint-template.js +270 -0
- package/src/templates/focused-endpoint-template.js +146 -0
- package/src/templates/header-template.js +92 -0
- package/src/templates/logo-template.js +21 -0
- package/src/templates/main-body-template.js +102 -0
- package/src/templates/navbar-template.js +498 -0
- package/src/templates/overview-template.js +104 -0
- package/src/templates/request-body-template.js +673 -0
- package/src/templates/request-params-template.js +485 -0
- package/src/templates/security-scheme-template.js +1272 -0
- package/src/templates/server-template.js +153 -0
- package/src/utils/README.md +146 -0
- package/src/utils/arazzo/README.md +119 -0
- package/src/utils/arazzo/components.js +15 -0
- package/src/utils/arazzo/graph.js +269 -0
- package/src/utils/arazzo/index.js +52 -0
- package/src/utils/arazzo/operations.js +106 -0
- package/src/utils/arazzo/parse.js +196 -0
- package/src/utils/arazzo/refs.js +40 -0
- package/src/utils/arazzo/svg.js +707 -0
- package/src/utils/auto-color-scheme.js +99 -0
- package/src/utils/bind-navbar.js +175 -0
- package/src/utils/color-utils.js +59 -0
- package/src/utils/common-utils.js +191 -0
- package/src/utils/curl-utils.js +126 -0
- package/src/utils/highlighter.js +419 -0
- package/src/utils/markdown-utils.js +110 -0
- package/src/utils/mcp/README.md +155 -0
- package/src/utils/mcp/common.js +166 -0
- package/src/utils/mcp/manifest.js +382 -0
- package/src/utils/mcp/project/server.ts +53 -0
- package/src/utils/mcp/project.js +88 -0
- package/src/utils/mcp/runtime.js +289 -0
- package/src/utils/mcp/schema.js +98 -0
- package/src/utils/mcp/workflow-runtime.js +374 -0
- package/src/utils/mcp/workflows.js +240 -0
- package/src/utils/mcp/zip.js +102 -0
- package/src/utils/mcp-generator.js +47 -0
- package/src/utils/mock-interceptor.js +557 -0
- package/src/utils/oauth-utils.js +87 -0
- package/src/utils/request-utils.js +264 -0
- package/src/utils/response-utils.js +203 -0
- package/src/utils/sanitize.js +57 -0
- package/src/utils/schema-utils.js +1954 -0
- package/src/utils/slugger.js +187 -0
- package/src/utils/spec-parser.js +941 -0
- package/src/utils/url-utils.js +246 -0
- package/src/utils/xml-utils.js +61 -0
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lays out a flow graph (see graph.js) and draws it as an SVG string. No DOM: text widths are
|
|
3
|
+
* estimated from character counts, so the same code runs in Node and the browser.
|
|
4
|
+
*
|
|
5
|
+
* Steps are stacked top to bottom between the workflow's inputs and outputs. Values passed between
|
|
6
|
+
* steps run in lanes on the right, and a goto draws a line through the lanes on the left to its
|
|
7
|
+
* target step. The inputs box (each input's type, required flag, default and enum values) and
|
|
8
|
+
* the outputs table are the same in both views; the views differ in the step boxes:
|
|
9
|
+
* - "expanded" (default): each step lists the values it sends and where they come from (input,
|
|
10
|
+
* step, fixed or expr), its checks, its actions as "condition → what happens", and the outputs
|
|
11
|
+
* something reads. Value lines run from the output row to the line that reads it, unlabelled.
|
|
12
|
+
* - "compact": each step shows only its name, method, path and number of checks; values passed
|
|
13
|
+
* to steps are labelled lines.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const BOX_W = 320; // compact
|
|
17
|
+
const BOX_MIN = 360; // expanded, grows with its longest line up to BOX_MAX
|
|
18
|
+
const BOX_MAX = 560;
|
|
19
|
+
const PAD = 16;
|
|
20
|
+
const ROW = 18;
|
|
21
|
+
const HEADER_H = 52; // a step's title and method/path lines
|
|
22
|
+
const GAP = 40;
|
|
23
|
+
const LANE = 20;
|
|
24
|
+
const MARGIN = 16;
|
|
25
|
+
const COMPACT_GOTO_Y = 30; // compact: first goto line leaves the box's left edge this far down
|
|
26
|
+
const ARRIVAL_ZONE = 18; // top strip of a step's left edge where goto lines arrive
|
|
27
|
+
const MAX_BINDINGS = 10; // more are summed up in one line; the step details list them all
|
|
28
|
+
const CHAR_W = 7.2; // average advance of a 12-13px sans character
|
|
29
|
+
const MONO_W = 7.4; // 12px monospace
|
|
30
|
+
const PILL_CHAR_W = 6.4; // 11px sans
|
|
31
|
+
|
|
32
|
+
export function escapeXml(text) {
|
|
33
|
+
return String(text ?? "")
|
|
34
|
+
.replace(/&/g, "&")
|
|
35
|
+
.replace(/</g, "<")
|
|
36
|
+
.replace(/>/g, ">")
|
|
37
|
+
.replace(/"/g, """)
|
|
38
|
+
.replace(/'/g, "'");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function truncate(text, maxChars) {
|
|
42
|
+
const s = String(text ?? "");
|
|
43
|
+
if (maxChars < 2) return "";
|
|
44
|
+
return s.length <= maxChars ? s : `${s.slice(0, maxChars - 1)}…`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A short action label for pills: "✗ retry 3× after 2s (if)". */
|
|
48
|
+
export function actionLabel(a) {
|
|
49
|
+
const mark = a.outcome === "failure" ? "✗" : "✓";
|
|
50
|
+
let text;
|
|
51
|
+
if (a.type === "retry") {
|
|
52
|
+
text = "retry";
|
|
53
|
+
if (a.retryLimit) text += ` ${a.retryLimit}×`;
|
|
54
|
+
if (a.retryAfter) text += ` after ${a.retryAfter}s`;
|
|
55
|
+
if (a.stepId) text += ` from ${a.stepId}`;
|
|
56
|
+
} else if (a.type === "goto") {
|
|
57
|
+
text = `goto ${a.stepId || a.workflowId}`;
|
|
58
|
+
} else text = "end";
|
|
59
|
+
if (a.conditional) text += " (if)";
|
|
60
|
+
return `${mark} ${text}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** An action with its criteria, without the mark: "retry 3× after 1s if $statusCode == 503". */
|
|
64
|
+
export function actionSentence(a) {
|
|
65
|
+
const text = actionLabel({ ...a, conditional: false }).slice(2);
|
|
66
|
+
const when = (a.criteria || []).map((c) => c.condition).join(" and ");
|
|
67
|
+
return when ? `${text} if ${when}` : text;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Shortens one condition for a step box: "$statusCode == 503" reads "status 503". */
|
|
71
|
+
function shortCondition(condition) {
|
|
72
|
+
const m = condition.match(/^\$statusCode\s*==\s*(\d+)$/);
|
|
73
|
+
return m ? `status ${m[1]}` : condition;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* An action in words, for a step box.
|
|
78
|
+
* `{ when: "status 503", does: "retry 3×, 1s apart", name: "retryOnServiceUnavailable",
|
|
79
|
+
* tag: "workflow" }`; `name` is the action's own name ("" when the file gives none), `tag` is
|
|
80
|
+
* "workflow" for an action the step inherits and "" for the step's own. An action without criteria applies whenever the
|
|
81
|
+
* step's checks fail (or pass); `checks` is how many the step has, for the wording. After other
|
|
82
|
+
* actions (`index` > 0 in its outcome's list) it reads "otherwise".
|
|
83
|
+
*/
|
|
84
|
+
export function describeAction(a, checks = 0, index = 0) {
|
|
85
|
+
const criteria = a.criteria || [];
|
|
86
|
+
let when;
|
|
87
|
+
if (criteria.length) when = criteria.map((c) => shortCondition(c.condition)).join(" and ");
|
|
88
|
+
else if (index > 0) when = "otherwise";
|
|
89
|
+
else if (a.outcome === "failure") {
|
|
90
|
+
when = checks > 1 ? "checks fail" : checks ? "check fails" : "request fails";
|
|
91
|
+
} else when = checks > 1 ? "checks pass" : checks ? "check passes" : "request succeeds";
|
|
92
|
+
let does;
|
|
93
|
+
if (a.type === "end") does = "stop the workflow";
|
|
94
|
+
else if (a.type === "goto")
|
|
95
|
+
does = a.stepId ? `go to ${a.stepId}` : `run workflow ${a.workflowId}`;
|
|
96
|
+
else {
|
|
97
|
+
does = "retry";
|
|
98
|
+
if (a.retryLimit) does += ` ${a.retryLimit}×`;
|
|
99
|
+
if (a.retryAfter) does += `, ${a.retryAfter}s apart`;
|
|
100
|
+
if (a.stepId) does += ` from ${a.stepId}`;
|
|
101
|
+
}
|
|
102
|
+
return { when, does, name: a.name || "", tag: a.inherited ? "workflow" : "" };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Greedy lane assignment: edges whose spans overlap get different lanes. */
|
|
106
|
+
function assignLanes(edges) {
|
|
107
|
+
const lanes = [];
|
|
108
|
+
const sorted = [...edges].sort((a, b) => a.span[1] - a.span[0] - (b.span[1] - b.span[0]));
|
|
109
|
+
for (const e of sorted) {
|
|
110
|
+
let lane = 0;
|
|
111
|
+
while ((lanes[lane] || []).some((o) => e.span[0] < o.span[1] && o.span[0] < e.span[1])) lane++;
|
|
112
|
+
(lanes[lane] ||= []).push(e);
|
|
113
|
+
e.lane = lane;
|
|
114
|
+
}
|
|
115
|
+
return lanes.length;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function textWidth(text, charW) {
|
|
119
|
+
return Math.ceil(String(text).length * charW);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// A row is one line of text in a box, made of parts: { text, cls, w (char width), x | dx }.
|
|
123
|
+
// `x` places a part from the box's left edge; `dx` follows the previous part.
|
|
124
|
+
const part = (text, cls, w, place = {}) => ({ text: String(text), cls, w, ...place });
|
|
125
|
+
|
|
126
|
+
function rowExtent(row) {
|
|
127
|
+
let pos = 0;
|
|
128
|
+
const starts = row.parts.map((p) => {
|
|
129
|
+
pos = p.x !== undefined ? p.x : pos + (p.dx || 0);
|
|
130
|
+
const start = pos;
|
|
131
|
+
pos += textWidth(p.text, p.w);
|
|
132
|
+
return start;
|
|
133
|
+
});
|
|
134
|
+
return { starts, end: pos };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Room a row's right-aligned tag takes, gap included
|
|
138
|
+
const tagWidth = (row) => (row.tag ? textWidth(row.tag, row.tagW || PILL_CHAR_W) + 16 : 0);
|
|
139
|
+
|
|
140
|
+
/** Cuts a row's last part so the row, and its tag, fit the box. */
|
|
141
|
+
function fitRow(row, boxW) {
|
|
142
|
+
const { starts } = rowExtent(row);
|
|
143
|
+
const last = row.parts.at(-1);
|
|
144
|
+
if (!last) return;
|
|
145
|
+
const room = boxW - PAD - tagWidth(row) - starts.at(-1);
|
|
146
|
+
last.text = truncate(last.text, Math.floor(room / last.w));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* One row as SVG text. A row with `fromStep` (an output row, or a value line that reads a step)
|
|
151
|
+
* carries `data-from-step`; with `interactive` it is also an `rf-jump` link to that step, and the
|
|
152
|
+
* step name in its tag is focusable.
|
|
153
|
+
*/
|
|
154
|
+
function renderRow(row, boxX, boxW, baseline, interactive = false) {
|
|
155
|
+
const jump = interactive && row.fromStep;
|
|
156
|
+
const from = row.fromStep ? ` data-from-step="${escapeXml(row.fromStep)}"` : "";
|
|
157
|
+
const rowCls = jump ? "rf-row rf-jump" : "rf-row";
|
|
158
|
+
const tspans = row.parts
|
|
159
|
+
.map((p) => {
|
|
160
|
+
const place = p.x !== undefined ? ` x="${boxX + p.x}"` : p.dx ? ` dx="${p.dx}"` : "";
|
|
161
|
+
return `<tspan class="${p.cls}"${place}>${escapeXml(p.text)}</tspan>`;
|
|
162
|
+
})
|
|
163
|
+
.join("");
|
|
164
|
+
const tag = row.tag
|
|
165
|
+
? `<text class="${row.tagCls || "rf-tag"}${jump ? " rf-jump" : ""}" x="${boxX + boxW - PAD}" y="${baseline}" text-anchor="end"${from}${jump ? ` tabindex="0" role="link" aria-label="${escapeXml(`Go to step ${row.fromStep}`)}"` : ""}>${escapeXml(row.tag)}</text>`
|
|
166
|
+
: "";
|
|
167
|
+
const tip = row.tip ? `<title>${escapeXml(row.tip)}</title>` : "";
|
|
168
|
+
const rule = row.rule
|
|
169
|
+
? `<path class="rf-sep" d="M${boxX} ${baseline - 13}H${boxX + boxW}"/>`
|
|
170
|
+
: "";
|
|
171
|
+
return `${rule}<text class="${rowCls}" y="${baseline}"${from}>${tip}${tspans}</text>${tag}`;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The word before each value in a step box or the outputs box, by source kind
|
|
175
|
+
const KEYWORDS = { input: "input", step: "step", value: "fixed", expression: "expr" };
|
|
176
|
+
const KEYWORD_W = 6 * MONO_W; // "input" plus a space
|
|
177
|
+
|
|
178
|
+
function sourceParts(source, col) {
|
|
179
|
+
const keyword = part(KEYWORDS[source.kind], `rf-kw rf-kw-${source.kind}`, MONO_W, { x: col });
|
|
180
|
+
// A step output reads "<step>.<output>": the step name in the step color, the output plain
|
|
181
|
+
if (source.kind === "step") {
|
|
182
|
+
return [
|
|
183
|
+
keyword,
|
|
184
|
+
part(source.stepId, "rf-step-name", MONO_W, { x: col + KEYWORD_W }),
|
|
185
|
+
part(`.${source.name}`, "rf-bind-val", MONO_W),
|
|
186
|
+
];
|
|
187
|
+
}
|
|
188
|
+
return [keyword, part(source.text, "rf-bind-val", MONO_W, { x: col + KEYWORD_W })];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** "$response.body#/status" as "response body /status", for a step output's source. */
|
|
192
|
+
function shortExpression(expr) {
|
|
193
|
+
const text = typeof expr === "string" ? expr : JSON.stringify(expr);
|
|
194
|
+
const m = text.match(/^\$response\.(body|header)(?:#(.*)|\.(.+))?$/);
|
|
195
|
+
if (m) return `response ${m[1]}${m[2] || m[3] ? ` ${m[2] || m[3]}` : ""}`;
|
|
196
|
+
if (text === "$statusCode") return "status code";
|
|
197
|
+
return text;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* The step's own outputs that something later reads, with where they come from. Lines to the
|
|
202
|
+
* steps and outputs that read them start at these rows. Unused outputs are counted on one line.
|
|
203
|
+
*/
|
|
204
|
+
function stepOutputRows(step) {
|
|
205
|
+
if (!step.usedOutputs.length) return [];
|
|
206
|
+
const nameChars = Math.min(24, Math.max(4, ...step.usedOutputs.map((o) => o.name.length)));
|
|
207
|
+
const col = PAD + nameChars * MONO_W + 12;
|
|
208
|
+
const rows = [
|
|
209
|
+
{ parts: [part("Outputs", "rf-sec-title", PILL_CHAR_W, { x: PAD })] },
|
|
210
|
+
...step.usedOutputs.map((o) => ({
|
|
211
|
+
departs: o.name,
|
|
212
|
+
tip: typeof o.expr === "string" ? o.expr : JSON.stringify(o.expr),
|
|
213
|
+
parts: [
|
|
214
|
+
part(truncate(o.name, nameChars), "rf-bind-name", MONO_W, { x: PAD }),
|
|
215
|
+
part(shortExpression(o.expr), "rf-out-from", MONO_W, { x: col }),
|
|
216
|
+
],
|
|
217
|
+
})),
|
|
218
|
+
];
|
|
219
|
+
const unused = step.outputExprs.length - step.usedOutputs.length;
|
|
220
|
+
if (unused) {
|
|
221
|
+
rows.push({ parts: [part(`+${unused} unused`, "rf-more", PILL_CHAR_W, { x: PAD })] });
|
|
222
|
+
}
|
|
223
|
+
return rows;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The step's success criteria. Not numbered: all must pass, in no order. Regex, JSONPath and
|
|
228
|
+
* XPath ones show their type and context.
|
|
229
|
+
*/
|
|
230
|
+
function checkRows(step) {
|
|
231
|
+
if (!step.criteria.length) return [];
|
|
232
|
+
return [
|
|
233
|
+
{ parts: [part("Checks", "rf-sec-title", PILL_CHAR_W, { x: PAD })] },
|
|
234
|
+
...step.criteria.map((c) => ({
|
|
235
|
+
parts: [
|
|
236
|
+
...(c.type !== "simple"
|
|
237
|
+
? [
|
|
238
|
+
part(`${c.type}${c.context ? ` on ${c.context}` : ""}:`, "rf-check-type", MONO_W, {
|
|
239
|
+
x: PAD,
|
|
240
|
+
}),
|
|
241
|
+
]
|
|
242
|
+
: []),
|
|
243
|
+
part(c.condition, "rf-check", MONO_W, c.type !== "simple" ? { dx: 8 } : { x: PAD }),
|
|
244
|
+
],
|
|
245
|
+
})),
|
|
246
|
+
];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* "On success" and "On failure" sections: one line per action, in the order a runner checks.
|
|
251
|
+
* Actions that can never run (`shadowed`) are left out; the graph's `warnings` name them.
|
|
252
|
+
*/
|
|
253
|
+
function actionSections(step) {
|
|
254
|
+
return ["success", "failure"]
|
|
255
|
+
.map((outcome) => {
|
|
256
|
+
const actions = step.actions.filter((a) => a.outcome === outcome && !a.shadowed);
|
|
257
|
+
if (!actions.length) return [];
|
|
258
|
+
const described = actions.map((a, i) => describeAction(a, step.criteria.length, i));
|
|
259
|
+
const whenChars = Math.min(34, Math.max(...described.map((d) => d.when.length)));
|
|
260
|
+
const col = PAD + whenChars * CHAR_W + 12;
|
|
261
|
+
return [
|
|
262
|
+
{
|
|
263
|
+
parts: [
|
|
264
|
+
part(`On ${outcome} · first match runs`, "rf-sec-title", PILL_CHAR_W, { x: PAD }),
|
|
265
|
+
],
|
|
266
|
+
},
|
|
267
|
+
...actions.map((a, i) => ({
|
|
268
|
+
action: a,
|
|
269
|
+
tag: described[i].tag,
|
|
270
|
+
parts: [
|
|
271
|
+
part(truncate(described[i].when, whenChars), "rf-act-when", CHAR_W, { x: PAD }),
|
|
272
|
+
part(`→ ${described[i].does}`, "rf-act-then", CHAR_W, { x: col }),
|
|
273
|
+
// The name often says what the action is for; it is cut first when the line is long
|
|
274
|
+
...(described[i].name
|
|
275
|
+
? [part(`(${described[i].name})`, "rf-act-name", CHAR_W, { dx: 8 })]
|
|
276
|
+
: []),
|
|
277
|
+
],
|
|
278
|
+
})),
|
|
279
|
+
];
|
|
280
|
+
})
|
|
281
|
+
.filter((rows) => rows.length);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function inputRows(graph) {
|
|
285
|
+
const nameChars = Math.min(22, Math.max(4, ...graph.inputDetails.map((i) => i.name.length)));
|
|
286
|
+
const col = PAD + nameChars * CHAR_W + 16;
|
|
287
|
+
const rows = [];
|
|
288
|
+
for (const i of graph.inputDetails) {
|
|
289
|
+
const parts = [part(truncate(i.name, nameChars), "rf-in-name", CHAR_W, { x: PAD })];
|
|
290
|
+
// As <rapi-doc> labels it (schema-utils getTypeInfo): a string enum reads "enum"
|
|
291
|
+
const base = i.enum?.length ? i.type.replace("string", "enum") : i.type;
|
|
292
|
+
const type = base ? `${base}${i.format ? ` (${i.format})` : ""}` : "any";
|
|
293
|
+
parts.push(part(type, "rf-in-type", MONO_W, { x: col }));
|
|
294
|
+
if (i.required) parts.push(part("required", "rf-req", PILL_CHAR_W, { dx: 8 }));
|
|
295
|
+
if (i.default !== undefined) {
|
|
296
|
+
const value = typeof i.default === "string" ? i.default : JSON.stringify(i.default);
|
|
297
|
+
parts.push(part(`= ${value}`, "rf-in-default", MONO_W, { dx: 8 }));
|
|
298
|
+
}
|
|
299
|
+
rows.push({ parts, tip: i.description });
|
|
300
|
+
if (i.enum?.length) {
|
|
301
|
+
const values = i.enum.map((v) => (typeof v === "string" ? v : JSON.stringify(v)));
|
|
302
|
+
rows.push({ parts: [part(values.join(" | "), "rf-in-enum", MONO_W, { x: col })] });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return rows;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function bindingRows(step) {
|
|
309
|
+
const shown = step.bindings.slice(0, MAX_BINDINGS);
|
|
310
|
+
const nameChars = Math.min(18, Math.max(4, ...shown.map((b) => b.name.length)));
|
|
311
|
+
const col = PAD + nameChars * MONO_W + 12;
|
|
312
|
+
const rows = shown.map((b) => ({
|
|
313
|
+
arrivals: b.fromOutputs,
|
|
314
|
+
fromStep: b.fromSteps[0],
|
|
315
|
+
parts: [
|
|
316
|
+
part(truncate(b.name, nameChars), "rf-bind-name", MONO_W, { x: PAD }),
|
|
317
|
+
...sourceParts(b.source, col),
|
|
318
|
+
],
|
|
319
|
+
}));
|
|
320
|
+
const hidden = step.bindings.slice(MAX_BINDINGS);
|
|
321
|
+
if (hidden.length) {
|
|
322
|
+
rows.push({
|
|
323
|
+
arrivals: hidden.flatMap((b) => b.fromOutputs),
|
|
324
|
+
parts: [
|
|
325
|
+
part(`+${hidden.length} more in the step details`, "rf-more", PILL_CHAR_W, { x: PAD }),
|
|
326
|
+
],
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return rows;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The outputs box as a table: one flat column of output names (the workflow's result is a flat
|
|
334
|
+
* object), and on the right the step each group comes from, shown once per group, with a rule
|
|
335
|
+
* between groups. Groups follow the order each step first appears; outputs not taken from a step
|
|
336
|
+
* come last, their source kind in the step column. A step output with another name follows the
|
|
337
|
+
* output's name as "← <name>". A step's line arrives at its group's first row, beside the name.
|
|
338
|
+
*/
|
|
339
|
+
function outputRows(graph) {
|
|
340
|
+
const groups = new Map();
|
|
341
|
+
for (const o of graph.outputs) {
|
|
342
|
+
const source = o.source || { kind: "expression", text: o.expr };
|
|
343
|
+
const key = o.stepId ? `step:${o.stepId}` : `kind:${source.kind}`;
|
|
344
|
+
if (!groups.has(key)) {
|
|
345
|
+
groups.set(key, { stepId: o.stepId, title: o.stepId || KEYWORDS[source.kind], items: [] });
|
|
346
|
+
}
|
|
347
|
+
groups.get(key).items.push({ ...o, source });
|
|
348
|
+
}
|
|
349
|
+
const ordered = [...groups.values()].sort((x, y) => Number(!x.stepId) - Number(!y.stepId));
|
|
350
|
+
return ordered.flatMap((g, gi) =>
|
|
351
|
+
g.items.map((o, i) => {
|
|
352
|
+
let from = "";
|
|
353
|
+
if (o.source.kind === "step") from = o.output && o.output !== o.name ? `← ${o.output}` : "";
|
|
354
|
+
else from = `${o.source.kind === "value" ? "=" : "←"} ${o.source.text}`;
|
|
355
|
+
return {
|
|
356
|
+
fromStep: g.stepId,
|
|
357
|
+
rule: gi > 0 && i === 0,
|
|
358
|
+
...(i === 0
|
|
359
|
+
? {
|
|
360
|
+
tag: g.title,
|
|
361
|
+
tagCls: g.stepId ? "rf-tag rf-out-step" : "rf-tag",
|
|
362
|
+
tagW: g.stepId ? CHAR_W : PILL_CHAR_W,
|
|
363
|
+
arrivals: g.items
|
|
364
|
+
.filter((x) => x.stepId && x.output)
|
|
365
|
+
.map((x) => ({ stepId: x.stepId, name: x.output })),
|
|
366
|
+
}
|
|
367
|
+
: {}),
|
|
368
|
+
parts: [
|
|
369
|
+
part(o.name, "rf-out-name", CHAR_W, { x: PAD }),
|
|
370
|
+
...(from ? [part(from, "rf-out-from", MONO_W, { dx: 8 })] : []),
|
|
371
|
+
],
|
|
372
|
+
};
|
|
373
|
+
}),
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function headerWidth(step) {
|
|
378
|
+
const op = step.operation;
|
|
379
|
+
const sub = op
|
|
380
|
+
? textWidth(op.method, MONO_W) + 8 + textWidth(op.path, MONO_W)
|
|
381
|
+
: textWidth(step.workflowId || step.operationRef, MONO_W) + 90;
|
|
382
|
+
return Math.max(textWidth(step.stepId, CHAR_W) + 90, sub) + PAD * 2;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Draws the diagram and reports which kinds of lines it drew, for a legend.
|
|
387
|
+
*
|
|
388
|
+
* @param {object} graph from buildFlowGraph()
|
|
389
|
+
* @param {object} [options]
|
|
390
|
+
* @param {'expanded'|'compact'} [options.view='expanded']
|
|
391
|
+
* @param {string} [options.idPrefix='rf'] prefix for marker ids, unique per page
|
|
392
|
+
* @param {boolean} [options.interactive=false] make operation steps focusable buttons
|
|
393
|
+
* @returns {{ svg: string, legend: string[] }} legend entries in drawing order, from
|
|
394
|
+
* `seq` (next step), `data` (value to a later step), `output` (value to the outputs box),
|
|
395
|
+
* `success` / `failure` (goto lines)
|
|
396
|
+
*/
|
|
397
|
+
export function renderFlowDiagram(graph, options = {}) {
|
|
398
|
+
if (!graph) return { svg: "", legend: [] };
|
|
399
|
+
const { idPrefix = "rf", interactive = false } = options;
|
|
400
|
+
const expanded = options.view !== "compact";
|
|
401
|
+
const drawn = new Set();
|
|
402
|
+
|
|
403
|
+
// ---- Nodes, top to bottom, with their rows ----
|
|
404
|
+
const nodes = [];
|
|
405
|
+
// The inputs and outputs boxes are the same in both views; only step boxes are compacted
|
|
406
|
+
if (graph.inputs.length) {
|
|
407
|
+
nodes.push({ key: "inputs", kind: "io", title: "Inputs", rows: inputRows(graph) });
|
|
408
|
+
}
|
|
409
|
+
for (const step of graph.steps) {
|
|
410
|
+
if (expanded) {
|
|
411
|
+
nodes.push({
|
|
412
|
+
key: step.stepId,
|
|
413
|
+
kind: "step",
|
|
414
|
+
step,
|
|
415
|
+
sections: [
|
|
416
|
+
bindingRows(step),
|
|
417
|
+
checkRows(step),
|
|
418
|
+
...actionSections(step),
|
|
419
|
+
stepOutputRows(step),
|
|
420
|
+
].filter((rows) => rows.length),
|
|
421
|
+
});
|
|
422
|
+
} else {
|
|
423
|
+
// Compact boxes show no actions; a step's own gotos still draw their lines
|
|
424
|
+
const gotos = step.actions.filter(
|
|
425
|
+
(a) => !a.inherited && !a.shadowed && a.type === "goto" && a.stepId,
|
|
426
|
+
);
|
|
427
|
+
nodes.push({ key: step.stepId, kind: "step", step, gotos, sections: [] });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
if (graph.outputs.length) {
|
|
431
|
+
nodes.push({ key: "outputs", kind: "io", title: "Outputs", rows: outputRows(graph) });
|
|
432
|
+
}
|
|
433
|
+
const index = new Map(nodes.map((n, i) => [n.key, i]));
|
|
434
|
+
const rowsOf = (n) => (n.kind === "io" ? n.rows : n.sections.flat());
|
|
435
|
+
|
|
436
|
+
// ---- Box width: the longest line, at least BOX_MIN (expanded) or BOX_W (compact) ----
|
|
437
|
+
const widths = nodes.flatMap((n) => [
|
|
438
|
+
...rowsOf(n).map((r) => rowExtent(r).end + tagWidth(r) + PAD),
|
|
439
|
+
n.kind === "step" && expanded ? headerWidth(n.step) : 0,
|
|
440
|
+
]);
|
|
441
|
+
const boxW = Math.min(BOX_MAX, Math.max(expanded ? BOX_MIN : BOX_W, ...widths));
|
|
442
|
+
for (const n of nodes) for (const r of rowsOf(n)) fitRow(r, boxW);
|
|
443
|
+
const boxChars = Math.floor((boxW - 32) / CHAR_W);
|
|
444
|
+
|
|
445
|
+
// ---- Heights and row baselines (relative to the node's top) ----
|
|
446
|
+
for (const n of nodes) {
|
|
447
|
+
if (n.kind === "io") {
|
|
448
|
+
n.rows.forEach((r, ri) => (r.by = 40 + ri * ROW));
|
|
449
|
+
n.h = 34 + n.rows.length * ROW;
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
if (!expanded) {
|
|
453
|
+
n.h = Math.max(60, COMPACT_GOTO_Y + n.gotos.length * 8 + 4);
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
let bottom = HEADER_H;
|
|
457
|
+
n.separators = [];
|
|
458
|
+
for (const rows of n.sections) {
|
|
459
|
+
const sepY = bottom + 6;
|
|
460
|
+
n.separators.push(sepY);
|
|
461
|
+
rows.forEach((r, ri) => (r.by = sepY + 18 + ri * ROW));
|
|
462
|
+
bottom = sepY + 18 + (rows.length - 1) * ROW + 6;
|
|
463
|
+
}
|
|
464
|
+
n.h = Math.max(60, bottom + 8);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// ---- Right-hand data edges ----
|
|
468
|
+
// Compact: one lane per source step and label, branching to the middle of each target.
|
|
469
|
+
// Expanded: one lane per source step, branching to each line that uses one of its outputs.
|
|
470
|
+
const groups = new Map();
|
|
471
|
+
const addTarget = (fromIdx, toIdx, label, rowBy, outputName) => {
|
|
472
|
+
const output = nodes[toIdx].key === "outputs";
|
|
473
|
+
const key = `${fromIdx}\u0000${output}\u0000${label}`;
|
|
474
|
+
const group = groups.get(key) || {
|
|
475
|
+
from: fromIdx,
|
|
476
|
+
label,
|
|
477
|
+
output,
|
|
478
|
+
targets: [],
|
|
479
|
+
departs: new Set(),
|
|
480
|
+
};
|
|
481
|
+
if (!group.targets.some((t) => t.node === toIdx && t.by === rowBy)) {
|
|
482
|
+
group.targets.push({ node: toIdx, by: rowBy });
|
|
483
|
+
}
|
|
484
|
+
if (outputName) group.departs.add(outputName);
|
|
485
|
+
groups.set(key, group);
|
|
486
|
+
};
|
|
487
|
+
if (expanded) {
|
|
488
|
+
// Lines start at each output row of the source step that is read
|
|
489
|
+
nodes.forEach((n, j) => {
|
|
490
|
+
for (const r of rowsOf(n)) {
|
|
491
|
+
for (const ref of r.arrivals || []) {
|
|
492
|
+
const i = index.get(ref.stepId);
|
|
493
|
+
if (i !== undefined && i < j) addTarget(i, j, "", r.by, ref.name);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
});
|
|
497
|
+
} else {
|
|
498
|
+
for (const e of graph.dataEdges) {
|
|
499
|
+
if (!index.has(e.from) || !index.has(e.to) || index.get(e.from) >= index.get(e.to)) continue;
|
|
500
|
+
if (e.to === "outputs") {
|
|
501
|
+
// Into the outputs table: arrive at the step's group, as in the expanded view
|
|
502
|
+
const row = rowsOf(nodes[index.get("outputs")]).find((r) =>
|
|
503
|
+
r.arrivals?.some((ref) => ref.stepId === e.from),
|
|
504
|
+
);
|
|
505
|
+
addTarget(index.get(e.from), index.get(e.to), "", row?.by);
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
addTarget(index.get(e.from), index.get(e.to), truncate(e.labels.join(", "), 28), undefined);
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const dataEdges = [...groups.values()].map((g) => {
|
|
512
|
+
const targets = g.targets.sort((a, b) => a.node - b.node || (a.by ?? 0) - (b.by ?? 0));
|
|
513
|
+
return { ...g, targets, span: [g.from, targets.at(-1).node] };
|
|
514
|
+
});
|
|
515
|
+
const dataLaneCount = assignLanes(dataEdges);
|
|
516
|
+
|
|
517
|
+
// ---- Left-hand goto edges, from the box's left edge (compact) or an action line (expanded) ----
|
|
518
|
+
const gotoEdges = [];
|
|
519
|
+
nodes.forEach((n, i) => {
|
|
520
|
+
if (n.kind !== "step") return;
|
|
521
|
+
const sources = expanded
|
|
522
|
+
? rowsOf(n)
|
|
523
|
+
.filter((r) => r.action)
|
|
524
|
+
.map((r) => ({ action: r.action, by: r.by }))
|
|
525
|
+
: n.gotos.map((a, gi) => ({ action: a, by: COMPACT_GOTO_Y + gi * 8 + 4 }));
|
|
526
|
+
for (const src of sources) {
|
|
527
|
+
const target = src.action.type === "goto" ? src.action.stepId : "";
|
|
528
|
+
if (!target || !index.has(target) || target === n.key) continue;
|
|
529
|
+
const j = index.get(target);
|
|
530
|
+
gotoEdges.push({ ...src, from: i, to: j, span: [Math.min(i, j), Math.max(i, j)] });
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
const gotoLaneCount = assignLanes(gotoEdges);
|
|
534
|
+
|
|
535
|
+
// ---- Horizontal geometry ----
|
|
536
|
+
const gotoW = gotoLaneCount ? gotoLaneCount * LANE + 8 : 0;
|
|
537
|
+
const boxX = MARGIN + gotoW;
|
|
538
|
+
const boxR = boxX + boxW;
|
|
539
|
+
const laneX = (lane) => boxR + 20 + lane * LANE;
|
|
540
|
+
const labelW = Math.max(
|
|
541
|
+
8,
|
|
542
|
+
...dataEdges.map((e) => (e.label ? textWidth(e.label, PILL_CHAR_W) + 14 : 0)),
|
|
543
|
+
);
|
|
544
|
+
const width = (dataLaneCount ? laneX(dataLaneCount - 1) + labelW : boxR) + MARGIN;
|
|
545
|
+
|
|
546
|
+
// ---- Vertical geometry ----
|
|
547
|
+
let y = MARGIN;
|
|
548
|
+
for (const n of nodes) {
|
|
549
|
+
n.y = y;
|
|
550
|
+
n.cy = y + n.h / 2;
|
|
551
|
+
y += n.h + GAP;
|
|
552
|
+
}
|
|
553
|
+
const height = y - GAP + MARGIN;
|
|
554
|
+
|
|
555
|
+
// Connection points on the right edge. Compact: arrivals above the middle, departures below.
|
|
556
|
+
// Expanded: departures beside the header, arrivals at the line that uses the value.
|
|
557
|
+
const ports = new Map();
|
|
558
|
+
const portY = (nodeIdx, dir, by) => {
|
|
559
|
+
const n = nodes[nodeIdx];
|
|
560
|
+
if (dir === "in" && by !== undefined) return n.y + by - 4;
|
|
561
|
+
const key = `${nodeIdx}:${dir}`;
|
|
562
|
+
const used = ports.get(key) || 0;
|
|
563
|
+
ports.set(key, used + 1);
|
|
564
|
+
if (expanded && dir === "out") return n.y + 18 + used * 8;
|
|
565
|
+
return dir === "in" ? n.cy - 6 - used * 8 : n.cy + 6 + used * 8;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
const out = [];
|
|
569
|
+
const marker = (id, cls) =>
|
|
570
|
+
`<marker id="${idPrefix}-${id}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0 0L10 5L0 10z" class="${cls}"/></marker>`;
|
|
571
|
+
out.push(
|
|
572
|
+
`<svg xmlns="http://www.w3.org/2000/svg" class="rf-svg rf-${expanded ? "expanded" : "compact"}" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="group" aria-label="${escapeXml(`Workflow ${graph.workflowId}`)}">`,
|
|
573
|
+
`<defs>${marker("seq", "rf-head-seq")}${marker("data", "rf-head-data")}${marker("output", "rf-head-output")}${marker("success", "rf-head-success")}${marker("failure", "rf-head-failure")}</defs>`,
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
// Sequence arrows. A step whose success always ends or jumps never falls through to the next
|
|
577
|
+
// step, so no arrow is drawn into the next step (the outputs box still gets one).
|
|
578
|
+
const stopsOnSuccess = (n) =>
|
|
579
|
+
n.kind === "step" &&
|
|
580
|
+
n.step.actions.some(
|
|
581
|
+
(a) => a.outcome === "success" && !a.conditional && (a.type === "end" || a.type === "goto"),
|
|
582
|
+
);
|
|
583
|
+
for (let i = 0; i + 1 < nodes.length; i++) {
|
|
584
|
+
if (nodes[i + 1].kind === "step" && stopsOnSuccess(nodes[i])) continue;
|
|
585
|
+
const cx = boxX + boxW / 2;
|
|
586
|
+
drawn.add("seq");
|
|
587
|
+
out.push(
|
|
588
|
+
`<path class="rf-seq" d="M${cx} ${nodes[i].y + nodes[i].h}V${nodes[i + 1].y - 2}" marker-end="url(#${idPrefix}-seq)"/>`,
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Data edges and their labels
|
|
593
|
+
const placedLabels = [];
|
|
594
|
+
for (const e of dataEdges) {
|
|
595
|
+
const x = laneX(e.lane);
|
|
596
|
+
const cls = e.output ? "rf-data rf-to-output" : "rf-data";
|
|
597
|
+
const head = e.output ? "output" : "data";
|
|
598
|
+
drawn.add(head);
|
|
599
|
+
const ys = e.targets.map((t) => portY(t.node, "in", t.by));
|
|
600
|
+
const source = nodes[e.from];
|
|
601
|
+
const departs = expanded
|
|
602
|
+
? rowsOf(source)
|
|
603
|
+
.filter((r) => r.departs && e.departs.has(r.departs))
|
|
604
|
+
.map((r) => source.y + r.by - 4)
|
|
605
|
+
: [];
|
|
606
|
+
if (!departs.length) departs.push(portY(e.from, "out"));
|
|
607
|
+
const y1 = Math.min(...departs);
|
|
608
|
+
const from = ` data-from-step="${escapeXml(source.key)}"`;
|
|
609
|
+
for (const dy of departs) out.push(`<path class="${cls}"${from} d="M${boxR} ${dy}H${x}"/>`);
|
|
610
|
+
out.push(`<path class="${cls}"${from} d="M${x} ${y1}V${Math.max(...ys)}"/>`);
|
|
611
|
+
for (const ty of ys) {
|
|
612
|
+
out.push(
|
|
613
|
+
`<path class="${cls}"${from} d="M${x} ${ty}H${boxR + 2}" marker-end="url(#${idPrefix}-${head})"/>`,
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
if (!e.label) continue;
|
|
617
|
+
const w = textWidth(e.label, PILL_CHAR_W) + 12;
|
|
618
|
+
let ly = (y1 + ys[0]) / 2 - 9;
|
|
619
|
+
while (placedLabels.some((l) => x < l.x + l.w && l.x < x + w && ly < l.y + 20 && l.y < ly + 20))
|
|
620
|
+
ly += 20;
|
|
621
|
+
placedLabels.push({ x, y: ly, w });
|
|
622
|
+
out.push(
|
|
623
|
+
`<g class="rf-data-label"><rect x="${x + 4}" y="${ly}" width="${w}" height="18" rx="9"/><text x="${x + 10}" y="${ly + 13}">${escapeXml(e.label)}</text></g>`,
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Goto edges
|
|
628
|
+
for (const e of gotoEdges) {
|
|
629
|
+
const from = nodes[e.from];
|
|
630
|
+
const to = nodes[e.to];
|
|
631
|
+
const x = MARGIN + (gotoLaneCount - 1 - e.lane) * LANE + 4;
|
|
632
|
+
const py = from.y + e.by - 4;
|
|
633
|
+
const start = boxX;
|
|
634
|
+
const ty = to.y + ARRIVAL_ZONE / 2;
|
|
635
|
+
drawn.add(e.action.outcome);
|
|
636
|
+
out.push(
|
|
637
|
+
`<path class="rf-goto rf-${e.action.outcome}" d="M${start} ${py}H${x}V${ty}H${boxX - 2}" marker-end="url(#${idPrefix}-${e.action.outcome})"/>`,
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// Nodes
|
|
642
|
+
for (const n of nodes) {
|
|
643
|
+
if (n.kind === "io") {
|
|
644
|
+
out.push(
|
|
645
|
+
`<g class="rf-node rf-io rf-${n.key}"><rect x="${boxX}" y="${n.y}" width="${boxW}" height="${n.h}" rx="8"/>`,
|
|
646
|
+
`<text class="rf-io-title" x="${boxX + PAD}" y="${n.y + 20}">${n.title}</text>`,
|
|
647
|
+
n.key === "outputs" && graph.outputs.some((o) => o.stepId)
|
|
648
|
+
? `<text class="rf-io-title" x="${boxX + boxW - PAD}" y="${n.y + 20}" text-anchor="end">From step</text>`
|
|
649
|
+
: "",
|
|
650
|
+
...n.rows.map((r) => renderRow(r, boxX, boxW, n.y + r.by, interactive)),
|
|
651
|
+
`</g>`,
|
|
652
|
+
);
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
const s = n.step;
|
|
656
|
+
const op = s.operation;
|
|
657
|
+
const clickable = interactive && (op || s.kind === "workflow");
|
|
658
|
+
let sub;
|
|
659
|
+
if (s.kind === "workflow") {
|
|
660
|
+
sub = `<text class="rf-sub" x="${boxX + PAD}" y="${n.y + 44}">workflow ▸ ${escapeXml(truncate(s.workflowId, boxChars - 11))}</text>`;
|
|
661
|
+
} else if (op) {
|
|
662
|
+
const method = op.method.toUpperCase();
|
|
663
|
+
const methodW = textWidth(method, MONO_W) + 8;
|
|
664
|
+
const pathChars = Math.floor((boxW - 32 - methodW) / MONO_W);
|
|
665
|
+
sub =
|
|
666
|
+
`<text class="rf-method rf-method-${op.method}" x="${boxX + PAD}" y="${n.y + 44}">${method}</text>` +
|
|
667
|
+
`<text class="rf-path" x="${boxX + PAD + methodW}" y="${n.y + 44}">${escapeXml(truncate(op.path, pathChars))}</text>`;
|
|
668
|
+
} else {
|
|
669
|
+
const note = graph.operationsKnown ? " (not found)" : "";
|
|
670
|
+
sub = `<text class="rf-sub rf-unresolved" x="${boxX + PAD}" y="${n.y + 44}">${escapeXml(truncate(s.operationRef, boxChars - note.length))}${note}</text>`;
|
|
671
|
+
}
|
|
672
|
+
const tip = [
|
|
673
|
+
s.description,
|
|
674
|
+
op?.summary,
|
|
675
|
+
s.inputs.length ? `Inputs: ${s.inputs.join(", ")}` : "",
|
|
676
|
+
s.outputs.length ? `Outputs: ${s.outputs.join(", ")}` : "",
|
|
677
|
+
]
|
|
678
|
+
.filter(Boolean)
|
|
679
|
+
.join("\n");
|
|
680
|
+
const label = op ? `${s.stepId}: ${op.method.toUpperCase()} ${op.path}` : s.stepId;
|
|
681
|
+
const attrs = clickable ? ` tabindex="0" role="button" aria-label="${escapeXml(label)}"` : "";
|
|
682
|
+
out.push(
|
|
683
|
+
`<g class="rf-node rf-step${clickable ? " rf-clickable" : ""}" data-step-id="${escapeXml(s.stepId)}"${attrs}>`,
|
|
684
|
+
tip ? `<title>${escapeXml(tip)}</title>` : "",
|
|
685
|
+
`<rect x="${boxX}" y="${n.y}" width="${boxW}" height="${n.h}" rx="8"/>`,
|
|
686
|
+
`<text class="rf-title" x="${boxX + PAD}" y="${n.y + 24}">${escapeXml(truncate(s.stepId, boxChars - 10))}</text>`,
|
|
687
|
+
sub,
|
|
688
|
+
s.successCriteria && !expanded
|
|
689
|
+
? `<text class="rf-criteria" x="${boxR - 12}" y="${n.y + 24}" text-anchor="end">${s.successCriteria} check${s.successCriteria > 1 ? "s" : ""}</text>`
|
|
690
|
+
: "",
|
|
691
|
+
...(n.separators || []).map(
|
|
692
|
+
(sy) => `<path class="rf-sep" d="M${boxX} ${n.y + sy}H${boxR}"/>`,
|
|
693
|
+
),
|
|
694
|
+
...n.sections.flat().map((r) => renderRow(r, boxX, boxW, n.y + r.by, interactive)),
|
|
695
|
+
`</g>`,
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
out.push("</svg>");
|
|
700
|
+
const order = ["seq", "data", "output", "success", "failure"];
|
|
701
|
+
return { svg: out.join(""), legend: order.filter((k) => drawn.has(k)) };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/** The diagram as an SVG string; see renderFlowDiagram for the options. */
|
|
705
|
+
export function renderFlowSvg(graph, options = {}) {
|
|
706
|
+
return renderFlowDiagram(graph, options).svg;
|
|
707
|
+
}
|