├── LICENSE ├── README.md ├── composites └── mermax.js ├── docs └── mermax.md ├── mermex-cli.js ├── package.json ├── src ├── origins │ └── mermaid.js └── targets │ └── xstate.js └── test ├── __snapshots__ └── snap.test.js.snap ├── mmd ├── bug.lone-final-outside.mmd ├── children.mmd ├── concurency-xstate.mmd ├── concurency.mmd ├── fix-events.mmd ├── mermaid.mmd ├── nested.mmd └── newmachine.mmd ├── paraell.stately.js ├── process-file.bun.js └── snap.test.js /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Mermex 4 | 5 | Instantly* transform your [MermaidJS](https://mermaid.js.org/syntax/stateDiagram.html) into a Finite State Machine / Statechart into actual code you can run in [XState](https://stately.ai/docs/xstate)! Save time over hand-coding both seperately. 6 | 7 | *OK a bit of a strech here; not 'instant', you do have to run this script, taking a few ms of your time. & the output is missing functions, might have some bugs, but gets you 70% of the way there! 8 | 9 | ## Example 10 | 11 | 13 | 14 |
15 | Click to show text example. 16 | 17 | ### 1: Mermaid State Diagram 18 | 19 | Similar to Markdown, MermaidJS is a simple text format for basic diagrams. 20 | 21 | Start & end points are flagged with `[*]`, `%%` preceed comments, and colins `:` flag an event (named transition). 22 | 23 | Below is a simple example from [MermaidJS docs](https://mermaid.js.org/syntax/stateDiagram.html), used as a source. 24 | 25 | ```go ! mermaid.mmd 26 | stateDiagram-v2 27 | id mermaidJS state diagram 28 | %% inspiration: https://mermaid.js.org/syntax/stateDiagram.html 29 | [*] --> Still 30 | Still --> [*] 31 | Still --> Moving 32 | Moving --> Still : STOP %% named transition / event 33 | Moving --> Crash 34 | Crash --> [*] 35 | ``` 36 | 37 | ### 2: Object State Machine 38 | 39 | Mermex takes the MermaidJS text file, then translates it into an "Object State Machine", an intermediate notation that is not meant to consumed directly (though still human readable). 40 | 41 | If there are no specified named transitions (events) in the Mermaid source, then programmically one is assigned with `[source]--[target]` nominclature. 42 | 43 | ```json ! mermaid.osm.json 44 | { 45 | "final": [ 46 | "Still", 47 | "Crash", 48 | ], 49 | "id": "mermaidJS state diagram", 50 | "initial": "Still", 51 | "isConcurrent": false, 52 | "states": { 53 | "Still": { 54 | "Still--FINIS": "FINIS", 55 | "Still--Moving": "Moving", 56 | }, 57 | "Moving": { 58 | "STOP": "Still", 59 | "Moving--Crash": "Crash", 60 | }, 61 | "Crash": { 62 | "Crash--FINIS": "FINIS", 63 | }, 64 | "FINIS": "final", 65 | }, 66 | } 67 | ``` 68 | 69 | ### 3: XState JavaScript 70 | 71 | Finally, the OSM has been transformed into executable code for [XState](stately.ai). 72 | 73 | Note: to create a "final" state for states that have more than one event, a state named "FINIS" is created to be assigned `type: "final"` 74 | 75 | ```js ! mermaid.xstate.js 76 | import { createMachine } from "xstate"; 77 | 78 | export const machinemermaidJSstatediagram = createMachine({ 79 | id: "mermaidJS state diagram", 80 | initial: "Still", 81 | states: { 82 | Still: { on: { 83 | "Still--FINIS": { 84 | target: "FINIS" 85 | }, 86 | "Still--Moving": { 87 | target: "Moving" 88 | } } }, 89 | Moving: { on: { 90 | STOP: { 91 | target: "Still" 92 | }, 93 | "Moving--Crash": { 94 | target: "Crash" 95 | } } }, 96 | Crash: { on: { 97 | "Crash--FINIS": { 98 | target: "FINIS" 99 | } } }, 100 | FINIS: { 101 | type: "final" 102 | } 103 | } }); 104 | ``` 105 | 106 |
107 | 108 | 109 | ## Usage 110 | 111 | Clone/download this repo, then try out the examples MermaidJS in `test/mmd`. You can use either [NodeJS](https://nodejs.org/) or [Bun](https://bun.sh/) to run `mermex-cli.js` 112 | ``` 113 | ❯ bun run mermex-cli.js 114 | Please input path & file for mermax to process: 115 | ? test/mmd/mermaid.mmd 116 | Please enter output path (empty/malformed = input path): 117 | ? test/xstate 118 | creating folder: test/xstate 119 | test/xstate/mermaid.machine.js 💾 written to file 120 | ``` 121 | *NOTE*: the CLI is a helper & example on how to use the files in the `/src` folder. If you want to use them in your own project, copy the contents of `/src` into your own project, then 122 | 123 | ```js 124 | import { mermaidToObject } from "YOURPATH/src/origins/mermaid.js" 125 | import { xstateObjToxstateJS } from "YOURPATH/src/targets/xstate.js" 126 | ... 127 | const xstateProgram = xstateObjToxstateJS( mermaidToObject(mermaidText) ) 128 | ``` 129 | 130 | ## Supported Features 131 | 132 | ### [MermaidJS state diagram](https://mermaid.js.org/syntax/stateDiagram.html) as source 133 | 134 | Supported: 135 | + Composite states (also called "children") 136 | + named transitions (also called "events"; without names the default is `SOURCE--DESTINATION`) 137 | + comments, both inline & End Of Line 138 | + Concurrency ("parallel states") 139 | + Start ("initial") and End ("type: final", may need `FINIS` state) 140 | 141 | NOT Supported: 142 | - `<>`, workaround: use `, if: guardName` (note comma) 143 | - `<>` (use `if` conditions) 144 | - `<> `(simply target the same state) 145 | - anything GUI; note, direction, classDefs 146 | 147 | ### XState as target 148 | 149 | Supported: 150 | + FSM, much of Statecharts 151 | + Events and transitions 152 | + Initial & Final (FINIS) states 153 | + Parent/Children states 154 | + Parallel states, including onDone transition 155 | 156 | NOT Supported (yet) 157 | - Context 158 | - functions for actions 159 | 160 | ### Maybe-future 161 | 162 | "State Machine Rosetta Stone", multiple source & desination formats 163 | 164 | 165 | ## Tech notes 166 | 167 | * 0 (no) Dependencies for mermex, only for CLI file I/O, & testing 168 | * `bun test` to test against snapshots, `bun test --update-snapshots` if you change behavour 169 | -------------------------------------------------------------------------------- /composites/mermax.js: -------------------------------------------------------------------------------- 1 | import { mermaidToObject } from "../src/origins/mermaid.js" 2 | import { xstateObjToxstateJS } from "../src/targets/xstate.js" 3 | 4 | export function mermax(mermaidText){ 5 | return xstateObjToxstateJS( mermaidToObject(mermaidText) ) 6 | } 7 | -------------------------------------------------------------------------------- /docs/mermax.md: -------------------------------------------------------------------------------- 1 | 2 | Mermax takes a [MermaidJS](https://mermaid.js.org/syntax/stateDiagram.html) 'text' diagram, then transforms it into JavaScript code for a Finite State Machine / Statechart runable in [XState](https://stately.ai/docs/xstate). Save time over hand-coding both seperately. 3 | 4 | 5 | ## Transformation Visualization: 6 | 7 | 8 | 9 | ## Concurent/Parallel States 10 | 11 | ##### MermaidJS 12 | 13 | ```md 14 | stateDiagram-v2 15 | id coffee 16 | %% https://stately.ai/docs/parallel-states#parallel-ondone-transition 17 | [*] --> preparing 18 | 19 | state preparing { 20 | id grindBeans 21 | [*] --> grindingBeans 22 | grindingBeans --> beansGround : BEANS_GROUND 23 | beansGround --> [*] 24 | -- 25 | id boilWater 26 | [*] --> boilingWater 27 | boilingWater --> waterBoiled : WATER_BOILED 28 | waterBoiled --> [*] 29 | } 30 | preparing --> noCoffee : onDone %%; if: error 31 | preparing --> makingCoffee : onDone 32 | noCoffee 33 | makingCoffee 34 | ``` 35 | 36 | ###### XState 37 | ```ts 38 | import { createMachine } from "xstate"; 39 | 40 | export const machinecoffee = createMachine({ 41 | id: "coffee", 42 | initial: "preparing", 43 | states: { 44 | preparing: { 45 | id: "", 46 | type: "parallel", 47 | onDone: [ 48 | { 49 | target: "noCoffee", 50 | guard: { 51 | type: "error" 52 | } 53 | }, 54 | { 55 | target: "makingCoffee" 56 | } 57 | ], 58 | states: { 59 | grindBeans: { 60 | id: "grindBeans", 61 | initial: "grindingBeans", 62 | states: { 63 | grindingBeans: { 64 | on: { 65 | BEANS_GROUND: { 66 | target: "beansGround" 67 | } 68 | } 69 | }, 70 | beansGround: { 71 | type: "final" 72 | } 73 | } 74 | }, 75 | boilWater: { 76 | id: "boilWater", 77 | initial: "boilingWater", 78 | states: { 79 | boilingWater: { 80 | on: { 81 | WATER_BOILED: { 82 | target: "waterBoiled" 83 | } 84 | } 85 | }, 86 | waterBoiled: { 87 | type: "final" 88 | } 89 | } 90 | } 91 | } 92 | }, 93 | noCoffee: {}, 94 | makingCoffee: {} 95 | } 96 | }, 97 | { 98 | actions: { 99 | }, 100 | actors: {}, 101 | guards: { 102 | error: ({ context, event }, params) => { 103 | return false; 104 | }, 105 | }, 106 | delays: {}, 107 | } 108 | ); 109 | ``` -------------------------------------------------------------------------------- /mermex-cli.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs' 2 | import * as readline from 'node:readline/promises'; 3 | import path from "node:path" 4 | import { argv, stdin as input, stdout as output } from 'node:process' 5 | import { mermaidToObject } from "./src/origins/mermaid.js" 6 | import { xstateObjToxstateJS } from "./src/targets/xstate.js" 7 | 8 | const rl = readline.createInterface({ input, output }) 9 | // https://regexr.com/80sgs 10 | const reFilePath = /^(((?:\.{0,2}\/)?(?:\.?\w+\/)*)([\w.+_-]+(\.?\w+)))$/ 11 | // https://regex101.com/r/kQpzzt/1 12 | const reFileName = /(\/?((?[\w._-]*)*))*/ 13 | 14 | let cliFilePath, outputPath = '' 15 | if (!argv[2] || !reFilePath.test(argv[2])){ 16 | cliFilePath = await rl.question('Please input path & file for mermax to process:\n? ') 17 | if (!reFilePath.test(cliFilePath)){ 18 | throw new TypeError("Needs well-formed input file path.") 19 | } 20 | outputPath = await rl.question('Please enter output path (empty/malformed = input path):\n? ') 21 | } else { 22 | cliFilePath = argv[2] 23 | } 24 | const inputPathName = cliFilePath.replace(/.mmd$/, '') 25 | const inputFileName = inputPathName +'.mmd' 26 | console.log('reading: ', inputFileName) 27 | const mermaidText = fs.readFileSync(inputFileName,'utf-8') 28 | 29 | const { filename } = reFileName.exec(inputPathName).groups 30 | let outputPathName = inputPathName 31 | if (argv[3] && reFilePath.test(argv[3])) { 32 | outputPath = argv[3] 33 | } else if (!outputPath || !reFilePath.test(outputPath)){ 34 | outputPath = inputPathName.substring(0, (inputPathName.length - filename.length -1) ) 35 | } 36 | outputPathName = outputPath +'/'+ filename 37 | console.log('outputPath',outputPath) 38 | if (!fs.existsSync(outputPath)){ 39 | console.log('creating folder:', outputPath) 40 | fs.mkdirSync(outputPath); 41 | } 42 | 43 | // // takes raw mermaidJS state diagram & translates into transitory object 44 | // const objectFSM = mermaidToObject(mermaidText) 45 | // const jsonFileName = outputPathName +'.json' 46 | // fs.writeFileSync(jsonFileName, JSON.stringify( objectFSM, null, 2), (err) => { 47 | // if (err) throw err; 48 | // }); 49 | // console.log(jsonFileName +' 💾 written to file') 50 | 51 | // takes raw mermaidJS state diagram & translates into XState machine configuration 52 | const jsFileName = outputPathName +'.machine.js' 53 | fs.writeFileSync(jsFileName, xstateObjToxstateJS( mermaidToObject(mermaidText)), (err) => { 54 | if (err) throw err; 55 | }); 56 | console.log(jsFileName +' 💾 written to file') 57 | process.exit() // hack to fix some weird readline bug in Bun 58 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mermex", 3 | "version": "0.1.0", 4 | "contributors": [ 5 | "Tom Byrer (https://github.com/tomByrer)" 6 | ], 7 | "license": "AGPL-3.0-only", 8 | "type": "module", 9 | "scripts": { 10 | "cli": "bun run mermex-cli.js", 11 | "test": "bun test", 12 | "test-update": "bun test --update-snapshots" 13 | }, 14 | "devDependencies": {}, 15 | "peerDependencies": {} 16 | } -------------------------------------------------------------------------------- /src/origins/mermaid.js: -------------------------------------------------------------------------------- 1 | /* 2 | read text markup Mermaid.js state, output object for FSM/behavor tree production 3 | mermaidToObject() = main() 4 | */ 5 | //fix null in final:() 6 | 7 | // https://gist.github.com/ahtcx/0cd94e62691f539160b32ecda18af3d6?permalink_comment_id=3889214#gistcomment-3889214 8 | function merge(source, target) { 9 | for (const [key, val] of Object.entries(source)) { 10 | if (val !== null && typeof val === `object`) { 11 | target[key] ??=new val.__proto__.constructor(); 12 | merge(val, target[key]); 13 | } else { 14 | switch (key) { 15 | case 'id': 16 | target.id = target?.id || source?.id //todo warn if overwrite with 2nd initial 17 | break 18 | case 'initial': 19 | target.initial = target?.initial || source?.initial //todo warn if overwrite with 2nd initial 20 | break 21 | case 'final': 22 | (source.final && target.final) ? target.final.concat(source.final) : target?.final ?? source.final 23 | default: 24 | target[key] = val 25 | } 26 | } 27 | } 28 | return target; // replace in-situ; more for chaining than anything else 29 | } 30 | function mergetLevel(temp, target, breadcrumbLevel=[]){ // temp into target 31 | // if (breadcrumbLevel.length){ //todo needed? 32 | for (let i=breadcrumbLevel.length-1; i>-1; i--) { 33 | //FIXME 34 | const tName = (breadcrumbLevel[i].startsWith('=Parallel') && !(temp?.id === "")) 35 | ? temp.id 36 | : breadcrumbLevel[i] 37 | // const tName = breadcrumbLevel[i] 38 | temp = { [tName]: temp } 39 | temp = { states: temp } 40 | //console.log('mergeLevel', i, breadcrumbLevel[i], '~~', JSON.stringify(temp, null, 2)); 41 | } 42 | // } 43 | 44 | return { 45 | id: target?.id || temp?.id, //todo warn if overwrite with 2nd initial 46 | initial: target?.initial || temp?.initial, //todo warn if overwrite with 2nd initial 47 | final: (temp.final && target.final) ? target.final.concat(temp.final) : target?.final ?? temp.final, 48 | isConcurrent: target.isConcurrent || temp.isConcurrent, 49 | states: (target?.states && temp?.states) ? merge(temp.states, target.states) : target?.states || temp.states, 50 | } 51 | 52 | } 53 | 54 | /* 55 | adds 'final' states if needed 56 | { 57 | machine: { initial: "", states: (), final: []}, 58 | stateCounts: [ stateName: [int, int],...] 59 | } 60 | */ 61 | function finalizeLevel(obj){ 62 | //console.log() 63 | //console.log(`finalizeLevel`, obj) 64 | const OMFinal = obj.machine.final 65 | if (OMFinal === undefined || OMFinal.length === 0) { 66 | // noop 67 | } 68 | // true single FINAL, without guards, multiple inlets, etc 69 | else if ( OMFinal.length == 1 && !(obj.machine['states'][OMFinal[0]])) { 70 | //console.log('final1: ', OMFinal[0]) 71 | // if ( !(OMFinal[0] in obj.stateCounts) ) { 72 | obj.machine['states'][OMFinal[0]] = "final" 73 | // } 74 | //fixme ? multi final, but all edd-ends? 75 | } 76 | // multi final, so make a true final state 77 | else { 78 | //fix: check if there is transistion name 79 | for (const f of OMFinal) { 80 | if (!obj.machine.states[f]) { obj.machine.states[f] = {} } 81 | const e = f +'--FINIS' 82 | obj.machine.states[f][e] = 'FINIS' 83 | } 84 | obj.machine.states.FINIS = "final" 85 | } 86 | 87 | return obj.machine 88 | } 89 | function finalize(machine, stateCounts){ 90 | // 1st root-parent level, which ignores children 91 | let stateMachineObjectModel = finalizeLevel({machine, stateCounts}) 92 | 93 | for (const [key, value] of Object.entries(machine.states)) { 94 | // isChild? 95 | if (value?.states) { // has child 96 | finalize(value, stateCounts) 97 | } 98 | } 99 | return stateMachineObjectModel 100 | } 101 | 102 | /** 103 | * Transforms MermaidJS state diagram into Object State Machine. 104 | * 105 | * (Output is meant to be transformed into other target code.) 106 | * 107 | * @param {string} mermaidText .MermaidJS text file. 108 | * 109 | * @return {Object} Object State Machine. 110 | */ 111 | export function mermaidToObject(mermaidText) { 112 | // https://regex101.com/r/3WMccA/16 by me; same copyright 113 | const reMDFSM = /(?:(?:"?(?\w+(?:[ -_]\w+)*)"?|(?\[\*\]))(?: --> )(?:"?(?\w+(?:[ -_]\w+)*)"?(?: : (?\w+|("\w+([ -_]\w+)*)"))?(?: %%; (?.+))?|(?\[\*\])))|(?}|--)|(state "?(?\w+(?:[ -_]\w+)*)"? {)|(?:(?\w+(?:[ -_]\w+)*))|(?stateDiagram-v2)|%%\s*(?.+)|"?(?\w+(?:[ -_]\w+)*)"?/gm 114 | const matches = [...mermaidText.matchAll(reMDFSM)] 115 | 116 | const initLevel = { id: "", initial:"", final:[], isConcurrent: false, states:{}, } // final = sources of endings 117 | let result = structuredClone(initLevel) 118 | //TODO error mulitple initial states 119 | let tLevel = structuredClone(initLevel) // temp for current level (depth of parent/child) of recursive depth 120 | // tracking times a state has outbound events (1st int) or inbound (2nd int) 121 | // needed to prove a final state is truly final 122 | let stateCounts = {} // stateName: [int, int] // NOT counting init or final, [out bound, in bound] 123 | let compositeList = [] // tracks all children machine names 124 | let breadcrumbLevel = [] // tracks child machine branch level, empty = root/main 125 | ////console.log('matches.', matches) //! long 126 | let isConcurrent = false 127 | for (let i=0; i there` 178 | if (G.source && G.target) { 179 | if (!e) { e = G.source +`--`+ G.target} 180 | if (G.msc) ( e += `~~`+ G.msc ) 181 | if (!tLevel.states[G.source]) { tLevel.states[G.source] = {} } 182 | tLevel.states[G.source][e] = G.target 183 | 184 | if (!stateCounts[G.source]) { stateCounts[G.source] = [0, 0] } 185 | stateCounts[G.source][0] += 1 186 | if (!stateCounts[G.target]) { stateCounts[G.target] = [0, 0] } 187 | stateCounts[G.target][1] += 1 188 | //console.log(G.source, '-->', G.target, stateCounts, tLevel.states) 189 | } 190 | // `ending --> [*]` 191 | else if (G.source && G.end) { 192 | tLevel.final.push(G.source) 193 | //console.log('G.end', breadcrumbLevel, tLevel) 194 | } 195 | // `[*] --> firstState` 196 | else if (G.start && G.target) { 197 | tLevel.initial = G.target 198 | //console.log('start', tLevel) 199 | } 200 | // XState likes an ID value for all machines; helps with targeting 201 | else if (G.id) { 202 | tLevel.id = G.id //todo check for dupes 203 | } 204 | // state that has no events nor targets nor is "end state" 205 | else if (G.leaf) { 206 | if (!tLevel.states[G.leaf]) { tLevel.states[G.leaf] = null } 207 | } 208 | // } (closes nested state), -- (concurrent/paralel child ) 209 | else if (G.compositeEnd){ 210 | //console.log(`G.compositeEnd`, breadcrumbLevel, isConcurrent, tLevel) 211 | 212 | if (G.compositeEnd === '}'){ 213 | if (isConcurrent){ 214 | newChildLevel(nameParallel(i)) 215 | breadcrumbLevel.pop() 216 | breadcrumbLevel.pop() 217 | } 218 | else { 219 | result = mergetLevel(tLevel, result, breadcrumbLevel) 220 | breadcrumbLevel.pop() //keep for pop() 221 | tLevel = structuredClone(initLevel) 222 | } 223 | } 224 | else { // (G.compositeEnd === '--'){ 225 | // 2 IDs, & 2 IDs in a row? 226 | 227 | if (isConcurrent){ 228 | breadcrumbLevel.pop() 229 | } 230 | else { 231 | // branch the concurrent state 232 | let concurrentLevel = structuredClone(initLevel) 233 | concurrentLevel.isConcurrent = true 234 | result = mergetLevel(concurrentLevel, result, breadcrumbLevel) 235 | } 236 | breadcrumbLevel.push(nameParallel(i)) 237 | compositeList.push(nameParallel(i)) 238 | result = mergetLevel(tLevel, result, breadcrumbLevel) 239 | breadcrumbLevel.pop() 240 | ////console.log('--- bcL:', breadcrumbLevel) 241 | breadcrumbLevel.push(nameParallel(i+1)) 242 | compositeList.push(nameParallel(i+1)) 243 | tLevel = structuredClone(initLevel) 244 | isConcurrent = true 245 | } 246 | // else if (isConcurrent && G.compositeEnd === '--'){ NoOp } 247 | 248 | //console.log(`G.compositeEnd post`, breadcrumbLevel, isConcurrent, tLevel) 249 | } 250 | // `state Child {` ~ starting a new state-group 251 | // break out of else chain; might be created by isConcurrent 252 | else if (G.composite) { 253 | newChildLevel(G.composite) //todo test me 254 | } 255 | // else if (G.msc) { 256 | // // logNote += ` 257 | // // 'msc' not implemented yet. 258 | // // ` 259 | // } 260 | // else { 261 | // // logNote += ` 262 | // // no match` 263 | // } 264 | } 265 | 266 | result = mergetLevel(tLevel, result) // save the last root-level progress 267 | 268 | return finalize(result, stateCounts) 269 | } 270 | -------------------------------------------------------------------------------- /src/targets/xstate.js: -------------------------------------------------------------------------------- 1 | /* 2 | Take State Machine Object Notation & transpile to XState JavaScript 3 | fsmObjToXstateObj() = main() 4 | */ 5 | 6 | /** 7 | * Transforms Object State Machine into XState JavaScript file. 8 | * 9 | * (Output may need additional hand coding for fucntions.) 10 | * 11 | * @param {Object} Object State Machine. 12 | * 13 | * @return {string} XState JavaScript file. 14 | */ 15 | export function xstateObjToxstateJS(obj, options={typescript: false,}){ 16 | let global = { 17 | events: new Set(), // for types 18 | actions: new Set(), 19 | actors: {}, 20 | delays: {}, 21 | guards: new Set(), 22 | } 23 | 24 | function transformState(input) { 25 | // used for FINIS states; where a multi-event state also has final state 26 | if (input==='final'){ 27 | return { 28 | type: 'final' 29 | } 30 | } 31 | 32 | // regex101.com/r/i5jNuU/8 33 | const mscRE = /^(?:"?(?\w+(?: \w+)*(?:--\w+(?: \w+)*)?)"?)?(?:~~(?:if:\s?"?(?\w+(?: \w+)*)"?)?(?: , )?(?:type:\s?(?"?\w+(?: \w+)*"?))?(?: , )?(?:action:\s?(?"?\w+(?: \w+)*"?))?)?/ 34 | let result = {} 35 | let eventNames = [] // temp tracker for duplicates 36 | for ( 37 | let i=0, 38 | keys = Object.keys(input); 39 | i{ 92 | const value = input.states[key] 93 | 94 | root.states[key] = (value?.states) // is child 95 | ? fsmObjToXstateObj(value) 96 | : (value) 97 | ? transformState(value) 98 | : {} 99 | }) 100 | 101 | if (input.final){ 102 | //console.log('x-final', input.final) 103 | for (let i=0; i < input.final.length; i++) { 104 | 105 | } 106 | } 107 | 108 | // prep to find onDone & straggler events 109 | delete input.id 110 | delete input.initial 111 | delete input.isConcurrent 112 | delete input.final 113 | delete input.states 114 | if (Object.keys(input).length > 0){ // maybe transtions attached directly to state that are not under 'states:' are onDone 115 | const {on} = transformState(input) 116 | //console.log('last Events', Object.keys(input), on) 117 | if (on?.onDone) 118 | root.onDone = on.onDone 119 | else 120 | root.on = on 121 | } 122 | 123 | return root 124 | } 125 | 126 | function objFormatStr(obj){ 127 | return JSON.stringify( obj, null, '\t') 128 | .replace(/"\w+":/g, match => match.replace(/"/g, '')) 129 | } 130 | function strFormatQuotes(str){ 131 | return (str.includes(' ')) 132 | ? `"${str}"` 133 | : str 134 | } 135 | 136 | const result = fsmObjToXstateObj(obj) 137 | 138 | let strOut = "" 139 | if (options.typescript){ 140 | result['types'] = "-REPLACEME-" 141 | let eventTypes = [] 142 | for (let value of global.events) { 143 | eventTypes.push(`{ type: "${value}" } `) 144 | } 145 | const replaceme = `{ events: {} as ${eventTypes.join('| ')} }` 146 | strOut = objFormatStr(result).replace(/"-REPLACEME-"/g, replaceme) 147 | } 148 | else { 149 | strOut = objFormatStr(result) 150 | } 151 | 152 | // not add implemeantion section 153 | strOut += `, 154 | { 155 | actions: { 156 | ` 157 | for (let value of global.actions) { 158 | strOut += ` ${strFormatQuotes(value)}: ({ context, event }) => {} 159 | ` 160 | } 161 | strOut += `}, 162 | actors: {}, 163 | guards: { 164 | ` 165 | for (let value of global.guards) { 166 | strOut += ` ${strFormatQuotes(value)}: ({ context, event }, params) => { 167 | return false; 168 | }, 169 | ` 170 | } 171 | strOut += `}, 172 | delays: {}, 173 | }` 174 | 175 | let events = new Set(global.events) 176 | 177 | return `import { createMachine } from "xstate"; 178 | 179 | export const machine${result.id.replace(/[\s-_]/g, '')} = createMachine(${ 180 | strOut 181 | } 182 | ); 183 | ` 184 | } 185 | -------------------------------------------------------------------------------- /test/__snapshots__/snap.test.js.snap: -------------------------------------------------------------------------------- 1 | // Bun Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`concurency-xstate.mmd to Object FSM 1`] = ` 4 | { 5 | "final": [], 6 | "id": "coffee", 7 | "initial": "preparing", 8 | "isConcurrent": false, 9 | "states": { 10 | "makingCoffee": null, 11 | "noCoffee": null, 12 | "preparing": { 13 | "final": [], 14 | "id": "", 15 | "initial": "", 16 | "isConcurrent": true, 17 | "onDone": "makingCoffee", 18 | "onDone~~if: error": "noCoffee", 19 | "states": { 20 | "boilWater": { 21 | "final": [ 22 | "waterBoiled", 23 | ], 24 | "id": "boilWater", 25 | "initial": "boilingWater", 26 | "isConcurrent": false, 27 | "states": { 28 | "boilingWater": { 29 | "WATER_BOILED": "waterBoiled", 30 | }, 31 | "waterBoiled": "final", 32 | }, 33 | }, 34 | "grindBeans": { 35 | "final": [ 36 | "beansGround", 37 | ], 38 | "id": "grindBeans", 39 | "initial": "grindingBeans", 40 | "isConcurrent": false, 41 | "states": { 42 | "beansGround": "final", 43 | "grindingBeans": { 44 | "BEANS_GROUND": "beansGround", 45 | }, 46 | }, 47 | }, 48 | }, 49 | }, 50 | }, 51 | } 52 | `; 53 | 54 | exports[`concurency-xstate.mmd to Xstate 1`] = ` 55 | "import { createMachine } from "xstate"; 56 | 57 | export const machinecoffee = createMachine({ 58 | id: "coffee", 59 | initial: "preparing", 60 | states: { 61 | preparing: { 62 | id: "", 63 | type: "parallel", 64 | onDone: [ 65 | { 66 | target: "noCoffee", 67 | guard: { 68 | type: "error" 69 | } 70 | }, 71 | { 72 | target: "makingCoffee" 73 | } 74 | ], 75 | states: { 76 | grindBeans: { 77 | id: "grindBeans", 78 | initial: "grindingBeans", 79 | states: { 80 | grindingBeans: { 81 | on: { 82 | BEANS_GROUND: { 83 | target: "beansGround" 84 | } 85 | } 86 | }, 87 | beansGround: { 88 | type: "final" 89 | } 90 | } 91 | }, 92 | boilWater: { 93 | id: "boilWater", 94 | initial: "boilingWater", 95 | states: { 96 | boilingWater: { 97 | on: { 98 | WATER_BOILED: { 99 | target: "waterBoiled" 100 | } 101 | } 102 | }, 103 | waterBoiled: { 104 | type: "final" 105 | } 106 | } 107 | } 108 | } 109 | }, 110 | noCoffee: {}, 111 | makingCoffee: {} 112 | } 113 | }, 114 | { 115 | actions: { 116 | }, 117 | actors: {}, 118 | guards: { 119 | error: ({ context, event }, params) => { 120 | return false; 121 | }, 122 | }, 123 | delays: {}, 124 | } 125 | ); 126 | " 127 | `; 128 | 129 | exports[`mermaid.mmd to Object FSM 1`] = ` 130 | { 131 | "final": [ 132 | "Still", 133 | "Crash", 134 | ], 135 | "id": "mermaidJS state diagram", 136 | "initial": "Still", 137 | "isConcurrent": false, 138 | "states": { 139 | "Crash": { 140 | "Crash--FINIS": "FINIS", 141 | }, 142 | "FINIS": "final", 143 | "Moving": { 144 | "Moving--Crash": "Crash", 145 | "STOP": "Still", 146 | }, 147 | "Still": { 148 | "Still--FINIS": "FINIS", 149 | "Still--Moving": "Moving", 150 | }, 151 | }, 152 | } 153 | `; 154 | 155 | exports[`mermaid.mmd to Xstate 1`] = ` 156 | "import { createMachine } from "xstate"; 157 | 158 | export const machinemermaidJSstatediagram = createMachine({ 159 | id: "mermaidJS state diagram", 160 | initial: "Still", 161 | states: { 162 | Still: { 163 | on: { 164 | "Still--Moving": { 165 | target: "Moving" 166 | }, 167 | "Still--FINIS": { 168 | target: "FINIS" 169 | } 170 | } 171 | }, 172 | Moving: { 173 | on: { 174 | STOP: { 175 | target: "Still" 176 | }, 177 | "Moving--Crash": { 178 | target: "Crash" 179 | } 180 | } 181 | }, 182 | Crash: { 183 | on: { 184 | "Crash--FINIS": { 185 | target: "FINIS" 186 | } 187 | } 188 | }, 189 | FINIS: { 190 | type: "final" 191 | } 192 | } 193 | }, 194 | { 195 | actions: { 196 | }, 197 | actors: {}, 198 | guards: { 199 | }, 200 | delays: {}, 201 | } 202 | ); 203 | " 204 | `; 205 | 206 | exports[`concurency.mmd to Object FSM 1`] = ` 207 | { 208 | "final": [], 209 | "id": "Parallel-Concurrency Keyboard", 210 | "initial": "Active", 211 | "isConcurrent": false, 212 | "states": { 213 | "Active": { 214 | "final": [], 215 | "id": "", 216 | "initial": "", 217 | "isConcurrent": true, 218 | "states": { 219 | "=Parallel008": { 220 | "final": [], 221 | "id": "", 222 | "initial": "NumLockOff", 223 | "isConcurrent": false, 224 | "states": { 225 | "NumLockOff": { 226 | "EvNumLockPressed": "NumLockOn", 227 | }, 228 | "NumLockOn": { 229 | "EvNumLockPressed": "NumLockOff", 230 | }, 231 | }, 232 | }, 233 | "=Parallel012": { 234 | "final": [], 235 | "id": "", 236 | "initial": "CapsLockOff", 237 | "isConcurrent": false, 238 | "states": { 239 | "CapsLockOff": { 240 | "EvCapsLockPressed": "CapsLockOn", 241 | }, 242 | "CapsLockOn": { 243 | "EvCapsLockPressed": "CapsLockOff", 244 | }, 245 | }, 246 | }, 247 | "=Parallel013": { 248 | "final": [], 249 | "id": "", 250 | "initial": "ScrollLockOff", 251 | "isConcurrent": false, 252 | "states": { 253 | "ScrollLockOff": { 254 | "EvScrollLockPressed": "ScrollLockOn", 255 | }, 256 | "ScrollLockOn": { 257 | "EvScrollLockPressed": "ScrollLockOff", 258 | }, 259 | }, 260 | }, 261 | }, 262 | }, 263 | }, 264 | } 265 | `; 266 | 267 | exports[`concurency.mmd to Xstate 1`] = ` 268 | "import { createMachine } from "xstate"; 269 | 270 | export const machineParallelConcurrencyKeyboard = createMachine({ 271 | id: "Parallel-Concurrency Keyboard", 272 | initial: "Active", 273 | states: { 274 | Active: { 275 | id: "", 276 | type: "parallel", 277 | onDone: [], 278 | states: { 279 | "=Parallel008": { 280 | id: "", 281 | initial: "NumLockOff", 282 | states: { 283 | NumLockOff: { 284 | on: { 285 | EvNumLockPressed: { 286 | target: "NumLockOn" 287 | } 288 | } 289 | }, 290 | NumLockOn: { 291 | on: { 292 | EvNumLockPressed: { 293 | target: "NumLockOff" 294 | } 295 | } 296 | } 297 | } 298 | }, 299 | "=Parallel012": { 300 | id: "", 301 | initial: "CapsLockOff", 302 | states: { 303 | CapsLockOff: { 304 | on: { 305 | EvCapsLockPressed: { 306 | target: "CapsLockOn" 307 | } 308 | } 309 | }, 310 | CapsLockOn: { 311 | on: { 312 | EvCapsLockPressed: { 313 | target: "CapsLockOff" 314 | } 315 | } 316 | } 317 | } 318 | }, 319 | "=Parallel013": { 320 | id: "", 321 | initial: "ScrollLockOff", 322 | states: { 323 | ScrollLockOff: { 324 | on: { 325 | EvScrollLockPressed: { 326 | target: "ScrollLockOn" 327 | } 328 | } 329 | }, 330 | ScrollLockOn: { 331 | on: { 332 | EvScrollLockPressed: { 333 | target: "ScrollLockOff" 334 | } 335 | } 336 | } 337 | } 338 | } 339 | } 340 | } 341 | } 342 | }, 343 | { 344 | actions: { 345 | }, 346 | actors: {}, 347 | guards: { 348 | }, 349 | delays: {}, 350 | } 351 | ); 352 | " 353 | `; 354 | 355 | exports[`bug.lone-final-outside.mmd to Object FSM 1`] = ` 356 | { 357 | "final": [ 358 | "endRoot", 359 | ], 360 | "id": "bug-lone-final-outside", 361 | "initial": "Inner", 362 | "isConcurrent": false, 363 | "states": { 364 | "Inner": { 365 | "final": [ 366 | "endInner", 367 | ], 368 | "id": "", 369 | "initial": "choose", 370 | "isConcurrent": false, 371 | "states": { 372 | "choose": { 373 | "choose--endInner": "endInner", 374 | "choose--endRoot": "endRoot", 375 | }, 376 | "endInner": "final", 377 | }, 378 | }, 379 | "endRoot": "final", 380 | }, 381 | } 382 | `; 383 | 384 | exports[`bug.lone-final-outside.mmd to Xstate 1`] = ` 385 | "import { createMachine } from "xstate"; 386 | 387 | export const machinebuglonefinaloutside = createMachine({ 388 | id: "bug-lone-final-outside", 389 | initial: "Inner", 390 | states: { 391 | Inner: { 392 | id: "", 393 | initial: "choose", 394 | states: { 395 | choose: { 396 | on: { 397 | "choose--endInner": { 398 | target: "endInner" 399 | }, 400 | "choose--endRoot": { 401 | target: "endRoot" 402 | } 403 | } 404 | }, 405 | endInner: { 406 | type: "final" 407 | } 408 | } 409 | }, 410 | endRoot: { 411 | type: "final" 412 | } 413 | } 414 | }, 415 | { 416 | actions: { 417 | }, 418 | actors: {}, 419 | guards: { 420 | }, 421 | delays: {}, 422 | } 423 | ); 424 | " 425 | `; 426 | 427 | exports[`fix-events.mmd to Object FSM 1`] = ` 428 | { 429 | "final": [ 430 | "Still", 431 | "Wreck", 432 | ], 433 | "id": "fix-events", 434 | "initial": "", 435 | "isConcurrent": false, 436 | "states": { 437 | "FINIS": "final", 438 | "Moving": { 439 | "CRASH": "Wreck", 440 | "STOP": "Still", 441 | }, 442 | "Still": { 443 | "CRASH": "Wreck", 444 | "Still--FINIS": "FINIS", 445 | "Still--Moving": "Moving", 446 | }, 447 | "Wreck": { 448 | "Wreck--FINIS": "FINIS", 449 | "final": [ 450 | "Assess", 451 | ], 452 | "id": "", 453 | "initial": "Assess", 454 | "isConcurrent": false, 455 | "states": { 456 | "Assess": { 457 | "Assess--FINIS": "FINIS", 458 | "Assess--Still": "Still", 459 | }, 460 | "FINIS": "final", 461 | "if": null, 462 | "isDriveable": null, 463 | }, 464 | }, 465 | "onDone": null, 466 | }, 467 | } 468 | `; 469 | 470 | exports[`fix-events.mmd to Xstate 1`] = ` 471 | "import { createMachine } from "xstate"; 472 | 473 | export const machinefixevents = createMachine({ 474 | id: "fix-events", 475 | initial: "", 476 | states: { 477 | Still: { 478 | on: { 479 | "Still--Moving": { 480 | target: "Moving" 481 | }, 482 | CRASH: { 483 | target: "Wreck" 484 | }, 485 | "Still--FINIS": { 486 | target: "FINIS" 487 | } 488 | } 489 | }, 490 | Moving: { 491 | on: { 492 | STOP: { 493 | target: "Still" 494 | }, 495 | CRASH: { 496 | target: "Wreck" 497 | } 498 | } 499 | }, 500 | Wreck: { 501 | id: "", 502 | initial: "Assess", 503 | states: { 504 | Assess: { 505 | on: { 506 | "Assess--Still": { 507 | target: "Still" 508 | }, 509 | "Assess--FINIS": { 510 | target: "FINIS" 511 | } 512 | } 513 | }, 514 | if: {}, 515 | isDriveable: {}, 516 | FINIS: { 517 | type: "final" 518 | } 519 | }, 520 | on: { 521 | "Wreck--FINIS": { 522 | target: "FINIS" 523 | } 524 | } 525 | }, 526 | onDone: {}, 527 | FINIS: { 528 | type: "final" 529 | } 530 | } 531 | }, 532 | { 533 | actions: { 534 | }, 535 | actors: {}, 536 | guards: { 537 | }, 538 | delays: {}, 539 | } 540 | ); 541 | " 542 | `; 543 | 544 | exports[`nested.mmd to Object FSM 1`] = ` 545 | { 546 | "final": [ 547 | "endRoot", 548 | ], 549 | "id": "nested", 550 | "initial": "start", 551 | "isConcurrent": false, 552 | "states": { 553 | "Nested": { 554 | "final": [ 555 | "endNested", 556 | ], 557 | "id": "", 558 | "initial": "firstNested", 559 | "isConcurrent": false, 560 | "states": { 561 | "InnerChild": { 562 | "final": [ 563 | "endInner", 564 | ], 565 | "id": "", 566 | "initial": "inside", 567 | "isConcurrent": false, 568 | "states": { 569 | "DeeperChild": { 570 | "final": [ 571 | "endDeeper", 572 | ], 573 | "id": "", 574 | "initial": "middle", 575 | "isConcurrent": false, 576 | "states": { 577 | "endDeeper": "final", 578 | "middle": { 579 | "middle--endDeeper": "endDeeper", 580 | "middle--preParent": "preParent", 581 | }, 582 | "preParent": { 583 | "preParent--endNested": "endNested", 584 | }, 585 | }, 586 | }, 587 | "endInner": "final", 588 | "inside": { 589 | "inside--DeeperChild": "DeeperChild", 590 | "inside--endInner": "endInner", 591 | "inside--preOutside": "preOutside", 592 | "inside--preParent": "preParent", 593 | }, 594 | "preOutside": { 595 | "preOutside--endRoot": "endRoot", 596 | }, 597 | }, 598 | }, 599 | "endNested": "final", 600 | "firstNested": { 601 | "firstNested--InnerChild": "InnerChild", 602 | "firstNested--endNested": "endNested", 603 | }, 604 | }, 605 | }, 606 | "endRoot": "final", 607 | "start": { 608 | "start--Nested": "Nested", 609 | }, 610 | }, 611 | } 612 | `; 613 | 614 | exports[`nested.mmd to Xstate 1`] = ` 615 | "import { createMachine } from "xstate"; 616 | 617 | export const machinenested = createMachine({ 618 | id: "nested", 619 | initial: "start", 620 | states: { 621 | start: { 622 | on: { 623 | "start--Nested": { 624 | target: "Nested" 625 | } 626 | } 627 | }, 628 | Nested: { 629 | id: "", 630 | initial: "firstNested", 631 | states: { 632 | firstNested: { 633 | on: { 634 | "firstNested--InnerChild": { 635 | target: "InnerChild" 636 | }, 637 | "firstNested--endNested": { 638 | target: "endNested" 639 | } 640 | } 641 | }, 642 | InnerChild: { 643 | id: "", 644 | initial: "inside", 645 | states: { 646 | inside: { 647 | on: { 648 | "inside--preOutside": { 649 | target: "preOutside" 650 | }, 651 | "inside--preParent": { 652 | target: "preParent" 653 | }, 654 | "inside--endInner": { 655 | target: "endInner" 656 | }, 657 | "inside--DeeperChild": { 658 | target: "DeeperChild" 659 | } 660 | } 661 | }, 662 | DeeperChild: { 663 | id: "", 664 | initial: "middle", 665 | states: { 666 | middle: { 667 | on: { 668 | "middle--preParent": { 669 | target: "preParent" 670 | }, 671 | "middle--endDeeper": { 672 | target: "endDeeper" 673 | } 674 | } 675 | }, 676 | preParent: { 677 | on: { 678 | "preParent--endNested": { 679 | target: "endNested" 680 | } 681 | } 682 | }, 683 | endDeeper: { 684 | type: "final" 685 | } 686 | } 687 | }, 688 | preOutside: { 689 | on: { 690 | "preOutside--endRoot": { 691 | target: "endRoot" 692 | } 693 | } 694 | }, 695 | endInner: { 696 | type: "final" 697 | } 698 | } 699 | }, 700 | endNested: { 701 | type: "final" 702 | } 703 | } 704 | }, 705 | endRoot: { 706 | type: "final" 707 | } 708 | } 709 | }, 710 | { 711 | actions: { 712 | }, 713 | actors: {}, 714 | guards: { 715 | }, 716 | delays: {}, 717 | } 718 | ); 719 | " 720 | `; 721 | 722 | exports[`newmachine.mmd to Object FSM 1`] = ` 723 | { 724 | "final": [], 725 | "id": "New Machine redux", 726 | "initial": "Initial state", 727 | "isConcurrent": false, 728 | "states": { 729 | "Another state": { 730 | "next~~fallback": "Initial state", 731 | "next~~if: \"some condition\" , type: track": "Parent state", 732 | }, 733 | "Initial state": { 734 | "next": "Another state", 735 | }, 736 | "Parent state": { 737 | "back~~action: reset %%fixme trigger": "Initial State", 738 | "final": [], 739 | "id": "", 740 | "initial": "Child state", 741 | "isConcurrent": false, 742 | "states": { 743 | "Another child state": { 744 | "final": [], 745 | "id": "", 746 | "initial": "", 747 | "isConcurrent": false, 748 | "states": {}, 749 | }, 750 | "Child state": { 751 | "final": [], 752 | "id": "", 753 | "initial": "", 754 | "isConcurrent": false, 755 | "next": "Another child state", 756 | "states": {}, 757 | }, 758 | }, 759 | }, 760 | }, 761 | } 762 | `; 763 | 764 | exports[`newmachine.mmd to Xstate 1`] = ` 765 | "import { createMachine } from "xstate"; 766 | 767 | export const machineNewMachineredux = createMachine({ 768 | id: "New Machine redux", 769 | initial: "Initial state", 770 | states: { 771 | "Initial state": { 772 | on: { 773 | next: { 774 | target: "Another state" 775 | } 776 | } 777 | }, 778 | "Another state": { 779 | on: { 780 | next: [ 781 | { 782 | target: "Parent state", 783 | guard: { 784 | type: "some condition" 785 | } 786 | }, 787 | { 788 | target: "Initial state" 789 | } 790 | ] 791 | } 792 | }, 793 | "Parent state": { 794 | id: "", 795 | initial: "Child state", 796 | states: { 797 | "Child state": { 798 | id: "", 799 | initial: "", 800 | states: {}, 801 | on: { 802 | next: { 803 | target: "Another child state" 804 | } 805 | } 806 | }, 807 | "Another child state": { 808 | id: "", 809 | initial: "", 810 | states: {} 811 | } 812 | }, 813 | on: { 814 | back: { 815 | target: "Initial State", 816 | actions: { 817 | type: "reset" 818 | } 819 | } 820 | } 821 | } 822 | } 823 | }, 824 | { 825 | actions: { 826 | reset: ({ context, event }) => {} 827 | }, 828 | actors: {}, 829 | guards: { 830 | "some condition": ({ context, event }, params) => { 831 | return false; 832 | }, 833 | }, 834 | delays: {}, 835 | } 836 | ); 837 | " 838 | `; 839 | 840 | exports[`children.mmd to Object FSM 1`] = ` 841 | { 842 | "final": [], 843 | "id": "children", 844 | "initial": "demos", 845 | "isConcurrent": false, 846 | "states": { 847 | "Child": { 848 | "final": [ 849 | "inner", 850 | ], 851 | "id": "", 852 | "initial": "inner", 853 | "isConcurrent": false, 854 | "states": { 855 | "inner": "final", 856 | }, 857 | }, 858 | "Dual": { 859 | "final": [ 860 | "last", 861 | ], 862 | "id": "", 863 | "initial": "first", 864 | "isConcurrent": false, 865 | "states": { 866 | "first": { 867 | "first--last": "last", 868 | }, 869 | "last": "final", 870 | }, 871 | }, 872 | "Orphan": { 873 | "final": [ 874 | "noInital", 875 | ], 876 | "id": "", 877 | "initial": "", 878 | "isConcurrent": false, 879 | "states": { 880 | "noInital": "final", 881 | }, 882 | }, 883 | "Siblings": { 884 | "final": [], 885 | "id": "", 886 | "initial": "branches", 887 | "isConcurrent": false, 888 | "states": { 889 | "branches": { 890 | "branches--inner": "inner", 891 | "branches--last": "last", 892 | "branches--noInital": "noInital", 893 | }, 894 | }, 895 | }, 896 | "demos": { 897 | "demos--introComposite1d": "introComposite1d", 898 | "demos--introComposite2d": "introComposite2d", 899 | "demos--introComposite3": "introComposite3", 900 | }, 901 | "introComposite1d": { 902 | "introComposite1d--Child": "Child", 903 | }, 904 | "introComposite2d": { 905 | "introComposite2d--Dual": "Dual", 906 | }, 907 | "introComposite3": { 908 | "introComposite3--Siblings": "Siblings", 909 | }, 910 | }, 911 | } 912 | `; 913 | 914 | exports[`children.mmd to Xstate 1`] = ` 915 | "import { createMachine } from "xstate"; 916 | 917 | export const machinechildren = createMachine({ 918 | id: "children", 919 | initial: "demos", 920 | states: { 921 | demos: { 922 | on: { 923 | "demos--introComposite1d": { 924 | target: "introComposite1d" 925 | }, 926 | "demos--introComposite2d": { 927 | target: "introComposite2d" 928 | }, 929 | "demos--introComposite3": { 930 | target: "introComposite3" 931 | } 932 | } 933 | }, 934 | introComposite1d: { 935 | on: { 936 | "introComposite1d--Child": { 937 | target: "Child" 938 | } 939 | } 940 | }, 941 | Child: { 942 | id: "", 943 | initial: "inner", 944 | states: { 945 | inner: { 946 | type: "final" 947 | } 948 | } 949 | }, 950 | introComposite2d: { 951 | on: { 952 | "introComposite2d--Dual": { 953 | target: "Dual" 954 | } 955 | } 956 | }, 957 | Dual: { 958 | id: "", 959 | initial: "first", 960 | states: { 961 | first: { 962 | on: { 963 | "first--last": { 964 | target: "last" 965 | } 966 | } 967 | }, 968 | last: { 969 | type: "final" 970 | } 971 | } 972 | }, 973 | introComposite3: { 974 | on: { 975 | "introComposite3--Siblings": { 976 | target: "Siblings" 977 | } 978 | } 979 | }, 980 | Siblings: { 981 | id: "", 982 | initial: "branches", 983 | states: { 984 | branches: { 985 | on: { 986 | "branches--inner": { 987 | target: "inner" 988 | }, 989 | "branches--last": { 990 | target: "last" 991 | }, 992 | "branches--noInital": { 993 | target: "noInital" 994 | } 995 | } 996 | } 997 | } 998 | }, 999 | Orphan: { 1000 | id: "", 1001 | initial: "", 1002 | states: { 1003 | noInital: { 1004 | type: "final" 1005 | } 1006 | } 1007 | } 1008 | } 1009 | }, 1010 | { 1011 | actions: { 1012 | }, 1013 | actors: {}, 1014 | guards: { 1015 | }, 1016 | delays: {}, 1017 | } 1018 | ); 1019 | " 1020 | `; 1021 | -------------------------------------------------------------------------------- /test/mmd/bug.lone-final-outside.mmd: -------------------------------------------------------------------------------- 1 | stateDiagram-v2 2 | id bug-lone-final-outside 3 | [*] --> Inner 4 | state Inner { 5 | [*] --> choose 6 | choose --> endInner 7 | choose --> endRoot 8 | endInner --> [*] 9 | } 10 | endRoot --> [*] 11 | -------------------------------------------------------------------------------- /test/mmd/children.mmd: -------------------------------------------------------------------------------- 1 | stateDiagram-v2 2 | id children 3 | [*] --> demos 4 | demos --> introComposite1d 5 | demos --> introComposite2d 6 | demos --> introComposite3 7 | 8 | %% composite 9 | introComposite1d --> Child 10 | state Child { 11 | [*] --> inner 12 | inner --> [*] 13 | } 14 | 15 | introComposite2d --> Dual 16 | state Dual { 17 | [*] --> first 18 | first --> last 19 | last --> [*] 20 | } 21 | 22 | introComposite3 --> Siblings 23 | state Siblings { 24 | [*] --> branches 25 | branches --> inner 26 | branches --> last 27 | branches --> noInital 28 | } 29 | 30 | state Orphan { 31 | noInital --> [*] 32 | } 33 | -------------------------------------------------------------------------------- /test/mmd/concurency-xstate.mmd: -------------------------------------------------------------------------------- 1 | stateDiagram-v2 2 | id coffee 3 | %% https://stately.ai/docs/parallel-states#parallel-ondone-transition 4 | [*] --> preparing 5 | 6 | state preparing { 7 | id grindBeans 8 | [*] --> grindingBeans 9 | grindingBeans --> beansGround : BEANS_GROUND 10 | beansGround --> [*] 11 | -- 12 | id boilWater 13 | [*] --> boilingWater 14 | boilingWater --> waterBoiled : WATER_BOILED 15 | waterBoiled --> [*] 16 | } 17 | preparing --> noCoffee : onDone %%; if: error 18 | preparing --> makingCoffee : onDone 19 | noCoffee 20 | makingCoffee 21 | -------------------------------------------------------------------------------- /test/mmd/concurency.mmd: -------------------------------------------------------------------------------- 1 | stateDiagram-v2 2 | id Parallel-Concurrency Keyboard 3 | %% https://mermaid.js.org/syntax/stateDiagram.html#concurrency 4 | [*] --> Active 5 | 6 | state Active { 7 | [*] --> NumLockOff 8 | NumLockOff --> NumLockOn : EvNumLockPressed 9 | NumLockOn --> NumLockOff : EvNumLockPressed 10 | -- 11 | [*] --> CapsLockOff 12 | CapsLockOff --> CapsLockOn : EvCapsLockPressed 13 | CapsLockOn --> CapsLockOff : EvCapsLockPressed 14 | -- 15 | [*] --> ScrollLockOff 16 | ScrollLockOff --> ScrollLockOn : EvScrollLockPressed 17 | ScrollLockOn --> ScrollLockOff : EvScrollLockPressed 18 | } 19 | -------------------------------------------------------------------------------- /test/mmd/fix-events.mmd: -------------------------------------------------------------------------------- 1 | stateDiagram-v2 2 | id fix-events 3 | %% inspiration: https://mermaid.js.org/syntax/stateDiagram.html 4 | Still --> [*] 5 | Still --> Moving 6 | Moving --> Still : STOP %% named transition / event 7 | Still --> Wreck : CRASH 8 | Moving --> Wreck : CRASH 9 | state Wreck { 10 | [*] --> Assess 11 | Assess --> Still , if: isDriveable 12 | Assess --> [*] 13 | } 14 | Wreck --> [*] : onDone 15 | -------------------------------------------------------------------------------- /test/mmd/mermaid.mmd: -------------------------------------------------------------------------------- 1 | stateDiagram-v2 2 | id mermaidJS state diagram 3 | %% inspiration: https://mermaid.js.org/syntax/stateDiagram.html 4 | [*] --> Still 5 | Still --> [*] 6 | Still --> Moving 7 | Moving --> Still : STOP %% named transition / event 8 | Moving --> Crash 9 | Crash --> [*] 10 | -------------------------------------------------------------------------------- /test/mmd/nested.mmd: -------------------------------------------------------------------------------- 1 | stateDiagram-v2 2 | id nested 3 | %% [*] --> Nested 4 | [*] --> start 5 | start --> Nested 6 | state Nested { 7 | [*] --> firstNested 8 | firstNested --> InnerChild 9 | state InnerChild { 10 | [*] --> inside 11 | inside --> preOutside 12 | inside --> preParent 13 | inside --> endInner 14 | inside --> DeeperChild 15 | state DeeperChild { 16 | [*] --> middle 17 | middle --> preParent 18 | middle --> endDeeper 19 | preParent --> endNested 20 | endDeeper --> [*] 21 | } 22 | preOutside --> endRoot 23 | endInner --> [*] 24 | } 25 | firstNested --> endNested 26 | endNested --> [*] 27 | } 28 | endRoot --> [*] 29 | -------------------------------------------------------------------------------- /test/mmd/newmachine.mmd: -------------------------------------------------------------------------------- 1 | stateDiagram-v2 2 | id New Machine redux 3 | %% from XState playground default 4 | %% state "Another state" <> conditionID 5 | [*] --> "Initial state" 6 | "Initial state" --> "Another state" : next 7 | "Another state" --> "Parent state" : next %%; if: "some condition" , type: track 8 | "Another state" --> "Initial state" : next %%; fallback 9 | state "Parent state" { 10 | [*] --> "Child state" 11 | state "Child state" { 12 | } 13 | "Child state" --> "Another child state" : next %%fixme inital 14 | state "Another child state" { 15 | } 16 | } 17 | "Parent state" --> "Initial State" : back %%; action: reset %%fixme trigger 18 | -------------------------------------------------------------------------------- /test/paraell.stately.js: -------------------------------------------------------------------------------- 1 | // https://stately.ai/docs/parallel-states#parallel-ondone-transition 2 | import { createMachine } from "xstate"; 3 | 4 | export const machine = createMachine({ 5 | id: "coffee", 6 | initial: "preparing", 7 | states: { 8 | preparing: { 9 | states: { 10 | grindBeans: { 11 | initial: "grindingBeans", 12 | states: { 13 | grindingBeans: { 14 | on: { 15 | BEANS_GROUND: { 16 | target: "beansGround", 17 | }, 18 | }, 19 | }, 20 | beansGround: { 21 | type: "final", 22 | }, 23 | }, 24 | }, 25 | boilWater: { 26 | initial: "boilingWater", 27 | states: { 28 | boilingWater: { 29 | on: { 30 | WATER_BOILED: { 31 | target: "waterBoiled", 32 | }, 33 | }, 34 | }, 35 | waterBoiled: { 36 | type: "final", 37 | }, 38 | }, 39 | }, 40 | }, 41 | type: "parallel", 42 | onDone: { 43 | target: "makingCoffee", 44 | }, 45 | }, 46 | makingCoffee: {}, 47 | }, 48 | }); 49 | 50 | -------------------------------------------------------------------------------- /test/process-file.bun.js: -------------------------------------------------------------------------------- 1 | import { mermaidToObject } from "../src/origins/mermaid.js" 2 | import { xstateObjToxstateJS } from "../src/targets/xstate.js" 3 | 4 | export async function mmdToObjectFSM(fullFilePath){ 5 | const file = Bun.file(fullFilePath) 6 | const mermaidText = await file.text() 7 | const obj = await mermaidToObject(mermaidText) 8 | return obj 9 | } 10 | 11 | export async function mmdToXstate(fullFilePath){ 12 | const file = Bun.file(fullFilePath) 13 | const mermaidText = await file.text() 14 | const obj = await mermaidToObject(mermaidText) 15 | const xstate = xstateObjToxstateJS(obj) 16 | return xstate 17 | } 18 | -------------------------------------------------------------------------------- /test/snap.test.js: -------------------------------------------------------------------------------- 1 | import { test, expect } from "bun:test" 2 | import { Glob } from "bun" 3 | import * as processFile from "./process-file.bun" 4 | 5 | const glob = new Glob(`./test/mmd/*.mmd`) 6 | const reFileName = /(\/?((?[\w._-]*)*))*/ 7 | 8 | for await (const fullFilePath of glob.scan()){ 9 | const {filename} = reFileName.exec(fullFilePath).groups 10 | console.log(filename) 11 | 12 | test(`${filename} to Object FSM`, ()=>{ 13 | expect( processFile.mmdToObjectFSM(fullFilePath) ).resolves.toMatchSnapshot() 14 | }) 15 | 16 | test(`${filename} to Xstate`, ()=>{ 17 | expect( processFile.mmdToXstate(fullFilePath) ).resolves.toMatchSnapshot() 18 | }) 19 | } 20 | --------------------------------------------------------------------------------