@vsirotin/ts-stop 1.13.1 → 2.4.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 +20 -5
- package/ai/skills/sfsm-compare-json-uml-diagram/SKILL.md +62 -0
- package/ai/skills/sfsm-json-to-uml-diagram/SKILL.md +50 -0
- package/ai/skills/sfsm-uml-diagram-to-json/SKILL.md +49 -0
- package/lib/esm/index.js +2 -9
- package/lib/esm/sfsm/CommandReceiver.js +17 -0
- package/lib/esm/sfsm/ControllerHub.js +82 -0
- package/lib/esm/sfsm/FaResolver.js +24 -3
- package/lib/esm/sfsm/Sfsm.js +48 -7
- package/lib/esm/sfsm/SignalSender.js +27 -0
- package/lib/esm/sfsm/index.js +3 -1
- package/lib/index.d.ts +1 -9
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -9
- package/lib/index.js.map +1 -1
- package/lib/sfsm/CommandReceiver.d.ts +23 -0
- package/lib/sfsm/CommandReceiver.d.ts.map +1 -0
- package/lib/sfsm/CommandReceiver.js +22 -0
- package/lib/sfsm/CommandReceiver.js.map +1 -0
- package/lib/sfsm/ControllerHub.d.ts +56 -0
- package/lib/sfsm/ControllerHub.d.ts.map +1 -0
- package/lib/sfsm/ControllerHub.js +87 -0
- package/lib/sfsm/ControllerHub.js.map +1 -0
- package/lib/sfsm/FaResolver.d.ts +14 -0
- package/lib/sfsm/FaResolver.d.ts.map +1 -1
- package/lib/sfsm/FaResolver.js +24 -3
- package/lib/sfsm/FaResolver.js.map +1 -1
- package/lib/sfsm/Sfsm.d.ts +26 -0
- package/lib/sfsm/Sfsm.d.ts.map +1 -1
- package/lib/sfsm/Sfsm.js +48 -7
- package/lib/sfsm/Sfsm.js.map +1 -1
- package/lib/sfsm/SignalSender.d.ts +24 -0
- package/lib/sfsm/SignalSender.d.ts.map +1 -0
- package/lib/sfsm/SignalSender.js +32 -0
- package/lib/sfsm/SignalSender.js.map +1 -0
- package/lib/sfsm/index.d.ts +3 -1
- package/lib/sfsm/index.d.ts.map +1 -1
- package/lib/sfsm/index.js +7 -3
- package/lib/sfsm/index.js.map +1 -1
- package/lib/sfsm/interfaces.d.ts +1 -1
- package/lib/sfsm/types.d.ts +21 -0
- package/lib/sfsm/types.d.ts.map +1 -1
- package/package.json +12 -9
- package/scripts/compare-compact-jsons.js +146 -0
- package/scripts/drawio-to-json.js +149 -0
- package/scripts/json-to-drawio-old.js +311 -0
- package/scripts/json-to-drawio.js +309 -0
- package/scripts/merge-fas.js +0 -0
- package/scripts/reduce-fa.js +0 -0
- package/scripts/update-fa.js +0 -0
- package/tutorial/01-finite-state-machine.md +223 -0
- package/tutorial/02-stacked-finite-state-machine.md +32 -0
- package/tutorial/03-advanced-themes.md +110 -0
- package/tutorial/04-tools.md +164 -0
- package/tutorial/images/Turnstile-img.png +0 -0
- package/tutorial/images/TurnstileBaseFA.png +0 -0
- package/LICENSE +0 -201
- package/LICENSE-COMMERCIAL.md +0 -41
- package/lib/DefaultState.d.ts +0 -9
- package/lib/DefaultState.d.ts.map +0 -1
- package/lib/DefaultState.js +0 -15
- package/lib/DefaultState.js.map +0 -1
- package/lib/FiniteStateMachine.d.ts +0 -227
- package/lib/FiniteStateMachine.d.ts.map +0 -1
- package/lib/FiniteStateMachine.js +0 -410
- package/lib/FiniteStateMachine.js.map +0 -1
- package/lib/IStateWithActions.d.ts +0 -18
- package/lib/IStateWithActions.d.ts.map +0 -1
- package/lib/IStateWithActions.js +0 -3
- package/lib/IStateWithActions.js.map +0 -1
- package/lib/IStateWithOutputSignal.d.ts +0 -4
- package/lib/IStateWithOutputSignal.d.ts.map +0 -1
- package/lib/IStateWithOutputSignal.js +0 -3
- package/lib/IStateWithOutputSignal.js.map +0 -1
- package/lib/MatrixBasedStateMachine.d.ts +0 -108
- package/lib/MatrixBasedStateMachine.d.ts.map +0 -1
- package/lib/MatrixBasedStateMachine.js +0 -132
- package/lib/MatrixBasedStateMachine.js.map +0 -1
- package/lib/TransitionMatrix.d.ts +0 -61
- package/lib/TransitionMatrix.d.ts.map +0 -1
- package/lib/TransitionMatrix.js +0 -104
- package/lib/TransitionMatrix.js.map +0 -1
- package/lib/esm/DefaultState.js +0 -10
- package/lib/esm/FiniteStateMachine.js +0 -405
- package/lib/esm/IStateWithActions.js +0 -1
- package/lib/esm/IStateWithOutputSignal.js +0 -1
- package/lib/esm/MatrixBasedStateMachine.js +0 -127
- package/lib/esm/TransitionMatrix.js +0 -98
- package/lib/esm/fa/DefaultState.js +0 -10
- package/lib/esm/fa/FiniteStateMachine.js +0 -405
- package/lib/esm/fa/IStateWithActions.js +0 -1
- package/lib/esm/fa/IStateWithOutputSignal.js +0 -1
- package/lib/esm/fa/MatrixBasedStateMachine.js +0 -127
- package/lib/esm/fa/TransitionMatrix.js +0 -98
- package/lib/esm/sfsm/ExternalWorldHub.js +0 -80
- package/lib/esm/sfsm/index.browser.js +0 -5
- package/lib/fa/DefaultState.d.ts +0 -9
- package/lib/fa/DefaultState.d.ts.map +0 -1
- package/lib/fa/DefaultState.js +0 -15
- package/lib/fa/DefaultState.js.map +0 -1
- package/lib/fa/FiniteStateMachine.d.ts +0 -227
- package/lib/fa/FiniteStateMachine.d.ts.map +0 -1
- package/lib/fa/FiniteStateMachine.js +0 -410
- package/lib/fa/FiniteStateMachine.js.map +0 -1
- package/lib/fa/IStateWithActions.d.ts +0 -18
- package/lib/fa/IStateWithActions.d.ts.map +0 -1
- package/lib/fa/IStateWithActions.js +0 -3
- package/lib/fa/IStateWithActions.js.map +0 -1
- package/lib/fa/IStateWithOutputSignal.d.ts +0 -4
- package/lib/fa/IStateWithOutputSignal.d.ts.map +0 -1
- package/lib/fa/IStateWithOutputSignal.js +0 -3
- package/lib/fa/IStateWithOutputSignal.js.map +0 -1
- package/lib/fa/MatrixBasedStateMachine.d.ts +0 -108
- package/lib/fa/MatrixBasedStateMachine.d.ts.map +0 -1
- package/lib/fa/MatrixBasedStateMachine.js +0 -132
- package/lib/fa/MatrixBasedStateMachine.js.map +0 -1
- package/lib/fa/TransitionMatrix.d.ts +0 -61
- package/lib/fa/TransitionMatrix.d.ts.map +0 -1
- package/lib/fa/TransitionMatrix.js +0 -104
- package/lib/fa/TransitionMatrix.js.map +0 -1
- package/lib/sfsm/ExternalWorldHub.d.ts +0 -52
- package/lib/sfsm/ExternalWorldHub.d.ts.map +0 -1
- package/lib/sfsm/ExternalWorldHub.js +0 -85
- package/lib/sfsm/ExternalWorldHub.js.map +0 -1
- package/lib/sfsm/index.browser.d.ts +0 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vsirotin/ts-stop",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.4.2",
|
|
4
4
|
"description": "State Oriented Programming library for TypeScript",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -13,20 +13,18 @@
|
|
|
13
13
|
},
|
|
14
14
|
"./sfsm": {
|
|
15
15
|
"types": "./lib/sfsm/index.d.ts",
|
|
16
|
-
"browser": "./lib/esm/sfsm/index.browser.js",
|
|
17
16
|
"import": "./lib/esm/sfsm/index.js",
|
|
18
17
|
"require": "./lib/sfsm/index.js"
|
|
19
18
|
}
|
|
20
19
|
},
|
|
21
20
|
"files": [
|
|
22
21
|
"lib",
|
|
23
|
-
"scripts
|
|
24
|
-
"
|
|
25
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"LICENSE-COMMERCIAL.md"
|
|
22
|
+
"scripts",
|
|
23
|
+
"ai/skills",
|
|
24
|
+
"tutorial",
|
|
25
|
+
"LICENSE-COMMERCIAL.md",
|
|
26
|
+
"LICENSE-PUBLIC.md",
|
|
27
|
+
"release-notes.md"
|
|
30
28
|
],
|
|
31
29
|
"bin": {
|
|
32
30
|
"reduce-fa": "./scripts/reduce-fa.js",
|
|
@@ -60,6 +58,7 @@
|
|
|
60
58
|
"@types/jest": "^29.5.14",
|
|
61
59
|
"@types/js-yaml": "^4.0.9",
|
|
62
60
|
"@types/mocha": "^10.0.10",
|
|
61
|
+
"@types/node": "^26.1.1",
|
|
63
62
|
"jest": "^29.5.0",
|
|
64
63
|
"js-yaml": "^4.1.1",
|
|
65
64
|
"ts-jest": "^29.1.0",
|
|
@@ -67,5 +66,9 @@
|
|
|
67
66
|
},
|
|
68
67
|
"publishConfig": {
|
|
69
68
|
"access": "public"
|
|
69
|
+
},
|
|
70
|
+
"dependencies": {
|
|
71
|
+
"elkjs": "^0.12.0",
|
|
72
|
+
"xml2js": "^0.6.2"
|
|
70
73
|
}
|
|
71
74
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Compare two compact SFSM JSON files
|
|
4
|
+
* Usage: node compare-compact-jsons.js <json-a> <json-b>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
|
|
9
|
+
// ============ MAIN ============
|
|
10
|
+
function main() {
|
|
11
|
+
const [jsonAPath, jsonBPath] = process.argv.slice(2);
|
|
12
|
+
|
|
13
|
+
if (!jsonAPath || !jsonBPath) {
|
|
14
|
+
console.error('Usage: node compare-compact-jsons.js <json-a> <json-b>');
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const jsonA = JSON.parse(fs.readFileSync(jsonAPath, 'utf-8'));
|
|
20
|
+
const jsonB = JSON.parse(fs.readFileSync(jsonBPath, 'utf-8'));
|
|
21
|
+
|
|
22
|
+
console.log('\n======== SFSM COMPACT JSON COMPARISON ========\n');
|
|
23
|
+
console.log(`File A: ${jsonAPath}`);
|
|
24
|
+
console.log(`File B: ${jsonBPath}\n`);
|
|
25
|
+
|
|
26
|
+
compareJsons(jsonA, jsonB);
|
|
27
|
+
|
|
28
|
+
} catch (err) {
|
|
29
|
+
console.error('Error:', err.message);
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ============ COMPARISON LOGIC ============
|
|
35
|
+
|
|
36
|
+
function compareJsons(jsonA, jsonB) {
|
|
37
|
+
const fasA = Object.keys(jsonA);
|
|
38
|
+
const fasB = Object.keys(jsonB);
|
|
39
|
+
|
|
40
|
+
console.log('--- FA NAMES ---');
|
|
41
|
+
console.log(`A: [${fasA.join(', ')}] (${fasA.length} FAs)`);
|
|
42
|
+
console.log(`B: [${fasB.join(', ')}] (${fasB.length} FAs)`);
|
|
43
|
+
|
|
44
|
+
const missingInB = fasA.filter(fa => !fasB.includes(fa));
|
|
45
|
+
const missingInA = fasB.filter(fa => !fasA.includes(fa));
|
|
46
|
+
|
|
47
|
+
if (missingInB.length > 0) {
|
|
48
|
+
console.log(` ❌ Missing in B: ${missingInB.join(', ')}`);
|
|
49
|
+
}
|
|
50
|
+
if (missingInA.length > 0) {
|
|
51
|
+
console.log(` ❌ Missing in A: ${missingInA.join(', ')}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const commonFas = fasA.filter(fa => fasB.includes(fa));
|
|
55
|
+
if (commonFas.length === fasA.length && commonFas.length === fasB.length) {
|
|
56
|
+
console.log(` ✓ All FAs match\n`);
|
|
57
|
+
} else {
|
|
58
|
+
console.log();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Compare transitions per FA
|
|
62
|
+
console.log('--- TRANSITIONS PER FA ---');
|
|
63
|
+
let totalTransitionsA = 0;
|
|
64
|
+
let totalTransitionsB = 0;
|
|
65
|
+
let totalDifferences = 0;
|
|
66
|
+
|
|
67
|
+
for (const fa of commonFas) {
|
|
68
|
+
const transA = jsonA[fa] || [];
|
|
69
|
+
const transB = jsonB[fa] || [];
|
|
70
|
+
|
|
71
|
+
totalTransitionsA += transA.length;
|
|
72
|
+
totalTransitionsB += transB.length;
|
|
73
|
+
|
|
74
|
+
console.log(`\nFA: ${fa}`);
|
|
75
|
+
console.log(` A: ${transA.length} transitions`);
|
|
76
|
+
console.log(` B: ${transB.length} transitions`);
|
|
77
|
+
|
|
78
|
+
// Normalize transitions for comparison
|
|
79
|
+
const transANorm = transA.map(tr => normalizeTransition(tr));
|
|
80
|
+
const transBNorm = transB.map(tr => normalizeTransition(tr));
|
|
81
|
+
|
|
82
|
+
// Find missing and extra transitions
|
|
83
|
+
const missingInBTrans = [];
|
|
84
|
+
const missingInATrans = [];
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < transANorm.length; i++) {
|
|
87
|
+
const trStr = transANorm[i];
|
|
88
|
+
const found = transBNorm.some(t => t === trStr);
|
|
89
|
+
if (!found) {
|
|
90
|
+
missingInBTrans.push(transA[i]);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (let i = 0; i < transBNorm.length; i++) {
|
|
95
|
+
const trStr = transBNorm[i];
|
|
96
|
+
const found = transANorm.some(t => t === trStr);
|
|
97
|
+
if (!found) {
|
|
98
|
+
missingInATrans.push(transB[i]);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (missingInBTrans.length === 0 && missingInATrans.length === 0) {
|
|
103
|
+
console.log(` ✓ All transitions match`);
|
|
104
|
+
} else {
|
|
105
|
+
if (missingInBTrans.length > 0) {
|
|
106
|
+
console.log(` ❌ Missing in B (${missingInBTrans.length}):`);
|
|
107
|
+
for (const tr of missingInBTrans) {
|
|
108
|
+
console.log(` ${JSON.stringify(tr)}`);
|
|
109
|
+
}
|
|
110
|
+
totalDifferences += missingInBTrans.length;
|
|
111
|
+
}
|
|
112
|
+
if (missingInATrans.length > 0) {
|
|
113
|
+
console.log(` ❌ Extra in B / Missing in A (${missingInATrans.length}):`);
|
|
114
|
+
for (const tr of missingInATrans) {
|
|
115
|
+
console.log(` ${JSON.stringify(tr)}`);
|
|
116
|
+
}
|
|
117
|
+
totalDifferences += missingInATrans.length;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Summary
|
|
123
|
+
console.log('\n--- SUMMARY ---');
|
|
124
|
+
console.log(`Total FAs: A=${fasA.length}, B=${fasB.length}`);
|
|
125
|
+
console.log(`Total transitions: A=${totalTransitionsA}, B=${totalTransitionsB}`);
|
|
126
|
+
console.log(`Total differences: ${totalDifferences}`);
|
|
127
|
+
|
|
128
|
+
if (totalDifferences === 0 && fasA.length === fasB.length && totalTransitionsA === totalTransitionsB) {
|
|
129
|
+
console.log('\n✓ FILES ARE IDENTICAL');
|
|
130
|
+
} else {
|
|
131
|
+
console.log('\n❌ FILES DIFFER');
|
|
132
|
+
}
|
|
133
|
+
console.log();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeTransition(tr) {
|
|
137
|
+
// Transition format: [from, signal, to] or [from, signal, to, command]
|
|
138
|
+
if (tr.length === 3) {
|
|
139
|
+
return `${tr[0]}|${tr[1]}|${tr[2]}`;
|
|
140
|
+
} else if (tr.length === 4) {
|
|
141
|
+
return `${tr[0]}|${tr[1]}|${tr[2]}|${tr[3]}`;
|
|
142
|
+
}
|
|
143
|
+
return '';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
main();
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Convert draw.io UML state diagram back to compact SFSM JSON
|
|
4
|
+
* Usage: node drawio-to-json.js <input-drawio> <output-json>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const xml2js = require('xml2js');
|
|
10
|
+
|
|
11
|
+
// ============ MAIN ============
|
|
12
|
+
async function main() {
|
|
13
|
+
const [inputDrawioPath, outputJsonPath] = process.argv.slice(2);
|
|
14
|
+
|
|
15
|
+
if (!inputDrawioPath || !outputJsonPath) {
|
|
16
|
+
console.error('Usage: node drawio-to-json.js <input-drawio> <output-json>');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const drawioContent = fs.readFileSync(inputDrawioPath, 'utf-8');
|
|
22
|
+
const json = await convertDrawioToJson(drawioContent);
|
|
23
|
+
|
|
24
|
+
// Ensure output directory exists
|
|
25
|
+
const outputDir = path.dirname(outputJsonPath);
|
|
26
|
+
if (!fs.existsSync(outputDir)) {
|
|
27
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
fs.writeFileSync(outputJsonPath, JSON.stringify(json, null, 2));
|
|
31
|
+
console.log(`Generated: ${outputJsonPath}`);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
console.error('Error:', err.message);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ============ CONVERSION LOGIC ============
|
|
39
|
+
|
|
40
|
+
async function convertDrawioToJson(drawioContent) {
|
|
41
|
+
const parser = new xml2js.Parser();
|
|
42
|
+
const result = await parser.parseStringPromise(drawioContent);
|
|
43
|
+
|
|
44
|
+
const root = result.mxfile.diagram[0].mxGraphModel[0].root[0];
|
|
45
|
+
const mxCells = root.mxCell || [];
|
|
46
|
+
|
|
47
|
+
// Parse cells
|
|
48
|
+
const cellsMap = new Map(); // id -> cell info
|
|
49
|
+
const transitionsByFa = new Map(); // faName -> [transitions]
|
|
50
|
+
const faNames = new Set();
|
|
51
|
+
|
|
52
|
+
for (const cell of mxCells) {
|
|
53
|
+
const id = cell.$.id;
|
|
54
|
+
const style = cell.$.style || '';
|
|
55
|
+
const value = cell.$.value || '';
|
|
56
|
+
|
|
57
|
+
// Extract SFSM metadata from style
|
|
58
|
+
const sfsmRole = extractStyleAttr(style, 'sfsmRole');
|
|
59
|
+
const sfsmFa = extractStyleAttr(style, 'sfsmFa');
|
|
60
|
+
const sfsmKey = extractStyleAttr(style, 'sfsmKey');
|
|
61
|
+
const sfsmSignal = extractStyleAttr(style, 'sfsmSignal');
|
|
62
|
+
const sfsmCommand = extractStyleAttr(style, 'sfsmCommand');
|
|
63
|
+
|
|
64
|
+
if (sfsmFa) {
|
|
65
|
+
faNames.add(sfsmFa);
|
|
66
|
+
if (!transitionsByFa.has(sfsmFa)) {
|
|
67
|
+
transitionsByFa.set(sfsmFa, []);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
cellsMap.set(id, {
|
|
72
|
+
id,
|
|
73
|
+
value,
|
|
74
|
+
sfsmRole,
|
|
75
|
+
sfsmFa,
|
|
76
|
+
sfsmKey,
|
|
77
|
+
sfsmSignal,
|
|
78
|
+
sfsmCommand,
|
|
79
|
+
source: cell.$.source,
|
|
80
|
+
target: cell.$.target
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build transitions
|
|
85
|
+
for (const [faName] of transitionsByFa) {
|
|
86
|
+
const faTransitions = [];
|
|
87
|
+
|
|
88
|
+
// Iterate through cells to find transition edges for this FA
|
|
89
|
+
for (const cell of mxCells) {
|
|
90
|
+
const cellId = cell.$.id;
|
|
91
|
+
const cellInfo = cellsMap.get(cellId);
|
|
92
|
+
|
|
93
|
+
if (!cellInfo) continue;
|
|
94
|
+
if (cellInfo.sfsmRole !== 'transition' || cellInfo.sfsmFa !== faName) continue;
|
|
95
|
+
|
|
96
|
+
const sourceId = cell.$.source;
|
|
97
|
+
const targetId = cell.$.target;
|
|
98
|
+
|
|
99
|
+
if (!sourceId || !targetId) continue;
|
|
100
|
+
|
|
101
|
+
const sourceCell = cellsMap.get(sourceId);
|
|
102
|
+
const targetCell = cellsMap.get(targetId);
|
|
103
|
+
|
|
104
|
+
if (!sourceCell || !targetCell) continue;
|
|
105
|
+
|
|
106
|
+
// Determine from state
|
|
107
|
+
let fromState;
|
|
108
|
+
if (sourceCell.sfsmRole === 'initial') {
|
|
109
|
+
fromState = 'I';
|
|
110
|
+
} else if (sourceCell.sfsmKey) {
|
|
111
|
+
fromState = sourceCell.sfsmKey;
|
|
112
|
+
} else {
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Determine to state
|
|
117
|
+
const toState = targetCell.sfsmKey;
|
|
118
|
+
if (!toState) continue;
|
|
119
|
+
|
|
120
|
+
const signal = cellInfo.sfsmSignal;
|
|
121
|
+
const command = cellInfo.sfsmCommand;
|
|
122
|
+
|
|
123
|
+
// Build transition tuple
|
|
124
|
+
if (command && command.trim()) {
|
|
125
|
+
faTransitions.push([fromState, signal, toState, command]);
|
|
126
|
+
} else {
|
|
127
|
+
faTransitions.push([fromState, signal, toState]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
transitionsByFa.set(faName, faTransitions);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Build compact JSON
|
|
135
|
+
const compactJson = {};
|
|
136
|
+
for (const faName of faNames) {
|
|
137
|
+
compactJson[faName] = transitionsByFa.get(faName) || [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return compactJson;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function extractStyleAttr(style, attrName) {
|
|
144
|
+
const regex = new RegExp(`${attrName}=([^;]+)`);
|
|
145
|
+
const match = style.match(regex);
|
|
146
|
+
return match ? match[1] : '';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
main();
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Convert SFSM compact-format FA JSON to draw.io UML state diagram using ELK.js for hierarchical layout
|
|
4
|
+
* Each FA is a separate container, sub-FAs are nested containers with light grey background
|
|
5
|
+
* Usage: node json-to-drawio.js <input-json> <output-drawio>
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const ELK = require('elkjs').default;
|
|
11
|
+
|
|
12
|
+
const elk = new ELK();
|
|
13
|
+
|
|
14
|
+
// ============ MAIN ============
|
|
15
|
+
async function main() {
|
|
16
|
+
const [inputJsonPath, outputDrawioPath] = process.argv.slice(2);
|
|
17
|
+
|
|
18
|
+
if (!inputJsonPath || !outputDrawioPath) {
|
|
19
|
+
console.error('Usage: node json-to-drawio.js <input-json> <output-drawio>');
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
const faData = JSON.parse(fs.readFileSync(inputJsonPath, 'utf-8'));
|
|
25
|
+
const drawio = await generateDrawio(faData);
|
|
26
|
+
|
|
27
|
+
// Ensure output directory exists
|
|
28
|
+
const outputDir = path.dirname(outputDrawioPath);
|
|
29
|
+
if (!fs.existsSync(outputDir)) {
|
|
30
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
fs.writeFileSync(outputDrawioPath, drawio);
|
|
34
|
+
console.log(`Generated: ${outputDrawioPath}`);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.error('Error:', err.message);
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============ CONVERSION LOGIC ============
|
|
42
|
+
|
|
43
|
+
async function generateDrawio(faData) {
|
|
44
|
+
const rootFaName = getRootFaName(faData);
|
|
45
|
+
const allFaNames = Object.keys(faData);
|
|
46
|
+
|
|
47
|
+
// Build hierarchical graph structure
|
|
48
|
+
const { cellsMap, edgeList } = await buildHierarchicalGraph(rootFaName, faData, allFaNames);
|
|
49
|
+
|
|
50
|
+
// Convert to drawio
|
|
51
|
+
return buildDrawioXml(cellsMap, edgeList);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getRootFaName(faData) {
|
|
55
|
+
const allFaNames = Object.keys(faData);
|
|
56
|
+
const referencedFas = new Set();
|
|
57
|
+
|
|
58
|
+
for (const faKey of allFaNames) {
|
|
59
|
+
const transitions = faData[faKey];
|
|
60
|
+
for (const tr of transitions) {
|
|
61
|
+
const toState = tr[2];
|
|
62
|
+
if (allFaNames.includes(toState)) {
|
|
63
|
+
referencedFas.add(toState);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const faName of allFaNames) {
|
|
69
|
+
if (!referencedFas.has(faName)) {
|
|
70
|
+
return faName;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return allFaNames[0];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function buildGraphForELK(rootFaName, faData, allFaNames) {
|
|
78
|
+
const nodes = [];
|
|
79
|
+
const edges = [];
|
|
80
|
+
const nodeMap = new Map(); // nodeId -> { faKey, state, label, type }
|
|
81
|
+
const edgeMap = new Map(); // edgeId -> { from, signal, to, command, faKey }
|
|
82
|
+
const stateNodeMap = new Map(); // `${faKey}:${state}` -> nodeId
|
|
83
|
+
|
|
84
|
+
let nodeCounter = 0;
|
|
85
|
+
let edgeCounter = 0;
|
|
86
|
+
|
|
87
|
+
// Process all FAs (both root and sub-FAs)
|
|
88
|
+
for (const faName of allFaNames) {
|
|
89
|
+
processFA(faName, faData, allFaNames, nodes, edges, nodeMap, edgeMap, stateNodeMap, nodeCounter, edgeCounter);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { nodes, edges: edges, nodeMap, edgeMap };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function processFA(faKey, faData, allFaNames, nodes, edges, nodeMap, edgeMap, stateNodeMap, nodeCounter, edgeCounter) {
|
|
96
|
+
const transitions = faData[faKey];
|
|
97
|
+
const states = new Set();
|
|
98
|
+
const exitStates = new Set();
|
|
99
|
+
|
|
100
|
+
// Collect states and exit states
|
|
101
|
+
for (const tr of transitions) {
|
|
102
|
+
const [from, signal, to, command] = tr;
|
|
103
|
+
states.add(from);
|
|
104
|
+
states.add(to);
|
|
105
|
+
if (to.startsWith('E_')) {
|
|
106
|
+
exitStates.add(to);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Create node for initial state
|
|
111
|
+
const initId = `init-${faKey}`;
|
|
112
|
+
nodeMap.set(initId, { faKey, state: 'I', label: '', type: 'initial' });
|
|
113
|
+
stateNodeMap.set(`${faKey}:I`, initId);
|
|
114
|
+
nodes.push({
|
|
115
|
+
id: initId,
|
|
116
|
+
width: 20,
|
|
117
|
+
height: 20,
|
|
118
|
+
properties: { sfsmRole: 'initial', sfsmFa: faKey }
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// Create nodes for other states
|
|
122
|
+
for (const state of states) {
|
|
123
|
+
if (state === 'I') continue;
|
|
124
|
+
|
|
125
|
+
let type = 'state';
|
|
126
|
+
let label = state;
|
|
127
|
+
|
|
128
|
+
if (exitStates.has(state)) {
|
|
129
|
+
type = 'exit';
|
|
130
|
+
} else if (allFaNames.includes(state)) {
|
|
131
|
+
type = 'subfa';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const nodeId = `state-${faKey}-${state}`;
|
|
135
|
+
nodeMap.set(nodeId, { faKey, state, label, type });
|
|
136
|
+
stateNodeMap.set(`${faKey}:${state}`, nodeId);
|
|
137
|
+
|
|
138
|
+
const width = type === 'exit' ? 70 : 140;
|
|
139
|
+
const height = type === 'exit' ? 70 : 50;
|
|
140
|
+
|
|
141
|
+
nodes.push({
|
|
142
|
+
id: nodeId,
|
|
143
|
+
width: width,
|
|
144
|
+
height: height,
|
|
145
|
+
labels: [{ text: label }],
|
|
146
|
+
properties: {
|
|
147
|
+
sfsmRole: type,
|
|
148
|
+
sfsmFa: faKey,
|
|
149
|
+
sfsmKey: state
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Create edges
|
|
155
|
+
for (const tr of transitions) {
|
|
156
|
+
const [from, signal, to, command] = tr;
|
|
157
|
+
const sourceId = stateNodeMap.get(`${faKey}:${from}`);
|
|
158
|
+
const targetId = stateNodeMap.get(`${faKey}:${to}`);
|
|
159
|
+
|
|
160
|
+
if (!sourceId || !targetId) continue;
|
|
161
|
+
|
|
162
|
+
const edgeLabel = command ? `${signal} / ${to}:${command}` : signal;
|
|
163
|
+
const edgeId = `tr-${faKey}-${edgeCounter++}`;
|
|
164
|
+
|
|
165
|
+
edgeMap.set(edgeId, { from, signal, to, command, faKey });
|
|
166
|
+
|
|
167
|
+
edges.push({
|
|
168
|
+
id: edgeId,
|
|
169
|
+
sources: [sourceId],
|
|
170
|
+
targets: [targetId],
|
|
171
|
+
labels: [{ text: edgeLabel }],
|
|
172
|
+
properties: {
|
|
173
|
+
sfsmRole: 'transition',
|
|
174
|
+
sfsmFa: faKey,
|
|
175
|
+
sfsmSignal: signal,
|
|
176
|
+
sfsmCommand: command || ''
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function buildDrawioXmlFromLayout(layout, nodeMap, edgeMap, faData, allFaNames) {
|
|
183
|
+
const cellsMap = new Map();
|
|
184
|
+
const edges = [];
|
|
185
|
+
|
|
186
|
+
// Convert ELK nodes to drawio cells
|
|
187
|
+
function convertNodes(elkNodes, parent = '1') {
|
|
188
|
+
if (!elkNodes) return;
|
|
189
|
+
|
|
190
|
+
for (const elkNode of elkNodes) {
|
|
191
|
+
const nodeInfo = nodeMap.get(elkNode.id);
|
|
192
|
+
if (!nodeInfo) continue;
|
|
193
|
+
|
|
194
|
+
const { faKey, state, label, type } = nodeInfo;
|
|
195
|
+
let style = '';
|
|
196
|
+
let width = 20;
|
|
197
|
+
let height = 20;
|
|
198
|
+
|
|
199
|
+
switch (type) {
|
|
200
|
+
case 'initial':
|
|
201
|
+
style = `ellipse;whiteSpace=wrap;html=1;fillColor=#000000;strokeColor=#000000;sfsmRole=initial;sfsmFa=${faKey};`;
|
|
202
|
+
width = 20;
|
|
203
|
+
height = 20;
|
|
204
|
+
break;
|
|
205
|
+
case 'exit':
|
|
206
|
+
style = `ellipse;whiteSpace=wrap;html=1;strokeWidth=3;fillColor=#FFFFFF;sfsmRole=exit;sfsmFa=${faKey};sfsmKey=${state};`;
|
|
207
|
+
width = 70;
|
|
208
|
+
height = 70;
|
|
209
|
+
break;
|
|
210
|
+
case 'subfa':
|
|
211
|
+
style = `swimlane;whiteSpace=wrap;html=1;startSize=26;sfsmRole=subfa;sfsmFa=${faKey};sfsmKey=${state};`;
|
|
212
|
+
width = 200;
|
|
213
|
+
height = 200;
|
|
214
|
+
break;
|
|
215
|
+
default:
|
|
216
|
+
style = `rounded=1;whiteSpace=wrap;html=1;arcSize=20;sfsmRole=state;sfsmFa=${faKey};sfsmKey=${state};`;
|
|
217
|
+
width = 140;
|
|
218
|
+
height = 50;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
cellsMap.set(elkNode.id, {
|
|
222
|
+
id: elkNode.id,
|
|
223
|
+
value: label,
|
|
224
|
+
style: style,
|
|
225
|
+
x: Math.round(elkNode.x || 0),
|
|
226
|
+
y: Math.round(elkNode.y || 0),
|
|
227
|
+
width: width,
|
|
228
|
+
height: height,
|
|
229
|
+
parent: parent
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Recursively process children
|
|
233
|
+
if (elkNode.children) {
|
|
234
|
+
convertNodes(elkNode.children, elkNode.id);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
convertNodes(layout.children);
|
|
240
|
+
|
|
241
|
+
// Convert ELK edges to drawio edges
|
|
242
|
+
if (layout.edges) {
|
|
243
|
+
for (const elkEdge of layout.edges) {
|
|
244
|
+
const edgeInfo = edgeMap.get(elkEdge.id);
|
|
245
|
+
if (!edgeInfo) continue;
|
|
246
|
+
|
|
247
|
+
const { from, signal, to, command, faKey } = edgeInfo;
|
|
248
|
+
const edgeLabel = command ? `${signal} / ${to}:${command}` : signal;
|
|
249
|
+
|
|
250
|
+
let style = `edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;sfsmRole=transition;sfsmFa=${faKey};sfsmSignal=${signal};sfsmCommand=${command || ''};`;
|
|
251
|
+
if (from === to) {
|
|
252
|
+
style += `exitX=0.25;exitY=0;exitDx=0;exitDy=0;entryX=0.75;entryY=0;entryDx=0;entryDy=0;`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
edges.push({
|
|
256
|
+
id: elkEdge.id,
|
|
257
|
+
value: edgeLabel,
|
|
258
|
+
style: style,
|
|
259
|
+
source: elkEdge.sources[0],
|
|
260
|
+
target: elkEdge.targets[0],
|
|
261
|
+
parent: '1'
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return buildDrawioXml(cellsMap, edges);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function buildDrawioXml(cellsMap, edges) {
|
|
270
|
+
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
271
|
+
<mxfile host="Electron">
|
|
272
|
+
<diagram id="sfsm-diagram" name="Page-1">
|
|
273
|
+
<mxGraphModel dx="800" dy="600" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1169" pageHeight="827" math="0" shadow="0">
|
|
274
|
+
<root>
|
|
275
|
+
<mxCell id="0"/>
|
|
276
|
+
<mxCell id="1" parent="0"/>
|
|
277
|
+
`;
|
|
278
|
+
|
|
279
|
+
for (const [cellId, cell] of cellsMap) {
|
|
280
|
+
xml += ` <mxCell id="${cell.id}" value="${escapeXml(cell.value)}" style="${cell.style}" vertex="1" parent="${cell.parent}">
|
|
281
|
+
<mxGeometry x="${cell.x}" y="${cell.y}" width="${cell.width}" height="${cell.height}" as="geometry"/>
|
|
282
|
+
</mxCell>
|
|
283
|
+
`;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
for (const edge of edges) {
|
|
287
|
+
xml += ` <mxCell id="${edge.id}" value="${escapeXml(edge.value)}" style="${edge.style}" edge="1" parent="${edge.parent}" source="${edge.source}" target="${edge.target}">
|
|
288
|
+
<mxGeometry relative="1" as="geometry"/>
|
|
289
|
+
</mxCell>
|
|
290
|
+
`;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
xml += ` </root>
|
|
294
|
+
</mxGraphModel>
|
|
295
|
+
</diagram>
|
|
296
|
+
</mxfile>`;
|
|
297
|
+
|
|
298
|
+
return xml;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function escapeXml(str) {
|
|
302
|
+
if (!str) return '';
|
|
303
|
+
return String(str)
|
|
304
|
+
.replace(/&/g, '&')
|
|
305
|
+
.replace(/</g, '<')
|
|
306
|
+
.replace(/>/g, '>')
|
|
307
|
+
.replace(/"/g, '"')
|
|
308
|
+
.replace(/'/g, ''');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
main();
|