@statewalker/fsm-charts 0.2.1 → 0.2.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.
@@ -399,6 +399,7 @@ function buildStatechartSvg({
399
399
  initialStateKey = "<initial>",
400
400
  finalStateKey = "<final>"
401
401
  }) {
402
+ if (!nodes.length && !edges.length) return;
402
403
  width = Math.ceil(width);
403
404
  height = Math.ceil(height);
404
405
  println(
@@ -579,24 +580,20 @@ function buildStatechartsPanel({
579
580
  println2(
580
581
  ` <summary class="state-details__label">${statechart2.text ?? statechart2.state}</summary>`
581
582
  );
582
- println2(
583
- ` <div class="state-details__content">`
584
- );
585
- println2(
586
- ` <div class="state-details__chart">`
587
- );
583
+ println2(` <div class="state-details__content">`);
588
584
  const printChildren = (str) => println2(` ${str}`);
589
- buildStatechartSvg({
590
- graph: statechart2,
591
- newId,
592
- println: printChildren,
593
- initialStateKey,
594
- finalStateKey
595
- });
596
- println2(` </div>`);
597
- println2(
598
- ` <div class="state-details__description">`
599
- );
585
+ if (statechart2.nodes.length) {
586
+ println2(` <div class="state-details__chart">`);
587
+ buildStatechartSvg({
588
+ graph: statechart2,
589
+ newId,
590
+ println: printChildren,
591
+ initialStateKey,
592
+ finalStateKey
593
+ });
594
+ println2(` </div>`);
595
+ }
596
+ println2(` <div class="state-details__description">`);
600
597
  println2(` </div>`);
601
598
  for (const child of statechart2.children || []) {
602
599
  render(child, printChildren);
@@ -53,6 +53,24 @@ function buildSections(root) {
53
53
  }
54
54
  return top.content.length === 0 && top.children.length === 1 && !top.header ? top.children[0] : top;
55
55
  }
56
+ function* visitSectionNodes(section) {
57
+ if (section.header) yield section.header;
58
+ yield* section.content;
59
+ for (const s of vistSections(section, true)) {
60
+ yield* visitSectionNodes(s);
61
+ }
62
+ }
63
+ function* vistSections(root, ignoreRoot = false) {
64
+ !ignoreRoot && (yield root);
65
+ for (const child of root.children) {
66
+ yield* vistSections(child, false);
67
+ }
68
+ }
69
+ function findSection(section, accept) {
70
+ for (const s of vistSections(section)) {
71
+ if (accept(s) === true) return s;
72
+ }
73
+ }
56
74
  function isDomElement(node) {
57
75
  return node.nodeType === Node.ELEMENT_NODE;
58
76
  }
@@ -76,10 +94,11 @@ function* visitDom(elm) {
76
94
  }
77
95
  }
78
96
 
79
- // src/dom/index.ts
80
- function getStateDescriptionRenderer({
97
+ // src/dom/getStatesDescriptions.ts
98
+ function* visitStateDescriptions({
81
99
  element,
82
100
  rootStateKey,
101
+ removeFromDom = true,
83
102
  splitHeader = (header) => {
84
103
  if (!header) return [void 0, ""];
85
104
  const text = header.innerText.trim();
@@ -94,29 +113,52 @@ function getStateDescriptionRenderer({
94
113
  }
95
114
  }) {
96
115
  const docSections = buildSections(element);
97
- const rootStateFilter = getStateDescriptionFilter(splitHeader, rootStateKey);
98
- const rootDescription = findSection(docSections, rootStateFilter);
99
- if (!rootDescription) {
100
- return (statesStack) => {
101
- return void 0;
102
- };
116
+ const rootDescription = findSection(docSections, (section) => {
117
+ if (!section.header) return false;
118
+ const [key] = splitHeader(section.header);
119
+ return key === rootStateKey;
120
+ });
121
+ if (!rootDescription) return;
122
+ if (removeFromDom) {
123
+ for (const node of visitSectionNodes(rootDescription)) {
124
+ if (node.nodeName[0] === "#") continue;
125
+ node.parentElement?.removeChild(node);
126
+ }
103
127
  }
104
- for (const node of visitSectionNodes(rootDescription)) {
105
- if (node.nodeName[0] === "#") continue;
106
- node.parentElement?.removeChild(node);
128
+ for (const section of vistSections(rootDescription, false)) {
129
+ if (!section.header) continue;
130
+ const [state, title] = splitHeader(section.header);
131
+ if (state)
132
+ yield {
133
+ state,
134
+ title,
135
+ ...section
136
+ };
107
137
  }
138
+ }
139
+ function getStatesDescriptions(options) {
140
+ const index = {};
141
+ for (const description of visitStateDescriptions(options)) {
142
+ index[description.state] = description;
143
+ }
144
+ return index;
145
+ }
146
+
147
+ // src/dom/description-panels.ts
148
+ function getStateDescriptionRenderer(options) {
149
+ const descriptionsIndex = getStatesDescriptions(options);
108
150
  return (statesStack) => {
109
151
  const stateKey = [...statesStack].pop();
110
- const filter = getStateDescriptionFilter(splitHeader, stateKey);
111
- const description = findSection(rootDescription, filter);
112
- if (!description) return;
152
+ if (!stateKey) return;
153
+ const section = descriptionsIndex[stateKey];
154
+ if (!section) return;
113
155
  const elm = renderSections({
114
- section: description,
115
- deep: (s) => !rootStateFilter(s),
116
- header: (header) => {
117
- const [key, label] = splitHeader(header);
118
- if (key === stateKey) {
119
- return label || key;
156
+ section,
157
+ deep: (section2) => false,
158
+ // section.state !== options.rootStateKey, // ???
159
+ header: (section2) => {
160
+ if (section2.state === stateKey) {
161
+ return section2.title || section2.state;
120
162
  }
121
163
  return;
122
164
  }
@@ -125,37 +167,11 @@ function getStateDescriptionRenderer({
125
167
  details && (details.open = true);
126
168
  return elm;
127
169
  };
128
- function getStateDescriptionFilter(splitHeader2, stateKey) {
129
- if (!stateKey) return () => false;
130
- return (section) => {
131
- if (!section.header) return false;
132
- const [key] = splitHeader2(section.header);
133
- return key === stateKey;
134
- };
135
- }
136
- }
137
- function* visitSectionNodes(section) {
138
- if (section.header) yield section.header;
139
- yield* section.content;
140
- for (const s of vistSections(section, true)) {
141
- yield* visitSectionNodes(s);
142
- }
143
- }
144
- function* vistSections(root, ignoreRoot = false) {
145
- !ignoreRoot && (yield root);
146
- for (const child of root.children) {
147
- yield* vistSections(child, false);
148
- }
149
- }
150
- function findSection(section, accept) {
151
- for (const s of vistSections(section)) {
152
- if (accept(s) === true) return s;
153
- }
154
170
  }
155
171
  function renderSections({
156
172
  section,
157
173
  deep = (s) => true,
158
- header = (headerElm) => headerElm
174
+ header = (section2) => section2.header?.cloneNode(true)
159
175
  }) {
160
176
  const container = document.createElement("div");
161
177
  const id = `id-${String(Math.random()).split(".")[1]}-${Date.now()}`;
@@ -197,7 +213,7 @@ function renderSections({
197
213
  details.appendChild(summary);
198
214
  const content = document.createElement("div");
199
215
  details.appendChild(content);
200
- const headerElm = header(section2.header);
216
+ const headerElm = header(section2);
201
217
  headerElm && summary.append(headerElm);
202
218
  for (const node of section2.content) {
203
219
  if (node.nodeName[0] === "#") continue;
@@ -212,4 +228,4 @@ function renderSections({
212
228
  }
213
229
  }
214
230
 
215
- export { findSection, getStateDescriptionRenderer, renderSections, visitSectionNodes, vistSections };
231
+ export { buildSections, findSection, getStateDescriptionRenderer, getStatesDescriptions, isDomElement, newTreeBuilder, renderSections, toDomNodesIterator, visitDom, visitSectionNodes, visitStateDescriptions, vistSections };
@@ -3,19 +3,47 @@ type DomSection = {
3
3
  content: Node[];
4
4
  children: DomSection[];
5
5
  };
6
+ declare function buildSections(root: HTMLElement | Iterable<Node>): DomSection;
7
+ declare function visitSectionNodes(section: DomSection): Iterable<Node>;
8
+ declare function vistSections(root: DomSection, ignoreRoot?: boolean): Generator<DomSection>;
9
+ declare function findSection(section: DomSection, accept: (section: DomSection) => boolean): DomSection | undefined;
10
+ declare function isDomElement(node: Node): node is HTMLElement;
11
+ declare function toDomNodesIterator(root: Node | Iterable<Node>): Iterable<Node>;
12
+ declare function visitDom(elm: Node): Iterable<Node>;
6
13
 
7
- declare function getStateDescriptionRenderer({ element, rootStateKey, splitHeader, }: {
14
+ declare function getStateDescriptionRenderer(options: {
8
15
  element: HTMLElement;
9
16
  rootStateKey: string;
10
17
  splitHeader?: (header?: HTMLElement) => [stateKey: undefined | string, label: string];
11
18
  }): (statesStack: string[]) => HTMLDivElement | undefined;
12
- declare function visitSectionNodes(section: DomSection): Iterable<Node>;
13
- declare function vistSections(root: DomSection, ignoreRoot?: boolean): Generator<DomSection>;
14
- declare function findSection(section: DomSection, accept: (section: DomSection) => boolean): DomSection | undefined;
15
- declare function renderSections({ section, deep, header, }: {
16
- section: DomSection;
17
- deep?: (s: DomSection) => boolean;
18
- header?: (header: HTMLElement) => undefined | string | HTMLElement;
19
+ declare function renderSections<T extends DomSection>({ section, deep, header, }: {
20
+ section: T;
21
+ deep?: (s: T) => boolean;
22
+ header?: (section: T) => undefined | string | HTMLElement;
19
23
  }): HTMLDivElement;
20
24
 
21
- export { findSection, getStateDescriptionRenderer, renderSections, visitSectionNodes, vistSections };
25
+ type StateDescriptionSection = DomSection & {
26
+ state: string;
27
+ title?: string;
28
+ };
29
+ declare function visitStateDescriptions({ element, rootStateKey, removeFromDom, splitHeader, }: {
30
+ element: HTMLElement;
31
+ rootStateKey: string;
32
+ removeFromDom?: boolean;
33
+ splitHeader?: (header?: HTMLElement) => [stateKey: undefined | string, label: string];
34
+ }): Generator<StateDescriptionSection>;
35
+ declare function getStatesDescriptions(options: {
36
+ element: HTMLElement;
37
+ rootStateKey: string;
38
+ removeFromDom?: boolean;
39
+ splitHeader?: (header?: HTMLElement) => [stateKey: undefined | string, label: string];
40
+ }): {
41
+ [state: string]: StateDescriptionSection;
42
+ };
43
+
44
+ declare function newTreeBuilder<T extends unknown[] = []>(context: {
45
+ enter: (level: number, ...params: T) => void;
46
+ exit: (level: number, ...params: T) => void;
47
+ }): (newLevel: number, ...params: T) => void;
48
+
49
+ export { type DomSection, type StateDescriptionSection, buildSections, findSection, getStateDescriptionRenderer, getStatesDescriptions, isDomElement, newTreeBuilder, renderSections, toDomNodesIterator, visitDom, visitSectionNodes, visitStateDescriptions, vistSections };
package/dist/index-dom.js CHANGED
@@ -1 +1 @@
1
- export { findSection, getStateDescriptionRenderer, renderSections, visitSectionNodes, vistSections } from './chunk-LRMTXZ55.js';
1
+ export { buildSections, findSection, getStateDescriptionRenderer, getStatesDescriptions, isDomElement, newTreeBuilder, renderSections, toDomNodesIterator, visitDom, visitSectionNodes, visitStateDescriptions, vistSections } from './chunk-VILLJJJD.js';
@@ -1,2 +1,2 @@
1
- export { activeStatesStyle, activeTransitionsStyle, buildStateDetailsStyle, buildStateDetailsStyleWithModifier, buildStatechartCss, buildStatechartStyles, buildStatechartStylesWithModifier, buildStatechartSvg, buildStatechartsPanel, selectedStatesStyle, selectedTransitionsStyle } from './chunk-G25OZI4I.js';
1
+ export { activeStatesStyle, activeTransitionsStyle, buildStateDetailsStyle, buildStateDetailsStyleWithModifier, buildStatechartCss, buildStatechartStyles, buildStatechartStylesWithModifier, buildStatechartSvg, buildStatechartsPanel, selectedStatesStyle, selectedTransitionsStyle } from './chunk-36ZLXJVD.js';
2
2
  import './chunk-FIQDLBWD.js';
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export { D as Dimensions, F as FsmStateConfig, G as GraphParamsProvider, L as Label, P as Padding, a as Position, S as StateChart, b as StateGraphEdge, c as StateGraphNode, T as Transition, d as TransitionsGraph } from './StateChart-BYBjTmC2.js';
2
- export { findSection, getStateDescriptionRenderer, renderSections, visitSectionNodes, vistSections } from './index-dom.js';
2
+ export { DomSection, StateDescriptionSection, buildSections, findSection, getStateDescriptionRenderer, getStatesDescriptions, isDomElement, newTreeBuilder, renderSections, toDomNodesIterator, visitDom, visitSectionNodes, visitStateDescriptions, vistSections } from './index-dom.js';
3
3
  export { concatTransitionsToString, serializeProcessConfig, splitTransitionString, toFsmStateConfig } from './index-config.js';
4
4
  export { buildCharts, buildFlatCharts, getGraphParamsProvider, getLabelDimensions } from './index-layout.js';
5
5
  export { C as CssTree, c as activeStatesStyle, d as activeTransitionsStyle, f as buildStateDetailsStyle, g as buildStateDetailsStyleWithModifier, b as buildStatechartCss, i as buildStatechartStyles, h as buildStatechartStylesWithModifier, j as buildStatechartSvg, a as buildStatechartsPanel, s as selectedStatesStyle, e as selectedTransitionsStyle, k as serializeCss } from './index-html-DgSCNHkR.js';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export { concatTransitionsToString, serializeProcessConfig, splitTransitionString, toFsmStateConfig } from './chunk-SVKVQY7B.js';
2
- export { findSection, getStateDescriptionRenderer, renderSections, visitSectionNodes, vistSections } from './chunk-LRMTXZ55.js';
3
- export { activeStatesStyle, activeTransitionsStyle, buildStateDetailsStyle, buildStateDetailsStyleWithModifier, buildStatechartCss, buildStatechartStyles, buildStatechartStylesWithModifier, buildStatechartSvg, buildStatechartsPanel, selectedStatesStyle, selectedTransitionsStyle, serializeCss, toKebabCase } from './chunk-G25OZI4I.js';
2
+ export { buildSections, findSection, getStateDescriptionRenderer, getStatesDescriptions, isDomElement, newTreeBuilder, renderSections, toDomNodesIterator, visitDom, visitSectionNodes, visitStateDescriptions, vistSections } from './chunk-VILLJJJD.js';
3
+ export { activeStatesStyle, activeTransitionsStyle, buildStateDetailsStyle, buildStateDetailsStyleWithModifier, buildStatechartCss, buildStatechartStyles, buildStatechartStylesWithModifier, buildStatechartSvg, buildStatechartsPanel, selectedStatesStyle, selectedTransitionsStyle, serializeCss, toKebabCase } from './chunk-36ZLXJVD.js';
4
4
  export { buildCharts, buildFlatCharts, getGraphParamsProvider, getLabelDimensions } from './chunk-AZBRQNTU.js';
5
5
  import './chunk-FIQDLBWD.js';
6
6
  export { RuntimeStatechartApi, StateChartIndex } from './chunk-TZHCBZVP.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statewalker/fsm-charts",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "HFSM Implementation",
5
5
  "keywords": [],
6
6
  "homepage": "https://github.com/statewalker/statewalker-fsm-charts",
@@ -53,6 +53,7 @@
53
53
  "build": "rm -fr ./dist && yarn test && tsup",
54
54
  "prepare": "yarn build",
55
55
  "watch": "tsup --watch",
56
+ "watch-for-viewer": "tsup --watch --outDir=../statewalker-fsm-viewer/src/.observablehq/cache/_node/@statewalker/fsm-charts@0.2.1",
56
57
  "clean": "rm -rf dist",
57
58
  "lint": "eslint",
58
59
  "lint:fix": "yarn lint -- --fix",
@@ -55,6 +55,33 @@ export function buildSections(root: HTMLElement | Iterable<Node>): DomSection {
55
55
  : top;
56
56
  }
57
57
 
58
+ export function* visitSectionNodes(section: DomSection): Iterable<Node> {
59
+ if (section.header) yield section.header;
60
+ yield* section.content;
61
+ for (const s of vistSections(section, true)) {
62
+ yield* visitSectionNodes(s);
63
+ }
64
+ }
65
+
66
+ export function* vistSections(
67
+ root: DomSection,
68
+ ignoreRoot: boolean = false,
69
+ ): Generator<DomSection> {
70
+ !ignoreRoot && (yield root);
71
+ for (const child of root.children) {
72
+ yield* vistSections(child, false);
73
+ }
74
+ }
75
+
76
+ export function findSection(
77
+ section: DomSection,
78
+ accept: (section: DomSection) => boolean,
79
+ ) {
80
+ for (const s of vistSections(section)) {
81
+ if (accept(s) === true) return s;
82
+ }
83
+ }
84
+
58
85
  // --------------------------------------------
59
86
  // DOM Iterator utilities
60
87
 
@@ -0,0 +1,100 @@
1
+ import { type DomSection } from "./buildSections.js";
2
+ import { getStatesDescriptions } from "./getStatesDescriptions.js";
3
+
4
+ export function getStateDescriptionRenderer(options: {
5
+ element: HTMLElement;
6
+ rootStateKey: string;
7
+ splitHeader?: (
8
+ header?: HTMLElement,
9
+ ) => [stateKey: undefined | string, label: string];
10
+ }) {
11
+ const descriptionsIndex = getStatesDescriptions(options);
12
+
13
+ return (statesStack: string[]) => {
14
+ const stateKey: string | undefined = [...statesStack].pop();
15
+ if (!stateKey) return;
16
+
17
+ const section = descriptionsIndex[stateKey];
18
+ if (!section) return;
19
+
20
+ const elm = renderSections({
21
+ section,
22
+ deep: (section) => false, // section.state !== options.rootStateKey, // ???
23
+ header: (section) => {
24
+ if (section.state === stateKey) {
25
+ return section.title || section.state;
26
+ }
27
+ return;
28
+ },
29
+ });
30
+ const details = elm?.querySelector("details");
31
+ details && (details.open = true);
32
+ return elm;
33
+ };
34
+ }
35
+
36
+ export function renderSections<T extends DomSection>({
37
+ section,
38
+ deep = (s) => true,
39
+ header = (section) => section.header?.cloneNode(true) as HTMLElement,
40
+ }: {
41
+ section: T;
42
+ deep?: (s: T) => boolean;
43
+ header?: (section: T) => undefined | string | HTMLElement;
44
+ }) {
45
+ const container = document.createElement("div");
46
+ const id = `id-${String(Math.random()).split(".")[1]}-${Date.now()}`;
47
+ container.setAttribute("id", id);
48
+ if (!section) return container;
49
+
50
+ const sectionView = renderSection(section);
51
+ container.append(sectionView);
52
+ const style = renderSectionStyles(id);
53
+ container.append(style);
54
+ return container;
55
+
56
+ function renderSectionStyles(id?: string) {
57
+ const prefix = id ? `#${id}` : "";
58
+ const style = document.createElement("style");
59
+ container.appendChild(style);
60
+ style.innerHTML = `
61
+ ${prefix} details {
62
+ border: 1px solid #aaa;
63
+ border-radius: 4px;
64
+ padding: 0.5em 0.5em 0;
65
+ margin-bottom: 0.5em;
66
+ }
67
+
68
+ ${prefix} details > summary {
69
+ font-weight: bold;
70
+ margin: -0.5em -0.5em 0;
71
+ padding: 0.5em;
72
+ cursor: pointer;
73
+ }
74
+
75
+ ${prefix} details[open] {
76
+ padding: 0.5em;
77
+ }
78
+ `;
79
+ return style;
80
+ }
81
+ function renderSection(section: T) {
82
+ const details = document.createElement("details");
83
+ const summary = document.createElement("summary");
84
+ details.appendChild(summary);
85
+ const content = document.createElement("div");
86
+ details.appendChild(content);
87
+ const headerElm = header(section);
88
+ headerElm && summary.append(headerElm);
89
+ for (const node of section.content) {
90
+ if (node.nodeName[0] === "#") continue;
91
+ content.append(node.cloneNode(true));
92
+ }
93
+ if (deep(section)) {
94
+ for (const child of section.children) {
95
+ content.append(renderSection(child as T));
96
+ }
97
+ }
98
+ return details;
99
+ }
100
+ }
@@ -0,0 +1,80 @@
1
+ import {
2
+ buildSections,
3
+ findSection,
4
+ visitSectionNodes,
5
+ vistSections,
6
+ type DomSection,
7
+ } from "./buildSections.js";
8
+
9
+ export type StateDescriptionSection = DomSection & {
10
+ state: string;
11
+ title?: string;
12
+ };
13
+ export function* visitStateDescriptions({
14
+ element,
15
+ rootStateKey,
16
+ removeFromDom = true,
17
+ splitHeader = (
18
+ header?: HTMLElement,
19
+ ): [stateKey: undefined | string, label: string] => {
20
+ if (!header) return [undefined, ""];
21
+ const text = header.innerText.trim();
22
+ let key: string | undefined;
23
+ let label: string = text;
24
+ text.replace(/^\s*\[(.*)\]\s*(.*)$/, (_, k, l) => {
25
+ key = k.trim();
26
+ label = l.trim() || key;
27
+ return "";
28
+ });
29
+ return [key, label];
30
+ },
31
+ }: {
32
+ element: HTMLElement;
33
+ rootStateKey: string;
34
+ removeFromDom?: boolean; // If true - removes state description nodes from the DOM
35
+ splitHeader?: (
36
+ header?: HTMLElement,
37
+ ) => [stateKey: undefined | string, label: string];
38
+ }): Generator<StateDescriptionSection> {
39
+ const docSections = buildSections(element);
40
+ const rootDescription = findSection(docSections, (section: DomSection) => {
41
+ if (!section.header) return false;
42
+ const [key] = splitHeader(section.header);
43
+ return key === rootStateKey;
44
+ });
45
+ if (!rootDescription) return;
46
+
47
+ // Remove description nodes from DOM
48
+ if (removeFromDom) {
49
+ for (const node of visitSectionNodes(rootDescription)) {
50
+ if (node.nodeName[0] === "#") continue;
51
+ node.parentElement?.removeChild(node);
52
+ }
53
+ }
54
+
55
+ for (const section of vistSections(rootDescription, false)) {
56
+ if (!section.header) continue;
57
+ const [state, title] = splitHeader(section.header);
58
+ if (state)
59
+ yield {
60
+ state,
61
+ title,
62
+ ...section,
63
+ };
64
+ }
65
+ }
66
+
67
+ export function getStatesDescriptions(options: {
68
+ element: HTMLElement;
69
+ rootStateKey: string;
70
+ removeFromDom?: boolean; // If true - removes state description nodes from the DOM
71
+ splitHeader?: (
72
+ header?: HTMLElement,
73
+ ) => [stateKey: undefined | string, label: string];
74
+ }): { [state: string]: StateDescriptionSection } {
75
+ const index: { [state: string]: StateDescriptionSection } = {};
76
+ for (const description of visitStateDescriptions(options)) {
77
+ index[description.state] = description;
78
+ }
79
+ return index;
80
+ }
package/src/dom/index.ts CHANGED
@@ -1,168 +1,4 @@
1
- import { buildSections, DomSection } from "./buildSections.js";
2
-
3
- export function getStateDescriptionRenderer({
4
- element,
5
- rootStateKey,
6
- splitHeader = (
7
- header?: HTMLElement,
8
- ): [stateKey: undefined | string, label: string] => {
9
- if (!header) return [undefined, ""];
10
- const text = header.innerText.trim();
11
- let key: string | undefined;
12
- let label: string = text;
13
- text.replace(/^\s*\[(.*)\]\s*(.*)$/, (_, k, l) => {
14
- key = k.trim();
15
- label = l.trim() || key;
16
- return "";
17
- });
18
- return [key, label];
19
- },
20
- }: {
21
- element: HTMLElement;
22
- rootStateKey: string;
23
- splitHeader?: (
24
- header?: HTMLElement,
25
- ) => [stateKey: undefined | string, label: string];
26
- }) {
27
- const docSections = buildSections(element);
28
- const rootStateFilter = getStateDescriptionFilter(splitHeader, rootStateKey);
29
- const rootDescription = findSection(docSections, rootStateFilter);
30
-
31
- if (!rootDescription) {
32
- return (statesStack: string[]) => {
33
- return undefined;
34
- };
35
- }
36
-
37
- // Remove description nodes from DOM
38
- for (const node of visitSectionNodes(rootDescription)) {
39
- if (node.nodeName[0] === "#") continue;
40
- node.parentElement?.removeChild(node);
41
- }
42
-
43
- return (statesStack: string[]) => {
44
- const stateKey = [...statesStack].pop();
45
- const filter = getStateDescriptionFilter(splitHeader, stateKey);
46
- const description = findSection(rootDescription, filter);
47
- if (!description) return;
48
- const elm = renderSections({
49
- section: description,
50
- deep: (s) => !rootStateFilter(s),
51
- header: (header) => {
52
- const [key, label] = splitHeader(header);
53
- if (key === stateKey) {
54
- return label || key;
55
- }
56
- return;
57
- },
58
- });
59
- const details = elm?.querySelector("details");
60
- details && (details.open = true);
61
- return elm;
62
- };
63
-
64
- function getStateDescriptionFilter(
65
- splitHeader: (header: HTMLElement) => [stateKey?: string, label?: string],
66
- stateKey?: string,
67
- ) {
68
- if (!stateKey) return () => false;
69
- return (section: DomSection) => {
70
- if (!section.header) return false;
71
- const [key] = splitHeader(section.header);
72
- return key === stateKey;
73
- };
74
- }
75
- }
76
-
77
- export function* visitSectionNodes(section: DomSection): Iterable<Node> {
78
- if (section.header) yield section.header;
79
- yield* section.content;
80
- for (const s of vistSections(section, true)) {
81
- yield* visitSectionNodes(s);
82
- }
83
- }
84
-
85
- export function* vistSections(
86
- root: DomSection,
87
- ignoreRoot: boolean = false,
88
- ): Generator<DomSection> {
89
- !ignoreRoot && (yield root);
90
- for (const child of root.children) {
91
- yield* vistSections(child, false);
92
- }
93
- }
94
-
95
- export function findSection(
96
- section: DomSection,
97
- accept: (section: DomSection) => boolean,
98
- ) {
99
- for (const s of vistSections(section)) {
100
- if (accept(s) === true) return s;
101
- }
102
- }
103
-
104
- export function renderSections({
105
- section,
106
- deep = (s) => true,
107
- header = (headerElm) => headerElm,
108
- }: {
109
- section: DomSection;
110
- deep?: (s: DomSection) => boolean;
111
- header?: (header: HTMLElement) => undefined | string | HTMLElement;
112
- }) {
113
- const container = document.createElement("div");
114
- const id = `id-${String(Math.random()).split(".")[1]}-${Date.now()}`;
115
- container.setAttribute("id", id);
116
- if (!section) return container;
117
-
118
- const sectionView = renderSection(section);
119
- container.append(sectionView);
120
- const style = renderSectionStyles(id);
121
- container.append(style);
122
- return container;
123
-
124
- function renderSectionStyles(id?: string) {
125
- const prefix = id ? `#${id}` : "";
126
- const style = document.createElement("style");
127
- container.appendChild(style);
128
- style.innerHTML = `
129
- ${prefix} details {
130
- border: 1px solid #aaa;
131
- border-radius: 4px;
132
- padding: 0.5em 0.5em 0;
133
- margin-bottom: 0.5em;
134
- }
135
-
136
- ${prefix} details > summary {
137
- font-weight: bold;
138
- margin: -0.5em -0.5em 0;
139
- padding: 0.5em;
140
- cursor: pointer;
141
- }
142
-
143
- ${prefix} details[open] {
144
- padding: 0.5em;
145
- }
146
- `;
147
- return style;
148
- }
149
- function renderSection(section: DomSection) {
150
- const details = document.createElement("details");
151
- const summary = document.createElement("summary");
152
- details.appendChild(summary);
153
- const content = document.createElement("div");
154
- details.appendChild(content);
155
- const headerElm = header(section.header!);
156
- headerElm && summary.append(headerElm);
157
- for (const node of section.content) {
158
- if (node.nodeName[0] === "#") continue;
159
- content.append(node.cloneNode(true));
160
- }
161
- if (deep(section)) {
162
- for (const child of section.children) {
163
- content.append(renderSection(child));
164
- }
165
- }
166
- return details;
167
- }
168
- }
1
+ export * from "./buildSections.js";
2
+ export * from "./description-panels.js";
3
+ export * from "./getStatesDescriptions.js";
4
+ export * from "./newTreeBuilder.js";
@@ -42,6 +42,7 @@ export function buildStatechartSvg({
42
42
  initialStateKey?: string;
43
43
  finalStateKey?: string;
44
44
  }) {
45
+ if (!nodes.length && !edges.length) return;
45
46
  width = Math.ceil(width);
46
47
  height = Math.ceil(height);
47
48
  println(
@@ -18,29 +18,25 @@ export function buildStatechartsPanel({
18
18
 
19
19
  function render(statechart: StateChart, println: (str: string) => void) {
20
20
  println(
21
- ` <details class="state-details" data-state-id="${statechart.id}">`
21
+ ` <details class="state-details" data-state-id="${statechart.id}">`,
22
22
  );
23
23
  println(
24
- ` <summary class="state-details__label">${statechart.text ?? statechart.state}</summary>`
25
- );
26
- println(
27
- ` <div class="state-details__content">`
28
- );
29
- println(
30
- ` <div class="state-details__chart">`
24
+ ` <summary class="state-details__label">${statechart.text ?? statechart.state}</summary>`,
31
25
  );
26
+ println(` <div class="state-details__content">`);
32
27
  const printChildren = (str: string) => println(` ${str}`);
33
- buildStatechartSvg({
34
- graph: statechart,
35
- newId,
36
- println: printChildren,
37
- initialStateKey,
38
- finalStateKey,
39
- });
40
- println(` </div>`);
41
- println(
42
- ` <div class="state-details__description">`
43
- );
28
+ if (statechart.nodes.length) {
29
+ println(` <div class="state-details__chart">`);
30
+ buildStatechartSvg({
31
+ graph: statechart,
32
+ newId,
33
+ println: printChildren,
34
+ initialStateKey,
35
+ finalStateKey,
36
+ });
37
+ println(` </div>`);
38
+ }
39
+ println(` <div class="state-details__description">`);
44
40
  println(` </div>`);
45
41
  for (const child of statechart.children || []) {
46
42
  render(child, printChildren);