@deckbuilderkit/editor 0.0.0-stage → 0.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/package.json +53 -4
- package/src/ArchiveView.tsx +111 -0
- package/src/CodeEditor.tsx +265 -0
- package/src/CodeView.tsx +67 -0
- package/src/DiffView.tsx +139 -0
- package/src/EnvView.tsx +42 -0
- package/src/FilePreview.tsx +108 -0
- package/src/FontView.tsx +48 -0
- package/src/HarView.tsx +70 -0
- package/src/HexView.tsx +28 -0
- package/src/HtmlPreview.tsx +23 -0
- package/src/MarkdownEditor.tsx +90 -0
- package/src/MarkdownToc.tsx +135 -0
- package/src/MarkdownView.tsx +84 -0
- package/src/MediaView.tsx +17 -0
- package/src/NotebookView.tsx +74 -0
- package/src/PdfView.tsx +289 -0
- package/src/RequestEditor.tsx +419 -0
- package/src/ResponseView.tsx +89 -0
- package/src/SqlGraph.tsx +322 -0
- package/src/SqlSteps.tsx +487 -0
- package/src/SqliteView.tsx +68 -0
- package/src/SvgView.tsx +231 -0
- package/src/TableView.tsx +85 -0
- package/src/VarSuggest.tsx +125 -0
- package/src/curl-file.ts +70 -0
- package/src/editorOptions.ts +83 -0
- package/src/editorOptionsContext.ts +4 -0
- package/src/fileKinds.ts +69 -0
- package/src/http-client.ts +9 -0
- package/src/http-import.ts +594 -0
- package/src/http.ts +251 -0
- package/src/import-warning.ts +8 -0
- package/src/index.ts +56 -0
- package/src/languages.ts +107 -0
- package/src/markdown.ts +123 -0
- package/src/markdownLive.ts +289 -0
- package/src/options.ts +3 -0
- package/src/pdf-url.d.ts +4 -0
- package/src/sql/format.ts +637 -0
- package/src/sql/formatOptions.ts +71 -0
- package/src/sql/lineage.ts +365 -0
- package/src/sql/lint.ts +951 -0
- package/src/sql/markerHighlight.ts +50 -0
- package/src/sql/markers.ts +138 -0
- package/src/sql/outline.ts +662 -0
- package/src/sql/statements.ts +83 -0
- package/src/sql/tokens.ts +156 -0
- package/src/sql/xmlQuery.ts +249 -0
- package/src/sqlProblems.ts +40 -0
- package/src/table.ts +89 -0
- package/src/text-save.ts +28 -0
- package/src/theme.ts +86 -0
- package/src/toc.ts +11 -0
- package/src/variables.ts +123 -0
- package/README.md +0 -3
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export interface SqlFormatOptions {
|
|
2
|
+
keywordCase: 'upper' | 'lower' | 'preserve';
|
|
3
|
+
functionCase: 'upper' | 'lower' | 'preserve';
|
|
4
|
+
indent: 2 | 4 | 'tab';
|
|
5
|
+
columnsPerLine: number;
|
|
6
|
+
newlineAfterSelect: boolean;
|
|
7
|
+
singleColumnInline: boolean;
|
|
8
|
+
conditionPerLine: boolean;
|
|
9
|
+
andOrPosition: 'before' | 'after';
|
|
10
|
+
joinConditionOnNewLine: boolean;
|
|
11
|
+
caseLayout: 'auto' | 'always' | 'never';
|
|
12
|
+
caseMaxWidth: number;
|
|
13
|
+
groupOrderListPerLine: boolean;
|
|
14
|
+
fromTablesPerLine: boolean;
|
|
15
|
+
setItemsPerLine: boolean;
|
|
16
|
+
spaceAroundOperators: boolean;
|
|
17
|
+
subqueryNewlineAfterParen: boolean;
|
|
18
|
+
subqueryCloseParenOnOwnLine: boolean;
|
|
19
|
+
blankLineBetweenStatements: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const DEFAULT_FORMAT_OPTIONS: SqlFormatOptions = {
|
|
23
|
+
keywordCase: 'upper',
|
|
24
|
+
functionCase: 'preserve',
|
|
25
|
+
indent: 2,
|
|
26
|
+
columnsPerLine: 3,
|
|
27
|
+
newlineAfterSelect: false,
|
|
28
|
+
singleColumnInline: true,
|
|
29
|
+
conditionPerLine: false,
|
|
30
|
+
andOrPosition: 'before',
|
|
31
|
+
joinConditionOnNewLine: true,
|
|
32
|
+
caseLayout: 'auto',
|
|
33
|
+
caseMaxWidth: 60,
|
|
34
|
+
groupOrderListPerLine: false,
|
|
35
|
+
fromTablesPerLine: true,
|
|
36
|
+
setItemsPerLine: true,
|
|
37
|
+
spaceAroundOperators: true,
|
|
38
|
+
subqueryNewlineAfterParen: true,
|
|
39
|
+
subqueryCloseParenOnOwnLine: true,
|
|
40
|
+
blankLineBetweenStatements: true,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export interface FormatOptionMeta {
|
|
44
|
+
key: keyof SqlFormatOptions;
|
|
45
|
+
kind: 'boolean' | 'enum' | 'number';
|
|
46
|
+
values?: readonly (string | number)[];
|
|
47
|
+
min?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const CASES = ['upper', 'lower', 'preserve'] as const;
|
|
51
|
+
|
|
52
|
+
export const FORMAT_OPTION_META: readonly FormatOptionMeta[] = [
|
|
53
|
+
{ key: 'keywordCase', kind: 'enum', values: CASES },
|
|
54
|
+
{ key: 'functionCase', kind: 'enum', values: CASES },
|
|
55
|
+
{ key: 'indent', kind: 'enum', values: [2, 4, 'tab'] },
|
|
56
|
+
{ key: 'columnsPerLine', kind: 'number', min: 1 },
|
|
57
|
+
{ key: 'newlineAfterSelect', kind: 'boolean' },
|
|
58
|
+
{ key: 'singleColumnInline', kind: 'boolean' },
|
|
59
|
+
{ key: 'conditionPerLine', kind: 'boolean' },
|
|
60
|
+
{ key: 'andOrPosition', kind: 'enum', values: ['before', 'after'] },
|
|
61
|
+
{ key: 'joinConditionOnNewLine', kind: 'boolean' },
|
|
62
|
+
{ key: 'caseLayout', kind: 'enum', values: ['auto', 'always', 'never'] },
|
|
63
|
+
{ key: 'caseMaxWidth', kind: 'number', min: 1 },
|
|
64
|
+
{ key: 'groupOrderListPerLine', kind: 'boolean' },
|
|
65
|
+
{ key: 'fromTablesPerLine', kind: 'boolean' },
|
|
66
|
+
{ key: 'setItemsPerLine', kind: 'boolean' },
|
|
67
|
+
{ key: 'spaceAroundOperators', kind: 'boolean' },
|
|
68
|
+
{ key: 'subqueryNewlineAfterParen', kind: 'boolean' },
|
|
69
|
+
{ key: 'subqueryCloseParenOnOwnLine', kind: 'boolean' },
|
|
70
|
+
{ key: 'blankLineBetweenStatements', kind: 'boolean' },
|
|
71
|
+
];
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import type { SqlJoinType, SqlStatementOutline } from './outline';
|
|
2
|
+
|
|
3
|
+
export type LineageNodeKind = 'table' | 'cte' | 'main' | 'target';
|
|
4
|
+
export type LineageJoin = SqlJoinType | 'from';
|
|
5
|
+
|
|
6
|
+
export interface LineageNode {
|
|
7
|
+
id: string;
|
|
8
|
+
kind: LineageNodeKind;
|
|
9
|
+
label: string;
|
|
10
|
+
/** Jump range: CTE name, step start, or first read of a table. */
|
|
11
|
+
from?: number;
|
|
12
|
+
to?: number;
|
|
13
|
+
/** Short badges: 'group by', 'distinct', 'dedupe', 'union all', 'filter ×3', 'agg ×2', 'recursive'. */
|
|
14
|
+
tags: string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LineageEdge {
|
|
18
|
+
from: string;
|
|
19
|
+
to: string;
|
|
20
|
+
join?: LineageJoin;
|
|
21
|
+
on?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface LineageGraph {
|
|
25
|
+
nodes: LineageNode[];
|
|
26
|
+
edges: LineageEdge[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Normalizes a SQL name for identity: quotes dropped, case-insensitive. */
|
|
30
|
+
export function sqlNameKey(name: string): string {
|
|
31
|
+
return name.replace(/["`[\]]/g, '').trim().toLowerCase();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const TARGET_KINDS = new Set(['insert', 'create', 'merge', 'update']);
|
|
35
|
+
|
|
36
|
+
function counted(label: string, n: number): string | undefined {
|
|
37
|
+
if (n <= 0) return undefined;
|
|
38
|
+
return n === 1 ? label : `${label} ×${n}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function lineageGraph(statement: SqlStatementOutline): LineageGraph {
|
|
42
|
+
const nodes: LineageNode[] = [];
|
|
43
|
+
const edges: LineageEdge[] = [];
|
|
44
|
+
const byId = new Map<string, LineageNode>();
|
|
45
|
+
const edgeKeys = new Set<string>();
|
|
46
|
+
if (!statement.steps.length) return { nodes, edges };
|
|
47
|
+
|
|
48
|
+
const cteKeys = new Set(statement.steps.filter((s) => s.kind === 'cte').map((s) => sqlNameKey(s.name)));
|
|
49
|
+
const stepId = (s: { kind: 'cte' | 'main'; name: string }) => (s.kind === 'main' ? 'main' : `cte:${sqlNameKey(s.name)}`);
|
|
50
|
+
const tables: LineageNode[] = [];
|
|
51
|
+
|
|
52
|
+
for (const step of statement.steps) {
|
|
53
|
+
const id = stepId(step);
|
|
54
|
+
if (byId.has(id)) continue;
|
|
55
|
+
const tags: string[] = [];
|
|
56
|
+
if (step.optional.length) tags.push(`?${step.optional.map((n) => `{${n}}`).join('')}`);
|
|
57
|
+
if (step.reads.some((r) => step.kind === 'cte' && sqlNameKey(r.name) === sqlNameKey(step.name))) tags.push('recursive');
|
|
58
|
+
if (step.distinct) tags.push('distinct');
|
|
59
|
+
if (step.dedupe) tags.push('dedupe');
|
|
60
|
+
if (step.groupBy.length) tags.push('group by');
|
|
61
|
+
for (const op of new Set(step.setOps)) tags.push(op);
|
|
62
|
+
const f = counted('filter', step.filters.length);
|
|
63
|
+
if (f) tags.push(f);
|
|
64
|
+
const a = counted('agg', step.aggregates.length);
|
|
65
|
+
if (a) tags.push(a);
|
|
66
|
+
const node: LineageNode =
|
|
67
|
+
step.kind === 'main'
|
|
68
|
+
? { id, kind: 'main', label: 'main', from: step.from, to: step.to, tags }
|
|
69
|
+
: { id, kind: 'cte', label: step.name, from: step.nameFrom ?? step.from, to: step.nameTo ?? step.to, tags };
|
|
70
|
+
byId.set(id, node);
|
|
71
|
+
nodes.push(node);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (const step of statement.steps) {
|
|
75
|
+
const to = stepId(step);
|
|
76
|
+
const used = new Set<number>();
|
|
77
|
+
step.reads.forEach((read, i) => {
|
|
78
|
+
const key = sqlNameKey(read.name);
|
|
79
|
+
let src: string;
|
|
80
|
+
if (cteKeys.has(key)) src = `cte:${key}`;
|
|
81
|
+
else {
|
|
82
|
+
src = `table:${key}`;
|
|
83
|
+
if (!byId.has(src)) {
|
|
84
|
+
const t: LineageNode = { id: src, kind: 'table', label: read.name, from: read.from, to: read.to, tags: [] };
|
|
85
|
+
byId.set(src, t);
|
|
86
|
+
tables.push(t);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (src === to) return;
|
|
90
|
+
let join: LineageJoin = 'from';
|
|
91
|
+
let on: string | undefined;
|
|
92
|
+
if (i > 0) {
|
|
93
|
+
const j = step.joins.findIndex(
|
|
94
|
+
(jn, k) => !used.has(k) && sqlNameKey(jn.target) === key && (jn.alias ?? '') === (read.alias ?? ''),
|
|
95
|
+
);
|
|
96
|
+
if (j >= 0) {
|
|
97
|
+
used.add(j);
|
|
98
|
+
join = step.joins[j]!.type;
|
|
99
|
+
on = step.joins[j]!.on;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const ek = `${src}\u0000${to}`;
|
|
103
|
+
if (edgeKeys.has(ek)) return;
|
|
104
|
+
edgeKeys.add(ek);
|
|
105
|
+
edges.push(on === undefined ? { from: src, to, join } : { from: src, to, join, on });
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (statement.target && TARGET_KINDS.has(statement.kind) && byId.has('main')) {
|
|
110
|
+
const t: LineageNode = { id: 'target', kind: 'target', label: statement.target, tags: [] };
|
|
111
|
+
nodes.push(t);
|
|
112
|
+
edges.push({ from: 'main', to: 'target' });
|
|
113
|
+
}
|
|
114
|
+
return { nodes: [...tables, ...nodes], edges };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** The node plus all its ancestors and descendants. */
|
|
118
|
+
export function lineageOf(graph: LineageGraph, nodeId: string): Set<string> {
|
|
119
|
+
const out = new Set<string>();
|
|
120
|
+
if (!graph.nodes.some((n) => n.id === nodeId)) return out;
|
|
121
|
+
const up = new Map<string, string[]>();
|
|
122
|
+
const down = new Map<string, string[]>();
|
|
123
|
+
for (const e of graph.edges) {
|
|
124
|
+
(up.get(e.to) ?? up.set(e.to, []).get(e.to)!).push(e.from);
|
|
125
|
+
(down.get(e.from) ?? down.set(e.from, []).get(e.from)!).push(e.to);
|
|
126
|
+
}
|
|
127
|
+
out.add(nodeId);
|
|
128
|
+
for (const adj of [up, down]) {
|
|
129
|
+
const seen = new Set<string>([nodeId]);
|
|
130
|
+
const stack = [nodeId];
|
|
131
|
+
while (stack.length) {
|
|
132
|
+
for (const next of adj.get(stack.pop()!) ?? []) {
|
|
133
|
+
if (seen.has(next)) continue;
|
|
134
|
+
seen.add(next);
|
|
135
|
+
out.add(next);
|
|
136
|
+
stack.push(next);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface LayoutOptions {
|
|
144
|
+
nodeWidth?: number;
|
|
145
|
+
nodeHeight?: number;
|
|
146
|
+
heightOf?: (node: LineageNode) => number;
|
|
147
|
+
rankGap?: number;
|
|
148
|
+
nodeGap?: number;
|
|
149
|
+
direction?: 'LR';
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export interface LayoutBox {
|
|
153
|
+
x: number;
|
|
154
|
+
y: number;
|
|
155
|
+
w: number;
|
|
156
|
+
h: number;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export interface LayoutPoint {
|
|
160
|
+
x: number;
|
|
161
|
+
y: number;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export interface Layout {
|
|
165
|
+
width: number;
|
|
166
|
+
height: number;
|
|
167
|
+
nodes: Map<string, LayoutBox>;
|
|
168
|
+
edges: { from: string; to: string; points: LayoutPoint[] }[];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Layered (Sugiyama-style) left-to-right layout. */
|
|
172
|
+
export function layoutGraph(graph: LineageGraph, opts: LayoutOptions = {}): Layout {
|
|
173
|
+
const nodeWidth = opts.nodeWidth ?? 168;
|
|
174
|
+
const nodeHeight = opts.nodeHeight ?? 44;
|
|
175
|
+
const rankGap = opts.rankGap ?? 64;
|
|
176
|
+
const nodeGap = opts.nodeGap ?? 20;
|
|
177
|
+
const n = graph.nodes.length;
|
|
178
|
+
const layout: Layout = { width: 0, height: 0, nodes: new Map(), edges: [] };
|
|
179
|
+
if (!n) return layout;
|
|
180
|
+
|
|
181
|
+
const index = new Map<string, number>();
|
|
182
|
+
graph.nodes.forEach((nd, i) => index.set(nd.id, i));
|
|
183
|
+
|
|
184
|
+
// Real edges (self-loops and dangling ends dropped).
|
|
185
|
+
const realEdges: { s: number; t: number; e: number }[] = [];
|
|
186
|
+
graph.edges.forEach((e, k) => {
|
|
187
|
+
const s = index.get(e.from);
|
|
188
|
+
const t = index.get(e.to);
|
|
189
|
+
if (s === undefined || t === undefined || s === t) return;
|
|
190
|
+
realEdges.push({ s, t, e: k });
|
|
191
|
+
});
|
|
192
|
+
const out: number[][] = Array.from({ length: n }, () => []);
|
|
193
|
+
realEdges.forEach((re, k) => out[re.s]!.push(k));
|
|
194
|
+
|
|
195
|
+
// Break cycles: iterative DFS, reverse back edges.
|
|
196
|
+
const reversed = new Uint8Array(realEdges.length);
|
|
197
|
+
const state = new Uint8Array(n); // 0 new, 1 on stack, 2 done
|
|
198
|
+
const indeg = new Int32Array(n);
|
|
199
|
+
for (const re of realEdges) indeg[re.t]!++;
|
|
200
|
+
const roots = [...Array(n).keys()].sort((a, b) => indeg[a]! - indeg[b]! || a - b);
|
|
201
|
+
for (const root of roots) {
|
|
202
|
+
if (state[root]) continue;
|
|
203
|
+
const stack: [number, number][] = [[root, 0]];
|
|
204
|
+
state[root] = 1;
|
|
205
|
+
while (stack.length) {
|
|
206
|
+
const top = stack[stack.length - 1]!;
|
|
207
|
+
const list = out[top[0]]!;
|
|
208
|
+
if (top[1] >= list.length) {
|
|
209
|
+
state[top[0]] = 2;
|
|
210
|
+
stack.pop();
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const k = list[top[1]++]!;
|
|
214
|
+
const t = realEdges[k]!.t;
|
|
215
|
+
if (state[t] === 1) reversed[k] = 1;
|
|
216
|
+
else if (state[t] === 0) {
|
|
217
|
+
state[t] = 1;
|
|
218
|
+
stack.push([t, 0]);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Ranks: longest path from sources (Kahn on the DAG).
|
|
224
|
+
const dagOut: number[][] = Array.from({ length: n }, () => []);
|
|
225
|
+
const din = new Int32Array(n);
|
|
226
|
+
realEdges.forEach((re, k) => {
|
|
227
|
+
const [s, t] = reversed[k] ? [re.t, re.s] : [re.s, re.t];
|
|
228
|
+
dagOut[s]!.push(t);
|
|
229
|
+
din[t]!++;
|
|
230
|
+
});
|
|
231
|
+
const rank = new Int32Array(n);
|
|
232
|
+
const queue: number[] = [];
|
|
233
|
+
for (let i = 0; i < n; i++) if (!din[i]) queue.push(i);
|
|
234
|
+
for (let qi = 0; qi < queue.length; qi++) {
|
|
235
|
+
const v = queue[qi]!;
|
|
236
|
+
for (const t of dagOut[v]!) {
|
|
237
|
+
if (rank[v]! + 1 > rank[t]!) rank[t] = rank[v]! + 1;
|
|
238
|
+
if (--din[t]! === 0) queue.push(t);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
let maxRank = 0;
|
|
242
|
+
graph.nodes.forEach((nd, i) => {
|
|
243
|
+
if (nd.kind !== 'target' && rank[i]! > maxRank) maxRank = rank[i]!;
|
|
244
|
+
});
|
|
245
|
+
graph.nodes.forEach((nd, i) => {
|
|
246
|
+
if (nd.kind === 'target') rank[i] = maxRank + 1;
|
|
247
|
+
});
|
|
248
|
+
for (let i = 0; i < n; i++) if (rank[i]! > maxRank) maxRank = rank[i]!;
|
|
249
|
+
|
|
250
|
+
// Layered graph with virtual nodes for long edges.
|
|
251
|
+
const vRank: number[] = Array.from(rank);
|
|
252
|
+
const h: number[] = graph.nodes.map((nd) => (opts.heightOf ? opts.heightOf(nd) : nodeHeight));
|
|
253
|
+
const preds: number[][] = Array.from({ length: n }, () => []);
|
|
254
|
+
const succs: number[][] = Array.from({ length: n }, () => []);
|
|
255
|
+
const chains: number[][] = [];
|
|
256
|
+
realEdges.forEach((re, k) => {
|
|
257
|
+
const [s, t] = reversed[k] ? [re.t, re.s] : [re.s, re.t];
|
|
258
|
+
const chain = [s];
|
|
259
|
+
for (let r = vRank[s]! + 1; r < vRank[t]!; r++) {
|
|
260
|
+
const v = vRank.length;
|
|
261
|
+
vRank.push(r);
|
|
262
|
+
h.push(0);
|
|
263
|
+
preds.push([]);
|
|
264
|
+
succs.push([]);
|
|
265
|
+
chain.push(v);
|
|
266
|
+
}
|
|
267
|
+
chain.push(t);
|
|
268
|
+
for (let i = 1; i < chain.length; i++) {
|
|
269
|
+
succs[chain[i - 1]!]!.push(chain[i]!);
|
|
270
|
+
preds[chain[i]!]!.push(chain[i - 1]!);
|
|
271
|
+
}
|
|
272
|
+
chains.push(chain);
|
|
273
|
+
});
|
|
274
|
+
const total = vRank.length;
|
|
275
|
+
const layers: number[][] = Array.from({ length: maxRank + 1 }, () => []);
|
|
276
|
+
for (let v = 0; v < total; v++) layers[vRank[v]!]!.push(v);
|
|
277
|
+
|
|
278
|
+
// Ordering: barycenter, 4 down/up sweeps.
|
|
279
|
+
const pos = new Float64Array(total);
|
|
280
|
+
const setPos = (layer: number[]) => layer.forEach((v, i) => (pos[v] = i));
|
|
281
|
+
layers.forEach(setPos);
|
|
282
|
+
const key = new Float64Array(total);
|
|
283
|
+
const sortLayer = (layer: number[], adj: number[][]) => {
|
|
284
|
+
for (const v of layer) {
|
|
285
|
+
const a = adj[v]!;
|
|
286
|
+
if (!a.length) key[v] = pos[v]!;
|
|
287
|
+
else {
|
|
288
|
+
let sum = 0;
|
|
289
|
+
for (const u of a) sum += pos[u]!;
|
|
290
|
+
key[v] = sum / a.length;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
layer.sort((a, b) => key[a]! - key[b]! || pos[a]! - pos[b]!);
|
|
294
|
+
setPos(layer);
|
|
295
|
+
};
|
|
296
|
+
for (let it = 0; it < 4; it++) {
|
|
297
|
+
for (let r = 1; r <= maxRank; r++) sortLayer(layers[r]!, preds);
|
|
298
|
+
for (let r = maxRank - 1; r >= 0; r--) sortLayer(layers[r]!, succs);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Coordinates: stack, then median alignment with order-preserving overlap resolution.
|
|
302
|
+
const y = new Float64Array(total);
|
|
303
|
+
for (const layer of layers) {
|
|
304
|
+
let acc = 0;
|
|
305
|
+
for (const v of layer) {
|
|
306
|
+
y[v] = acc;
|
|
307
|
+
acc += h[v]! + nodeGap;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const center = (v: number) => y[v]! + h[v]! / 2;
|
|
311
|
+
const align = (layer: number[], adj: number[][]) => {
|
|
312
|
+
const m = layer.length;
|
|
313
|
+
if (!m) return;
|
|
314
|
+
const d = new Float64Array(m);
|
|
315
|
+
for (let i = 0; i < m; i++) {
|
|
316
|
+
const v = layer[i]!;
|
|
317
|
+
const a = adj[v]!;
|
|
318
|
+
if (!a.length) d[i] = y[v]!;
|
|
319
|
+
else {
|
|
320
|
+
const cs = a.map(center).sort((p, q) => p - q);
|
|
321
|
+
const mid = cs.length >> 1;
|
|
322
|
+
const med = cs.length % 2 ? cs[mid]! : (cs[mid - 1]! + cs[mid]!) / 2;
|
|
323
|
+
d[i] = med - h[v]! / 2;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const f = new Float64Array(m);
|
|
327
|
+
const b = new Float64Array(m);
|
|
328
|
+
f[0] = d[0]!;
|
|
329
|
+
for (let i = 1; i < m; i++) f[i] = Math.max(d[i]!, f[i - 1]! + h[layer[i - 1]!]! + nodeGap);
|
|
330
|
+
b[m - 1] = d[m - 1]!;
|
|
331
|
+
for (let i = m - 2; i >= 0; i--) b[i] = Math.min(d[i]!, b[i + 1]! - nodeGap - h[layer[i]!]!);
|
|
332
|
+
for (let i = 0; i < m; i++) y[layer[i]!] = (f[i]! + b[i]!) / 2;
|
|
333
|
+
};
|
|
334
|
+
for (let it = 0; it < 4; it++) {
|
|
335
|
+
if (it % 2 === 0) for (let r = 1; r <= maxRank; r++) align(layers[r]!, preds);
|
|
336
|
+
else for (let r = maxRank - 1; r >= 0; r--) align(layers[r]!, succs);
|
|
337
|
+
}
|
|
338
|
+
let minY = Infinity;
|
|
339
|
+
let maxY = 0;
|
|
340
|
+
for (let v = 0; v < total; v++) minY = Math.min(minY, y[v]!);
|
|
341
|
+
for (let v = 0; v < total; v++) {
|
|
342
|
+
y[v] = y[v]! - minY;
|
|
343
|
+
maxY = Math.max(maxY, y[v]! + h[v]!);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const colX = (r: number) => r * (nodeWidth + rankGap);
|
|
347
|
+
graph.nodes.forEach((nd, i) => layout.nodes.set(nd.id, { x: colX(rank[i]!), y: y[i]!, w: nodeWidth, h: h[i]! }));
|
|
348
|
+
realEdges.forEach((re, k) => {
|
|
349
|
+
const chain = chains[k]!;
|
|
350
|
+
const s = chain[0]!;
|
|
351
|
+
const t = chain[chain.length - 1]!;
|
|
352
|
+
const points: LayoutPoint[] = [{ x: colX(vRank[s]!) + nodeWidth, y: center(s) }];
|
|
353
|
+
for (let i = 1; i < chain.length - 1; i++) {
|
|
354
|
+
const v = chain[i]!;
|
|
355
|
+
points.push({ x: colX(vRank[v]!), y: y[v]! }, { x: colX(vRank[v]!) + nodeWidth, y: y[v]! });
|
|
356
|
+
}
|
|
357
|
+
points.push({ x: colX(vRank[t]!), y: center(t) });
|
|
358
|
+
if (reversed[k]) points.reverse();
|
|
359
|
+
const e = graph.edges[re.e]!;
|
|
360
|
+
layout.edges.push({ from: e.from, to: e.to, points });
|
|
361
|
+
});
|
|
362
|
+
layout.width = colX(maxRank) + nodeWidth;
|
|
363
|
+
layout.height = maxY;
|
|
364
|
+
return layout;
|
|
365
|
+
}
|