@percy/cli-command 1.32.2-beta.0 → 1.32.3-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +0 -1
- package/package.json +5 -11
- package/dist/graphTrace.js +0 -157
- package/dist/graphTraceTemplate.html +0 -349
- package/dist/lockfileDiff.js +0 -184
- package/dist/smartsnap.js +0 -700
package/dist/index.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
export { default, command, _resetShutdownForTest } from './command.js';
|
|
2
2
|
export { legacyCommand, legacyFlags as flags } from './legacy.js';
|
|
3
|
-
export { applySmartSnap, SmartSnapBailError } from './smartsnap.js';
|
|
4
3
|
// export common packages to avoid dependency resolution issues
|
|
5
4
|
export { default as PercyConfig } from '@percy/config';
|
|
6
5
|
export { default as logger } from '@percy/logger';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@percy/cli-command",
|
|
3
|
-
"version": "1.32.
|
|
3
|
+
"version": "1.32.3-beta.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
".": "./dist/index.js",
|
|
28
28
|
"./flags": "./dist/flags.js",
|
|
29
29
|
"./utils": "./dist/utils.js",
|
|
30
|
-
"./smartsnap": "./dist/smartsnap.js",
|
|
31
30
|
"./test/helpers": "./test/helpers.js"
|
|
32
31
|
},
|
|
33
32
|
"scripts": {
|
|
@@ -37,14 +36,9 @@
|
|
|
37
36
|
"test:coverage": "yarn test --coverage"
|
|
38
37
|
},
|
|
39
38
|
"dependencies": {
|
|
40
|
-
"@percy/config": "1.32.
|
|
41
|
-
"@percy/core": "1.32.
|
|
42
|
-
"@percy/logger": "1.32.
|
|
43
|
-
"glob-to-regexp": "^0.4.1",
|
|
44
|
-
"stream-json": "^1.8.0"
|
|
39
|
+
"@percy/config": "1.32.3-beta.0",
|
|
40
|
+
"@percy/core": "1.32.3-beta.0",
|
|
41
|
+
"@percy/logger": "1.32.3-beta.0"
|
|
45
42
|
},
|
|
46
|
-
"
|
|
47
|
-
"snyk-nodejs-lockfile-parser": "2.7.1"
|
|
48
|
-
},
|
|
49
|
-
"gitHead": "b9f4f5ccfa31695047c2b9b1a4549403af075f7a"
|
|
43
|
+
"gitHead": "e8af175695b4afdb78cdd48b67037710f5c8b1c9"
|
|
50
44
|
}
|
package/dist/graphTrace.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import url from 'url';
|
|
4
|
-
|
|
5
|
-
// Template resolution mirrors core/utils.js's secretPatterns.yml lookup:
|
|
6
|
-
// resolves relative to this file's URL so it works under src/ (dev) and
|
|
7
|
-
// dist/ (installed) without bundler help. The .html file is copied alongside
|
|
8
|
-
// by babel's copyFiles when cli-command is built.
|
|
9
|
-
const TEMPLATE_PATH = path.resolve(url.fileURLToPath(import.meta.url), '../graphTraceTemplate.html');
|
|
10
|
-
|
|
11
|
-
// Maps a (raw kind, changed) pair to the kind value the template expects:
|
|
12
|
-
// 'package' | 'component' | 'story' | 'is_relevant'. `changed: true` wins
|
|
13
|
-
// over the underlying kind so any node touched in the diff renders purple.
|
|
14
|
-
function templateKindOf(v) {
|
|
15
|
-
if (v.changed) return 'is_relevant';
|
|
16
|
-
switch (v.kind) {
|
|
17
|
-
case 'dependency':
|
|
18
|
-
return 'package';
|
|
19
|
-
case 'component':
|
|
20
|
-
return 'component';
|
|
21
|
-
case 'story':
|
|
22
|
-
return 'story';
|
|
23
|
-
default:
|
|
24
|
-
return 'component';
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Sort order within a column: packages left, components middle, stories right.
|
|
29
|
-
// `is_relevant` shares rank with components so a changed node doesn't jump
|
|
30
|
-
// out of its own group — it just recolors.
|
|
31
|
-
const KIND_RANK = {
|
|
32
|
-
package: 0,
|
|
33
|
-
component: 1,
|
|
34
|
-
is_relevant: 1,
|
|
35
|
-
story: 2
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
// Layout algorithm (ported from the original Ruby renderer):
|
|
39
|
-
// 1. col = longest-path depth reaching the vertex (read from the
|
|
40
|
-
// transitive-closure triples the API sends), with dependencies pinned
|
|
41
|
-
// to col 0.
|
|
42
|
-
// 2. Propagate over edges so col[target] > col[source]. Bounded loop
|
|
43
|
-
// guards against degenerate inputs.
|
|
44
|
-
// 3. Stories pushed past the rightmost non-story column.
|
|
45
|
-
// 4. Within each column, sort by (kind-rank, name) and assign row.
|
|
46
|
-
function computeLayout(rawVertices, edges, transitiveClosure) {
|
|
47
|
-
const n = rawVertices.length;
|
|
48
|
-
const vertices = rawVertices.map((v, i) => ({
|
|
49
|
-
index: i,
|
|
50
|
-
name: v.file_path,
|
|
51
|
-
kind: v.kind,
|
|
52
|
-
changed: !!v.changed,
|
|
53
|
-
row: 0,
|
|
54
|
-
col: 0
|
|
55
|
-
}));
|
|
56
|
-
|
|
57
|
-
// 1. Seed col from incoming transitive-closure lengths.
|
|
58
|
-
const incomingMax = new Array(n).fill(0);
|
|
59
|
-
for (const triple of transitiveClosure) {
|
|
60
|
-
const [u, v, val] = triple;
|
|
61
|
-
if (u === v || val <= 0) continue;
|
|
62
|
-
if (v < 0 || v >= n) continue;
|
|
63
|
-
if (val > incomingMax[v]) incomingMax[v] = val;
|
|
64
|
-
}
|
|
65
|
-
for (let i = 0; i < n; i++) {
|
|
66
|
-
vertices[i].col = vertices[i].kind === 'dependency' ? 0 : incomingMax[i] + 1;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// 2. Propagate edge constraint. n+2 iterations is enough for any DAG
|
|
70
|
-
// and bounds the work on accidentally-cyclic input.
|
|
71
|
-
const iterations = n + 2;
|
|
72
|
-
for (let iter = 0; iter < iterations; iter++) {
|
|
73
|
-
let changed = false;
|
|
74
|
-
for (const [s, t] of edges) {
|
|
75
|
-
if (s < 0 || s >= n || t < 0 || t >= n) continue;
|
|
76
|
-
if (vertices[s].col < vertices[t].col) continue;
|
|
77
|
-
vertices[t].col = vertices[s].col + 1;
|
|
78
|
-
changed = true;
|
|
79
|
-
}
|
|
80
|
-
if (!changed) break;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// 3. Stories rightmost. Two passes: max across non-stories first, then
|
|
84
|
-
// push every story past that boundary. Folding into one loop would let
|
|
85
|
-
// stories visited before the last non-story keep a stale max.
|
|
86
|
-
let furthestNonStory = 0;
|
|
87
|
-
for (const v of vertices) {
|
|
88
|
-
if (v.kind === 'story') continue;
|
|
89
|
-
if (v.col > furthestNonStory) furthestNonStory = v.col;
|
|
90
|
-
}
|
|
91
|
-
for (const v of vertices) {
|
|
92
|
-
if (v.kind !== 'story') continue;
|
|
93
|
-
if (v.col < furthestNonStory + 1) v.col = furthestNonStory + 1;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// 4. Group by column, sort by (kind-rank, name), assign row.
|
|
97
|
-
const groups = new Map();
|
|
98
|
-
for (const v of vertices) {
|
|
99
|
-
let list = groups.get(v.col);
|
|
100
|
-
if (!list) groups.set(v.col, list = []);
|
|
101
|
-
list.push(v);
|
|
102
|
-
}
|
|
103
|
-
const rankOf = v => {
|
|
104
|
-
const r = KIND_RANK[templateKindOf(v)];
|
|
105
|
-
/* istanbul ignore next: templateKindOf always returns a kind present in
|
|
106
|
-
KIND_RANK, so the `=== undefined` fallback is defensive */
|
|
107
|
-
return r === undefined ? 99 : r;
|
|
108
|
-
};
|
|
109
|
-
for (const list of groups.values()) {
|
|
110
|
-
list.sort((a, b) => {
|
|
111
|
-
const ra = rankOf(a);
|
|
112
|
-
const rb = rankOf(b);
|
|
113
|
-
if (ra !== rb) return ra - rb;
|
|
114
|
-
// Byte-wise compare on name to match Ruby's String#<=> behaviour.
|
|
115
|
-
if (a.name < b.name) return -1;
|
|
116
|
-
if (a.name > b.name) return 1;
|
|
117
|
-
return 0;
|
|
118
|
-
});
|
|
119
|
-
list.forEach((v, row) => {
|
|
120
|
-
v.row = row;
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// 5. Final shape the template consumes: drop `changed`, fold it into kind.
|
|
125
|
-
return vertices.map(v => ({
|
|
126
|
-
index: v.index,
|
|
127
|
-
name: v.name,
|
|
128
|
-
row: v.row,
|
|
129
|
-
col: v.col,
|
|
130
|
-
kind: templateKindOf(v)
|
|
131
|
-
}));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Escapes characters that have meaning inside a <script> block so user-derived
|
|
135
|
-
// strings (e.g. a vertex file_path) can't break out of the surrounding tag.
|
|
136
|
-
// `</` covers `</script>`; `<!--`/`-->` cover HTML comment confusion; U+2028
|
|
137
|
-
// and U+2029 are valid JSON but illegal in JS string literals pre-ES2019 and
|
|
138
|
-
// have historically been XSS sinks.
|
|
139
|
-
const LS = String.fromCharCode(0x2028);
|
|
140
|
-
const PS = String.fromCharCode(0x2029);
|
|
141
|
-
function safeJson(obj) {
|
|
142
|
-
return JSON.stringify(obj).replace(/<\//g, '<\\/').replace(/<!--/g, '<\\!--').replace(/--(!?)>/g, '--$1\\>').split(LS).join('\\u2028').split(PS).join('\\u2029');
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Populates the trace template with the three JSON payloads the page needs.
|
|
146
|
-
// Input shape matches the API's graph data: `vertices` carries `kind`,
|
|
147
|
-
// `file_path`, `changed`; `edges` and `transitive_closure_matrix_sparse`
|
|
148
|
-
// are arrays of integer tuples.
|
|
149
|
-
export function renderGraphTraceHtml({
|
|
150
|
-
vertices,
|
|
151
|
-
edges,
|
|
152
|
-
transitiveClosureMatrixSparse
|
|
153
|
-
}) {
|
|
154
|
-
const laidOutVertices = computeLayout(vertices || [], edges || [], transitiveClosureMatrixSparse || []);
|
|
155
|
-
const template = fs.readFileSync(TEMPLATE_PATH, 'utf8');
|
|
156
|
-
return template.replace('__VERTICES_JSON__', safeJson(laidOutVertices)).replace('__EDGES_JSON__', safeJson(edges || [])).replace('__TRANSITIVE_CLOSURE_JSON__', safeJson(transitiveClosureMatrixSparse || []));
|
|
157
|
-
}
|
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
<!DOCTYPE html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8">
|
|
5
|
-
<title>Smartsnap Graph</title>
|
|
6
|
-
<link
|
|
7
|
-
rel="stylesheet"
|
|
8
|
-
href="https://cdn.jsdelivr.net/npm/drawflow@0.0.60/dist/drawflow.min.css"
|
|
9
|
-
integrity="sha384-IFh+Q6zh+LRcTjqVmAKetdGY59dT485vtvWT5DAKQy8iv5+fYWHXisHP7mFKcFqV"
|
|
10
|
-
crossorigin="anonymous">
|
|
11
|
-
<script
|
|
12
|
-
src="https://cdn.jsdelivr.net/npm/drawflow@0.0.60/dist/drawflow.min.js"
|
|
13
|
-
integrity="sha384-JsnM4f1X8b5e9VbQCj6Bt6840HQNDYVKohzbXJnqOetMno8i6cmKNLVkyK3snqSw"
|
|
14
|
-
crossorigin="anonymous"></script>
|
|
15
|
-
<style>
|
|
16
|
-
html, body {
|
|
17
|
-
margin: 0; padding: 0;
|
|
18
|
-
font-family: sans-serif; background: #fafafa;
|
|
19
|
-
width: 100%; height: 100%; overflow: hidden;
|
|
20
|
-
}
|
|
21
|
-
#drawflow {
|
|
22
|
-
position: fixed;
|
|
23
|
-
top: 0; left: 0; right: 0; bottom: 0;
|
|
24
|
-
background: #fafafa;
|
|
25
|
-
}
|
|
26
|
-
#legend {
|
|
27
|
-
position: fixed; top: 8px; right: 8px;
|
|
28
|
-
background: rgba(255,255,255,0.95);
|
|
29
|
-
padding: 8px 12px; border: 1px solid #999;
|
|
30
|
-
font-size: 12px; z-index: 100;
|
|
31
|
-
}
|
|
32
|
-
#legend span {
|
|
33
|
-
display: inline-block; width: 12px; height: 12px;
|
|
34
|
-
vertical-align: middle; margin-right: 4px; border: 1px solid #333;
|
|
35
|
-
}
|
|
36
|
-
#controls {
|
|
37
|
-
position: fixed; top: 8px; left: 8px;
|
|
38
|
-
background: rgba(255,255,255,0.95);
|
|
39
|
-
border: 1px solid #999; z-index: 100;
|
|
40
|
-
display: flex; gap: 2px;
|
|
41
|
-
}
|
|
42
|
-
#controls button {
|
|
43
|
-
border: 0; background: transparent;
|
|
44
|
-
padding: 6px 10px; cursor: pointer;
|
|
45
|
-
font-size: 14px; font-family: inherit;
|
|
46
|
-
}
|
|
47
|
-
#controls button:hover { background: #eee; }
|
|
48
|
-
|
|
49
|
-
.drawflow .drawflow-node {
|
|
50
|
-
width: max-content;
|
|
51
|
-
min-width: 0; max-width: none;
|
|
52
|
-
height: auto; min-height: 0;
|
|
53
|
-
padding: 8px 14px;
|
|
54
|
-
white-space: nowrap;
|
|
55
|
-
font-size: 12px;
|
|
56
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
57
|
-
background: #fff;
|
|
58
|
-
border: 2px solid #333;
|
|
59
|
-
border-radius: 4px;
|
|
60
|
-
cursor: pointer;
|
|
61
|
-
transition: opacity 150ms;
|
|
62
|
-
}
|
|
63
|
-
.drawflow .drawflow-node .drawflow_content_node {
|
|
64
|
-
width: max-content;
|
|
65
|
-
white-space: nowrap;
|
|
66
|
-
}
|
|
67
|
-
.drawflow .drawflow-node.package { background: #d4e5f7; border-color: #2a6db5; }
|
|
68
|
-
.drawflow .drawflow-node.component { background: #daf0c1; border-color: #4a8e1f; }
|
|
69
|
-
.drawflow .drawflow-node.story { background: #fce4b6; border-color: #b07310; }
|
|
70
|
-
.drawflow .drawflow-node.is-relevant { background: #d9b3ff; border-color: #6f30b0; }
|
|
71
|
-
|
|
72
|
-
.drawflow .drawflow-node .input,
|
|
73
|
-
.drawflow .drawflow-node .output {
|
|
74
|
-
width: 10px !important;
|
|
75
|
-
height: 10px !important;
|
|
76
|
-
border-width: 1px !important;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
.drawflow .connection,
|
|
80
|
-
.drawflow .connection *,
|
|
81
|
-
.drawflow svg.connection,
|
|
82
|
-
.drawflow svg.connection * { pointer-events: none !important; }
|
|
83
|
-
|
|
84
|
-
.drawflow .drawflow-node.is-dim { opacity: 0.25; }
|
|
85
|
-
.drawflow .connection.is-dim { visibility: hidden; }
|
|
86
|
-
|
|
87
|
-
.drawflow .drawflow-node.is-selected {
|
|
88
|
-
box-shadow: 0 0 0 3px #2a6db5, 0 2px 8px rgba(0,0,0,0.2);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.measure-node {
|
|
92
|
-
position: absolute;
|
|
93
|
-
top: -9999px; left: -9999px;
|
|
94
|
-
opacity: 0;
|
|
95
|
-
pointer-events: none;
|
|
96
|
-
white-space: nowrap;
|
|
97
|
-
padding: 8px 14px;
|
|
98
|
-
font-size: 12px;
|
|
99
|
-
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
100
|
-
border: 2px solid transparent;
|
|
101
|
-
}
|
|
102
|
-
</style>
|
|
103
|
-
</head>
|
|
104
|
-
<body>
|
|
105
|
-
<div id="controls">
|
|
106
|
-
<button id="zoom-in" title="Zoom in">+</button>
|
|
107
|
-
<button id="zoom-out" title="Zoom out">-</button>
|
|
108
|
-
<button id="zoom-reset" title="Reset">reset</button>
|
|
109
|
-
</div>
|
|
110
|
-
<div id="legend">
|
|
111
|
-
<div><span style="background:#d4e5f7"></span>Package Dependency</div>
|
|
112
|
-
<div><span style="background:#daf0c1"></span>Component</div>
|
|
113
|
-
<div><span style="background:#fce4b6"></span>Story</div>
|
|
114
|
-
<div><span style="background:#d9b3ff"></span>Changed file</div>
|
|
115
|
-
</div>
|
|
116
|
-
<div id="drawflow"></div>
|
|
117
|
-
<script type="module">
|
|
118
|
-
// ---------------------------------------------------------------------------
|
|
119
|
-
// Payload populated by the CLI before this template is written to disk.
|
|
120
|
-
// Tokens are replaced by graphTrace.js via plain string substitution.
|
|
121
|
-
// vertices: [{ index, name, row, col, kind }, ...]
|
|
122
|
-
// edges: [[srcIndex, tgtIndex], ...]
|
|
123
|
-
// transitive_closure_matrix_sparse: [[srcIndex, tgtIndex, length], ...]
|
|
124
|
-
// `kind` is one of: 'package' | 'component' | 'story' | 'is_relevant'.
|
|
125
|
-
// ---------------------------------------------------------------------------
|
|
126
|
-
const vertices = __VERTICES_JSON__;
|
|
127
|
-
const edges = __EDGES_JSON__;
|
|
128
|
-
const transitive_closure_matrix_sparse = __TRANSITIVE_CLOSURE_JSON__;
|
|
129
|
-
const n = vertices.length;
|
|
130
|
-
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
// Width measurement: each node grows to fit its file path. We need the
|
|
133
|
-
// rendered width (DOM-dependent) to lay out columns without overlap.
|
|
134
|
-
// ---------------------------------------------------------------------------
|
|
135
|
-
function measureWidth(text) {
|
|
136
|
-
const el = document.createElement('div');
|
|
137
|
-
el.className = 'measure-node';
|
|
138
|
-
el.textContent = text;
|
|
139
|
-
document.body.appendChild(el);
|
|
140
|
-
const w = el.offsetWidth;
|
|
141
|
-
el.remove();
|
|
142
|
-
return w;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Drawflow adds port circles on each side (~25px) + border; pad generously.
|
|
146
|
-
const PORT_PAD = 60;
|
|
147
|
-
const nodeWidth = vertices.map(v => measureWidth(v.name) + PORT_PAD);
|
|
148
|
-
|
|
149
|
-
// ---------------------------------------------------------------------------
|
|
150
|
-
// Column X positions (cumulative by max width per column).
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
const PAD_X = 60, PAD_Y = 60, COL_GAP = 140, ROW_HEIGHT = 70;
|
|
153
|
-
|
|
154
|
-
const colMaxWidth = {};
|
|
155
|
-
for (let i = 0; i < n; i++) {
|
|
156
|
-
const c = vertices[i].col;
|
|
157
|
-
if (!colMaxWidth[c] || nodeWidth[i] > colMaxWidth[c]) colMaxWidth[c] = nodeWidth[i];
|
|
158
|
-
}
|
|
159
|
-
const sortedCols = Object.keys(colMaxWidth).map(Number).sort((a, b) => a - b);
|
|
160
|
-
const colX = {};
|
|
161
|
-
let cursor = PAD_X;
|
|
162
|
-
for (const c of sortedCols) {
|
|
163
|
-
colX[c] = cursor;
|
|
164
|
-
cursor += colMaxWidth[c] + COL_GAP;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
// Boot Drawflow.
|
|
169
|
-
// ---------------------------------------------------------------------------
|
|
170
|
-
const container = document.getElementById('drawflow');
|
|
171
|
-
const editor = new Drawflow(container);
|
|
172
|
-
editor.reroute = false;
|
|
173
|
-
editor.start();
|
|
174
|
-
|
|
175
|
-
function escapeHtml(s) {
|
|
176
|
-
return String(s)
|
|
177
|
-
.replace(/&/g, '&')
|
|
178
|
-
.replace(/</g, '<')
|
|
179
|
-
.replace(/>/g, '>')
|
|
180
|
-
.replace(/"/g, '"')
|
|
181
|
-
.replace(/'/g, ''');
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// CSS classes per kind. `is_relevant` becomes `is-relevant` so it matches
|
|
185
|
-
// the stylesheet rule.
|
|
186
|
-
function cssClassFor(kind) {
|
|
187
|
-
if (kind === 'is_relevant') return 'is-relevant';
|
|
188
|
-
return kind;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// ---------------------------------------------------------------------------
|
|
192
|
-
// Add nodes. row/col come from the server; pixel positions are computed
|
|
193
|
-
// here using the measured widths so columns never overlap.
|
|
194
|
-
// ---------------------------------------------------------------------------
|
|
195
|
-
const nodeIds = new Array(n);
|
|
196
|
-
for (let i = 0; i < n; i++) {
|
|
197
|
-
const v = vertices[i];
|
|
198
|
-
const id = editor.addNode(
|
|
199
|
-
'node',
|
|
200
|
-
1, 1,
|
|
201
|
-
colX[v.col],
|
|
202
|
-
PAD_Y + v.row * ROW_HEIGHT,
|
|
203
|
-
cssClassFor(v.kind),
|
|
204
|
-
{ index: v.index, kind: v.kind, name: v.name },
|
|
205
|
-
escapeHtml(v.name),
|
|
206
|
-
);
|
|
207
|
-
nodeIds[i] = id;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
// ---------------------------------------------------------------------------
|
|
211
|
-
// Connections.
|
|
212
|
-
// ---------------------------------------------------------------------------
|
|
213
|
-
for (const e of edges) {
|
|
214
|
-
editor.addConnection(nodeIds[e[0]], nodeIds[e[1]], 'output_1', 'input_1');
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
editor.editor_mode = 'fixed';
|
|
218
|
-
|
|
219
|
-
// ---------------------------------------------------------------------------
|
|
220
|
-
// Arrowheads on connection paths.
|
|
221
|
-
// ---------------------------------------------------------------------------
|
|
222
|
-
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
223
|
-
function addArrowhead(svg) {
|
|
224
|
-
if (svg.querySelector('marker')) return;
|
|
225
|
-
let defs = svg.querySelector('defs');
|
|
226
|
-
if (!defs) {
|
|
227
|
-
defs = document.createElementNS(SVG_NS, 'defs');
|
|
228
|
-
svg.insertBefore(defs, svg.firstChild);
|
|
229
|
-
}
|
|
230
|
-
const marker = document.createElementNS(SVG_NS, 'marker');
|
|
231
|
-
marker.setAttribute('id', 'arrowhead');
|
|
232
|
-
marker.setAttribute('viewBox', '0 0 10 10');
|
|
233
|
-
marker.setAttribute('refX', '9');
|
|
234
|
-
marker.setAttribute('refY', '5');
|
|
235
|
-
marker.setAttribute('markerWidth', '5');
|
|
236
|
-
marker.setAttribute('markerHeight', '5');
|
|
237
|
-
marker.setAttribute('orient', 'auto');
|
|
238
|
-
const tip = document.createElementNS(SVG_NS, 'path');
|
|
239
|
-
tip.setAttribute('d', 'M 0 0 L 10 5 L 0 10 z');
|
|
240
|
-
tip.setAttribute('fill', '#4ea9ff');
|
|
241
|
-
marker.appendChild(tip);
|
|
242
|
-
defs.appendChild(marker);
|
|
243
|
-
const path = svg.querySelector('path.main-path') || svg.querySelector('path');
|
|
244
|
-
if (path) path.setAttribute('marker-end', 'url(#arrowhead)');
|
|
245
|
-
}
|
|
246
|
-
container.querySelectorAll('svg.connection').forEach(addArrowhead);
|
|
247
|
-
|
|
248
|
-
new MutationObserver((muts) => {
|
|
249
|
-
for (const m of muts) {
|
|
250
|
-
for (const node of m.addedNodes) {
|
|
251
|
-
if (node.nodeType !== 1) continue;
|
|
252
|
-
if (node.matches && node.matches('svg.connection')) addArrowhead(node);
|
|
253
|
-
if (node.querySelectorAll) {
|
|
254
|
-
node.querySelectorAll('svg.connection').forEach(addArrowhead);
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
}).observe(container, { subtree: true, childList: true });
|
|
259
|
-
|
|
260
|
-
// ---------------------------------------------------------------------------
|
|
261
|
-
// Click-to-select with bidirectional reachability highlight.
|
|
262
|
-
//
|
|
263
|
-
// Reachability comes straight from transitive_closure_matrix_sparse: for a click
|
|
264
|
-
// on vertex `start`, anything appearing as the target of a triple
|
|
265
|
-
// (start, j, _) is reachable from `start`, and anything appearing as the
|
|
266
|
-
// source of a triple (i, start, _) can reach `start`. No adjacency list —
|
|
267
|
-
// we just iterate the triples each time. With small click counts and a
|
|
268
|
-
// sparse matrix this is fast enough and avoids carrying redundant graph
|
|
269
|
-
// state in the page.
|
|
270
|
-
// ---------------------------------------------------------------------------
|
|
271
|
-
const idxByDfId = new Map();
|
|
272
|
-
for (let i = 0; i < n; i++) {
|
|
273
|
-
if (nodeIds[i] !== undefined) idxByDfId.set(nodeIds[i], i);
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function connectedTo(start) {
|
|
277
|
-
const seen = new Set([start]);
|
|
278
|
-
for (const transitive_edge of transitive_closure_matrix_sparse) {
|
|
279
|
-
if (transitive_edge[0] === start) seen.add(transitive_edge[1]);
|
|
280
|
-
else if (transitive_edge[1] === start) seen.add(transitive_edge[0]);
|
|
281
|
-
}
|
|
282
|
-
return seen;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function parseEndpointIds(svg) {
|
|
286
|
-
let outId = null, inId = null;
|
|
287
|
-
for (const cls of svg.classList) {
|
|
288
|
-
if (cls.startsWith('node_out_node-')) outId = parseInt(cls.slice('node_out_node-'.length), 10);
|
|
289
|
-
else if (cls.startsWith('node_in_node-')) inId = parseInt(cls.slice('node_in_node-'.length), 10);
|
|
290
|
-
}
|
|
291
|
-
return { outId, inId };
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
let currentSelected = null;
|
|
295
|
-
|
|
296
|
-
function clearSelection() {
|
|
297
|
-
container.querySelectorAll('.drawflow-node.is-dim, .drawflow-node.is-selected')
|
|
298
|
-
.forEach(el => el.classList.remove('is-dim', 'is-selected'));
|
|
299
|
-
container.querySelectorAll('svg.connection.is-dim')
|
|
300
|
-
.forEach(el => el.classList.remove('is-dim'));
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
function applySelection(startIdx) {
|
|
304
|
-
const connected = connectedTo(startIdx);
|
|
305
|
-
|
|
306
|
-
for (let i = 0; i < n; i++) {
|
|
307
|
-
const el = document.getElementById('node-' + nodeIds[i]);
|
|
308
|
-
if (!el) continue;
|
|
309
|
-
el.classList.toggle('is-dim', !connected.has(i));
|
|
310
|
-
el.classList.toggle('is-selected', i === startIdx);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
container.querySelectorAll('svg.connection').forEach((svg) => {
|
|
314
|
-
const { outId, inId } = parseEndpointIds(svg);
|
|
315
|
-
const s = idxByDfId.get(outId);
|
|
316
|
-
const t = idxByDfId.get(inId);
|
|
317
|
-
const keep = (s !== undefined && t !== undefined
|
|
318
|
-
&& connected.has(s) && connected.has(t));
|
|
319
|
-
svg.classList.toggle('is-dim', !keep);
|
|
320
|
-
});
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
container.addEventListener('click', (evt) => {
|
|
324
|
-
const nodeEl = evt.target.closest('.drawflow-node');
|
|
325
|
-
if (!nodeEl) return;
|
|
326
|
-
const m = nodeEl.id.match(/^node-(\d+)$/);
|
|
327
|
-
if (!m) return;
|
|
328
|
-
const dfId = parseInt(m[1], 10);
|
|
329
|
-
const idx = idxByDfId.get(dfId);
|
|
330
|
-
if (idx === undefined) return;
|
|
331
|
-
|
|
332
|
-
if (currentSelected === idx) {
|
|
333
|
-
currentSelected = null;
|
|
334
|
-
clearSelection();
|
|
335
|
-
} else {
|
|
336
|
-
currentSelected = idx;
|
|
337
|
-
applySelection(idx);
|
|
338
|
-
}
|
|
339
|
-
});
|
|
340
|
-
|
|
341
|
-
// ---------------------------------------------------------------------------
|
|
342
|
-
// Toolbar.
|
|
343
|
-
// ---------------------------------------------------------------------------
|
|
344
|
-
document.getElementById('zoom-in').onclick = () => editor.zoom_in();
|
|
345
|
-
document.getElementById('zoom-out').onclick = () => editor.zoom_out();
|
|
346
|
-
document.getElementById('zoom-reset').onclick = () => { editor.zoom_reset(); currentSelected = null; clearSelection(); };
|
|
347
|
-
</script>
|
|
348
|
-
</body>
|
|
349
|
-
</html>
|