@jarenjs/mermaid 0.72.3 → 0.73.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/README.md CHANGED
@@ -360,7 +360,9 @@ Every subpath a consumer can import, derived from the manifest by
360
360
  | `@jarenjs/mermaid/schemas/jaren-workflow.schema.json` | schema | — |
361
361
  | `@jarenjs/mermaid/stylesheets/dag-to-flowchart.jslt.json` | asset | — |
362
362
  | `@jarenjs/mermaid/stylesheets/flowchart-to-dag.jslt.json` | asset | — |
363
+ | `@jarenjs/mermaid/stylesheets/state-to-statechart.jslt.json` | asset | — |
363
364
  | `@jarenjs/mermaid/stylesheets/state-to-workflow.jslt.json` | asset | — |
365
+ | `@jarenjs/mermaid/stylesheets/statechart-to-state.jslt.json` | asset | — |
364
366
  | `@jarenjs/mermaid/stylesheets/workflow-to-state.jslt.json` | asset | — |
365
367
  | `@jarenjs/mermaid/package.json` | metadata | — |
366
368
  <!--/fact-->
@@ -11,9 +11,8 @@
11
11
  * fits no UML pattern reads whole as the event, so plain labels keep
12
12
  * their historical meaning byte for byte.
13
13
  *
14
- * Composite states (`state Foo { … }`) are flattened one level: the
15
- * inner transitions are captured with their parent recorded, keeping the
16
- * AST geometry-free.
14
+ * Composite states (`state Foo { … }`) stay in a flat table with parent
15
+ * ids on both states and transitions, at arbitrary nesting depth.
17
16
  */
18
17
  /**
19
18
  * @param {string[]} lines
@@ -261,7 +261,7 @@ primitive.
261
261
 
262
262
  ### 5.1 Semantic projections
263
263
 
264
- The geometry-free AST doubles as a domain model, and four JSLT
264
+ The geometry-free AST doubles as a domain model, and six JSLT
265
265
  stylesheets in [`stylesheets/`](../stylesheets/) project it both ways —
266
266
  plain data documents, no code:
267
267
 
@@ -269,6 +269,8 @@ plain data documents, no code:
269
269
  |---|---|
270
270
  | `state-to-workflow.jslt.json` | state DiagramDocument → executable machine (`@jarenjs/flow`'s jaren-fsm superset shape) |
271
271
  | `workflow-to-state.jslt.json` | machine document → state AST (print with `toMermaid`) |
272
+ | `state-to-statechart.jslt.json` | compound state DiagramDocument → jaren-fsm 0.2 (`compileStatechart`) |
273
+ | `statechart-to-state.jslt.json` | statechart document → state AST, preserving compound parents, initials and finals |
272
274
  | `flowchart-to-dag.jslt.json` | flowchart DiagramDocument → jaren-dag **skeleton** (every node a `task` stub named by its id; edge labels become `port`s verbatim) |
273
275
  | `dag-to-flowchart.jslt.json` | jaren-dag document → flowchart AST |
274
276
 
@@ -278,6 +280,19 @@ The forward state projection maps the parsed UML parts: `event` and
278
280
  registry name by convention. The reverse composes the label from the
279
281
  machine's parts and stays consistent with the parser by construction.
280
282
 
283
+ The compound projection uses `parent` on states as well as transitions;
284
+ the parser records arbitrary nesting depth and the printer retains transition
285
+ order while reopening compound scopes when necessary. A scope's unlabelled
286
+ `[*]` entry supplies its `initial`; an unlabelled edge to `[*]` declares its
287
+ source terminal. Missing or ambiguous initials, a terminal with other outgoing
288
+ transitions, or labelled pseudo-state edges refuse at statechart compilation.
289
+ The 0.1 projection keeps its original flat behavior. Statechart history,
290
+ parallel types, internal/external distinctions, delays, completion/eventless
291
+ triggers and state entry/exit effects have no lossless spelling in this
292
+ projection: author and retain those in JSON. The reverse is a display
293
+ projection outside the compound/initial/final subset, not a serialization
294
+ format for a full statechart.
295
+
281
296
  The dag projection renders each node kind as a fixed flowchart shape:
282
297
 
283
298
  | kind | shape |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jarenjs/mermaid",
3
3
  "private": false,
4
- "version": "0.72.3",
4
+ "version": "0.73.0",
5
5
  "type": "module",
6
6
  "main": "./src/index.js",
7
7
  "types": "./dist/types/index.d.ts",
@@ -77,8 +77,8 @@
77
77
  "prepack": "npm run build:types"
78
78
  },
79
79
  "dependencies": {
80
- "@jarenjs/charts": "^0.72.3",
81
- "@jarenjs/core": "^0.72.3",
82
- "@jarenjs/view": "^0.72.3"
80
+ "@jarenjs/charts": "^0.73.0",
81
+ "@jarenjs/core": "^0.73.0",
82
+ "@jarenjs/view": "^0.73.0"
83
83
  }
84
84
  }
@@ -12,9 +12,8 @@
12
12
  * fits no UML pattern reads whole as the event, so plain labels keep
13
13
  * their historical meaning byte for byte.
14
14
  *
15
- * Composite states (`state Foo { … }`) are flattened one level: the
16
- * inner transitions are captured with their parent recorded, keeping the
17
- * AST geometry-free.
15
+ * Composite states (`state Foo { … }`) stay in a flat table with parent
16
+ * ids on both states and transitions, at arbitrary nesting depth.
18
17
  */
19
18
 
20
19
  /** `A --> B` / `A --> B : label`. */
@@ -79,17 +78,19 @@ export function parseState(lines) {
79
78
  /** @type {string[]} */
80
79
  const parentStack = [];
81
80
 
82
- const ensure = (id, label) => {
81
+ const ensure = (id, label, declaration = false) => {
83
82
  if (id === '[*]') return;
84
83
  let st = stateMap.get(id);
85
84
  if (st === undefined) {
86
- st = { id, label: label ?? id };
85
+ st = { id, label: label ?? id,
86
+ ...(parentStack.length ? { parent: parentStack.at(-1) } : {}) };
87
87
  stateMap.set(id, st);
88
88
  order.push(id);
89
89
  }
90
90
  else if (label != null) {
91
91
  st.label = label;
92
92
  }
93
+ if (declaration && parentStack.length) st.parent = parentStack.at(-1);
93
94
  };
94
95
 
95
96
  for (let li = 0; li < lines.length; li++) {
@@ -101,13 +102,13 @@ export function parseState(lines) {
101
102
  if (line.startsWith('state ') && line.endsWith('{')) {
102
103
  const inner = line.slice('state '.length, -1).trim();
103
104
  const id = inner.split(/\s+/)[0];
104
- ensure(id, id);
105
+ ensure(id, null, true);
105
106
  parentStack.push(id);
106
107
  continue;
107
108
  }
108
109
 
109
110
  const sa = RE_STATE_AS.exec(line);
110
- if (sa !== null) { ensure(sa[2], sa[1]); continue; }
111
+ if (sa !== null) { ensure(sa[2], sa[1], true); continue; }
111
112
 
112
113
  const tr = RE_TRANSITION.exec(line);
113
114
  if (tr !== null) {
@@ -127,7 +128,7 @@ export function parseState(lines) {
127
128
  continue;
128
129
  }
129
130
 
130
- if (line.startsWith('state ')) { ensure(line.slice('state '.length).trim(), null); continue; }
131
+ if (line.startsWith('state ')) { ensure(line.slice('state '.length).trim(), null, true); continue; }
131
132
 
132
133
  const sd = RE_STATE_DESC.exec(line);
133
134
  if (sd !== null && sd[1] !== '[*]') { ensure(sd[1], sd[2].trim()); continue; }
package/src/to-mermaid.js CHANGED
@@ -253,6 +253,34 @@ function printClass(ast) {
253
253
  */
254
254
  function printState(ast) {
255
255
  const out = ['stateDiagram-v2'];
256
+ if (ast.states.some((s) => s.parent != null) || ast.transitions.some((t) => t.parent != null)) {
257
+ const states = new Map(ast.states.map((s) => [s.id, s]));
258
+ let opened = [];
259
+ const scope = (parent) => {
260
+ const target = [];
261
+ for (let id = parent; id != null; id = states.get(id)?.parent) {
262
+ if (target.includes(id)) throw new TypeError('cyclic state-diagram parent');
263
+ target.unshift(id);
264
+ }
265
+ let shared = 0;
266
+ while (shared < opened.length && opened[shared] === target[shared]) shared++;
267
+ for (let i = opened.length; i > shared; i--) out.push(' '.repeat(i - 1) + '}');
268
+ for (let i = shared; i < target.length; i++) out.push(' '.repeat(i) + `state ${target[i]} {`);
269
+ opened = target;
270
+ };
271
+ // Declare every id first, then retain transition document order even
272
+ // when that requires reopening a compound scope. Priority survives print.
273
+ for (const s of ast.states) {
274
+ scope(s.parent);
275
+ out.push(' '.repeat(opened.length) + (s.label !== s.id ? `${s.id} : ${s.label}` : `state ${s.id}`));
276
+ }
277
+ for (const t of ast.transitions) {
278
+ scope(t.parent);
279
+ out.push(' '.repeat(opened.length) + `${t.from} --> ${t.to}${t.label ? ' : ' + t.label : ''}`);
280
+ }
281
+ scope(null);
282
+ return out.join('\n') + '\n';
283
+ }
256
284
  // an id-only state no transition mentions must still be DECLARED, or
257
285
  // the canonical round trip silently drops it (`state x` is the
258
286
  // parser's own spelling for exactly that)
@@ -0,0 +1,168 @@
1
+ {
2
+ "$jslt": "0.1",
3
+ "rules": [
4
+ {
5
+ "match": "$",
6
+ "body": {
7
+ "$map": [
8
+ [
9
+ "$$fsm",
10
+ "0.2"
11
+ ],
12
+ [
13
+ "initial",
14
+ {
15
+ "$for": {
16
+ "t": "$.ast.transitions[*]"
17
+ },
18
+ "$where": {
19
+ "$and": [
20
+ {
21
+ "$eq": [
22
+ "$t.from",
23
+ "[*]"
24
+ ]
25
+ },
26
+ {
27
+ "$eq": [
28
+ "$t.parent",
29
+ null
30
+ ]
31
+ }
32
+ ]
33
+ },
34
+ "$return": "$t.to"
35
+ }
36
+ ],
37
+ [
38
+ "states",
39
+ [
40
+ {
41
+ "$for": {
42
+ "s": "$.ast.states[*]"
43
+ },
44
+ "$return": {
45
+ "id": "$s.id",
46
+ "parent": {
47
+ "$if": [
48
+ "$s.parent",
49
+ "$s.parent"
50
+ ]
51
+ },
52
+ "initial": {
53
+ "$for": {
54
+ "t": "$.ast.transitions[*]"
55
+ },
56
+ "$where": {
57
+ "$and": [
58
+ {
59
+ "$eq": [
60
+ "$t.from",
61
+ "[*]"
62
+ ]
63
+ },
64
+ {
65
+ "$eq": [
66
+ "$t.parent",
67
+ "$s.id"
68
+ ]
69
+ }
70
+ ]
71
+ },
72
+ "$return": "$t.to"
73
+ },
74
+ "final": {
75
+ "$if": [
76
+ {
77
+ "$exists": {
78
+ "$for": {
79
+ "t": "$.ast.transitions[*]"
80
+ },
81
+ "$where": {
82
+ "$and": [
83
+ {
84
+ "$eq": [
85
+ "$t.to",
86
+ "[*]"
87
+ ]
88
+ },
89
+ {
90
+ "$eq": [
91
+ "$t.from",
92
+ "$s.id"
93
+ ]
94
+ }
95
+ ]
96
+ },
97
+ "$return": "$t.from"
98
+ }
99
+ },
100
+ true
101
+ ]
102
+ }
103
+ }
104
+ }
105
+ ]
106
+ ],
107
+ [
108
+ "transitions",
109
+ [
110
+ {
111
+ "$for": {
112
+ "t": "$.ast.transitions[*]"
113
+ },
114
+ "$return": {
115
+ "from": "$t.from",
116
+ "event": "$t.event",
117
+ "guard": {
118
+ "$if": [
119
+ "$t.guard",
120
+ "$t.guard"
121
+ ]
122
+ },
123
+ "to": "$t.to",
124
+ "effects": {
125
+ "$if": [
126
+ "$t.effect",
127
+ [
128
+ {
129
+ "run": "$t.effect"
130
+ }
131
+ ]
132
+ ]
133
+ }
134
+ },
135
+ "$where": {
136
+ "$or": [
137
+ {
138
+ "$and": [
139
+ {
140
+ "$ne": [
141
+ "$t.from",
142
+ "[*]"
143
+ ]
144
+ },
145
+ {
146
+ "$ne": [
147
+ "$t.to",
148
+ "[*]"
149
+ ]
150
+ }
151
+ ]
152
+ },
153
+ {
154
+ "$ne": [
155
+ "$t.label",
156
+ null
157
+ ]
158
+ }
159
+ ]
160
+ }
161
+ }
162
+ ]
163
+ ]
164
+ ]
165
+ }
166
+ }
167
+ ]
168
+ }
@@ -0,0 +1,240 @@
1
+ {
2
+ "$jslt": "0.1",
3
+ "rules": [
4
+ {
5
+ "match": "$",
6
+ "body": {
7
+ "states": [
8
+ {
9
+ "$for": {
10
+ "s": "$.states[*]"
11
+ },
12
+ "$return": {
13
+ "id": {
14
+ "$if": [
15
+ {
16
+ "$is-string": "$s"
17
+ },
18
+ "$s",
19
+ "$s.id"
20
+ ]
21
+ },
22
+ "label": {
23
+ "$if": [
24
+ {
25
+ "$is-string": "$s"
26
+ },
27
+ "$s",
28
+ "$s.id"
29
+ ]
30
+ },
31
+ "parent": {
32
+ "$if": [
33
+ "$s.parent",
34
+ "$s.parent"
35
+ ]
36
+ }
37
+ }
38
+ }
39
+ ],
40
+ "transitions": [
41
+ {
42
+ "$if": [
43
+ "$.initial",
44
+ {
45
+ "from": "[*]",
46
+ "to": "$.initial",
47
+ "label": null,
48
+ "event": null,
49
+ "guard": null,
50
+ "effect": null,
51
+ "parent": null
52
+ }
53
+ ]
54
+ },
55
+ {
56
+ "$for": {
57
+ "s": "$.states[*]"
58
+ },
59
+ "$where": {
60
+ "$exists": "$s.initial"
61
+ },
62
+ "$return": {
63
+ "from": "[*]",
64
+ "to": "$s.initial",
65
+ "label": null,
66
+ "event": null,
67
+ "guard": null,
68
+ "effect": null,
69
+ "parent": "$s.id"
70
+ }
71
+ },
72
+ {
73
+ "$for": {
74
+ "t": "$.transitions[*]"
75
+ },
76
+ "$return": {
77
+ "$let": {
78
+ "guardText": {
79
+ "$if": [
80
+ "$t.guard",
81
+ {
82
+ "$concat": [
83
+ "[",
84
+ {
85
+ "$if": [
86
+ {
87
+ "$is-string": "$t.guard"
88
+ },
89
+ "$t.guard",
90
+ "…"
91
+ ]
92
+ },
93
+ "]"
94
+ ]
95
+ }
96
+ ]
97
+ },
98
+ "effectText": {
99
+ "$if": [
100
+ {
101
+ "$exists": "$t.effects[*]"
102
+ },
103
+ {
104
+ "$concat": [
105
+ "/ ",
106
+ {
107
+ "$string-join": [
108
+ "$t.effects[*].run",
109
+ ", "
110
+ ]
111
+ }
112
+ ]
113
+ }
114
+ ]
115
+ }
116
+ },
117
+ "$return": {
118
+ "$let": {
119
+ "pieces": {
120
+ "$seq": [
121
+ {
122
+ "$if": [
123
+ "$t.event",
124
+ "$t.event"
125
+ ]
126
+ },
127
+ "$guardText",
128
+ "$effectText"
129
+ ]
130
+ }
131
+ },
132
+ "$return": {
133
+ "from": "$t.from",
134
+ "to": "$t.to",
135
+ "label": {
136
+ "$coalesce": [
137
+ {
138
+ "$if": [
139
+ {
140
+ "$exists": "$pieces"
141
+ },
142
+ {
143
+ "$string-join": [
144
+ "$pieces",
145
+ " "
146
+ ]
147
+ }
148
+ ]
149
+ },
150
+ null
151
+ ]
152
+ },
153
+ "event": {
154
+ "$coalesce": [
155
+ "$t.event",
156
+ null
157
+ ]
158
+ },
159
+ "guard": {
160
+ "$coalesce": [
161
+ {
162
+ "$if": [
163
+ "$t.guard",
164
+ {
165
+ "$if": [
166
+ {
167
+ "$is-string": "$t.guard"
168
+ },
169
+ "$t.guard",
170
+ "…"
171
+ ]
172
+ }
173
+ ]
174
+ },
175
+ null
176
+ ]
177
+ },
178
+ "effect": {
179
+ "$coalesce": [
180
+ {
181
+ "$if": [
182
+ {
183
+ "$exists": "$t.effects[*]"
184
+ },
185
+ {
186
+ "$string-join": [
187
+ "$t.effects[*].run",
188
+ ", "
189
+ ]
190
+ }
191
+ ]
192
+ },
193
+ null
194
+ ]
195
+ },
196
+ "parent": null
197
+ }
198
+ }
199
+ }
200
+ },
201
+ {
202
+ "$for": {
203
+ "s": "$.states[*]"
204
+ },
205
+ "$where": {
206
+ "$or": [
207
+ {
208
+ "$eq": [
209
+ "$s.final",
210
+ true
211
+ ]
212
+ },
213
+ {
214
+ "$eq": [
215
+ "$s.type",
216
+ "final"
217
+ ]
218
+ }
219
+ ]
220
+ },
221
+ "$return": {
222
+ "from": "$s.id",
223
+ "to": "[*]",
224
+ "label": null,
225
+ "event": null,
226
+ "guard": null,
227
+ "effect": null,
228
+ "parent": {
229
+ "$coalesce": [
230
+ "$s.parent",
231
+ null
232
+ ]
233
+ }
234
+ }
235
+ }
236
+ ]
237
+ }
238
+ }
239
+ ]
240
+ }