@dooc-dev/dooc 0.0.0-stage → 0.0.2
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 +26 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +311 -0
- package/dist/code.global.js +285 -0
- package/dist/compiler/bundle-urls.d.ts +3 -0
- package/dist/compiler/bundle-urls.js +294 -0
- package/dist/compiler/bundle.d.ts +2 -0
- package/dist/compiler/bundle.js +100 -0
- package/dist/compiler/check-prose.d.ts +11 -0
- package/dist/compiler/check-prose.js +138 -0
- package/dist/compiler/check-types.d.ts +44 -0
- package/dist/compiler/check-types.js +1 -0
- package/dist/compiler/check.d.ts +5 -0
- package/dist/compiler/check.js +450 -0
- package/dist/compiler/code-blocks.d.ts +17 -0
- package/dist/compiler/code-blocks.js +198 -0
- package/dist/compiler/code-languages.d.ts +10 -0
- package/dist/compiler/code-languages.js +109 -0
- package/dist/compiler/compile.d.ts +95 -0
- package/dist/compiler/compile.js +410 -0
- package/dist/compiler/config.d.ts +7 -0
- package/dist/compiler/config.js +52 -0
- package/dist/compiler/dialect.d.ts +24 -0
- package/dist/compiler/dialect.js +198 -0
- package/dist/compiler/drawio.d.ts +26 -0
- package/dist/compiler/drawio.js +176 -0
- package/dist/compiler/figures.d.ts +14 -0
- package/dist/compiler/figures.js +221 -0
- package/dist/compiler/file-cache.d.ts +14 -0
- package/dist/compiler/file-cache.js +61 -0
- package/dist/compiler/files.d.ts +29 -0
- package/dist/compiler/files.js +431 -0
- package/dist/compiler/fonts.d.ts +10 -0
- package/dist/compiler/fonts.js +54 -0
- package/dist/compiler/format.d.ts +1 -0
- package/dist/compiler/format.js +42 -0
- package/dist/compiler/from-mdast.d.ts +16 -0
- package/dist/compiler/from-mdast.js +142 -0
- package/dist/compiler/icons.d.ts +14 -0
- package/dist/compiler/icons.js +85 -0
- package/dist/compiler/language-codes.d.ts +7 -0
- package/dist/compiler/language-codes.js +17 -0
- package/dist/compiler/locales.d.ts +7 -0
- package/dist/compiler/locales.js +67 -0
- package/dist/compiler/manifest.d.ts +6 -0
- package/dist/compiler/manifest.js +30 -0
- package/dist/compiler/ops.d.ts +44 -0
- package/dist/compiler/ops.js +362 -0
- package/dist/compiler/public-file.d.ts +2 -0
- package/dist/compiler/public-file.js +93 -0
- package/dist/compiler/serialize.d.ts +3 -0
- package/dist/compiler/serialize.js +16 -0
- package/dist/compiler/terms.d.ts +9 -0
- package/dist/compiler/terms.js +44 -0
- package/dist/compiler/textfile-remote.d.ts +12 -0
- package/dist/compiler/textfile-remote.js +124 -0
- package/dist/compiler/textfile.d.ts +9 -0
- package/dist/compiler/textfile.js +113 -0
- package/dist/compiler/theme-variants.d.ts +4 -0
- package/dist/compiler/theme-variants.js +22 -0
- package/dist/compiler/zip.d.ts +6 -0
- package/dist/compiler/zip.js +48 -0
- package/dist/core/blocks.d.ts +99 -0
- package/dist/core/blocks.js +29 -0
- package/dist/core/code-file.d.ts +12 -0
- package/dist/core/code-file.js +68 -0
- package/dist/core/content-metadata.d.ts +48 -0
- package/dist/core/content-metadata.js +264 -0
- package/dist/core/content-types.d.ts +45 -0
- package/dist/core/content-types.js +1 -0
- package/dist/core/extension-package.d.ts +34 -0
- package/dist/core/extension-package.js +147 -0
- package/dist/core/figure-frame.d.ts +12 -0
- package/dist/core/figure-frame.js +108 -0
- package/dist/core/file-embed.d.ts +13 -0
- package/dist/core/file-embed.js +43 -0
- package/dist/core/files.d.ts +15 -0
- package/dist/core/files.js +59 -0
- package/dist/core/flow-base-css.generated.d.ts +1 -0
- package/dist/core/flow-base-css.generated.js +4 -0
- package/dist/core/flow-css.d.ts +9 -0
- package/dist/core/flow-css.js +54 -0
- package/dist/core/graph-common.d.ts +32 -0
- package/dist/core/graph-common.js +113 -0
- package/dist/core/graph.d.ts +117 -0
- package/dist/core/graph.js +480 -0
- package/dist/core/html.d.ts +29 -0
- package/dist/core/html.js +444 -0
- package/dist/core/icons.d.ts +2 -0
- package/dist/core/icons.js +2 -0
- package/dist/core/inline.d.ts +27 -0
- package/dist/core/inline.js +42 -0
- package/dist/core/language.d.ts +41 -0
- package/dist/core/language.js +44 -0
- package/dist/core/languages/ja.d.ts +2 -0
- package/dist/core/languages/ja.js +5 -0
- package/dist/core/markdown.d.ts +27 -0
- package/dist/core/markdown.js +154 -0
- package/dist/core/previewers.d.ts +37 -0
- package/dist/core/previewers.js +103 -0
- package/dist/core/routing.d.ts +20 -0
- package/dist/core/routing.js +64 -0
- package/dist/core/schema-comments.d.ts +14 -0
- package/dist/core/schema-comments.js +52 -0
- package/dist/core/schema-graph-flow.d.ts +9 -0
- package/dist/core/schema-graph-flow.js +379 -0
- package/dist/core/schema-graph-mount.d.ts +9 -0
- package/dist/core/schema-graph-mount.js +105 -0
- package/dist/core/schema-graph.d.ts +254 -0
- package/dist/core/schema-graph.js +981 -0
- package/dist/core/table-ui.d.ts +2 -0
- package/dist/core/table-ui.js +180 -0
- package/dist/core/taxonomy.d.ts +12 -0
- package/dist/core/taxonomy.js +49 -0
- package/dist/core/term-reference.d.ts +5 -0
- package/dist/core/term-reference.js +19 -0
- package/dist/core/terms-ui.d.ts +6 -0
- package/dist/core/terms-ui.js +22 -0
- package/dist/core/textfile.d.ts +10 -0
- package/dist/core/textfile.js +39 -0
- package/dist/core/theme-variants.d.ts +3 -0
- package/dist/core/theme-variants.js +37 -0
- package/dist/core/toc-ui.d.ts +2 -0
- package/dist/core/toc-ui.js +161 -0
- package/dist/core/video-ui.d.ts +2 -0
- package/dist/core/video-ui.js +253 -0
- package/dist/core/workspace.d.ts +15 -0
- package/dist/core/workspace.js +28 -0
- package/dist/edit.global.js +306 -0
- package/dist/flow.global.js +4959 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +31 -0
- package/dist/mcp.d.ts +24 -0
- package/dist/mcp.js +467 -0
- package/dist/plugins/actions.d.ts +45 -0
- package/dist/plugins/actions.js +125 -0
- package/dist/plugins/blocks.d.ts +93 -0
- package/dist/plugins/blocks.js +100 -0
- package/dist/plugins/index.d.ts +4 -0
- package/dist/plugins/index.js +1 -0
- package/dist/plugins/local-storage.d.ts +18 -0
- package/dist/plugins/local-storage.js +115 -0
- package/dist/plugins/oauth.d.ts +14 -0
- package/dist/plugins/oauth.js +170 -0
- package/dist/plugins/runtime.d.ts +26 -0
- package/dist/plugins/runtime.js +395 -0
- package/dist/plugins/types.d.ts +59 -0
- package/dist/plugins/types.js +1 -0
- package/dist/plugins/view.d.ts +7 -0
- package/dist/plugins/view.js +21 -0
- package/dist/ui.global.js +78 -0
- package/dist/viewer/assets.d.ts +2 -0
- package/dist/viewer/assets.js +3 -0
- package/dist/viewer/build.d.ts +12 -0
- package/dist/viewer/build.js +55 -0
- package/dist/viewer/bundle-network.d.ts +2 -0
- package/dist/viewer/bundle-network.js +88 -0
- package/dist/viewer/code-file.d.ts +20 -0
- package/dist/viewer/code-file.js +42 -0
- package/dist/viewer/editor-tools.d.ts +38 -0
- package/dist/viewer/editor-tools.js +211 -0
- package/dist/viewer/extension-pack.d.ts +2 -0
- package/dist/viewer/extension-pack.js +36 -0
- package/dist/viewer/extensions.d.ts +10 -0
- package/dist/viewer/extensions.js +302 -0
- package/dist/viewer/file-references.d.ts +14 -0
- package/dist/viewer/file-references.js +38 -0
- package/dist/viewer/font-picker.d.ts +4 -0
- package/dist/viewer/font-picker.js +145 -0
- package/dist/viewer/font-settings.d.ts +11 -0
- package/dist/viewer/font-settings.js +50 -0
- package/dist/viewer/fonts/LICENSE.txt +119 -0
- package/dist/viewer/fonts/udev-gothic-regular.woff2 +0 -0
- package/dist/viewer/fonts.d.ts +2 -0
- package/dist/viewer/fonts.js +13 -0
- package/dist/viewer/git-diff.d.ts +3 -0
- package/dist/viewer/git-diff.js +33 -0
- package/dist/viewer/git-types.d.ts +73 -0
- package/dist/viewer/git-types.js +1 -0
- package/dist/viewer/git.d.ts +5 -0
- package/dist/viewer/git.js +337 -0
- package/dist/viewer/hmr.d.ts +53 -0
- package/dist/viewer/hmr.js +337 -0
- package/dist/viewer/i18n.d.ts +6 -0
- package/dist/viewer/i18n.js +65 -0
- package/dist/viewer/ja-lint.d.ts +3 -0
- package/dist/viewer/ja-lint.js +44 -0
- package/dist/viewer/language-picker.d.ts +2 -0
- package/dist/viewer/language-picker.js +233 -0
- package/dist/viewer/languages.d.ts +11 -0
- package/dist/viewer/languages.js +20 -0
- package/dist/viewer/locales/eng.json +317 -0
- package/dist/viewer/locales/jpn.json +317 -0
- package/dist/viewer/locales/zho.json +317 -0
- package/dist/viewer/nav-focus.d.ts +2 -0
- package/dist/viewer/nav-focus.js +64 -0
- package/dist/viewer/plugin-blocks.d.ts +3 -0
- package/dist/viewer/plugin-blocks.js +115 -0
- package/dist/viewer/plugin-config.d.ts +35 -0
- package/dist/viewer/plugin-config.js +57 -0
- package/dist/viewer/plugin-view.d.ts +2 -0
- package/dist/viewer/plugin-view.js +6 -0
- package/dist/viewer/plugins.d.ts +12 -0
- package/dist/viewer/plugins.js +267 -0
- package/dist/viewer/resize.d.ts +4 -0
- package/dist/viewer/resize.js +238 -0
- package/dist/viewer/resource-library.d.ts +29 -0
- package/dist/viewer/resource-library.js +104 -0
- package/dist/viewer/search.d.ts +16 -0
- package/dist/viewer/search.js +358 -0
- package/dist/viewer/serve.d.ts +210 -0
- package/dist/viewer/serve.js +3312 -0
- package/dist/viewer/site-icon.d.ts +8 -0
- package/dist/viewer/site-icon.js +56 -0
- package/dist/viewer/textfile.d.ts +9 -0
- package/dist/viewer/textfile.js +50 -0
- package/dist/viewer/theme-picker.d.ts +2 -0
- package/dist/viewer/theme-picker.js +135 -0
- package/dist/viewer/workspace.d.ts +4 -0
- package/dist/viewer/workspace.js +47 -0
- package/package.json +128 -5
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
// SoT for the content dialect `graphth` (node / edge DSL) — DSL parsing / graph model /
|
|
2
|
+
// deterministic layout. Browser-safe. html.ts owns the markup (SVG); this module returns only the
|
|
3
|
+
// semantics and the geometry computed with Dagre (= every consumer gets the same picture via
|
|
4
|
+
// docHtml).
|
|
5
|
+
//
|
|
6
|
+
// DSL (written inside a graphth fenced code block):
|
|
7
|
+
// [<id>]: <label> @<region> node declaration (label / region optional; one region per node)
|
|
8
|
+
// [<id>] @<region> label omitted = the id is displayed as-is
|
|
9
|
+
// [a] -> [b] edge (solid, directed = dependency / flow)
|
|
10
|
+
// [a] --> [b] edge (dashed, directed = optional / async)
|
|
11
|
+
// [a] -- [b] edge (undirected = no arrow)
|
|
12
|
+
// [a] -> [b] -> [c] chain (expands to 2 edges; can't take a label)
|
|
13
|
+
// [a] -> [b]: <label> edge label (faint text beside the line)
|
|
14
|
+
// # <text> comment (leading `#`; not rendered)
|
|
15
|
+
//
|
|
16
|
+
// **Ids are always wrapped in `[...]`**. The brackets are the delimiter, so an id may contain any
|
|
17
|
+
// character except `]` (hyphenated real names like `[web-app-v2]` can be written as-is).
|
|
18
|
+
// Whitespace around arrows is optional. A region is a single tag attached to a node and needs no
|
|
19
|
+
// declaration line (frames appear in order of first occurrence). The frame label is the region
|
|
20
|
+
// name itself (Japanese allowed, no whitespace). A node without a region becomes a standalone
|
|
21
|
+
// group with no frame (= `model.regions` partitions all nodes). References to undeclared nodes /
|
|
22
|
+
// duplicate declarations / self-loops / lines readable as neither node nor edge are all
|
|
23
|
+
// validation errors (= cannot be distributed) (picked up by dialect.ts).
|
|
24
|
+
//
|
|
25
|
+
// Placement (SoT for layoutGraph): regions go into Dagre's compound graph as parents and nodes as
|
|
26
|
+
// children. Directed edges flow left→right (`rankdir: LR`); region frames use the parent bounds
|
|
27
|
+
// computed by Dagre as-is. Undirected edges have no rank, so they are left out of the layout input
|
|
28
|
+
// and drawn between the placed nodes by our own router (routeGraphEdges).
|
|
29
|
+
import { default as dagre, } from "@dagrejs/dagre";
|
|
30
|
+
import { hashId, orthPath, round1, textW } from "./graph-common.js";
|
|
31
|
+
/** Edge kind = line style. solid / dashed are directed (with arrow); undirected has no arrow. */
|
|
32
|
+
export const GRAPH_EDGE_KINDS = ["solid", "dashed", "undirected"];
|
|
33
|
+
// Node ids are always wrapped in `[...]` (= the brackets are the delimiter, so the content may be
|
|
34
|
+
// any character except `[` `]`; hyphenated real names like `[web-app-v2]` can be written as-is).
|
|
35
|
+
const NODE_ID = /^\[([^[\]\n]*)\]/;
|
|
36
|
+
// Arrow (placed right after an id). The brackets pin down the id, so surrounding whitespace is
|
|
37
|
+
// optional. `-->` is listed before `->`/`--` (longest match).
|
|
38
|
+
const ARROW = /^(-->|->|--)/;
|
|
39
|
+
// Trailing region (`... @<region>`). Whitespace is required before `@`, so `a@b` inside a label
|
|
40
|
+
// isn't matched.
|
|
41
|
+
const REGION = /\s+@(\S+)\s*$/;
|
|
42
|
+
/** Explains in one sentence why a line couldn't be read (always shows the full syntax). */
|
|
43
|
+
const UNREADABLE = (text) => `"${text}" is neither a node declaration ("[<id>]: <label> @<region>") nor an edge ("[a] -> [b]") (notes start with "#")`;
|
|
44
|
+
const EDGE_KIND = {
|
|
45
|
+
"->": "solid",
|
|
46
|
+
"-->": "dashed",
|
|
47
|
+
"--": "undirected",
|
|
48
|
+
};
|
|
49
|
+
/** Edge dedup key. Undirected edges have no direction, so the ids are sorted to equate them. */
|
|
50
|
+
const edgeKey = (e) => e.kind === "undirected" ? `${[e.from, e.to].sort().join("|")}|u` : `${e.from}|${e.to}`;
|
|
51
|
+
/**
|
|
52
|
+
* Node / edge DSL → graph model + errors. The model is returned as a partial result even when
|
|
53
|
+
* there are errors (so the preview can show "validation errors + as much as can be drawn" at once).
|
|
54
|
+
*/
|
|
55
|
+
export const parseGraphSource = (source) => {
|
|
56
|
+
const errors = [];
|
|
57
|
+
const nodes = [];
|
|
58
|
+
const edges = [];
|
|
59
|
+
const regions = [];
|
|
60
|
+
const regionIndex = new Map();
|
|
61
|
+
const declared = new Set();
|
|
62
|
+
// Edges may reference nodes before they are declared, so existence is checked after all lines
|
|
63
|
+
// have been read.
|
|
64
|
+
const edgeLine = [];
|
|
65
|
+
const seenEdge = new Map();
|
|
66
|
+
const err = (line, message) => {
|
|
67
|
+
errors.push({ line, message });
|
|
68
|
+
};
|
|
69
|
+
const declare = (line, id, label, region) => {
|
|
70
|
+
if (declared.has(id)) {
|
|
71
|
+
err(line, `node "${id}" is declared twice`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
declared.add(id);
|
|
75
|
+
nodes.push({ id, label, ...(region == null ? {} : { region }) });
|
|
76
|
+
if (region == null) {
|
|
77
|
+
regions.push({ nodes: [id] });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
let ri = regionIndex.get(region);
|
|
81
|
+
if (ri == null) {
|
|
82
|
+
ri = regions.length;
|
|
83
|
+
regions.push({ label: region, nodes: [] });
|
|
84
|
+
regionIndex.set(region, ri);
|
|
85
|
+
}
|
|
86
|
+
regions[ri]?.nodes.push(id);
|
|
87
|
+
};
|
|
88
|
+
source.split("\n").forEach((raw, line) => {
|
|
89
|
+
const text = raw.trim();
|
|
90
|
+
if (text === "" || text.startsWith("#")) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (text.startsWith("```")) {
|
|
94
|
+
err(line, "``` is not allowed (write nodes / edges directly inside the graphth code block)");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// Every line starts with `[<id>]`. If an arrow follows immediately it's an edge; anything
|
|
98
|
+
// else (end of line / ":" / "@") is a node declaration — the brackets already pin down the
|
|
99
|
+
// id, so this one character decides the branch.
|
|
100
|
+
const head = NODE_ID.exec(text);
|
|
101
|
+
if (head == null) {
|
|
102
|
+
err(line, UNREADABLE(text));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const headId = (head[1] ?? "").trim();
|
|
106
|
+
const tail = text.slice(head[0].length);
|
|
107
|
+
// ---- edge line ----
|
|
108
|
+
if (ARROW.test(tail.trimStart())) {
|
|
109
|
+
const ids = [headId];
|
|
110
|
+
const arrows = [];
|
|
111
|
+
let label;
|
|
112
|
+
let cursor = tail.trimStart();
|
|
113
|
+
while (cursor !== "") {
|
|
114
|
+
const arrow = ARROW.exec(cursor);
|
|
115
|
+
if (arrow != null) {
|
|
116
|
+
arrows.push(arrow[1] ?? "");
|
|
117
|
+
cursor = cursor.slice(arrow[0].length).trimStart();
|
|
118
|
+
const next = NODE_ID.exec(cursor);
|
|
119
|
+
if (next == null) {
|
|
120
|
+
err(line, UNREADABLE(text));
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
ids.push((next[1] ?? "").trim());
|
|
124
|
+
cursor = cursor.slice(next[0].length).trimStart();
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (cursor.startsWith(":")) {
|
|
128
|
+
label = cursor.slice(1).trim();
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
err(line, UNREADABLE(text));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (label === "") {
|
|
135
|
+
err(line, "edge label is empty (`[a] -> [b]: <label>`)");
|
|
136
|
+
}
|
|
137
|
+
if (label != null && label !== "" && arrows.length > 1) {
|
|
138
|
+
err(line, "a chain (`[a] -> [b] -> [c]`) cannot have a label (write each edge separately)");
|
|
139
|
+
}
|
|
140
|
+
if (ids.some((id) => id === "")) {
|
|
141
|
+
err(line, "node id is empty (`[]` is not allowed)");
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
arrows.forEach((arrow, k) => {
|
|
145
|
+
const from = ids[k];
|
|
146
|
+
const to = ids[k + 1];
|
|
147
|
+
const kind = EDGE_KIND[arrow];
|
|
148
|
+
if (from == null || to == null || kind == null) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (from === to) {
|
|
152
|
+
err(line, `edge to itself ("${from}") cannot be drawn`);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const edge = {
|
|
156
|
+
from,
|
|
157
|
+
to,
|
|
158
|
+
kind,
|
|
159
|
+
...(label == null || label === "" || arrows.length > 1 ? {} : { label }),
|
|
160
|
+
};
|
|
161
|
+
const key = edgeKey(edge);
|
|
162
|
+
if (seenEdge.has(key)) {
|
|
163
|
+
err(line, `edge between "${from}" and "${to}" is declared twice`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
seenEdge.set(key, edges.length);
|
|
167
|
+
edges.push(edge);
|
|
168
|
+
edgeLine.push(line);
|
|
169
|
+
});
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// ---- node declaration line ----
|
|
173
|
+
const region = REGION.exec(tail)?.[1];
|
|
174
|
+
const rest = (region == null ? tail : tail.replace(REGION, "")).trim();
|
|
175
|
+
if (rest !== "" && !rest.startsWith(":")) {
|
|
176
|
+
err(line, UNREADABLE(text));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (headId === "") {
|
|
180
|
+
err(line, "node id is empty (`[]` is not allowed)");
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const label = rest === "" ? headId : rest.slice(1).trim();
|
|
184
|
+
if (label === "") {
|
|
185
|
+
err(line, `node "${headId}" has an empty label (omit the ":" too for no label)`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
declare(line, headId, label, region);
|
|
189
|
+
});
|
|
190
|
+
edges.forEach((e, i) => {
|
|
191
|
+
const line = edgeLine[i] ?? 0;
|
|
192
|
+
for (const id of [e.from, e.to]) {
|
|
193
|
+
if (!declared.has(id)) {
|
|
194
|
+
err(line, `edge node "${id}" is not declared`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
return { model: { nodes, edges, regions }, errors };
|
|
199
|
+
};
|
|
200
|
+
/** model → deterministic short id (namespace for SVG classes / marker ids). */
|
|
201
|
+
export const graphUid = (model) => hashId(JSON.stringify(model));
|
|
202
|
+
/**
|
|
203
|
+
* Legend text (shown only when there are dashed / undirected edges). html.ts places the
|
|
204
|
+
* markup, but layout also needs the text length to keep the canvas at least this wide (= so the
|
|
205
|
+
* text isn't cut off on a narrow graph), so the text is declared here, in one place.
|
|
206
|
+
*/
|
|
207
|
+
export const GRAPH_LEGEND = "Solid = dependency / dashed = optional or async / no arrow = undirected / hover to highlight related edges";
|
|
208
|
+
// Geometry constants (px). Font metrics can't be measured, so widths are textW estimates
|
|
209
|
+
// (= deterministic).
|
|
210
|
+
const M = {
|
|
211
|
+
nodeH: 40,
|
|
212
|
+
minW: 128,
|
|
213
|
+
padX: 14,
|
|
214
|
+
/** Gap between nodes stacked in the same column. */
|
|
215
|
+
gapY: 20,
|
|
216
|
+
/** Gap between Dagre ranks (along the arrow direction). */
|
|
217
|
+
gapBandX: 60,
|
|
218
|
+
/** Minimum gap between nodes / regions that Dagre places in the same rank. */
|
|
219
|
+
gapRegionY: 40,
|
|
220
|
+
framePad: 13,
|
|
221
|
+
frameLabelH: 22,
|
|
222
|
+
margin: 16,
|
|
223
|
+
corner: 7,
|
|
224
|
+
radius: 10,
|
|
225
|
+
legendH: 24,
|
|
226
|
+
/** How far the text baseline sits below the box center (assumes a font of about 12.5px). */
|
|
227
|
+
baselineDrop: 4.5,
|
|
228
|
+
/** Max center-x difference for vertically stacked boxes to be joined directly. */
|
|
229
|
+
verticalEps: 8,
|
|
230
|
+
};
|
|
231
|
+
/** Left edge of the legend / group labels (used by the renderer). */
|
|
232
|
+
export const GRAPH_LAYOUT_MARGIN = M.margin;
|
|
233
|
+
/** Minimum gap between groups stacked in the same band (tests use it to check non-overlap). */
|
|
234
|
+
export const GRAPH_GAP_REGION_Y = M.gapRegionY;
|
|
235
|
+
/** Height / padding of a group frame's label area (tests use it to derive the "label top"). */
|
|
236
|
+
export const GRAPH_FRAME_LABEL_H = M.frameLabelH;
|
|
237
|
+
export const GRAPH_FRAME_PAD = M.framePad;
|
|
238
|
+
/**
|
|
239
|
+
* Graph model → deterministic layout. Dagre's compound graph computes node / region positions and
|
|
240
|
+
* sizes. Edges are then routed between the placed boxes by routeGraphEdges, which keeps the
|
|
241
|
+
* per-kind SVG representation and the hover indices. The Dagre graph is built in model
|
|
242
|
+
* declaration order, so the same input always yields the same coordinates.
|
|
243
|
+
*/
|
|
244
|
+
export const layoutGraph = (model) => {
|
|
245
|
+
const indexOf = new Map(model.nodes.map((n, i) => [n.id, i]));
|
|
246
|
+
const measured = model.nodes.map((n) => ({
|
|
247
|
+
w: Math.max(M.minW, Math.ceil(textW(n.label, 12.5 / 12) + M.padX * 2)),
|
|
248
|
+
h: M.nodeH,
|
|
249
|
+
}));
|
|
250
|
+
const graph = new dagre.graphlib.Graph({
|
|
251
|
+
compound: true,
|
|
252
|
+
multigraph: true,
|
|
253
|
+
})
|
|
254
|
+
.setGraph({
|
|
255
|
+
rankdir: "LR",
|
|
256
|
+
nodesep: M.gapRegionY,
|
|
257
|
+
edgesep: M.gapY,
|
|
258
|
+
ranksep: M.gapBandX,
|
|
259
|
+
marginx: M.margin,
|
|
260
|
+
marginy: M.margin,
|
|
261
|
+
acyclicer: "greedy",
|
|
262
|
+
ranker: "network-simplex",
|
|
263
|
+
})
|
|
264
|
+
.setDefaultEdgeLabel(() => ({}));
|
|
265
|
+
// Internal parent id that can't collide with DSL node ids: the source can't contain NUL, so the
|
|
266
|
+
// prefix acts as a namespace.
|
|
267
|
+
const parentId = (regionIndex) => `\0region:${regionIndex}`;
|
|
268
|
+
model.regions.forEach((region, ri) => {
|
|
269
|
+
if (region.label == null || region.label === "") {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
graph.setNode(parentId(ri), {
|
|
273
|
+
width: Math.max(1, Math.ceil(textW(region.label, 11.5 / 12) + 8)),
|
|
274
|
+
height: M.frameLabelH,
|
|
275
|
+
label: region.label,
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
model.nodes.forEach((node, i) => {
|
|
279
|
+
const size = measured[i] ?? { w: M.minW, h: M.nodeH };
|
|
280
|
+
graph.setNode(node.id, { width: size.w, height: size.h, label: node.label });
|
|
281
|
+
});
|
|
282
|
+
model.regions.forEach((region, ri) => {
|
|
283
|
+
if (region.label == null || region.label === "") {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
for (const id of region.nodes) {
|
|
287
|
+
if (indexOf.has(id)) {
|
|
288
|
+
graph.setParent(id, parentId(ri));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
model.edges.forEach((edge, i) => {
|
|
293
|
+
// Don't give undirected edges a one-sided rank. At draw time they are connected between the
|
|
294
|
+
// placed nodes like any other edge.
|
|
295
|
+
if (edge.kind === "undirected" || !indexOf.has(edge.from) || !indexOf.has(edge.to)) {
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
graph.setEdge(edge.from, edge.to, {
|
|
299
|
+
minlen: 1,
|
|
300
|
+
weight: edge.kind === "solid" ? 2 : 1,
|
|
301
|
+
...(edge.label == null
|
|
302
|
+
? {}
|
|
303
|
+
: {
|
|
304
|
+
width: Math.ceil(textW(edge.label, 10.5 / 12) + 8),
|
|
305
|
+
height: 16,
|
|
306
|
+
}),
|
|
307
|
+
}, `edge:${i}`);
|
|
308
|
+
});
|
|
309
|
+
dagre.layout(graph);
|
|
310
|
+
const nodes = model.nodes.flatMap((node, i) => {
|
|
311
|
+
const size = measured[i];
|
|
312
|
+
const placed = graph.node(node.id);
|
|
313
|
+
if (size == null || placed?.x == null || placed.y == null) {
|
|
314
|
+
return [];
|
|
315
|
+
}
|
|
316
|
+
const x = round1(placed.x - size.w / 2);
|
|
317
|
+
const y = round1(placed.y - size.h / 2);
|
|
318
|
+
return [
|
|
319
|
+
{
|
|
320
|
+
id: node.id,
|
|
321
|
+
index: i,
|
|
322
|
+
label: node.label,
|
|
323
|
+
x,
|
|
324
|
+
y,
|
|
325
|
+
w: size.w,
|
|
326
|
+
h: size.h,
|
|
327
|
+
baseline: round1(y + size.h / 2 + M.baselineDrop),
|
|
328
|
+
textX: round1(x + size.w / 2),
|
|
329
|
+
radius: M.radius,
|
|
330
|
+
},
|
|
331
|
+
];
|
|
332
|
+
});
|
|
333
|
+
const regions = model.regions.flatMap((region, ri) => {
|
|
334
|
+
if (region.label == null || region.label === "") {
|
|
335
|
+
return [];
|
|
336
|
+
}
|
|
337
|
+
const placed = graph.node(parentId(ri));
|
|
338
|
+
if (placed?.x == null ||
|
|
339
|
+
placed.y == null ||
|
|
340
|
+
placed.width == null ||
|
|
341
|
+
placed.height == null) {
|
|
342
|
+
return [];
|
|
343
|
+
}
|
|
344
|
+
return [
|
|
345
|
+
{
|
|
346
|
+
label: region.label,
|
|
347
|
+
x: round1(placed.x - placed.width / 2),
|
|
348
|
+
y: round1(placed.y - placed.height / 2),
|
|
349
|
+
w: round1(placed.width),
|
|
350
|
+
h: round1(placed.height),
|
|
351
|
+
members: region.nodes,
|
|
352
|
+
},
|
|
353
|
+
];
|
|
354
|
+
});
|
|
355
|
+
const edges = routeGraphEdges(nodes, model.edges);
|
|
356
|
+
const hasLegend = model.edges.some((e) => e.kind !== "solid");
|
|
357
|
+
const graphWidth = graph.graph().width ?? 0;
|
|
358
|
+
const graphHeight = graph.graph().height ?? 0;
|
|
359
|
+
const width = Math.max(M.minW + M.margin * 2,
|
|
360
|
+
// With a legend, always reserve enough width for its text (so it isn't clipped on graphs
|
|
361
|
+
// with few nodes).
|
|
362
|
+
hasLegend ? Math.ceil(M.margin * 2 + textW(GRAPH_LEGEND, 10.5 / 12)) : 0, Math.ceil(graphWidth), Math.ceil(nodes.reduce((w, n) => Math.max(w, n.x + n.w), 0) + M.margin));
|
|
363
|
+
const legendY = hasLegend ? graphHeight + M.legendH - 8 : undefined;
|
|
364
|
+
const height = Math.ceil(graphHeight + (hasLegend ? M.legendH : 0));
|
|
365
|
+
return {
|
|
366
|
+
width,
|
|
367
|
+
height,
|
|
368
|
+
regions,
|
|
369
|
+
nodes,
|
|
370
|
+
edges,
|
|
371
|
+
...(legendY == null ? {} : { legendY }),
|
|
372
|
+
};
|
|
373
|
+
};
|
|
374
|
+
/**
|
|
375
|
+
* Boxes + edges → routes. A pure function so that layout (at build time) and a future interactive
|
|
376
|
+
* UI that moves boxes share the same rules (= a single SoT for how edges are drawn).
|
|
377
|
+
*/
|
|
378
|
+
export const routeGraphEdges = (boxes, edges) => {
|
|
379
|
+
const byId = new Map(boxes.map((b) => [b.id, b]));
|
|
380
|
+
const cx = (b) => b.x + b.w / 2;
|
|
381
|
+
const cy = (b) => b.y + b.h / 2;
|
|
382
|
+
// Pass 1 = pick the exit / entry side of each edge (pass 2 spreads the coordinates evenly).
|
|
383
|
+
const routes = edges.map((edge) => {
|
|
384
|
+
const s = byId.get(edge.from);
|
|
385
|
+
const t = byId.get(edge.to);
|
|
386
|
+
if (s == null || t == null) {
|
|
387
|
+
return undefined;
|
|
388
|
+
}
|
|
389
|
+
const side = Math.abs(cx(s) - cx(t)) < M.verticalEps ? "v" : cx(t) >= cx(s) ? "r" : "l";
|
|
390
|
+
return { edge, from: s.index, to: t.index, side };
|
|
391
|
+
});
|
|
392
|
+
// Pass 2 = split a side's height evenly among the edges meeting at the same side of the same
|
|
393
|
+
// box (ordered by the other box's center y).
|
|
394
|
+
const slots = new Map();
|
|
395
|
+
const slotOf = (key, box, isSource) => {
|
|
396
|
+
const found = slots.get(key);
|
|
397
|
+
if (found != null) {
|
|
398
|
+
return found;
|
|
399
|
+
}
|
|
400
|
+
const slot = { box, isSource, items: [] };
|
|
401
|
+
slots.set(key, slot);
|
|
402
|
+
return slot;
|
|
403
|
+
};
|
|
404
|
+
for (const r of routes) {
|
|
405
|
+
if (r == null || r.side === "v") {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
const s = byId.get(r.edge.from);
|
|
409
|
+
const t = byId.get(r.edge.to);
|
|
410
|
+
if (s == null || t == null) {
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
slotOf(`s${r.from}:${r.side}`, s, true).items.push({ route: r, key: cy(t) });
|
|
414
|
+
slotOf(`t${r.to}:${r.side}`, t, false).items.push({ route: r, key: cy(s) });
|
|
415
|
+
}
|
|
416
|
+
const exitY = new Map();
|
|
417
|
+
const entryY = new Map();
|
|
418
|
+
for (const slot of slots.values()) {
|
|
419
|
+
const sorted = [...slot.items].sort((a, b) => a.key - b.key);
|
|
420
|
+
sorted.forEach((item, k) => {
|
|
421
|
+
const y = slot.box.y + (slot.box.h * (k + 1)) / (sorted.length + 1);
|
|
422
|
+
(slot.isSource ? exitY : entryY).set(item.route, y);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
const out = [];
|
|
426
|
+
for (const r of routes) {
|
|
427
|
+
if (r == null) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
const s = byId.get(r.edge.from);
|
|
431
|
+
const t = byId.get(r.edge.to);
|
|
432
|
+
if (s == null || t == null) {
|
|
433
|
+
continue;
|
|
434
|
+
}
|
|
435
|
+
let d;
|
|
436
|
+
let labelX;
|
|
437
|
+
let labelY;
|
|
438
|
+
let labelAnchor;
|
|
439
|
+
if (r.side === "v") {
|
|
440
|
+
// Vertically stacked boxes are joined directly, bottom edge → top edge (mirrored if
|
|
441
|
+
// reversed).
|
|
442
|
+
const down = cy(t) >= cy(s);
|
|
443
|
+
const x = round1(Math.min(cx(s), cx(t)));
|
|
444
|
+
const y0 = down ? s.y + s.h : s.y;
|
|
445
|
+
const y1 = down ? t.y : t.y + t.h;
|
|
446
|
+
d = `M${x},${round1(y0)} L${x},${round1(y1)}`;
|
|
447
|
+
labelX = x + 6;
|
|
448
|
+
labelY = (y0 + y1) / 2;
|
|
449
|
+
labelAnchor = "start";
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
const rightward = r.side === "r";
|
|
453
|
+
const sx = rightward ? s.x + s.w : s.x;
|
|
454
|
+
const tx = rightward ? t.x : t.x + t.w;
|
|
455
|
+
const y0 = exitY.get(r) ?? cy(s);
|
|
456
|
+
const y1 = entryY.get(r) ?? cy(t);
|
|
457
|
+
const corridor = (sx + tx) / 2;
|
|
458
|
+
d = orthPath(sx, y0, corridor, y1, tx, M.corner);
|
|
459
|
+
const straight = Math.abs(y0 - y1) < 0.6;
|
|
460
|
+
labelX = straight ? (sx + tx) / 2 : corridor + 6;
|
|
461
|
+
labelY = straight ? y0 - 7 : (y0 + y1) / 2;
|
|
462
|
+
labelAnchor = straight ? "middle" : "start";
|
|
463
|
+
}
|
|
464
|
+
out.push({
|
|
465
|
+
from: r.from,
|
|
466
|
+
to: r.to,
|
|
467
|
+
kind: r.edge.kind,
|
|
468
|
+
d,
|
|
469
|
+
...(r.edge.label == null
|
|
470
|
+
? {}
|
|
471
|
+
: {
|
|
472
|
+
label: r.edge.label,
|
|
473
|
+
labelX: round1(labelX),
|
|
474
|
+
labelY: round1(labelY),
|
|
475
|
+
labelAnchor,
|
|
476
|
+
}),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
return out;
|
|
480
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Block, TocEntry } from "./blocks.js";
|
|
2
|
+
import type { FigureEntry, TermEntry } from "./content-types.js";
|
|
3
|
+
import { type LanguageAdapter } from "./language.js";
|
|
4
|
+
import { type Frontmatter } from "./taxonomy.js";
|
|
5
|
+
export interface RenderCtx {
|
|
6
|
+
/** Source file to published document route. */
|
|
7
|
+
docLinks?: ReadonlyMap<string, string>;
|
|
8
|
+
/** Figures including their markup (name → entry). */
|
|
9
|
+
figures: ReadonlyMap<string, FigureEntry>;
|
|
10
|
+
terms: readonly TermEntry[];
|
|
11
|
+
termLinks?: ReadonlyMap<string, string>;
|
|
12
|
+
/** The doc's language tag (BCP 47), emitted as `<article lang>`. */
|
|
13
|
+
lang?: string;
|
|
14
|
+
/** Localized in-page table of contents title. Default: Contents. */
|
|
15
|
+
tocTitle?: string;
|
|
16
|
+
/** Document URL for the viewer's section-copy buttons. */
|
|
17
|
+
sectionUrl?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Adapter for the doc's language (term ordering). Default: generic
|
|
20
|
+
* Unicode rules (language.ts).
|
|
21
|
+
*/
|
|
22
|
+
language?: LanguageAdapter;
|
|
23
|
+
}
|
|
24
|
+
export declare const emptyCtx: RenderCtx;
|
|
25
|
+
export declare const escapeHtml: (s: string) => string;
|
|
26
|
+
/** Shared dictionary entries for definitions and the Resources list. */
|
|
27
|
+
export declare const termsHtml: (terms: readonly TermEntry[], language?: LanguageAdapter, sortable?: boolean) => string;
|
|
28
|
+
export declare const blocksHtml: (blocks: Block[], ctx?: RenderCtx) => string;
|
|
29
|
+
export declare const docHtml: (fm: Frontmatter, blocks: Block[], errors: string[], ctx?: RenderCtx, toc?: TocEntry[]) => string;
|