iobroker.statechart 0.1.1 → 0.2.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 +130 -21
- package/admin/build/tab.js +132 -130
- package/admin/statechart.png +0 -0
- package/build/lib/chart-codegen.js +338 -0
- package/build/lib/chart-codegen.js.map +7 -0
- package/build/lib/chart-helpers.js +90 -0
- package/build/lib/chart-helpers.js.map +7 -0
- package/build/lib/chart-model.js +353 -0
- package/build/lib/chart-model.js.map +7 -0
- package/build/lib/chart-runtime.js +876 -0
- package/build/lib/chart-runtime.js.map +7 -0
- package/build/lib/chart-structure.js +101 -0
- package/build/lib/chart-structure.js.map +7 -0
- package/build/lib/chart-validation.js +303 -0
- package/build/lib/chart-validation.js.map +7 -0
- package/build/main.js +342 -0
- package/build/main.js.map +7 -0
- package/io-package.json +14 -1
- package/package.json +20 -17
- package/lib/adapter-config.d.ts +0 -19
- package/lib/chart-codegen.js +0 -436
- package/lib/chart-codegen.test.js +0 -132
- package/lib/chart-helpers.js +0 -74
- package/lib/chart-helpers.test.js +0 -38
- package/lib/chart-model.js +0 -577
- package/lib/chart-model.test.js +0 -189
- package/lib/chart-runtime.js +0 -797
- package/lib/chart-runtime.test.js +0 -699
- package/lib/chart-structure.js +0 -106
- package/lib/chart-validation.js +0 -328
- package/lib/chart-validation.test.js +0 -149
- package/main.js +0 -279
package/lib/chart-model.js
DELETED
|
@@ -1,577 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
|
|
3
|
-
/*
|
|
4
|
-
* The chart model shared by the editor (admin tab) and the runtime (adapter).
|
|
5
|
-
*
|
|
6
|
-
* A chart is stored as object "statechart.<instance>.charts.<chartId>" (type "channel"),
|
|
7
|
-
* the model itself lives in `native.chart` of that object.
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/** Current version of the chart model, increase on incompatible changes */
|
|
11
|
-
const CHART_MODEL_VERSION = 1;
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @typedef {"input" | "output" | "local"} SymbolScope
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* A variable which can be used in conditions and actions.
|
|
19
|
-
* Inputs and outputs are mapped to ioBroker states, locals only live inside the chart.
|
|
20
|
-
*
|
|
21
|
-
* @typedef {object} ChartSymbol
|
|
22
|
-
* @property {string} name Name used in conditions and actions, must be a valid JavaScript identifier
|
|
23
|
-
* @property {SymbolScope} scope Input (read from state), output (written to state) or local
|
|
24
|
-
* @property {string} [stateId] ioBroker state ID for inputs and outputs
|
|
25
|
-
* @property {any} [initial] Initial value for locals
|
|
26
|
-
* @property {InputTrigger} [trigger] Inputs only: wake up the chart on every update (default) or only on changes
|
|
27
|
-
* @property {AckFilter} [ack] Inputs only: which updates are used, depending on their ack flag, default "any"
|
|
28
|
-
* @property {number} [debounceMs] Inputs only: wake up the chart only after the input had no update for this time
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* @typedef {"update" | "change"} InputTrigger
|
|
33
|
-
* @typedef {"any" | "ack" | "command"} AckFilter "ack": only confirmed values, "command": only commands (ack=false)
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* @typedef {object} ChartState
|
|
38
|
-
* @property {string} id Unique and stable ID inside the chart
|
|
39
|
-
* @property {string} name Display name, also used for the "activeState" state
|
|
40
|
-
* @property {string | null} [parent] ID of the parent state, null/undefined for top level states
|
|
41
|
-
* @property {string} [initial] ID of the initial child state (only for states with children)
|
|
42
|
-
* @property {string} [entry] Action executed when the state is entered
|
|
43
|
-
* @property {string} [during] Action executed on every wakeup while the state is active and no transition is taken
|
|
44
|
-
* @property {string} [exit] Action executed when the state is left
|
|
45
|
-
* @property {string} [comment] Free text for the documentation, no influence on the execution
|
|
46
|
-
* @property {number} x Position in the editor, relative to the parent state
|
|
47
|
-
* @property {number} y Position in the editor, relative to the parent state
|
|
48
|
-
* @property {number} [width] Size in the editor
|
|
49
|
-
* @property {number} [height] Size in the editor
|
|
50
|
-
*/
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* @typedef {object} ChartTransition
|
|
54
|
-
* @property {string} id Unique and stable ID inside the chart
|
|
55
|
-
* @property {string} source ID of the source state
|
|
56
|
-
* @property {string} target ID of the target state
|
|
57
|
-
* @property {string} [label] Transition label in Stateflow syntax: event[condition]{action}
|
|
58
|
-
* @property {number} [priority] Evaluation order of transitions with the same source, lower is first
|
|
59
|
-
* @property {boolean} [reenterParent] Only for transitions from a state to one of its own children: leave and
|
|
60
|
-
* enter the source state again. Default is an inner transition, which keeps the source state active.
|
|
61
|
-
* @property {string} [comment] Free text for the documentation, no influence on the execution
|
|
62
|
-
* @property {string} [sourceHandle] Side of the source state where the arrow starts (editor only)
|
|
63
|
-
* @property {string} [targetHandle] Side of the target state where the arrow ends (editor only)
|
|
64
|
-
*/
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* A block with JavaScript code, executed once when the chart starts.
|
|
68
|
-
* The functions declared in it can be used in all conditions and actions of the chart.
|
|
69
|
-
*
|
|
70
|
-
* @typedef {object} ChartFunction
|
|
71
|
-
* @property {string} id Unique and stable ID inside the chart
|
|
72
|
-
* @property {string} code JavaScript, usually one or more function declarations
|
|
73
|
-
* @property {string} [comment] Free text for the documentation, no influence on the execution
|
|
74
|
-
* @property {number} x Position in the editor
|
|
75
|
-
* @property {number} y Position in the editor
|
|
76
|
-
* @property {number} [width] Size in the editor
|
|
77
|
-
* @property {number} [height] Size in the editor
|
|
78
|
-
*/
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* A note on the canvas of the editor, like an annotation in Stateflow. No influence on the execution.
|
|
82
|
-
*
|
|
83
|
-
* @typedef {object} ChartNote
|
|
84
|
-
* @property {string} id Unique and stable ID inside the chart
|
|
85
|
-
* @property {string} text
|
|
86
|
-
* @property {string | null} [parent] ID of the state the note is placed in, it moves together with it
|
|
87
|
-
* @property {number} x Position in the editor, relative to the parent state
|
|
88
|
-
* @property {number} y Position in the editor, relative to the parent state
|
|
89
|
-
* @property {number} [width] Size in the editor
|
|
90
|
-
* @property {number} [height] Size in the editor
|
|
91
|
-
*/
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* @typedef {object} Chart
|
|
95
|
-
* @property {number} version Model version, see CHART_MODEL_VERSION
|
|
96
|
-
* @property {string} name Display name
|
|
97
|
-
* @property {boolean} enabled If the chart is executed by the adapter
|
|
98
|
-
* @property {boolean} [restoreState] Continue with the state from before a restart of the adapter, default true
|
|
99
|
-
* @property {string} [initial] ID of the initial top level state (default transition)
|
|
100
|
-
* @property {ChartSymbol[]} symbols
|
|
101
|
-
* @property {ChartState[]} states
|
|
102
|
-
* @property {ChartTransition[]} transitions
|
|
103
|
-
* @property {ChartFunction[]} [functions] Missing in charts created before function blocks existed
|
|
104
|
-
* @property {ChartNote[]} [notes] Missing in charts created before notes existed
|
|
105
|
-
* @property {string} [comment] Free text for the documentation, no influence on the execution
|
|
106
|
-
*/
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* @typedef {object} TransitionLabel
|
|
110
|
-
* @property {string} [event] Name of the triggering event (an input symbol), undefined = any wakeup
|
|
111
|
-
* @property {number} [afterMs] Delay of a temporal transition after(n, unit) in milliseconds
|
|
112
|
-
* @property {string} [condition] Guard expression
|
|
113
|
-
* @property {string} [action] Transition action
|
|
114
|
-
*/
|
|
115
|
-
|
|
116
|
-
const TIME_UNITS = {
|
|
117
|
-
msec: 1,
|
|
118
|
-
sec: 1000,
|
|
119
|
-
min: 60_000,
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Creates a new, empty chart. It is paused, the user starts it when it is ready.
|
|
124
|
-
*
|
|
125
|
-
* @param {string} name Display name
|
|
126
|
-
* @returns {Chart}
|
|
127
|
-
*/
|
|
128
|
-
function createChart(name) {
|
|
129
|
-
return {
|
|
130
|
-
version: CHART_MODEL_VERSION,
|
|
131
|
-
name,
|
|
132
|
-
enabled: false,
|
|
133
|
-
symbols: [],
|
|
134
|
-
states: [],
|
|
135
|
-
transitions: [],
|
|
136
|
-
functions: [],
|
|
137
|
-
notes: [],
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Converts a display name to an ID which can be used in ioBroker object IDs
|
|
143
|
-
*
|
|
144
|
-
* @param {string} name
|
|
145
|
-
* @returns {string}
|
|
146
|
-
*/
|
|
147
|
-
function nameToId(name) {
|
|
148
|
-
return (
|
|
149
|
-
name
|
|
150
|
-
.trim()
|
|
151
|
-
.toLowerCase()
|
|
152
|
-
.replace(/ä/g, "ae")
|
|
153
|
-
.replace(/ö/g, "oe")
|
|
154
|
-
.replace(/ü/g, "ue")
|
|
155
|
-
.replace(/ß/g, "ss")
|
|
156
|
-
.replace(/[^a-z0-9_-]+/g, "_")
|
|
157
|
-
.replace(/^_+|_+$/g, "") || "chart"
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* @typedef {object} TextRange
|
|
163
|
-
* @property {number} from Index of the first character
|
|
164
|
-
* @property {number} to Index after the last character
|
|
165
|
-
*/
|
|
166
|
-
|
|
167
|
-
/**
|
|
168
|
-
* @typedef {object} TransitionLabelRanges Positions of the parts in the label text
|
|
169
|
-
* @property {TextRange} [event]
|
|
170
|
-
* @property {TextRange} [after] The whole after(...) expression
|
|
171
|
-
* @property {TextRange} [condition] Content of [...] without the brackets
|
|
172
|
-
* @property {TextRange} [action] Content of {...} without the brackets
|
|
173
|
-
*/
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Fills the %s placeholders of a message
|
|
177
|
-
*
|
|
178
|
-
* @param {string} key Message with %s placeholders
|
|
179
|
-
* @param {string[]} args
|
|
180
|
-
* @returns {string}
|
|
181
|
-
*/
|
|
182
|
-
function formatMessage(key, args) {
|
|
183
|
-
let i = 0;
|
|
184
|
-
return key.replace(/%s/g, () => args[i++] ?? "");
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
/**
|
|
188
|
-
* Error in the structure of a transition label, knows where in the label the problem is
|
|
189
|
-
*/
|
|
190
|
-
class LabelSyntaxError extends Error {
|
|
191
|
-
/**
|
|
192
|
-
* @param {string} key Message with %s placeholders, used to translate the message
|
|
193
|
-
* @param {string[]} args
|
|
194
|
-
* @param {number} from
|
|
195
|
-
* @param {number} to
|
|
196
|
-
*/
|
|
197
|
-
constructor(key, args, from, to) {
|
|
198
|
-
super(formatMessage(key, args));
|
|
199
|
-
this.key = key;
|
|
200
|
-
this.args = args;
|
|
201
|
-
this.from = from;
|
|
202
|
-
this.to = to;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* Reads a part enclosed in brackets, nested brackets of the same type are allowed
|
|
208
|
-
*
|
|
209
|
-
* @param {string} text
|
|
210
|
-
* @param {number} start Index of the opening bracket
|
|
211
|
-
* @param {string} open
|
|
212
|
-
* @param {string} close
|
|
213
|
-
* @returns {{ content: string; from: number; end: number }} from is the index of the (trimmed) content,
|
|
214
|
-
* end is the index after the closing bracket
|
|
215
|
-
*/
|
|
216
|
-
function readEnclosed(text, start, open, close) {
|
|
217
|
-
let depth = 0;
|
|
218
|
-
for (let i = start; i < text.length; i++) {
|
|
219
|
-
if (text[i] === open) {
|
|
220
|
-
depth++;
|
|
221
|
-
} else if (text[i] === close) {
|
|
222
|
-
depth--;
|
|
223
|
-
if (depth === 0) {
|
|
224
|
-
const raw = text.substring(start + 1, i);
|
|
225
|
-
const content = raw.trim();
|
|
226
|
-
return { content, from: start + 1 + raw.indexOf(content), end: i + 1 };
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
throw new LabelSyntaxError('Missing "%s"', [close], start, text.length);
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/**
|
|
234
|
-
* Parses a transition label and returns where its parts are, see parseTransitionLabel
|
|
235
|
-
*
|
|
236
|
-
* @param {string | undefined} label
|
|
237
|
-
* @returns {{ label: TransitionLabel; ranges: TransitionLabelRanges }}
|
|
238
|
-
* @throws {LabelSyntaxError}
|
|
239
|
-
*/
|
|
240
|
-
function parseTransitionLabelWithRanges(label) {
|
|
241
|
-
/** @type {TransitionLabel} */
|
|
242
|
-
const result = {};
|
|
243
|
-
/** @type {TransitionLabelRanges} */
|
|
244
|
-
const ranges = {};
|
|
245
|
-
const text = label || "";
|
|
246
|
-
let pos = 0;
|
|
247
|
-
|
|
248
|
-
const skipSpace = () => {
|
|
249
|
-
while (pos < text.length && /\s/.test(text[pos])) {
|
|
250
|
-
pos++;
|
|
251
|
-
}
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
skipSpace();
|
|
255
|
-
const eventMatch = /[A-Za-z_$][\w$]*/y;
|
|
256
|
-
eventMatch.lastIndex = pos;
|
|
257
|
-
const event = eventMatch.exec(text);
|
|
258
|
-
if (event) {
|
|
259
|
-
const from = pos;
|
|
260
|
-
pos += event[0].length;
|
|
261
|
-
skipSpace();
|
|
262
|
-
if (event[0] === "after" && text[pos] === "(") {
|
|
263
|
-
const { content, end } = readEnclosed(text, pos, "(", ")");
|
|
264
|
-
const afterMatch = /^(\d+(?:\.\d+)?)\s*,\s*(msec|sec|min)$/.exec(content);
|
|
265
|
-
if (!afterMatch) {
|
|
266
|
-
throw new LabelSyntaxError(
|
|
267
|
-
'Invalid temporal expression "%s", expected e.g. after(5, sec)',
|
|
268
|
-
[`after(${content})`],
|
|
269
|
-
from,
|
|
270
|
-
end,
|
|
271
|
-
);
|
|
272
|
-
}
|
|
273
|
-
result.afterMs = parseFloat(afterMatch[1]) * TIME_UNITS[afterMatch[2]];
|
|
274
|
-
ranges.after = { from, to: end };
|
|
275
|
-
pos = end;
|
|
276
|
-
} else {
|
|
277
|
-
result.event = event[0];
|
|
278
|
-
ranges.event = { from, to: from + event[0].length };
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
skipSpace();
|
|
283
|
-
if (text[pos] === "[") {
|
|
284
|
-
const { content, from, end } = readEnclosed(text, pos, "[", "]");
|
|
285
|
-
result.condition = content;
|
|
286
|
-
ranges.condition = { from, to: from + content.length };
|
|
287
|
-
pos = end;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
skipSpace();
|
|
291
|
-
if (text[pos] === "{") {
|
|
292
|
-
const { content, from, end } = readEnclosed(text, pos, "{", "}");
|
|
293
|
-
result.action = content;
|
|
294
|
-
ranges.action = { from, to: from + content.length };
|
|
295
|
-
pos = end;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
skipSpace();
|
|
299
|
-
if (pos < text.length) {
|
|
300
|
-
throw new LabelSyntaxError(
|
|
301
|
-
'Unexpected "%s", expected event[condition]{action}',
|
|
302
|
-
[text.substring(pos).trim()],
|
|
303
|
-
pos,
|
|
304
|
-
text.length,
|
|
305
|
-
);
|
|
306
|
-
}
|
|
307
|
-
return { label: result, ranges };
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
/**
|
|
311
|
-
* Parses a transition label in Stateflow syntax: event[condition]{action}
|
|
312
|
-
* All parts are optional. Instead of an event, after(n, sec|msec|min) creates a temporal transition.
|
|
313
|
-
*
|
|
314
|
-
* @param {string | undefined} label
|
|
315
|
-
* @returns {TransitionLabel}
|
|
316
|
-
* @throws {LabelSyntaxError}
|
|
317
|
-
*/
|
|
318
|
-
function parseTransitionLabel(label) {
|
|
319
|
-
return parseTransitionLabelWithRanges(label).label;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/** Identifies files with exported charts */
|
|
323
|
-
const EXPORT_FORMAT = "iobroker.statechart";
|
|
324
|
-
|
|
325
|
-
/** Files exported before the adapter was renamed from "state-machine" to "statechart" */
|
|
326
|
-
const LEGACY_EXPORT_FORMATS = ["iobroker.state-machine"];
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* The content of an export file
|
|
330
|
-
*
|
|
331
|
-
* @param {Chart[]} charts
|
|
332
|
-
* @returns {{ format: string; version: number; exported: string; charts: Chart[] }}
|
|
333
|
-
*/
|
|
334
|
-
function createExport(charts) {
|
|
335
|
-
return { format: EXPORT_FORMAT, version: CHART_MODEL_VERSION, exported: new Date().toISOString(), charts };
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
339
|
-
const SCOPES = ["input", "output", "local"];
|
|
340
|
-
|
|
341
|
-
/**
|
|
342
|
-
* @param {any} value
|
|
343
|
-
* @returns {value is Record<string, any>}
|
|
344
|
-
*/
|
|
345
|
-
function isObject(value) {
|
|
346
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
/**
|
|
350
|
-
* Checks a chart of an export file and fills in defaults for missing optional values
|
|
351
|
-
*
|
|
352
|
-
* @param {any} data
|
|
353
|
-
* @param {string} where Prefix of error messages, e.g. "Chart 1"
|
|
354
|
-
* @returns {Chart}
|
|
355
|
-
* @throws {Error} if the chart is invalid
|
|
356
|
-
*/
|
|
357
|
-
function normalizeChart(data, where) {
|
|
358
|
-
/** @param {string} message */
|
|
359
|
-
const fail = message => {
|
|
360
|
-
throw new Error(`${where}: ${message}`);
|
|
361
|
-
};
|
|
362
|
-
if (!isObject(data)) {
|
|
363
|
-
fail("not an object");
|
|
364
|
-
}
|
|
365
|
-
if (typeof data.version === "number" && data.version > CHART_MODEL_VERSION) {
|
|
366
|
-
fail(`created with a newer version of the adapter (chart version ${data.version})`);
|
|
367
|
-
}
|
|
368
|
-
for (const list of ["states", "transitions", "symbols"]) {
|
|
369
|
-
if (!Array.isArray(data[list])) {
|
|
370
|
-
fail(`"${list}" is missing or no list`);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
for (const list of ["functions", "notes"]) {
|
|
374
|
-
if (data[list] !== undefined && !Array.isArray(data[list])) {
|
|
375
|
-
fail(`"${list}" is no list`);
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
/** @type {any[]} */
|
|
379
|
-
const functions = data.functions || [];
|
|
380
|
-
/** @type {any[]} */
|
|
381
|
-
const notes = data.notes || [];
|
|
382
|
-
|
|
383
|
-
// states, function blocks and notes are all nodes in the editor, so their IDs must be unique together
|
|
384
|
-
const nodeIds = new Set();
|
|
385
|
-
/**
|
|
386
|
-
* @param {any} item
|
|
387
|
-
* @param {string} kind
|
|
388
|
-
* @param {number} index
|
|
389
|
-
* @returns {string} the ID
|
|
390
|
-
*/
|
|
391
|
-
const checkId = (item, kind, index) => {
|
|
392
|
-
if (!isObject(item) || typeof item.id !== "string" || !item.id) {
|
|
393
|
-
fail(`${kind} ${index + 1} has no ID`);
|
|
394
|
-
}
|
|
395
|
-
if (nodeIds.has(item.id)) {
|
|
396
|
-
fail(`${kind} ${item.id}: the ID is used more than once`);
|
|
397
|
-
}
|
|
398
|
-
nodeIds.add(item.id);
|
|
399
|
-
return item.id;
|
|
400
|
-
};
|
|
401
|
-
/**
|
|
402
|
-
* @param {any} value
|
|
403
|
-
* @param {string} message
|
|
404
|
-
*/
|
|
405
|
-
const optionalString = (value, message) => {
|
|
406
|
-
if (value !== undefined && value !== null && typeof value !== "string") {
|
|
407
|
-
fail(message);
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
data.states.forEach((/** @type {any} */ state, /** @type {number} */ i) => checkId(state, "state", i));
|
|
412
|
-
functions.forEach((/** @type {any} */ fn, /** @type {number} */ i) => checkId(fn, "function block", i));
|
|
413
|
-
notes.forEach((/** @type {any} */ note, /** @type {number} */ i) => checkId(note, "note", i));
|
|
414
|
-
const stateIds = new Set(data.states.map((/** @type {any} */ s) => s.id));
|
|
415
|
-
optionalString(data.comment, '"comment" is no text');
|
|
416
|
-
|
|
417
|
-
/** @type {ChartState[]} */
|
|
418
|
-
const states = data.states.map((/** @type {any} */ state) => {
|
|
419
|
-
const at = `state ${state.id}`;
|
|
420
|
-
if (state.parent && !stateIds.has(state.parent)) {
|
|
421
|
-
fail(`${at}: unknown parent state "${state.parent}"`);
|
|
422
|
-
}
|
|
423
|
-
for (const field of ["entry", "during", "exit", "initial", "comment"]) {
|
|
424
|
-
optionalString(state[field], `${at}: "${field}" is no text`);
|
|
425
|
-
}
|
|
426
|
-
return {
|
|
427
|
-
...state,
|
|
428
|
-
name: typeof state.name === "string" && state.name ? state.name : state.id,
|
|
429
|
-
parent: state.parent || null,
|
|
430
|
-
x: Number.isFinite(state.x) ? state.x : 0,
|
|
431
|
-
y: Number.isFinite(state.y) ? state.y : 0,
|
|
432
|
-
};
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
// a parent must not be its own (indirect) child
|
|
436
|
-
const parentOf = new Map(states.map(s => [s.id, s.parent]));
|
|
437
|
-
for (const state of states) {
|
|
438
|
-
let parent = state.parent;
|
|
439
|
-
for (let i = 0; parent; i++) {
|
|
440
|
-
if (parent === state.id || i > states.length) {
|
|
441
|
-
fail(`state ${state.id}: the parent states form a cycle`);
|
|
442
|
-
}
|
|
443
|
-
parent = parentOf.get(parent) ?? null;
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
const transitionIds = new Set();
|
|
448
|
-
/** @type {ChartTransition[]} */
|
|
449
|
-
const transitions = data.transitions.map((/** @type {any} */ transition, /** @type {number} */ i) => {
|
|
450
|
-
if (!isObject(transition) || typeof transition.id !== "string" || !transition.id) {
|
|
451
|
-
fail(`transition ${i + 1} has no ID`);
|
|
452
|
-
}
|
|
453
|
-
const at = `transition ${transition.id}`;
|
|
454
|
-
if (transitionIds.has(transition.id)) {
|
|
455
|
-
fail(`${at}: the ID is used more than once`);
|
|
456
|
-
}
|
|
457
|
-
transitionIds.add(transition.id);
|
|
458
|
-
for (const end of ["source", "target"]) {
|
|
459
|
-
if (!stateIds.has(transition[end])) {
|
|
460
|
-
fail(`${at}: unknown ${end} state "${transition[end]}"`);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
optionalString(transition.label, `${at}: "label" is no text`);
|
|
464
|
-
optionalString(transition.comment, `${at}: "comment" is no text`);
|
|
465
|
-
if (transition.priority !== undefined && !Number.isFinite(transition.priority)) {
|
|
466
|
-
fail(`${at}: "priority" is no number`);
|
|
467
|
-
}
|
|
468
|
-
return { ...transition };
|
|
469
|
-
});
|
|
470
|
-
|
|
471
|
-
const symbolNames = new Set();
|
|
472
|
-
/** @type {ChartSymbol[]} */
|
|
473
|
-
const symbols = data.symbols.map((/** @type {any} */ symbol, /** @type {number} */ i) => {
|
|
474
|
-
if (!isObject(symbol) || typeof symbol.name !== "string" || !IDENTIFIER.test(symbol.name)) {
|
|
475
|
-
fail(`variable ${i + 1}: invalid name`);
|
|
476
|
-
}
|
|
477
|
-
if (symbolNames.has(symbol.name)) {
|
|
478
|
-
fail(`variable ${symbol.name}: the name is used more than once`);
|
|
479
|
-
}
|
|
480
|
-
symbolNames.add(symbol.name);
|
|
481
|
-
if (!SCOPES.includes(symbol.scope)) {
|
|
482
|
-
fail(`variable ${symbol.name}: invalid kind "${symbol.scope}"`);
|
|
483
|
-
}
|
|
484
|
-
optionalString(symbol.stateId, `variable ${symbol.name}: "stateId" is no text`);
|
|
485
|
-
if (symbol.trigger !== undefined && !["update", "change"].includes(symbol.trigger)) {
|
|
486
|
-
fail(`variable ${symbol.name}: invalid trigger "${symbol.trigger}"`);
|
|
487
|
-
}
|
|
488
|
-
if (symbol.ack !== undefined && !["any", "ack", "command"].includes(symbol.ack)) {
|
|
489
|
-
fail(`variable ${symbol.name}: invalid ack filter "${symbol.ack}"`);
|
|
490
|
-
}
|
|
491
|
-
if (symbol.debounceMs !== undefined && !(Number.isFinite(symbol.debounceMs) && symbol.debounceMs >= 0)) {
|
|
492
|
-
fail(`variable ${symbol.name}: "debounceMs" is no number of 0 or more`);
|
|
493
|
-
}
|
|
494
|
-
return { ...symbol };
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
/** @type {ChartFunction[]} */
|
|
498
|
-
const normalizedFunctions = functions.map(fn => {
|
|
499
|
-
if (typeof fn.code !== "string") {
|
|
500
|
-
fail(`function block ${fn.id}: "code" is no text`);
|
|
501
|
-
}
|
|
502
|
-
optionalString(fn.comment, `function block ${fn.id}: "comment" is no text`);
|
|
503
|
-
return { ...fn, x: Number.isFinite(fn.x) ? fn.x : 0, y: Number.isFinite(fn.y) ? fn.y : 0 };
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
/** @type {ChartNote[]} */
|
|
507
|
-
const normalizedNotes = notes.map(note => {
|
|
508
|
-
optionalString(note.text, `note ${note.id}: "text" is no text`);
|
|
509
|
-
if (note.parent && !stateIds.has(note.parent)) {
|
|
510
|
-
fail(`note ${note.id}: unknown parent state "${note.parent}"`);
|
|
511
|
-
}
|
|
512
|
-
return {
|
|
513
|
-
...note,
|
|
514
|
-
text: note.text || "",
|
|
515
|
-
parent: note.parent || null,
|
|
516
|
-
x: Number.isFinite(note.x) ? note.x : 0,
|
|
517
|
-
y: Number.isFinite(note.y) ? note.y : 0,
|
|
518
|
-
};
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
return {
|
|
522
|
-
...data,
|
|
523
|
-
version: CHART_MODEL_VERSION,
|
|
524
|
-
name: typeof data.name === "string" && data.name.trim() ? data.name.trim() : where,
|
|
525
|
-
initial: typeof data.initial === "string" && stateIds.has(data.initial) ? data.initial : undefined,
|
|
526
|
-
enabled: data.enabled !== false,
|
|
527
|
-
symbols,
|
|
528
|
-
states,
|
|
529
|
-
transitions,
|
|
530
|
-
functions: normalizedFunctions,
|
|
531
|
-
notes: normalizedNotes,
|
|
532
|
-
};
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Reads the charts of an export file. A single chart (without the export wrapper) is accepted as well.
|
|
537
|
-
*
|
|
538
|
-
* @param {string} text Content of the file
|
|
539
|
-
* @returns {Chart[]}
|
|
540
|
-
* @throws {Error} if the file is invalid
|
|
541
|
-
*/
|
|
542
|
-
function parseExport(text) {
|
|
543
|
-
let data;
|
|
544
|
-
try {
|
|
545
|
-
data = JSON.parse(text);
|
|
546
|
-
} catch (err) {
|
|
547
|
-
throw new Error(`No valid JSON: ${err.message}`);
|
|
548
|
-
}
|
|
549
|
-
if (isObject(data) && (data.format === EXPORT_FORMAT || LEGACY_EXPORT_FORMATS.includes(data.format))) {
|
|
550
|
-
if (typeof data.version === "number" && data.version > CHART_MODEL_VERSION) {
|
|
551
|
-
throw new Error(`The file was created with a newer version of the adapter (version ${data.version})`);
|
|
552
|
-
}
|
|
553
|
-
if (!Array.isArray(data.charts) || !data.charts.length) {
|
|
554
|
-
throw new Error("The file contains no charts");
|
|
555
|
-
}
|
|
556
|
-
return data.charts.map((/** @type {any} */ chart, /** @type {number} */ i) =>
|
|
557
|
-
normalizeChart(chart, `Chart ${i + 1}`),
|
|
558
|
-
);
|
|
559
|
-
}
|
|
560
|
-
if (isObject(data) && Array.isArray(data.states)) {
|
|
561
|
-
return [normalizeChart(data, "Chart")];
|
|
562
|
-
}
|
|
563
|
-
throw new Error("The file contains no charts of the statechart adapter");
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
module.exports = {
|
|
567
|
-
CHART_MODEL_VERSION,
|
|
568
|
-
EXPORT_FORMAT,
|
|
569
|
-
LabelSyntaxError,
|
|
570
|
-
createChart,
|
|
571
|
-
createExport,
|
|
572
|
-
formatMessage,
|
|
573
|
-
nameToId,
|
|
574
|
-
parseExport,
|
|
575
|
-
parseTransitionLabel,
|
|
576
|
-
parseTransitionLabelWithRanges,
|
|
577
|
-
};
|