@galaxy-tool-util/schema 1.13.1 → 1.14.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.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -2
- package/dist/index.js.map +1 -1
- package/dist/test-format/tests.schema.generated.d.ts +47 -0
- package/dist/test-format/tests.schema.generated.d.ts.map +1 -1
- package/dist/test-format/tests.schema.generated.js +62 -0
- package/dist/test-format/tests.schema.generated.js.map +1 -1
- package/dist/workflow/draft-checks.d.ts.map +1 -1
- package/dist/workflow/draft-checks.js +105 -59
- package/dist/workflow/draft-checks.js.map +1 -1
- package/dist/workflow/index.d.ts +4 -2
- package/dist/workflow/index.d.ts.map +1 -1
- package/dist/workflow/index.js +4 -2
- package/dist/workflow/index.js.map +1 -1
- package/dist/workflow/layout-properties.d.ts +10 -0
- package/dist/workflow/layout-properties.d.ts.map +1 -0
- package/dist/workflow/layout-properties.js +112 -0
- package/dist/workflow/layout-properties.js.map +1 -0
- package/dist/workflow/layout.d.ts +84 -0
- package/dist/workflow/layout.d.ts.map +1 -0
- package/dist/workflow/layout.js +356 -0
- package/dist/workflow/layout.js.map +1 -0
- package/dist/workflow/lint-rules.d.ts +6 -0
- package/dist/workflow/lint-rules.d.ts.map +1 -1
- package/dist/workflow/lint-rules.js +6 -0
- package/dist/workflow/lint-rules.js.map +1 -1
- package/dist/workflow/lint.d.ts +1 -1
- package/dist/workflow/lint.d.ts.map +1 -1
- package/dist/workflow/lint.js +83 -15
- package/dist/workflow/lint.js.map +1 -1
- package/dist/workflow/lint_profiles.yml +1 -0
- package/dist/workflow/normalized/expanded.d.ts +12 -0
- package/dist/workflow/normalized/expanded.d.ts.map +1 -1
- package/dist/workflow/normalized/expanded.js +42 -1
- package/dist/workflow/normalized/expanded.js.map +1 -1
- package/dist/workflow/normalized/index.d.ts +2 -2
- package/dist/workflow/normalized/index.d.ts.map +1 -1
- package/dist/workflow/normalized/index.js +2 -2
- package/dist/workflow/normalized/index.js.map +1 -1
- package/dist/workflow/normalized/labels.d.ts +7 -0
- package/dist/workflow/normalized/labels.d.ts.map +1 -1
- package/dist/workflow/normalized/labels.js +11 -0
- package/dist/workflow/normalized/labels.js.map +1 -1
- package/dist/workflow/normalized/toFormat2.d.ts +1 -0
- package/dist/workflow/normalized/toFormat2.d.ts.map +1 -1
- package/dist/workflow/normalized/toFormat2.js +4 -12
- package/dist/workflow/normalized/toFormat2.js.map +1 -1
- package/dist/workflow/normalized/toNative.d.ts +16 -1
- package/dist/workflow/normalized/toNative.d.ts.map +1 -1
- package/dist/workflow/normalized/toNative.js +23 -6
- package/dist/workflow/normalized/toNative.js.map +1 -1
- package/dist/workflow/semantic-validators.d.ts +9 -0
- package/dist/workflow/semantic-validators.d.ts.map +1 -1
- package/dist/workflow/semantic-validators.js +27 -14
- package/dist/workflow/semantic-validators.js.map +1 -1
- package/package.json +1 -1
package/dist/workflow/index.js
CHANGED
|
@@ -5,7 +5,7 @@ export { GalaxyWorkflowSchema, GalaxyWorkflowDraftSchema, DraftWorkflowStepSchem
|
|
|
5
5
|
// Plain JSON Schema siblings for downstream packagers (issue #108)
|
|
6
6
|
export { galaxyWorkflowDraftJsonSchema } from "./json-schemas.js";
|
|
7
7
|
// Normalized workflow types + normalizers
|
|
8
|
-
export { normalizedFormat2, normalizeStepIn, normalizeStepOut, NormalizedFormat2WorkflowSchema, NormalizedFormat2StepSchema, NormalizedFormat2InputSchema, NormalizedFormat2OutputSchema, NormalizedFormat2StepInputSchema, NormalizedFormat2StepOutputSchema, GalaxyUserToolStubSchema, isGalaxyUserToolRun, normalizedNative, NormalizedNativeWorkflowSchema, NormalizedNativeStepSchema, ToolReferenceSchema, toFormat2, toNative, toFormat2Stateful, toNativeStateful, ensureFormat2, ensureNative, expandedFormat2, expandedNative, isTrsUrl, MAX_EXPANSION_DEPTH, flattenCommentData, unflattenCommentData, resolveSourceReference, stepRenderIdentity, rawStepRenderIdentity, UNLABELED_INPUT_PREFIX, UNLABELED_STEP_PREFIX, } from "./normalized/index.js";
|
|
8
|
+
export { normalizedFormat2, normalizeStepIn, normalizeStepOut, NormalizedFormat2WorkflowSchema, NormalizedFormat2StepSchema, NormalizedFormat2InputSchema, NormalizedFormat2OutputSchema, NormalizedFormat2StepInputSchema, NormalizedFormat2StepOutputSchema, GalaxyUserToolStubSchema, isGalaxyUserToolRun, normalizedNative, NormalizedNativeWorkflowSchema, NormalizedNativeStepSchema, ToolReferenceSchema, toFormat2, toNative, toFormat2Stateful, toNativeStateful, ensureFormat2, ensureNative, expandedFormat2, expandedNative, isTrsUrl, trsDescriptorUrl, MAX_EXPANSION_DEPTH, flattenCommentData, unflattenCommentData, resolveSourceReference, stepRenderIdentity, rawStepRenderIdentity, unlabeledNodeId, UNLABELED_INPUT_PREFIX, UNLABELED_STEP_PREFIX, } from "./normalized/index.js";
|
|
9
9
|
export { flatStatePath, keysStartingWith, repeatInputsToArray, selectWhichWhen, } from "./walk-helpers.js";
|
|
10
10
|
export { injectConnectionsIntoState, stripConnectedValues, nativeConnectionsFromFormat2In, } from "./state-merge.js";
|
|
11
11
|
export { expandToolStateDefaults } from "./fill-defaults.js";
|
|
@@ -23,6 +23,8 @@ export { cleanWorkflow } from "./clean.js";
|
|
|
23
23
|
export { workflowToMermaid } from "./mermaid.js";
|
|
24
24
|
export { edgeAnnotationKey } from "./edge-annotation.js";
|
|
25
25
|
export { cytoscapeElements } from "./cytoscape.js";
|
|
26
|
+
export { applyLayout, layeredPositions, layoutPositions, AUTO as LAYOUT_AUTO, LayoutCycleError, } from "./layout.js";
|
|
27
|
+
export { GRAPH_PROPERTY_CHECKERS } from "./layout-properties.js";
|
|
26
28
|
export { elementsToList, } from "./cytoscape-models.js";
|
|
27
29
|
export { COL_STRIDE, ROW_STRIDE, LAYOUT_NAMES, bakesCoordinates, isLayoutName, topologicalPositions, } from "./cytoscape-layout.js";
|
|
28
30
|
export { detectFormat } from "./detect-format.js";
|
|
@@ -31,7 +33,7 @@ export { stripPlanFields, promoteFullyConcreteDrafts, } from "./promote-draft.js
|
|
|
31
33
|
export { withClass, validateFormat2, validateFormat2Strict, validateNative, validateNativeStrict, validatorForFixture, } from "./validators.js";
|
|
32
34
|
export { validateEncodingNative, validateEncodingFormat2, checkStrictEncoding, checkStrictStructure, } from "./strict-checks.js";
|
|
33
35
|
export { LintContext, LintMessage, Linter, LEVEL_ERROR, LEVEL_WARN, lintNative, lintFormat2, lintWorkflow, lintBestPracticesFormat2, lintBestPracticesNative, } from "./lint.js";
|
|
34
|
-
export { NativeStepKeyNotInteger } from "./lint-rules.js";
|
|
36
|
+
export { NativeStepKeyNotInteger, StepLabelNotUnique } from "./lint-rules.js";
|
|
35
37
|
export { parseLintProfiles, lintProfilesById, rulesForProfile, iwcRuleIds, LINT_PROFILES_FILENAME, IWC_PROFILE_NAMES, } from "./lint-profiles.js";
|
|
36
38
|
// Structured report models (mirrors Python _report_models.py)
|
|
37
39
|
export { SKIP_STATUSES, validationSummary, validationFailures, cleanDisplayLabel, categoryOf, groupByCategory, buildSingleValidationReport, buildSingleLintReport, buildSingleCleanReport, buildSingleDraftValidationReport, buildSingleDraftExtractReport, buildWorkflowValidationResult, buildLintWorkflowResult, buildWorkflowCleanResult, buildTreeValidationReport, buildLintTreeReport, buildTreeCleanReport, buildWorkflowExportResult, buildExportTreeReport, buildWorkflowToNativeResult, buildToNativeTreeReport, buildRoundTripTreeReport, } from "./report-models.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/workflow/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/workflow/index.ts"],"names":[],"mappings":"AAAA,qCAAqC;AACrC,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAiC,MAAM,gBAAgB,CAAC;AAEjG,wCAAwC;AACxC,OAAO,EAEL,oBAAoB,EAEpB,yBAAyB,EAEzB,uBAAuB,EAEvB,0BAA0B,GAC3B,MAAM,gBAAgB,CAAC;AAExB,mEAAmE;AACnE,OAAO,EAAE,6BAA6B,EAAE,MAAM,mBAAmB,CAAC;AAElE,0CAA0C;AAC1C,OAAO,EACL,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EAChB,+BAA+B,EAC/B,2BAA2B,EAC3B,4BAA4B,EAC5B,6BAA6B,EAC7B,gCAAgC,EAChC,iCAAiC,EACjC,wBAAwB,EACxB,mBAAmB,EAQnB,gBAAgB,EAChB,8BAA8B,EAC9B,0BAA0B,EAC1B,mBAAmB,EAInB,SAAS,EAGT,QAAQ,EAER,iBAAiB,EACjB,gBAAgB,EAMhB,aAAa,EACb,YAAY,EACZ,eAAe,EACf,cAAc,EACd,QAAQ,EACR,gBAAgB,EAChB,mBAAmB,EAOnB,kBAAkB,EAClB,oBAAoB,EACpB,sBAAsB,EACtB,kBAAkB,EAClB,qBAAqB,EACrB,eAAe,EACf,sBAAsB,EACtB,qBAAqB,GACtB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,aAAa,EACb,gBAAgB,EAChB,mBAAmB,EACnB,eAAe,GAChB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,0BAA0B,EAC1B,oBAAoB,EACpB,8BAA8B,GAC/B,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EAAE,uBAAuB,EAAE,MAAM,oBAAoB,CAAC;AAC7D,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAC;AAChE,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,SAAS,GAEV,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EAAE,mBAAmB,EAAkC,MAAM,uBAAuB,CAAC;AAE5F,OAAO,EACL,aAAa,GAId,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,UAAU,EACV,eAAe,EACf,oBAAoB,GAGrB,MAAM,aAAa,CAAC;AAErB,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,qBAAqB,EACrB,mBAAmB,GAEpB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,sBAAsB,EACtB,kBAAkB,GAGnB,MAAM,eAAe,CAAC;AAEvB,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,EACvB,wBAAwB,EACxB,8BAA8B,GAG/B,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EAAE,eAAe,EAA8B,MAAM,uBAAuB,CAAC;AAEpF,OAAO,EACL,iBAAiB,GAMlB,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,aAAa,EAAuD,MAAM,YAAY,CAAC;AAEhG,OAAO,EAAE,iBAAiB,EAAuB,MAAM,cAAc,CAAC;AAEtE,OAAO,EAAE,iBAAiB,EAAuB,MAAM,sBAAsB,CAAC;AAE9E,OAAO,EAAE,iBAAiB,EAAyB,MAAM,gBAAgB,CAAC;AAC1E,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,eAAe,EACf,IAAI,IAAI,WAAW,EACnB,gBAAgB,GAIjB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,uBAAuB,EAA6B,MAAM,wBAAwB,CAAC;AAC5F,OAAO,EACL,cAAc,GAUf,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EACL,UAAU,EACV,UAAU,EACV,YAAY,EACZ,gBAAgB,EAChB,YAAY,EACZ,oBAAoB,GAErB,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EAAE,YAAY,EAAuB,MAAM,oBAAoB,CAAC;AAEvE,OAAO,EACL,qBAAqB,EACrB,WAAW,EACX,WAAW,EACX,cAAc,EACd,eAAe,EACf,WAAW,EACX,aAAa,EACb,aAAa,EACb,qBAAqB,EACrB,mBAAmB,EACnB,aAAa,GAiBd,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EACL,eAAe,EACf,0BAA0B,GAI3B,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,SAAS,EACT,eAAe,EACf,qBAAqB,EACrB,cAAc,EACd,oBAAoB,EACpB,mBAAmB,GACpB,MAAM,iBAAiB,CAAC;AAEzB,OAAO,EACL,sBAAsB,EACtB,uBAAuB,EACvB,mBAAmB,EACnB,oBAAoB,GACrB,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,WAAW,EACX,WAAW,EACX,MAAM,EACN,WAAW,EACX,UAAU,EACV,UAAU,EACV,WAAW,EACX,YAAY,EACZ,wBAAwB,EACxB,uBAAuB,GAKxB,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAE,uBAAuB,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAE9E,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,eAAe,EACf,UAAU,EACV,sBAAsB,EACtB,iBAAiB,GAElB,MAAM,oBAAoB,CAAC;AAE5B,8DAA8D;AAC9D,OAAO,EAEL,aAAa,EAwDb,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,UAAU,EACV,eAAe,EACf,2BAA2B,EAC3B,qBAAqB,EACrB,sBAAsB,EACtB,gCAAgC,EAChC,6BAA6B,EAC7B,6BAA6B,EAC7B,uBAAuB,EACvB,wBAAwB,EACxB,yBAAyB,EACzB,mBAAmB,EACnB,oBAAoB,EACpB,yBAAyB,EACzB,qBAAqB,EACrB,2BAA2B,EAC3B,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural graph properties any conforming workflow layout must satisfy.
|
|
3
|
+
*
|
|
4
|
+
* Port of gxformat2/layout/_properties.py. The property names are the
|
|
5
|
+
* cross-language contract for `graph_properties` in layout.yml expectations;
|
|
6
|
+
* each checker takes a laid-out document and throws on violation.
|
|
7
|
+
*/
|
|
8
|
+
export type GraphPropertyChecker = (workflow: unknown) => void;
|
|
9
|
+
export declare const GRAPH_PROPERTY_CHECKERS: Record<string, GraphPropertyChecker>;
|
|
10
|
+
//# sourceMappingURL=layout-properties.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout-properties.d.ts","sourceRoot":"","sources":["../../src/workflow/layout-properties.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAeH,MAAM,MAAM,oBAAoB,GAAG,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;AAkH/D,eAAO,MAAM,uBAAuB,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAKxE,CAAC"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural graph properties any conforming workflow layout must satisfy.
|
|
3
|
+
*
|
|
4
|
+
* Port of gxformat2/layout/_properties.py. The property names are the
|
|
5
|
+
* cross-language contract for `graph_properties` in layout.yml expectations;
|
|
6
|
+
* each checker takes a laid-out document and throws on violation.
|
|
7
|
+
*/
|
|
8
|
+
import { extractGraph, format2NodeId, isGraphDocument, isNativeDocument, iterSubworkflows, layoutElements, nativeNodeId, } from "./layout.js";
|
|
9
|
+
function isDict(value) {
|
|
10
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
11
|
+
}
|
|
12
|
+
/** Read `{nodeId: position}` straight from the document (not from cytoscape). */
|
|
13
|
+
function readNodePositions(workflow) {
|
|
14
|
+
const positions = new Map();
|
|
15
|
+
if (isNativeDocument(workflow)) {
|
|
16
|
+
if (isDict(workflow.steps)) {
|
|
17
|
+
for (const [key, step] of Object.entries(workflow.steps)) {
|
|
18
|
+
if (isDict(step) && step.position != null) {
|
|
19
|
+
positions.set(nativeNodeId(key, step), step.position);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return positions;
|
|
24
|
+
}
|
|
25
|
+
for (const containerKey of ["inputs", "steps"]) {
|
|
26
|
+
const container = workflow[containerKey];
|
|
27
|
+
const isInput = containerKey === "inputs";
|
|
28
|
+
const entries = isDict(container)
|
|
29
|
+
? Object.entries(container)
|
|
30
|
+
: Array.isArray(container)
|
|
31
|
+
? container.map((item) => [null, item])
|
|
32
|
+
: [];
|
|
33
|
+
for (const [key, item] of entries) {
|
|
34
|
+
const nodeId = format2NodeId(item, key, isInput);
|
|
35
|
+
if (isDict(item) && item.position != null && nodeId !== null) {
|
|
36
|
+
positions.set(nodeId, item.position);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return positions;
|
|
41
|
+
}
|
|
42
|
+
function graph(workflow, graphEntries) {
|
|
43
|
+
const [nodeIds, edges] = extractGraph(layoutElements(workflow, graphEntries));
|
|
44
|
+
return [nodeIds, edges, readNodePositions(workflow)];
|
|
45
|
+
}
|
|
46
|
+
function check(condition, message) {
|
|
47
|
+
if (!condition)
|
|
48
|
+
throw new Error(message);
|
|
49
|
+
}
|
|
50
|
+
/** Every edge points strictly rightward: `target.left > source.left`. */
|
|
51
|
+
function downstreamRightOfUpstream([, edges, positions]) {
|
|
52
|
+
for (const [source, target] of edges) {
|
|
53
|
+
const s = positions.get(source);
|
|
54
|
+
const t = positions.get(target);
|
|
55
|
+
check(s !== undefined, `edge ${source}->${target}: source ${source} has no position`);
|
|
56
|
+
check(t !== undefined, `edge ${source}->${target}: target ${target} has no position`);
|
|
57
|
+
check(t.left > s.left, `edge ${source}->${target}: target.left ${t.left} not strictly right of source.left ${s.left}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Every graph node has a position in the document. */
|
|
61
|
+
function allNodesPositioned([nodeIds, , positions]) {
|
|
62
|
+
const missing = nodeIds.filter((id) => !positions.has(id)).sort();
|
|
63
|
+
check(missing.length === 0, `nodes missing a position: ${JSON.stringify(missing)}`);
|
|
64
|
+
}
|
|
65
|
+
/** No two nodes occupy the same `{left, top}` coordinate. */
|
|
66
|
+
function noPositionCollisions([, , positions]) {
|
|
67
|
+
const seen = new Map();
|
|
68
|
+
for (const [id, pos] of positions) {
|
|
69
|
+
const key = `${pos.left},${pos.top}`;
|
|
70
|
+
check(!seen.has(key), `nodes ${seen.get(key)} and ${id} share position (${key})`);
|
|
71
|
+
seen.set(key, id);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Nodes with no incoming edge sit at the minimum `left`. */
|
|
75
|
+
function rootsLeftmost([nodeIds, edges, positions]) {
|
|
76
|
+
const targets = new Set(edges.map(([, target]) => target));
|
|
77
|
+
const roots = nodeIds.filter((id) => !targets.has(id));
|
|
78
|
+
if (roots.length === 0 || positions.size === 0)
|
|
79
|
+
return;
|
|
80
|
+
const minLeft = Math.min(...[...positions.values()].map((p) => p.left));
|
|
81
|
+
for (const id of roots) {
|
|
82
|
+
const pos = positions.get(id);
|
|
83
|
+
check(pos !== undefined, `root ${id} has no position`);
|
|
84
|
+
check(pos.left === minLeft, `root ${id} at left ${pos.left} is not at the minimum left ${minLeft}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** The document plus every in-file subworkflow / `$graph` entry it lays out. */
|
|
88
|
+
function* iterLayoutUnits(workflow, graphEntries = []) {
|
|
89
|
+
if (isGraphDocument(workflow)) {
|
|
90
|
+
const raw = workflow.$graph;
|
|
91
|
+
const entries = (Array.isArray(raw) ? raw : isDict(raw) ? Object.values(raw) : []).filter(isDict);
|
|
92
|
+
for (const entry of entries)
|
|
93
|
+
yield* iterLayoutUnits(entry, entries);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
yield graph(workflow, graphEntries);
|
|
97
|
+
for (const sub of iterSubworkflows(workflow))
|
|
98
|
+
yield* iterLayoutUnits(sub);
|
|
99
|
+
}
|
|
100
|
+
function recursive(checker) {
|
|
101
|
+
return (workflow) => {
|
|
102
|
+
for (const unit of iterLayoutUnits(workflow))
|
|
103
|
+
checker(unit);
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
export const GRAPH_PROPERTY_CHECKERS = {
|
|
107
|
+
downstream_right_of_upstream: recursive(downstreamRightOfUpstream),
|
|
108
|
+
all_nodes_positioned: recursive(allNodesPositioned),
|
|
109
|
+
no_position_collisions: recursive(noPositionCollisions),
|
|
110
|
+
roots_leftmost: recursive(rootsLeftmost),
|
|
111
|
+
};
|
|
112
|
+
//# sourceMappingURL=layout-properties.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout-properties.js","sourceRoot":"","sources":["../../src/workflow/layout-properties.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute and apply node positions for Galaxy workflows.
|
|
3
|
+
*
|
|
4
|
+
* Port of gxformat2/layout. Two strategies: `topological` (the cross-language
|
|
5
|
+
* layering from `cytoscape-layout.ts`) and `layered` (barycenter Sugiyama
|
|
6
|
+
* crossing reduction). Layered coordinates are not a cross-language contract;
|
|
7
|
+
* both strategies are validated by the structural checkers in
|
|
8
|
+
* `layout-properties.ts`. Cyclic workflows raise {@link LayoutCycleError}.
|
|
9
|
+
*/
|
|
10
|
+
import type { CytoscapeElements, CytoscapePosition } from "./cytoscape-models.js";
|
|
11
|
+
/** Sentinel value for `position:` that requests automatic layout. */
|
|
12
|
+
export declare const AUTO = "auto";
|
|
13
|
+
export type LayoutStrategy = "topological" | "layered";
|
|
14
|
+
export interface LayoutPosition {
|
|
15
|
+
left: number;
|
|
16
|
+
top: number;
|
|
17
|
+
}
|
|
18
|
+
export interface ApplyLayoutOptions {
|
|
19
|
+
strategy?: LayoutStrategy;
|
|
20
|
+
/** Replace explicit positions too (an `auto` sentinel is always replaced). */
|
|
21
|
+
overwrite?: boolean;
|
|
22
|
+
/** Also lay out embedded in-file subworkflows. Defaults to true. */
|
|
23
|
+
recursive?: boolean;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Raised when a workflow graph contains a cycle. The bake path refuses to lay
|
|
27
|
+
* out a cyclic graph; the cytoscape viz path keeps its diagonal fallback.
|
|
28
|
+
*/
|
|
29
|
+
export declare class LayoutCycleError extends Error {
|
|
30
|
+
constructor(message: string);
|
|
31
|
+
}
|
|
32
|
+
type Dict = Record<string, unknown>;
|
|
33
|
+
export type Edge = [string, string];
|
|
34
|
+
/** Pull `[nodeIds, edges]` out of cytoscape elements, dropping dangling edges. */
|
|
35
|
+
export declare function extractGraph(elements: CytoscapeElements): [string[], Edge[]];
|
|
36
|
+
/**
|
|
37
|
+
* Longest-path layering via Kahn topological sort with declaration-index tie
|
|
38
|
+
* break (same as `topologicalPositions`). Throws {@link LayoutCycleError} when
|
|
39
|
+
* the graph is not a DAG.
|
|
40
|
+
*/
|
|
41
|
+
export declare function layerAssignment(nodeIds: string[], edges: Edge[]): Map<string, number>;
|
|
42
|
+
/** Throw {@link LayoutCycleError} if `elements` describes a cyclic graph. */
|
|
43
|
+
export declare function checkAcyclic(elements: CytoscapeElements): void;
|
|
44
|
+
/**
|
|
45
|
+
* Sugiyama-style layered layout: longest-path layering, barycenter crossing
|
|
46
|
+
* reduction, then order-index rows per layer. Skip-level edges are not split
|
|
47
|
+
* into dummy nodes, so results can be suboptimal but every edge still points
|
|
48
|
+
* rightward.
|
|
49
|
+
*/
|
|
50
|
+
export declare function layeredPositions(elements: CytoscapeElements): Map<string, CytoscapePosition>;
|
|
51
|
+
export declare function isNativeDocument(workflow: Dict): boolean;
|
|
52
|
+
/** A `$graph` multi-workflow document (many workflows, one file). */
|
|
53
|
+
export declare function isGraphDocument(workflow: unknown): workflow is Dict;
|
|
54
|
+
/**
|
|
55
|
+
* Yield in-file subworkflows embedded in `workflow`'s steps (native inline
|
|
56
|
+
* `subworkflow`, Format2 embedded `run`). String references are not followed.
|
|
57
|
+
*/
|
|
58
|
+
export declare function iterSubworkflows(workflow: Dict): Generator<Dict>;
|
|
59
|
+
/**
|
|
60
|
+
* Id cytoscape assigns to a Format2 node: inputs by id, steps by render
|
|
61
|
+
* identity. `key` is the dict-container key, or null for list form.
|
|
62
|
+
*/
|
|
63
|
+
export declare function format2NodeId(item: unknown, key: string | null, isInput: boolean): string | null;
|
|
64
|
+
/** Id normalization assigns to a native step (label, else unlabeled sentinel). */
|
|
65
|
+
export declare function nativeNodeId(key: string, step: Dict): string;
|
|
66
|
+
/**
|
|
67
|
+
* Compute node positions keyed by node id (Format2 input ids / step render
|
|
68
|
+
* identities — the ids the cytoscape builder emits).
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* Preset cytoscape elements for one layout unit. `graphEntries` are the
|
|
72
|
+
* sibling entries when `workflow` is part of a `$graph` document.
|
|
73
|
+
*/
|
|
74
|
+
export declare function layoutElements(workflow: unknown, graphEntries?: Dict[]): CytoscapeElements;
|
|
75
|
+
export declare function layoutPositions(workflow: unknown, strategy?: LayoutStrategy, graphEntries?: Dict[]): Map<string, LayoutPosition>;
|
|
76
|
+
/**
|
|
77
|
+
* Merge computed positions into a Format2 or native workflow dict, mutating it
|
|
78
|
+
* in place and returning it. Explicit positions are kept unless `overwrite`;
|
|
79
|
+
* `position: auto` is always replaced. With `recursive` (default), embedded
|
|
80
|
+
* subworkflows and every `$graph` entry get their own coordinate space.
|
|
81
|
+
*/
|
|
82
|
+
export declare function applyLayout(workflow: Dict, opts?: ApplyLayoutOptions): Dict;
|
|
83
|
+
export {};
|
|
84
|
+
//# sourceMappingURL=layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"layout.d.ts","sourceRoot":"","sources":["../../src/workflow/layout.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAKlF,qEAAqE;AACrE,eAAO,MAAM,IAAI,SAAS,CAAC;AAE3B,MAAM,MAAM,cAAc,GAAG,aAAa,GAAG,SAAS,CAAC;AAEvD,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,8EAA8E;IAC9E,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oEAAoE;IACpE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;GAGG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;gBAC7B,OAAO,EAAE,MAAM;CAI5B;AAED,KAAK,IAAI,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;AACpC,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAcpC,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,QAAQ,EAAE,iBAAiB,GAAG,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,CAAC,CAS5E;AAED;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAmCrF;AAED,6EAA6E;AAC7E,wBAAgB,YAAY,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAG9D;AAkED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,iBAAiB,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAgC5F;AAID,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,IAAI,GAAG,OAAO,CAExD;AAED,qEAAqE;AACrE,wBAAgB,eAAe,CAAC,QAAQ,EAAE,OAAO,GAAG,QAAQ,IAAI,IAAI,CAEnE;AAMD;;;GAGG;AACH,wBAAiB,gBAAgB,CAAC,QAAQ,EAAE,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAOjE;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAShG;AAED,kFAAkF;AAClF,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,MAAM,CAM5D;AAoBD;;;GAGG;AACH;;;GAGG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,OAAO,EAAE,YAAY,GAAE,IAAI,EAAO,GAAG,iBAAiB,CAU9F;AAED,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,OAAO,EACjB,QAAQ,GAAE,cAA8B,EACxC,YAAY,GAAE,IAAI,EAAO,GACxB,GAAG,CAAC,MAAM,EAAE,cAAc,CAAC,CAgB7B;AAoCD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,IAAI,EAAE,IAAI,GAAE,kBAAuB,GAAG,IAAI,CAO/E"}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compute and apply node positions for Galaxy workflows.
|
|
3
|
+
*
|
|
4
|
+
* Port of gxformat2/layout. Two strategies: `topological` (the cross-language
|
|
5
|
+
* layering from `cytoscape-layout.ts`) and `layered` (barycenter Sugiyama
|
|
6
|
+
* crossing reduction). Layered coordinates are not a cross-language contract;
|
|
7
|
+
* both strategies are validated by the structural checkers in
|
|
8
|
+
* `layout-properties.ts`. Cyclic workflows raise {@link LayoutCycleError}.
|
|
9
|
+
*/
|
|
10
|
+
import { COL_STRIDE, ROW_STRIDE, topologicalPositions } from "./cytoscape-layout.js";
|
|
11
|
+
import { cytoscapeElements } from "./cytoscape.js";
|
|
12
|
+
import { rawStepRenderIdentity, unlabeledNodeId } from "./normalized/labels.js";
|
|
13
|
+
import { INPUT_STEP_TYPES } from "./normalized/toFormat2.js";
|
|
14
|
+
/** Sentinel value for `position:` that requests automatic layout. */
|
|
15
|
+
export const AUTO = "auto";
|
|
16
|
+
/**
|
|
17
|
+
* Raised when a workflow graph contains a cycle. The bake path refuses to lay
|
|
18
|
+
* out a cyclic graph; the cytoscape viz path keeps its diagonal fallback.
|
|
19
|
+
*/
|
|
20
|
+
export class LayoutCycleError extends Error {
|
|
21
|
+
constructor(message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = "LayoutCycleError";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isDict(value) {
|
|
27
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
28
|
+
}
|
|
29
|
+
function containerItems(container) {
|
|
30
|
+
if (Array.isArray(container))
|
|
31
|
+
return container;
|
|
32
|
+
if (isDict(container))
|
|
33
|
+
return Object.values(container);
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
// --- Graph + layering -------------------------------------------------------
|
|
37
|
+
/** Pull `[nodeIds, edges]` out of cytoscape elements, dropping dangling edges. */
|
|
38
|
+
export function extractGraph(elements) {
|
|
39
|
+
const nodeIds = elements.nodes.map((n) => n.data.id);
|
|
40
|
+
const idSet = new Set(nodeIds);
|
|
41
|
+
const edges = [];
|
|
42
|
+
for (const edge of elements.edges) {
|
|
43
|
+
const { source, target } = edge.data;
|
|
44
|
+
if (idSet.has(source) && idSet.has(target))
|
|
45
|
+
edges.push([source, target]);
|
|
46
|
+
}
|
|
47
|
+
return [nodeIds, edges];
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Longest-path layering via Kahn topological sort with declaration-index tie
|
|
51
|
+
* break (same as `topologicalPositions`). Throws {@link LayoutCycleError} when
|
|
52
|
+
* the graph is not a DAG.
|
|
53
|
+
*/
|
|
54
|
+
export function layerAssignment(nodeIds, edges) {
|
|
55
|
+
const indexById = new Map(nodeIds.map((id, i) => [id, i]));
|
|
56
|
+
const incoming = new Map(nodeIds.map((id) => [id, []]));
|
|
57
|
+
const dependents = new Map(nodeIds.map((id) => [id, []]));
|
|
58
|
+
for (const [source, target] of edges) {
|
|
59
|
+
incoming.get(target).push(source);
|
|
60
|
+
dependents.get(source).push(target);
|
|
61
|
+
}
|
|
62
|
+
const inDegree = new Map(nodeIds.map((id) => [id, incoming.get(id).length]));
|
|
63
|
+
const column = new Map();
|
|
64
|
+
const queue = nodeIds.filter((id) => inDegree.get(id) === 0);
|
|
65
|
+
while (queue.length > 0) {
|
|
66
|
+
let best = 0;
|
|
67
|
+
for (let i = 1; i < queue.length; i++) {
|
|
68
|
+
if (indexById.get(queue[i]) < indexById.get(queue[best]))
|
|
69
|
+
best = i;
|
|
70
|
+
}
|
|
71
|
+
const id = queue.splice(best, 1)[0];
|
|
72
|
+
const sources = incoming.get(id);
|
|
73
|
+
column.set(id, sources.length === 0 ? 0 : Math.max(...sources.map((s) => column.get(s) + 1)));
|
|
74
|
+
for (const dep of dependents.get(id)) {
|
|
75
|
+
const remaining = inDegree.get(dep) - 1;
|
|
76
|
+
inDegree.set(dep, remaining);
|
|
77
|
+
if (remaining === 0)
|
|
78
|
+
queue.push(dep);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (column.size !== nodeIds.length) {
|
|
82
|
+
const unplaced = nodeIds.filter((id) => !column.has(id)).sort();
|
|
83
|
+
throw new LayoutCycleError("Workflow graph contains a cycle; cannot compute a layout. " +
|
|
84
|
+
`Nodes on or downstream of the cycle: ${JSON.stringify(unplaced)}`);
|
|
85
|
+
}
|
|
86
|
+
return column;
|
|
87
|
+
}
|
|
88
|
+
/** Throw {@link LayoutCycleError} if `elements` describes a cyclic graph. */
|
|
89
|
+
export function checkAcyclic(elements) {
|
|
90
|
+
const [nodeIds, edges] = extractGraph(elements);
|
|
91
|
+
layerAssignment(nodeIds, edges);
|
|
92
|
+
}
|
|
93
|
+
/** Barycenter sweeps to attempt; the best ordering seen is kept. */
|
|
94
|
+
const MAX_SWEEPS = 8;
|
|
95
|
+
function countCrossingsBetween(upper, lower, successors) {
|
|
96
|
+
const posLower = new Map(lower.map((id, i) => [id, i]));
|
|
97
|
+
const edges = [];
|
|
98
|
+
upper.forEach((u, uIdx) => {
|
|
99
|
+
for (const v of successors.get(u)) {
|
|
100
|
+
const vIdx = posLower.get(v);
|
|
101
|
+
if (vIdx !== undefined)
|
|
102
|
+
edges.push([uIdx, vIdx]);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
let crossings = 0;
|
|
106
|
+
for (let i = 0; i < edges.length; i++) {
|
|
107
|
+
const [ui, vi] = edges[i];
|
|
108
|
+
for (let j = i + 1; j < edges.length; j++) {
|
|
109
|
+
const [uj, vj] = edges[j];
|
|
110
|
+
if ((ui < uj && vi > vj) || (ui > uj && vi < vj))
|
|
111
|
+
crossings += 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return crossings;
|
|
115
|
+
}
|
|
116
|
+
function countAllCrossings(layers, successors) {
|
|
117
|
+
let total = 0;
|
|
118
|
+
for (let col = 0; col < layers.length - 1; col++) {
|
|
119
|
+
total += countCrossingsBetween(layers[col], layers[col + 1], successors);
|
|
120
|
+
}
|
|
121
|
+
return total;
|
|
122
|
+
}
|
|
123
|
+
/** Stable reorder of `layer` by mean position of each node's fixed neighbors. */
|
|
124
|
+
function orderByBarycenter(layer, neighbors, posInFixed) {
|
|
125
|
+
const keyed = layer.map((id, idx) => {
|
|
126
|
+
const fixed = neighbors
|
|
127
|
+
.get(id)
|
|
128
|
+
.map((m) => posInFixed.get(m))
|
|
129
|
+
.filter((p) => p !== undefined);
|
|
130
|
+
const key = fixed.length > 0 ? fixed.reduce((a, b) => a + b, 0) / fixed.length : idx;
|
|
131
|
+
return { id, key };
|
|
132
|
+
});
|
|
133
|
+
// Array.prototype.sort is stable, so ties preserve input order.
|
|
134
|
+
return keyed.sort((a, b) => a.key - b.key).map((k) => k.id);
|
|
135
|
+
}
|
|
136
|
+
function sweep(layers, neighbors, down) {
|
|
137
|
+
const columns = [];
|
|
138
|
+
if (down)
|
|
139
|
+
for (let col = 1; col < layers.length; col++)
|
|
140
|
+
columns.push(col);
|
|
141
|
+
else
|
|
142
|
+
for (let col = layers.length - 2; col >= 0; col--)
|
|
143
|
+
columns.push(col);
|
|
144
|
+
for (const col of columns) {
|
|
145
|
+
const fixed = layers[down ? col - 1 : col + 1];
|
|
146
|
+
const posInFixed = new Map(fixed.map((id, i) => [id, i]));
|
|
147
|
+
layers[col] = orderByBarycenter(layers[col], neighbors, posInFixed);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Sugiyama-style layered layout: longest-path layering, barycenter crossing
|
|
152
|
+
* reduction, then order-index rows per layer. Skip-level edges are not split
|
|
153
|
+
* into dummy nodes, so results can be suboptimal but every edge still points
|
|
154
|
+
* rightward.
|
|
155
|
+
*/
|
|
156
|
+
export function layeredPositions(elements) {
|
|
157
|
+
const [nodeIds, edges] = extractGraph(elements);
|
|
158
|
+
const column = layerAssignment(nodeIds, edges);
|
|
159
|
+
const maxCol = Math.max(-1, ...column.values());
|
|
160
|
+
const layers = Array.from({ length: maxCol + 1 }, () => []);
|
|
161
|
+
for (const id of nodeIds)
|
|
162
|
+
layers[column.get(id)].push(id);
|
|
163
|
+
const predecessors = new Map(nodeIds.map((id) => [id, []]));
|
|
164
|
+
const successors = new Map(nodeIds.map((id) => [id, []]));
|
|
165
|
+
for (const [source, target] of edges) {
|
|
166
|
+
successors.get(source).push(target);
|
|
167
|
+
predecessors.get(target).push(source);
|
|
168
|
+
}
|
|
169
|
+
let bestLayers = layers.map((l) => [...l]);
|
|
170
|
+
let bestCrossings = countAllCrossings(bestLayers, successors);
|
|
171
|
+
for (let i = 0; i < MAX_SWEEPS && bestCrossings > 0; i++) {
|
|
172
|
+
const goingDown = i % 2 === 0;
|
|
173
|
+
sweep(layers, goingDown ? predecessors : successors, goingDown);
|
|
174
|
+
const crossings = countAllCrossings(layers, successors);
|
|
175
|
+
if (crossings < bestCrossings) {
|
|
176
|
+
bestCrossings = crossings;
|
|
177
|
+
bestLayers = layers.map((l) => [...l]);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const positions = new Map();
|
|
181
|
+
bestLayers.forEach((layer, col) => {
|
|
182
|
+
layer.forEach((id, row) => positions.set(id, { x: col * COL_STRIDE, y: row * ROW_STRIDE }));
|
|
183
|
+
});
|
|
184
|
+
return positions;
|
|
185
|
+
}
|
|
186
|
+
// --- Document helpers (shared with layout-properties.ts) ---------------------
|
|
187
|
+
export function isNativeDocument(workflow) {
|
|
188
|
+
return workflow.a_galaxy_workflow === "true";
|
|
189
|
+
}
|
|
190
|
+
/** A `$graph` multi-workflow document (many workflows, one file). */
|
|
191
|
+
export function isGraphDocument(workflow) {
|
|
192
|
+
return isDict(workflow) && "$graph" in workflow && !("class" in workflow);
|
|
193
|
+
}
|
|
194
|
+
function isEmbeddedWorkflow(value) {
|
|
195
|
+
return isDict(value) && (value.class === "GalaxyWorkflow" || value.a_galaxy_workflow === "true");
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Yield in-file subworkflows embedded in `workflow`'s steps (native inline
|
|
199
|
+
* `subworkflow`, Format2 embedded `run`). String references are not followed.
|
|
200
|
+
*/
|
|
201
|
+
export function* iterSubworkflows(workflow) {
|
|
202
|
+
const native = isNativeDocument(workflow);
|
|
203
|
+
for (const step of containerItems(workflow.steps)) {
|
|
204
|
+
if (!isDict(step))
|
|
205
|
+
continue;
|
|
206
|
+
const candidate = native ? step.subworkflow : step.run;
|
|
207
|
+
if (isEmbeddedWorkflow(candidate))
|
|
208
|
+
yield candidate;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Id cytoscape assigns to a Format2 node: inputs by id, steps by render
|
|
213
|
+
* identity. `key` is the dict-container key, or null for list form.
|
|
214
|
+
*/
|
|
215
|
+
export function format2NodeId(item, key, isInput) {
|
|
216
|
+
if (isInput) {
|
|
217
|
+
if (key !== null)
|
|
218
|
+
return key;
|
|
219
|
+
return isDict(item) && typeof item.id === "string" ? item.id : null;
|
|
220
|
+
}
|
|
221
|
+
if (key !== null)
|
|
222
|
+
return rawStepRenderIdentity(item, key);
|
|
223
|
+
if (!isDict(item))
|
|
224
|
+
return null;
|
|
225
|
+
const id = item.label || item.id;
|
|
226
|
+
return id ?? null;
|
|
227
|
+
}
|
|
228
|
+
/** Id normalization assigns to a native step (label, else unlabeled sentinel). */
|
|
229
|
+
export function nativeNodeId(key, step) {
|
|
230
|
+
return unlabeledNodeId(step.label, step.id ?? key, INPUT_STEP_TYPES.has(step.type));
|
|
231
|
+
}
|
|
232
|
+
function stripPositions(workflow) {
|
|
233
|
+
const cleaned = JSON.parse(JSON.stringify(workflow));
|
|
234
|
+
const containerKeys = isNativeDocument(cleaned) ? ["steps"] : ["inputs", "steps"];
|
|
235
|
+
for (const containerKey of containerKeys) {
|
|
236
|
+
for (const item of containerItems(cleaned[containerKey])) {
|
|
237
|
+
if (isDict(item))
|
|
238
|
+
delete item.position;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return cleaned;
|
|
242
|
+
}
|
|
243
|
+
function shouldWrite(existing, overwrite) {
|
|
244
|
+
if (existing == null || existing === AUTO)
|
|
245
|
+
return true;
|
|
246
|
+
return overwrite;
|
|
247
|
+
}
|
|
248
|
+
// --- Public API ---------------------------------------------------------------
|
|
249
|
+
/**
|
|
250
|
+
* Compute node positions keyed by node id (Format2 input ids / step render
|
|
251
|
+
* identities — the ids the cytoscape builder emits).
|
|
252
|
+
*/
|
|
253
|
+
/**
|
|
254
|
+
* Preset cytoscape elements for one layout unit. `graphEntries` are the
|
|
255
|
+
* sibling entries when `workflow` is part of a `$graph` document.
|
|
256
|
+
*/
|
|
257
|
+
export function layoutElements(workflow, graphEntries = []) {
|
|
258
|
+
// Strip positions first: strategies ignore them and `auto` isn't a valid position.
|
|
259
|
+
let source = isDict(workflow) ? stripPositions(workflow) : workflow;
|
|
260
|
+
if (graphEntries.length > 0 && isDict(source)) {
|
|
261
|
+
// Normalize the entry as `main` alongside its siblings so `#id` run
|
|
262
|
+
// references resolve.
|
|
263
|
+
const siblings = graphEntries.filter((e) => e !== workflow && e.id !== "main");
|
|
264
|
+
source = { $graph: [{ ...source, id: "main" }, ...siblings] };
|
|
265
|
+
}
|
|
266
|
+
return cytoscapeElements(source, { layout: "preset", draftOverlay: null });
|
|
267
|
+
}
|
|
268
|
+
export function layoutPositions(workflow, strategy = "topological", graphEntries = []) {
|
|
269
|
+
if (strategy !== "topological" && strategy !== "layered") {
|
|
270
|
+
throw new Error(`Unknown layout strategy "${strategy}". Valid values: topological, layered.`);
|
|
271
|
+
}
|
|
272
|
+
const elements = layoutElements(workflow, graphEntries);
|
|
273
|
+
let positions;
|
|
274
|
+
if (strategy === "topological") {
|
|
275
|
+
// topologicalPositions silently falls back on cycles; refuse up front.
|
|
276
|
+
checkAcyclic(elements);
|
|
277
|
+
positions = topologicalPositions(elements);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
positions = layeredPositions(elements);
|
|
281
|
+
}
|
|
282
|
+
const result = new Map();
|
|
283
|
+
for (const [id, p] of positions)
|
|
284
|
+
result.set(id, { left: p.x, top: p.y });
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
function applyToContainer(container, positions, overwrite, isInput) {
|
|
288
|
+
if (isDict(container)) {
|
|
289
|
+
for (const key of Object.keys(container)) {
|
|
290
|
+
const pos = positions.get(format2NodeId(container[key], key, isInput) ?? "");
|
|
291
|
+
if (pos === undefined)
|
|
292
|
+
continue;
|
|
293
|
+
// Promote Format2 shorthand (e.g. `x: data`) to mapping form.
|
|
294
|
+
const value = isDict(container[key]) ? container[key] : { type: container[key] };
|
|
295
|
+
if (shouldWrite(value.position, overwrite))
|
|
296
|
+
value.position = { ...pos };
|
|
297
|
+
container[key] = value;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
else if (Array.isArray(container)) {
|
|
301
|
+
for (const item of container) {
|
|
302
|
+
if (!isDict(item))
|
|
303
|
+
continue;
|
|
304
|
+
const pos = positions.get(format2NodeId(item, null, isInput) ?? "");
|
|
305
|
+
if (pos !== undefined && shouldWrite(item.position, overwrite))
|
|
306
|
+
item.position = { ...pos };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return container;
|
|
310
|
+
}
|
|
311
|
+
function applyNative(workflow, positions, overwrite) {
|
|
312
|
+
if (!isDict(workflow.steps))
|
|
313
|
+
return;
|
|
314
|
+
for (const [key, step] of Object.entries(workflow.steps)) {
|
|
315
|
+
if (!isDict(step))
|
|
316
|
+
continue;
|
|
317
|
+
const pos = positions.get(nativeNodeId(key, step));
|
|
318
|
+
if (pos !== undefined && shouldWrite(step.position, overwrite))
|
|
319
|
+
step.position = { ...pos };
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Merge computed positions into a Format2 or native workflow dict, mutating it
|
|
324
|
+
* in place and returning it. Explicit positions are kept unless `overwrite`;
|
|
325
|
+
* `position: auto` is always replaced. With `recursive` (default), embedded
|
|
326
|
+
* subworkflows and every `$graph` entry get their own coordinate space.
|
|
327
|
+
*/
|
|
328
|
+
export function applyLayout(workflow, opts = {}) {
|
|
329
|
+
if (isGraphDocument(workflow)) {
|
|
330
|
+
const entries = containerItems(workflow.$graph).filter(isDict);
|
|
331
|
+
for (const entry of entries)
|
|
332
|
+
applyLayoutUnit(entry, opts, entries);
|
|
333
|
+
return workflow;
|
|
334
|
+
}
|
|
335
|
+
return applyLayoutUnit(workflow, opts, []);
|
|
336
|
+
}
|
|
337
|
+
function applyLayoutUnit(workflow, opts, graphEntries) {
|
|
338
|
+
const { strategy = "topological", overwrite = false, recursive = true } = opts;
|
|
339
|
+
const positions = layoutPositions(workflow, strategy, graphEntries);
|
|
340
|
+
if (isNativeDocument(workflow)) {
|
|
341
|
+
applyNative(workflow, positions, overwrite);
|
|
342
|
+
}
|
|
343
|
+
else {
|
|
344
|
+
for (const containerKey of ["inputs", "steps"]) {
|
|
345
|
+
if (workflow[containerKey] == null)
|
|
346
|
+
continue;
|
|
347
|
+
workflow[containerKey] = applyToContainer(workflow[containerKey], positions, overwrite, containerKey === "inputs");
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (recursive) {
|
|
351
|
+
for (const sub of iterSubworkflows(workflow))
|
|
352
|
+
applyLayout(sub, opts);
|
|
353
|
+
}
|
|
354
|
+
return workflow;
|
|
355
|
+
}
|
|
356
|
+
//# sourceMappingURL=layout.js.map
|