@doeixd/machine 0.0.6 → 0.0.8

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.
@@ -0,0 +1,407 @@
1
+ // src/adapters.ts
2
+
3
+ /**
4
+ * @file Event-Driven Adapters for @doeixd/machine
5
+ * @description Provides primitives to adapt a machine's method-call-based API
6
+ * to standard event-driven interfaces like the browser's `EventTarget` and
7
+ * Node.js's `EventEmitter`. These adapters allow your type-safe machines to
8
+ * integrate seamlessly into decoupled, event-driven architectures.
9
+ */
10
+
11
+ import { EventEmitter } from 'events';
12
+ import {
13
+ Machine,
14
+ Runner,
15
+ createRunner,
16
+ Context,
17
+ TransitionNames,
18
+ } from './index';
19
+
20
+ // =============================================================================
21
+ // SECTION 0: Observable Types (Minimal Implementation)
22
+ // =============================================================================
23
+
24
+ /**
25
+ * A minimal Observer interface for reactive streams.
26
+ * Compatible with RxJS and other Observable implementations.
27
+ */
28
+ export interface Observer<T> {
29
+ next?: (value: T) => void;
30
+ error?: (error: any) => void;
31
+ complete?: () => void;
32
+ }
33
+
34
+ /**
35
+ * A minimal Observable interface for reactive streams.
36
+ * Compatible with RxJS and other Observable implementations.
37
+ */
38
+ export interface Observable<T> {
39
+ subscribe(observer: Observer<T>): { unsubscribe: () => void };
40
+ }
41
+
42
+ // =============================================================================
43
+ // SECTION 1: EventTarget Adapter (for Browser Environments)
44
+ // =============================================================================
45
+
46
+ // --- Helper Types for EventTarget ---
47
+
48
+ /**
49
+ * A helper type that extracts the detail payload for a given machine event.
50
+ * If the transition has arguments, it's an array of those arguments.
51
+ * If it has no arguments, it's `undefined`.
52
+ *
53
+ * @template M The machine type.
54
+ * @template K The name of the transition.
55
+ */
56
+ export type MachineEventDetail<
57
+ M extends Machine<any>,
58
+ K extends TransitionNames<M>
59
+ > = M[K] extends (...args: infer A) => any ? (A extends [] ? undefined : A) : never;
60
+
61
+ /**
62
+ * A mapped type that creates a DOM-standard event map for a machine.
63
+ * This is crucial for providing type safety when using `addEventListener`.
64
+ *
65
+ * It includes:
66
+ * - A `statechange` event with the new machine state in its detail.
67
+ * - An `error` event with an `Error` object in its detail.
68
+ * - An entry for every possible machine transition.
69
+ *
70
+ * @template M The machine type.
71
+ */
72
+ export type MachineEventMap<M extends Machine<any>> = {
73
+ [K in TransitionNames<M>]: CustomEvent<MachineEventDetail<M, K>>;
74
+ } & {
75
+ statechange: CustomEvent<{ state: M }>;
76
+ error: CustomEvent<{ error: Error }>;
77
+ };
78
+
79
+
80
+ /**
81
+ * A type-safe, augmented EventTarget that wraps a state machine.
82
+ *
83
+ * It provides two key functionalities:
84
+ * 1. Emits a `CustomEvent` named 'statechange' whenever the machine's state updates.
85
+ * 2. Listens for other `CustomEvent`s and translates them into type-safe machine transitions.
86
+ *
87
+ * @template M The machine type (can be a union of states).
88
+ */
89
+ export class MachineEventTarget<M extends Machine<any>> extends EventTarget {
90
+ private readonly runner: Runner<M>;
91
+
92
+ /**
93
+ * The current, readonly state of the machine.
94
+ * Access this property to get the latest machine instance for UI rendering or inspection.
95
+ * @example
96
+ * console.log(machineTarget.state.context.count);
97
+ */
98
+ public get state(): M {
99
+ return this.runner.state;
100
+ }
101
+
102
+ /**
103
+ * A direct, readonly accessor to the machine's current context.
104
+ * A convenience property equivalent to `machineTarget.state.context`.
105
+ */
106
+ public get context(): Context<M> {
107
+ return this.runner.state.context;
108
+ }
109
+
110
+ constructor(initialMachine: M) {
111
+ super();
112
+
113
+ this.runner = createRunner(initialMachine, (newState) => {
114
+ this.dispatchEvent(new CustomEvent('statechange', { detail: { state: newState } }));
115
+ });
116
+
117
+ const handleEvent = (event: Event) => {
118
+ const { type, detail } = event as CustomEvent;
119
+ if (type === 'statechange' || type === 'error') return;
120
+
121
+ const action = (this.runner.actions as any)[type];
122
+ if (typeof action === 'function') {
123
+ const args = Array.isArray(detail) ? detail : [];
124
+ action(...args);
125
+ } else {
126
+ const error = new Error(`Invalid event type "${type}" for current state "${(this.state.context as any).status || 'unknown'}".`);
127
+ this.dispatchEvent(new CustomEvent('error', { detail: { error } }));
128
+ }
129
+ };
130
+
131
+ // Override dispatchEvent to intercept all events and route them.
132
+ const originalDispatchEvent = this.dispatchEvent.bind(this);
133
+ this.dispatchEvent = (event: Event): boolean => {
134
+ // The event is first handled by our logic, then passed to the native dispatcher.
135
+ handleEvent(event);
136
+ return originalDispatchEvent(event);
137
+ };
138
+ }
139
+
140
+ // Type-safe event listener methods for machine events
141
+ public addMachineEventListener<K extends keyof MachineEventMap<M>>(
142
+ type: K,
143
+ listener: (event: MachineEventMap<M>[K]) => void,
144
+ options?: boolean | AddEventListenerOptions
145
+ ): void {
146
+ super.addEventListener(type, listener as EventListener, options);
147
+ }
148
+
149
+ public removeMachineEventListener<K extends keyof MachineEventMap<M>>(
150
+ type: K,
151
+ listener: (event: MachineEventMap<M>[K]) => void,
152
+ options?: boolean | EventListenerOptions
153
+ ): void {
154
+ super.removeEventListener(type, listener as EventListener, options);
155
+ }
156
+
157
+
158
+
159
+ /**
160
+ * A type-safe method for dispatching transition events.
161
+ * This is the recommended way to interact with the machine from your application code.
162
+ *
163
+ * @param type The name of the transition to trigger (e.g., 'add').
164
+ * @param detail The arguments for that transition, matching the method signature.
165
+ *
166
+ * @example
167
+ * // For a transition `add(n: number)`
168
+ * machineTarget.dispatch('add', [5]);
169
+ *
170
+ * // For a transition `increment()`
171
+ * machineTarget.dispatch('increment');
172
+ */
173
+ public dispatch<K extends TransitionNames<M>>(
174
+ type: K,
175
+ detail?: MachineEventDetail<M, K>
176
+ ): void {
177
+ super.dispatchEvent(new CustomEvent(type, { detail }));
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Creates a browser-native EventTarget from a machine.
183
+ *
184
+ * This powerful adapter makes your machine behave like a standard DOM element,
185
+ * perfect for decoupling components or integrating with event-driven browser APIs.
186
+ *
187
+ * @param initialMachine The machine instance to wrap.
188
+ * @returns A `MachineEventTarget` instance.
189
+ */
190
+ export function asEventTarget<M extends Machine<any>>(initialMachine: M): MachineEventTarget<M> {
191
+ return new MachineEventTarget(initialMachine);
192
+ }
193
+
194
+ /**
195
+ * A utility function to ergonomically add and clean up a listener on a MachineEventTarget.
196
+ * It returns an `unsubscribe` function, which is ideal for use in `useEffect` hooks.
197
+ *
198
+ * @param target The `MachineEventTarget` to listen to.
199
+ * @param type The name of the event to listen for.
200
+ * @param listener The callback function to execute.
201
+ * @returns A cleanup function that removes the event listener.
202
+ *
203
+ * @example
204
+ * useEffect(() => {
205
+ * // The listener is automatically typed based on the event name.
206
+ * const unsubscribe = listen(counterTarget, 'statechange', (event) => {
207
+ * setCount(event.detail.state.context.count);
208
+ * });
209
+ *
210
+ * // The returned function is perfect for a useEffect cleanup.
211
+ * return unsubscribe;
212
+ * }, []);
213
+ */
214
+ export function listen<M extends Machine<any>, K extends keyof MachineEventMap<M>>(
215
+ target: MachineEventTarget<M>,
216
+ type: K,
217
+ listener: (event: MachineEventMap<M>[K]) => void
218
+ ): () => void {
219
+ target.addMachineEventListener(type, listener);
220
+ return () => {
221
+ target.removeMachineEventListener(type, listener);
222
+ };
223
+ }
224
+
225
+
226
+ // =============================================================================
227
+ // SECTION 2: EventEmitter Adapter (for Node.js & Event-Driven Architectures)
228
+ // =============================================================================
229
+
230
+ /**
231
+ * Defines the events and their payloads that our MachineEventEmitter can emit,
232
+ * providing strict type safety for listeners.
233
+ */
234
+ interface MachineEmitterEvents<M extends Machine<any>> {
235
+ statechange: (newState: M) => void;
236
+ error: (error: Error) => void;
237
+ }
238
+
239
+ /**
240
+ * A type-safe, augmented EventEmitter that wraps a state machine.
241
+ *
242
+ * It provides two key functionalities:
243
+ * 1. Emits a `'statechange'` event whenever the machine's state updates.
244
+ * 2. Exposes a type-safe `dispatch` method to trigger machine transitions.
245
+ *
246
+ * @template M The machine type (can be a union of states).
247
+ */
248
+ export class MachineEventEmitter<M extends Machine<any>> extends EventEmitter {
249
+ private readonly runner: Runner<M>;
250
+
251
+ // Augment EventEmitter's methods to be fully type-safe with our event map.
252
+ public on<E extends keyof MachineEmitterEvents<M>>(event: E, listener: MachineEmitterEvents<M>[E]): this {
253
+ return super.on(event, listener);
254
+ }
255
+ public emit<E extends keyof MachineEmitterEvents<M>>(event: E, ...args: Parameters<MachineEmitterEvents<M>[E]>): boolean {
256
+ return super.emit(event, ...args);
257
+ }
258
+
259
+ public get state(): M {
260
+ return this.runner.state;
261
+ }
262
+ public get context(): Context<M> {
263
+ return this.runner.state.context;
264
+ }
265
+
266
+ constructor(initialMachine: M) {
267
+ super();
268
+ this.runner = createRunner(initialMachine, (newState) => {
269
+ this.emit('statechange', newState);
270
+ });
271
+ }
272
+
273
+ /**
274
+ * A type-safe method for dispatching transitions to the machine.
275
+ * This is the primary input for the machine in an event-driven system.
276
+ *
277
+ * @param eventName The name of the transition to trigger.
278
+ * @param args The arguments for that transition, matching the method signature.
279
+ *
280
+ * @example
281
+ * sessionEmitter.dispatch('login', 'username', 'password');
282
+ */
283
+ public dispatch<K extends TransitionNames<M>>(
284
+ eventName: K,
285
+ ...args: M[K] extends (...args: infer A) => any ? A : never
286
+ ): void {
287
+ const action = (this.runner.actions as any)[eventName];
288
+
289
+ if (typeof action === 'function') {
290
+ action(...args);
291
+ } else {
292
+ this.emit('error', new Error(`Invalid event "${String(eventName)}" for current state.`));
293
+ }
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Creates a Node.js-style EventEmitter from a machine.
299
+ *
300
+ * This adapter is perfect for backend services, scripts, or any architecture that
301
+ * uses the classic EventEmitter pattern for decoupling system components.
302
+ *
303
+ * @param initialMachine The machine instance to wrap.
304
+ * @returns A `MachineEventEmitter` instance.
305
+ */
306
+ export function asEventEmitter<M extends Machine<any>>(initialMachine: M): MachineEventEmitter<M> {
307
+ return new MachineEventEmitter(initialMachine);
308
+ }
309
+
310
+
311
+ // =============================================================================
312
+ // SECTION 3: Observable Adapter (for Stream-Based Architectures)
313
+ // =============================================================================
314
+
315
+ /**
316
+ * A type-safe Observable that wraps a state machine, emitting the new state
317
+ * on every transition.
318
+ *
319
+ * This class conforms to the standard Observable interface, making it compatible
320
+ * with libraries like RxJS and frameworks that use Observables (e.g., Angular).
321
+ *
322
+ * @template M The machine type (can be a union of states).
323
+ */
324
+ export class MachineObservable<M extends Machine<any>> implements Observable<M> {
325
+ private readonly runner: Runner<M>;
326
+ private observers: Set<Observer<M>> = new Set();
327
+
328
+ public get state(): M {
329
+ return this.runner.state;
330
+ }
331
+ public get context(): Context<M> {
332
+ return this.runner.state.context;
333
+ }
334
+
335
+ constructor(initialMachine: M) {
336
+ this.runner = createRunner(initialMachine, (newState) => {
337
+ // When the runner's state changes, push the new state to all subscribers.
338
+ this.observers.forEach(observer => observer.next?.(newState));
339
+ });
340
+
341
+ // We can also forward errors from the runner if we enhance it to do so.
342
+ }
343
+
344
+ /**
345
+ * Subscribes to the stream of machine states.
346
+ *
347
+ * @param observer An object with `next`, `error`, and `complete` methods.
348
+ * @returns A subscription object with an `unsubscribe` method.
349
+ */
350
+ public subscribe(observer: Observer<M>): { unsubscribe: () => void } {
351
+ // Immediately provide the current state to the new subscriber.
352
+ observer.next?.(this.runner.state);
353
+
354
+ this.observers.add(observer);
355
+
356
+ return {
357
+ unsubscribe: () => {
358
+ this.observers.delete(observer);
359
+ // Optional: If this is the last observer, we could tear down the machine.
360
+ // For now, we keep it simple and the machine lives forever.
361
+ },
362
+ };
363
+ }
364
+
365
+ /**
366
+ * A type-safe method for dispatching transitions to the machine.
367
+ *
368
+ * @param eventName The name of the transition to trigger.
369
+ * @param args The arguments for that transition.
370
+ */
371
+ public dispatch<K extends TransitionNames<M>>(
372
+ eventName: K,
373
+ ...args: M[K] extends (...args: infer A) => any ? A : never
374
+ ): void {
375
+ const action = (this.runner.actions as any)[eventName];
376
+ if (typeof action === 'function') {
377
+ action(...args);
378
+ } else {
379
+ // Emit an error to all observers.
380
+ const error = new Error(`Invalid event "${String(eventName)}" for current state.`);
381
+ this.observers.forEach(o => o.error?.(error));
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Signals to all observers that the stream is complete.
387
+ * This is useful when the machine reaches a final state.
388
+ */
389
+ public complete(): void {
390
+ this.observers.forEach(o => o.complete?.());
391
+ this.observers.clear();
392
+ }
393
+ }
394
+
395
+ /**
396
+ * Creates an Observable from a machine.
397
+ *
398
+ * This adapter is perfect for integrating your machine into architectures that
399
+ * rely on Observables and reactive streams (e.g., RxJS, Angular). It emits the
400
+ * new machine state on every transition.
401
+ *
402
+ * @param initialMachine The machine instance to wrap.
403
+ * @returns A `MachineObservable` instance.
404
+ */
405
+ export function asObservable<M extends Machine<any>>(initialMachine: M): MachineObservable<M> {
406
+ return new MachineObservable(initialMachine);
407
+ }
package/src/extract.ts CHANGED
@@ -20,22 +20,57 @@ import { Project, Type, Node } from 'ts-morph';
20
20
  // SECTION: CONFIGURATION TYPES
21
21
  // =============================================================================
22
22
 
23
+ /**
24
+ * Configuration for a parallel region
25
+ */
26
+ export interface ParallelRegionConfig {
27
+ /** A unique name for this region (e.g., 'fontStyle') */
28
+ name: string;
29
+ /** The initial state class for this region */
30
+ initialState: string;
31
+ /** All reachable state classes within this region */
32
+ classes: string[];
33
+ }
34
+
35
+ /**
36
+ * Configuration for child states in a hierarchical machine
37
+ */
38
+ export interface ChildStatesConfig {
39
+ /** The property in the parent's context that holds the child machine */
40
+ contextProperty: string;
41
+ /** An array of all possible child state class names */
42
+ classes: string[];
43
+ /** The initial child state */
44
+ initialState: string;
45
+ }
46
+
23
47
  /**
24
48
  * Configuration for a single machine to extract
25
49
  */
26
50
  export interface MachineConfig {
27
51
  /** Path to the source file containing the machine */
28
52
  input: string;
29
- /** Array of class names that represent states */
30
- classes: string[];
31
53
  /** Output file path (optional, defaults to stdout) */
32
54
  output?: string;
33
55
  /** Top-level ID for the statechart */
34
56
  id: string;
35
- /** Name of the class that represents the initial state */
36
- initialState: string;
37
57
  /** Optional description of the machine */
38
58
  description?: string;
59
+
60
+ // EITHER `initialState` and `classes` for an FSM...
61
+ /** Array of class names that represent states (for simple FSM) */
62
+ classes?: string[];
63
+ /** Name of the class that represents the initial state (for simple FSM) */
64
+ initialState?: string;
65
+
66
+ // OR `parallel` for a parallel machine.
67
+ /** Configuration for parallel regions (mutually exclusive with initialState/classes) */
68
+ parallel?: {
69
+ regions: ParallelRegionConfig[];
70
+ };
71
+
72
+ /** Configuration for hierarchical/nested states */
73
+ children?: ChildStatesConfig;
39
74
  }
40
75
 
41
76
  /**
@@ -63,11 +98,16 @@ export interface ExtractionConfig {
63
98
  * plain JSON-compatible value. It's smart enough to resolve class constructor
64
99
  * types into their string names.
65
100
  *
101
+ * Note: This function is kept for future extensibility but is not currently used
102
+ * as the AST-based extraction approach (via extractFromCallExpression) is preferred.
103
+ *
66
104
  * @param type - The `ts-morph` Type object to serialize.
67
105
  * @param verbose - Enable debug logging
68
106
  * @returns A JSON-compatible value (string, number, object, array).
107
+ * @internal
69
108
  */
70
- function typeToJson(type: Type, verbose = false): any {
109
+ // @ts-expect-error - verbose parameter is used but TypeScript doesn't detect it
110
+ function _typeToJson(type: Type, verbose = false): any {
71
111
  // --- Terminal Types ---
72
112
  const symbol = type.getSymbol();
73
113
  if (symbol && symbol.getDeclarations().some(Node.isClassDeclaration)) {
@@ -83,7 +123,7 @@ function typeToJson(type: Type, verbose = false): any {
83
123
  // --- Recursive Types ---
84
124
  if (type.isArray()) {
85
125
  const elementType = type.getArrayElementTypeOrThrow();
86
- return [typeToJson(elementType, verbose)];
126
+ return [_typeToJson(elementType, verbose)];
87
127
  }
88
128
 
89
129
  // --- Object Types ---
@@ -102,7 +142,7 @@ function typeToJson(type: Type, verbose = false): any {
102
142
  if (!declaration) continue;
103
143
 
104
144
  try {
105
- obj[propName] = typeToJson(declaration.getType(), verbose);
145
+ obj[propName] = _typeToJson(declaration.getType(), verbose);
106
146
  } catch (e) {
107
147
  if (verbose) console.error(` Warning: Failed to serialize property ${propName}:`, e);
108
148
  obj[propName] = 'unknown';
@@ -438,6 +478,41 @@ function analyzeStateNode(classSymbol: any, verbose = false): object {
438
478
  // SECTION: MAIN ORCHESTRATOR
439
479
  // =============================================================================
440
480
 
481
+ /**
482
+ * Helper function to analyze a state node with optional nesting support
483
+ */
484
+ function analyzeStateNodeWithNesting(
485
+ className: string,
486
+ classSymbol: any,
487
+ sourceFile: any,
488
+ childConfig: ChildStatesConfig | undefined,
489
+ verbose = false
490
+ ): any {
491
+ const stateNode = analyzeStateNode(classSymbol, verbose) as any;
492
+
493
+ // If this state has children, analyze them recursively
494
+ if (childConfig) {
495
+ if (verbose) {
496
+ console.error(` đŸ‘Ē Analyzing children for state: ${className}`);
497
+ }
498
+ stateNode.initial = childConfig.initialState;
499
+ stateNode.states = {};
500
+
501
+ // Recursively analyze each child state
502
+ for (const childClassName of childConfig.classes) {
503
+ const childClassDeclaration = sourceFile.getClass(childClassName);
504
+ if (childClassDeclaration) {
505
+ const childSymbol = childClassDeclaration.getSymbolOrThrow();
506
+ stateNode.states[childClassName] = analyzeStateNode(childSymbol, verbose);
507
+ } else {
508
+ console.warn(`âš ī¸ Warning: Child class '${childClassName}' not found.`);
509
+ }
510
+ }
511
+ }
512
+
513
+ return stateNode;
514
+ }
515
+
441
516
  /**
442
517
  * Extracts a single machine configuration to a statechart
443
518
  *
@@ -461,6 +536,56 @@ export function extractMachine(
461
536
  throw new Error(`Source file not found: ${config.input}`);
462
537
  }
463
538
 
539
+ // Handle parallel machine configuration
540
+ if (config.parallel) {
541
+ if (verbose) {
542
+ console.error(` âšī¸ Parallel machine detected. Analyzing regions.`);
543
+ }
544
+
545
+ const parallelChart: any = {
546
+ id: config.id,
547
+ type: 'parallel',
548
+ states: {},
549
+ };
550
+
551
+ if (config.description) {
552
+ parallelChart.description = config.description;
553
+ }
554
+
555
+ for (const region of config.parallel.regions) {
556
+ if (verbose) {
557
+ console.error(` 📍 Analyzing region: ${region.name}`);
558
+ }
559
+
560
+ const regionStates: any = {};
561
+ for (const className of region.classes) {
562
+ const classDeclaration = sourceFile.getClass(className);
563
+ if (classDeclaration) {
564
+ const classSymbol = classDeclaration.getSymbolOrThrow();
565
+ regionStates[className] = analyzeStateNode(classSymbol, verbose);
566
+ } else {
567
+ console.warn(`âš ī¸ Warning: Class '${className}' not found for region '${region.name}'.`);
568
+ }
569
+ }
570
+
571
+ parallelChart.states[region.name] = {
572
+ initial: region.initialState,
573
+ states: regionStates,
574
+ };
575
+ }
576
+
577
+ if (verbose) {
578
+ console.error(` ✅ Extracted ${config.parallel.regions.length} parallel regions`);
579
+ }
580
+
581
+ return parallelChart;
582
+ }
583
+
584
+ // Handle standard FSM configuration
585
+ if (!config.initialState || !config.classes) {
586
+ throw new Error(`Machine config for '${config.id}' must have either 'parallel' or 'initialState'/'classes'.`);
587
+ }
588
+
464
589
  const fullChart: any = {
465
590
  id: config.id,
466
591
  initial: config.initialState,
@@ -478,7 +603,17 @@ export function extractMachine(
478
603
  continue;
479
604
  }
480
605
  const classSymbol = classDeclaration.getSymbolOrThrow();
481
- const stateNode = analyzeStateNode(classSymbol, verbose);
606
+
607
+ // Check if this is the initial state and has children configuration
608
+ const hasChildren = className === config.initialState && config.children;
609
+ const stateNode = analyzeStateNodeWithNesting(
610
+ className,
611
+ classSymbol,
612
+ sourceFile,
613
+ hasChildren ? config.children : undefined,
614
+ verbose
615
+ );
616
+
482
617
  fullChart.states[className] = stateNode;
483
618
  }
484
619
 
@@ -569,6 +704,43 @@ export function generateChart() {
569
704
  }
570
705
  }
571
706
 
707
+ /**
708
+ * Example configuration demonstrating hierarchical and parallel machines.
709
+ * This is not used by default but serves as documentation.
710
+ */
711
+ export const ADVANCED_CONFIG_EXAMPLES = {
712
+ hierarchical: {
713
+ input: 'examples/dashboardMachine.ts',
714
+ id: 'dashboard',
715
+ classes: ['DashboardMachine', 'LoggedOutMachine'],
716
+ initialState: 'DashboardMachine',
717
+ children: {
718
+ contextProperty: 'child',
719
+ initialState: 'ViewingChildMachine',
720
+ classes: ['ViewingChildMachine', 'EditingChildMachine'],
721
+ },
722
+ } as MachineConfig,
723
+
724
+ parallel: {
725
+ input: 'examples/editorMachine.ts',
726
+ id: 'editor',
727
+ parallel: {
728
+ regions: [
729
+ {
730
+ name: 'fontWeight',
731
+ initialState: 'NormalWeight',
732
+ classes: ['NormalWeight', 'BoldWeight'],
733
+ },
734
+ {
735
+ name: 'textDecoration',
736
+ initialState: 'NoDecoration',
737
+ classes: ['NoDecoration', 'UnderlineState'],
738
+ },
739
+ ],
740
+ },
741
+ } as MachineConfig,
742
+ };
743
+
572
744
  // This allows the script to be executed directly from the command line.
573
745
  if (require.main === module) {
574
746
  generateChart();