├── README.md ├── css ├── github.png ├── icon.png └── main.css ├── index.html └── js ├── animation.js ├── back.js ├── front.js ├── full.render.js └── viz.js /README.md: -------------------------------------------------------------------------------- 1 | # Regex Visualizer 2 | 3 | Welcome! This tool is used to visualize which strings exactly match a regular expression and which don't, using finite state machines. Check it out at https://27t.github.io/regex-visualizer/ -------------------------------------------------------------------------------- /css/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/27t/regex-visualizer/1a89d323fd5ac9f5bb1b5548c1578d4963b36c0a/css/github.png -------------------------------------------------------------------------------- /css/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/27t/regex-visualizer/1a89d323fd5ac9f5bb1b5548c1578d4963b36c0a/css/icon.png -------------------------------------------------------------------------------- /css/main.css: -------------------------------------------------------------------------------- 1 | html { 2 | height: 100%; 3 | } 4 | 5 | body { 6 | background: linear-gradient(45deg, rgba(252, 129, 192, 0.2), rgba(46, 255, 175, 0.3), rgba(46, 234, 255, 0.3), rgba(35, 213, 171, 0.3)); 7 | background-size: cover; 8 | margin: 0; 9 | display: flex; 10 | flex-flow: column; 11 | width: 100%; 12 | min-width: 1100px; 13 | height: 100%; 14 | background-attachment: fixed; 15 | 16 | font-family: 'Mako'; 17 | } 18 | 19 | /* 20 | Header and header menus styling 21 | */ 22 | #header { 23 | width: 100%; 24 | height: 100%; 25 | min-height: 110px; 26 | display: flex; 27 | justify-content: space-between; 28 | align-items: center; 29 | background-color: rgba(255,255,255,0.3); 30 | } 31 | 32 | #inp, #options, #next_step, #previous_step, #help { 33 | width: 20%; 34 | min-width: 220px; 35 | height: 100px; 36 | align-items: center; 37 | text-align: center; 38 | } 39 | 40 | #options, #next_step, #previous_step, #help { 41 | opacity: 0; 42 | } 43 | 44 | /* 45 | The main regex input field and corresponding convert button 46 | */ 47 | #convert { 48 | width: 200px; 49 | font-size: 20px; 50 | margin: 10px auto; 51 | display: block; 52 | cursor: pointer; 53 | outline: none; 54 | background: none; 55 | border: 2px solid rgba(15,105,102,0.8); 56 | } 57 | 58 | #convert:hover { 59 | border: 2px solid rgb(15,105,102); 60 | } 61 | 62 | #convert:disabled { 63 | cursor: default; 64 | border: 2px solid rgba(15,105,102,0.5); 65 | } 66 | 67 | #regex { 68 | background-color: rgba(255,255,255,0.3); 69 | border: 1px solid rgba(170,170,170,0.5); 70 | border-radius: 0px; 71 | width: 192px; 72 | font-size: 30px; 73 | display: block; 74 | margin: 10px auto; 75 | padding: 2px 4px; 76 | } 77 | 78 | #regex:focus { 79 | outline: none; 80 | } 81 | 82 | #validBorder, #invalidBorder { 83 | pointer-events: none; 84 | position: absolute; 85 | transform: translate(-50%); 86 | margin: 10px 0 0 0; 87 | } 88 | 89 | .greenPath, .redPath { 90 | stroke-dashoffset: 240px; 91 | stroke-dasharray: 240px; 92 | transition: stroke-dashoffset 1s; 93 | } 94 | 95 | .enabled { 96 | stroke-dashoffset: 0px; 97 | } 98 | 99 | #options1, #options2, #options3 { 100 | margin: 5px auto; 101 | } 102 | 103 | #help { 104 | display: flex; 105 | flex-flow: column; 106 | } 107 | 108 | #help button { 109 | margin: 10px 10px 0 10px; 110 | width: 50px; 111 | } 112 | 113 | .githubbutton img { 114 | vertical-align: middle; 115 | padding: 5px 0; 116 | } 117 | 118 | #line { 119 | display:none; 120 | background-color: black; 121 | min-height: 1px; 122 | width: 100%; 123 | bottom: 0%; 124 | opacity: 0; 125 | margin: 0; 126 | } 127 | 128 | .beginbuttons { 129 | width: 200px; 130 | margin: -5px auto; 131 | display: flex; 132 | flex-flow: row; 133 | font-size: 14px; 134 | } 135 | 136 | .helpbutton2 { 137 | border-bottom: 1px solid black; 138 | width: 30px; 139 | margin-right: 47px; 140 | text-align: left; 141 | cursor: pointer; 142 | } 143 | 144 | .examplebutton { 145 | border-bottom: 1px solid black; 146 | width: 123px; 147 | text-align: right; 148 | cursor: pointer; 149 | } 150 | 151 | 152 | #header button { 153 | cursor: pointer; 154 | outline: none; 155 | background: none; 156 | border: 2px solid rgba(15,105,102,0.75); 157 | font-family: 'Mako'; 158 | padding-top: 0; 159 | padding-bottom: 0; 160 | } 161 | 162 | #header button:hover { 163 | border: 2px solid rgb(15,105,102); 164 | } 165 | 166 | #header button:active { 167 | background: rgba(255,255,255,0.3); 168 | } 169 | 170 | #header button:disabled { 171 | cursor: default; 172 | border: 2px solid rgba(15,105,102,0.5); 173 | } 174 | 175 | /* 176 | Styling for the automaton itself 177 | */ 178 | #FA { 179 | display: none; 180 | min-height: 300px; 181 | min-width: 1100px; 182 | width: 100%; 183 | flex: 1 1 auto; 184 | position: relative; 185 | overflow: hidden; 186 | } 187 | 188 | #FA svg { 189 | overflow: hidden; 190 | top: 0; 191 | left: 0; 192 | width: 100%; 193 | height: 100%; 194 | } 195 | 196 | /* Disappear animation when converting another expression */ 197 | svg.disappear { 198 | -webkit-animation: disap 0.9s; 199 | animation: disap 0.9s; 200 | animation-fill-mode: forwards; 201 | } 202 | 203 | @keyframes disap { 204 | 0% {transform: scale(1)} 205 | 65% {transform: scale(1.15)} 206 | 100% {transform: scale(0)} 207 | } 208 | 209 | @-webkit-keyframes disap { 210 | 0% {transform: scale(1)} 211 | 65% {transform: scale(1.15)} 212 | 100% {transform: scale(0)} 213 | } 214 | 215 | /* 216 | Buttons to go to the next or previous step 217 | */ 218 | #stepr, #stepl { 219 | opacity: 0; 220 | flex-direction: row; 221 | justify-content: center; 222 | height: 80px; 223 | width: 190px; 224 | margin: 10px auto; 225 | border: 1px solid rgb(15,105,102); 226 | } 227 | 228 | .step { 229 | display: none; 230 | } 231 | 232 | .stepbutton:hover { 233 | cursor: pointer; 234 | } 235 | 236 | .step:hover .buttonPath { 237 | -webkit-animation: glow 4s infinite; 238 | animation: glow 4s infinite; 239 | } 240 | 241 | @keyframes glow { 242 | 0% {stroke-dashoffset: 56.85} 243 | 65% {stroke-dashoffset: -56.85} 244 | 100% {stroke-dashoffset: -56.85} 245 | } 246 | 247 | @-webkit-keyframes glow { 248 | 0% {stroke-dashoffset: 56.85} 249 | 65% {stroke-dashoffset: -56.85} 250 | 100% {stroke-dashoffset: -56.85} 251 | } 252 | 253 | .steptext { 254 | font-size: 24px; 255 | font-weight: 800; 256 | color: rgba(15,105,102,1); 257 | display: flex; 258 | justify-content: center; 259 | flex-direction: column; 260 | } 261 | 262 | .buttonPath { 263 | stroke-dasharray: 56.85; 264 | stroke-dashoffset: 56.85; 265 | } 266 | 267 | #stepmatch { 268 | margin-top: 10px; 269 | } 270 | 271 | #stepmatch div { 272 | margin: 5px; 273 | } 274 | 275 | #stepmatch input { 276 | height: 20px; 277 | max-width: 150px; 278 | background-color: rgba(255,255,255,0.3); 279 | border: 1px solid rgba(170,170,170,0.5); 280 | border-radius: 0px; 281 | font-size: 17px; 282 | } 283 | 284 | #stepmatch button { 285 | height: 24px; 286 | } 287 | 288 | #stepmatch input:focus { 289 | outline: none; 290 | } 291 | 292 | /* 293 | Message with info about next step 294 | */ 295 | #step_message, #error_message { 296 | width: 100%; 297 | min-width: 1100px; 298 | margin: 0; 299 | height: 24px; 300 | min-height: 24px; 301 | text-align: center; 302 | background-color: rgba(255,255,255,0.3); 303 | } 304 | 305 | #error_message { 306 | height: auto; 307 | min-height: 0px; 308 | color: red; 309 | } 310 | /* 311 | Infoboxes 312 | */ 313 | ellipse { 314 | transition: rx 0.6s 0.9s, ry 0.6s 0.9s; 315 | } 316 | 317 | .selected_small { 318 | transition: rx 0.6s 0.3s, ry 0.6s 0.3s; 319 | rx: 20px; 320 | ry: 20px; 321 | } 322 | 323 | .selected_big { 324 | transition: rx 0.6s 0.3s, ry 0.6s 0.3s; 325 | rx: 23px; 326 | ry: 23px; 327 | } 328 | 329 | .info_path { 330 | transition: stroke-dashoffset 1.1s, fill-opacity 0.2s; 331 | fill-opacity: 0; 332 | } 333 | 334 | .info_path_selected { 335 | transition: stroke-dashoffset 1.1s 0.7s, fill-opacity 0.2s 1.4s; 336 | fill-opacity: 1; 337 | stroke-dashoffset: 0 !important; 338 | } 339 | 340 | .info_text { 341 | transition: opacity 0.2s; 342 | opacity: 0; 343 | } 344 | 345 | .info_text_selected { 346 | transition: opacity 0.2s 1.4s; 347 | opacity: 1; 348 | } 349 | 350 | .info_text_big { 351 | font-size: 10.5px; 352 | font-weigth: 500; 353 | } 354 | 355 | .info_text_small { 356 | font-size: 7.5px; 357 | } 358 | 359 | .info_box { 360 | pointer-events: none; 361 | } 362 | 363 | .info_rect { 364 | pointer-events: none; 365 | } 366 | 367 | .info_rect_selected { 368 | pointer-events: all; 369 | } 370 | /* 371 | DFA Animation Table 372 | */ 373 | #table_container { 374 | width: 20%; 375 | min-width: 390px; 376 | height: 100%; 377 | display: flex; 378 | flex-flow: column; 379 | border-left: 1px solid black; 380 | border-right: 1px solid black; 381 | overflow: auto; 382 | } 383 | 384 | .table_row { 385 | display: flex; 386 | flex-flow: row; 387 | justify-content: space-between; 388 | } 389 | 390 | #table_head div { 391 | font-size: 20px; 392 | font-weight: 700; 393 | } 394 | 395 | .table_text_left { 396 | min-width: 35%; 397 | width: 35%; 398 | padding: 5px; 399 | vertical-align: middle; 400 | text-align: center; 401 | white-space: nowrap; 402 | border-bottom: 1px solid black; 403 | border-right: 1px solid black; 404 | } 405 | 406 | .table_text_right { 407 | width: 65%; 408 | padding: 5px; 409 | vertical-align: middle; 410 | text-align: center; 411 | white-space: nowrap; 412 | border-bottom: 1px solid black; 413 | } 414 | 415 | /* 416 | Help menu 417 | */ 418 | .helpmenu { 419 | display: none; 420 | z-index: 1; 421 | padding: 5px 10px 20px 5px; 422 | } 423 | 424 | .helpmenu_selected { 425 | display: block; 426 | } 427 | 428 | .bigbox { 429 | position: absolute; 430 | top: 50%; 431 | left: 50%; 432 | transform: translate(-50%, -50%); 433 | width: 50%; 434 | height: 50%; 435 | min-width: 780px; 436 | min-height: 560px; 437 | background: white; 438 | border-radius: 10px; 439 | border: 1px solid black; 440 | } 441 | 442 | @media (max-width: 1100px) { 443 | .bigbox { 444 | left: 150px; 445 | transform: translate(0, -50%); 446 | } 447 | } 448 | @media (max-height: 800px) { 449 | .bigbox { 450 | top: 100px; 451 | bottom: 100px; 452 | transform: translate(-50%, 0); 453 | } 454 | } 455 | @media (max-width: 1100px) and (max-height: 800px) { 456 | .bigbox { 457 | left: 150px; 458 | top: 100px; 459 | bottom: 100px; 460 | transform: none; 461 | } 462 | } 463 | 464 | .helpheader { 465 | height: 30px; 466 | width: 100%; 467 | background: white; 468 | position: relative; 469 | } 470 | 471 | .helptitle { 472 | width: 15%; 473 | font-size: 22px; 474 | margin: 10px; 475 | padding-left: 1%; 476 | } 477 | 478 | .helpexit { 479 | position: absolute; 480 | right: 0; 481 | } 482 | 483 | .helpexit:hover { 484 | cursor: pointer; 485 | } 486 | 487 | .helpbody { 488 | height: 530px; 489 | width: 100%; 490 | display: flex; 491 | flex-flow: row; 492 | } 493 | 494 | .helptabs { 495 | width: 15%; 496 | height: 100%; 497 | display: flex; 498 | flex-flow: column; 499 | align-items: center; 500 | } 501 | 502 | .helpcontent { 503 | width: 85%; 504 | height: 100%; 505 | position: relative; 506 | overflow-y: scroll; 507 | padding-right: 15px; 508 | padding-left: 5px; 509 | border: 1px solid rgba(0, 0, 0, 0.3); 510 | border-radius: 2px; 511 | } 512 | 513 | .helpsectionbutton { 514 | width: 80%; 515 | text-align: center; 516 | margin: 10px; 517 | } 518 | 519 | .regextext { 520 | font-weight: 600; 521 | color: #0055bb; 522 | display: inline-block; 523 | } 524 | 525 | h2 { 526 | margin-top: 0; 527 | margin-bottom: 5px; 528 | } 529 | 530 | .helpcontent div { 531 | margin-bottom: 30px; 532 | } 533 | 534 | ::-webkit-scrollbar { 535 | width: 10px; 536 | height: 10px; 537 | } 538 | 539 | ::-webkit-scrollbar-track { 540 | background: #f1f1f1; 541 | } 542 | 543 | ::-webkit-scrollbar-thumb { 544 | background: #888; 545 | } 546 | 547 | ::-webkit-scrollbar-thumb:hover { 548 | background: #555; 549 | } 550 | 551 | /* 552 | Starting screen popup 553 | */ 554 | #startpopup { 555 | text-align: center; 556 | opacity: 0; 557 | } 558 | 559 | .pop1 { 560 | font-size: 35px; 561 | font-weight: bold; 562 | } 563 | 564 | .pop2 { 565 | font-size: 22px; 566 | } 567 | 568 | .pop3, .pop4 { 569 | font-size: 18px; 570 | } 571 | 572 | /* Popup path animation initialization */ 573 | #pa1 { 574 | stroke-dasharray: 235; 575 | stroke-dashoffset: 235; 576 | } 577 | 578 | #pa2 { 579 | stroke-dasharray: 540.68; 580 | stroke-dashoffset: 540.68; 581 | } 582 | 583 | #pa3 { 584 | stroke-dasharray: 470.68; 585 | stroke-dashoffset: 470.68; 586 | } 587 | 588 | #cp11, #cp12, #cp21, #cp22 { 589 | stroke-dasharray: 94.26; 590 | stroke-dashoffset: 94.26; 591 | } 592 | 593 | #cp31, #cp32 { 594 | stroke-dasharray: 234.26; 595 | stroke-dashoffset: 234.26; 596 | } 597 | 598 | #t1, #t2, #t3 { 599 | opacity: 0; 600 | } 601 | 602 | #startrect:hover { 603 | cursor: pointer; 604 | } 605 | 606 | #popupanim { 607 | pointer-events: none; 608 | } 609 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Regex Visualizer 16 | 17 | 18 | 19 | 20 | 77 |

78 |

79 |
80 |
81 |
82 |
83 |
84 | 85 | 86 | 87 | 88 |
89 |
90 | Help menu 91 |
92 |
93 |
94 |
95 | 96 | 97 | 98 | 99 | 100 |
101 |
102 |
103 |

Regular expressions

104 |

A regular expression is a sequence of characters that define a search pattern. 105 |
106 | The regular expressions supported by this tool may contain: 107 |

    108 |
  • Letters (a-z, A-Z)
  • 109 |
  • The quantifiers *, meaning 0 or more of the previous token, and +, meaning 1 or more of the previous token
  • 110 |
  • Alternation |, meaning either the expression to the left or to the right of the | has to match.
  • 111 |
  • Parenthesis (), to group things together
  • 112 |
  • The number 0, meaning the empty string
  • 113 |
  • Concatenation
  • 114 |
115 |

116 |

An example regular expression using these symbols is (ab|cd)+(e|0). 117 | The strings that match this regular expression are exactly the strings that start with 1 or more instances of the strings ab or cd, and after that either the character e, or nothing. 118 | Some examples of strings that match this regular expression are ab, cde, abcde, cdcdabcd.

119 |

For more information about regular expressions, click here (some different notation and way more possibilities, but the same concept)

120 |
121 |
122 |

Finite state machines

123 |

124 | A finite state machine (also called a finite automaton) is a finite set of states N, together with a transition function. 125 | This transition function takes a state and a symbol as input, and returns a set of states, a subset of N. 126 | These returned states are the states that can be reached by starting from the given input state and reading the given input symbol. 127 | The set of symbols an automaton contains is called the alphabet. 128 |

129 |

130 | A finite state machine also has one starting state, the state we are in after reading no input symbols. 131 | Finally, any of the states in N can be accepting/final. If we end up in an accepting state after reading a string, we say that string is accepted by the machine. 132 | This tool will create finite state machines, such that the accepted strings are those that exactly match the given regular expression. 133 |

134 | Here states will be represented as circles, accepting states as double circles. 135 | The transition function will be represented as directed edges between these states, meaning you can go from one state to another using the symbol in the edge's label. 136 | The starting state will have an incoming edge from nowhere. 137 |

138 |
139 |
140 |

Types of finite state machines

141 |

142 | We will define 4 different types of finite state machines, each with a more strict definition than the previous. 143 |

144 |
    145 |
  • Nondeterministic finite automaton with λ-transitions (NFA-λ):
    This is the most general, least strict type of state machine. Here, the transition function may return multiple states for any input state and symbol. 146 | This means that in some cases we have a choice what state to go to next, given an input state and symbol (nondeterminism). In the visual representation used here, this means there are multiple outgoing edges from the input state with the same symbol. The transition function is also allowed to return no states, in which case reading the input symbol from the input state is not possible. In the visual representation used here, this means there are no outgoing edges from the input state with the input symbol. 147 | The state machine may also contain λ-transitions. These transitions (edges) are labeled λ and allow you to freely move from one state to another without reading any input symbols.
  • 148 |
  • Nondeterministic finite automaton (NFA):
    This type is the same as the NFA-λ, except that λ-transitions are not allowed.
  • 149 |
  • Deterministic finite automaton (DFA):
    In deterministic finite automata, the transition function has to return exactly one state for each input state and symbol in the alphabet. 150 | This means that for every string of characters in the alphabet, there is one and at most one path you can follow. In the visual representation used here, this means that every state has exactly one outgoing edge for each symbol in the alphabet.
  • 151 |
  • Minimal deterministic finite automaton (DFAm):
    We call a DFA minimal if there is no other DFA with less states that accepts the exact same strings as the original DFA. 152 | It can be proven that for each DFA, there exists a unique DFAm, up to the naming of the states.
  • 153 |
154 |
155 |
156 |

Algorithms

157 |

158 | To convert a regular expression to a DFAm, we use 4 different algorithms, each resulting in the next type from the previous section. 159 | Here follows a brief overview of all the algorithms, but for more in depth explanations, I used the book "Introduction to Languages and the Theory of Computation" by John C. Martin. 160 |

161 |

162 | The first algorithm converts a regular expression to a NFA-λ. This is done using a bottom-up construction. 163 | We start with finite state machines for a single character, and 3 template state machines for the 3 main operations (alternation, concatenation and the quantifiers). 164 | We then substitute the simple state machines into the template machine we need, until we have a finite state machine for the entire regular expression. 165 | Finally, some basic simplification is done to make the state machine more readable. 166 |

167 |

168 | The second algorithm converts a NFA-λ to a NFA by replacing the λ-transitions with normal transitions. 169 | We start off by determining the λ-closure of each state: The set of states that can be reached from that state using only λ-transitions. Next, we make any state whose λ-closure contains an accepting state also accepting. 170 | Then, for every state p and for every state r in the λ-closure of p, we duplicate all the normal outgoing edges from r, and make their starting point p. 171 | Finally, we remove all the λ-transitions, and if during this process any of the states have become unreachable from the starting state, we remove them. 172 |

173 |

174 | The third algorithm converts a NFA to a DFA using an algorithm called "subset construction". 175 | We start with a new starting state that corresponds to the old starting state. We then determine for each of the symbols in the alphabet, the set of states can be reached from the starting state using that symbol. 176 | If there is not yet a new state corresponding to this old set of states, we create this new state and add an edge from the starting state to this new state with the given symbol. 177 | If there is already a new state corresponding to this old set of states, we simply add an edge from the starting state to this state with the given symbol. 178 | We continue this process for each of the newly created states, until all of the new states have an outgoing edge for every symbol in the alphabet. 179 | A new state in this DFA is accepting, if any of the corresponding old states were accepting in the NFA. 180 |

181 |

182 | The last algorithm converts a DFA to a DFAm by minimizing it. 183 | This is done by determining which of the states of the DFA are equivalent, and merging them. Two states are called equivalent, 184 | if the strings that cause you to end up in an accepting state starting from that state, are exactly the same for both states. 185 | To determine which states are equivalent, we start off by marking each pair of states as equivalent, and step by step mark pairs as not equivalent if we find proof they are not. 186 | In the first step, we mark every pair of states, where one of the states is accepting and the other one is non-accepting, as non-equivalent, since for example the empty string causes one of them to end up in an accepting state, but not the other one. 187 | In each of the next steps, we take a pair of states that is marked equivalent, and for each symbol in the alphabet, we check if the new pair of states that we get from following the edge with that symbol from the starting pair, is marked as equivalent. 188 | If this new pair is marked as not-equivalent, we have found a symbol that leads the original pair of states to two non-equivalent states. This implies that the original pair of states is also not equivalent, and so we mark it as such. 189 | We do this until we cannot mark any more pairs as non-equivalent. Finally, we merge the states that have been found to be equivalent, and we are done. 190 |

191 |
192 |
193 |

Features

194 |
    195 |
  • Convert a regular expression into a DFAm, using custom animations
  • 196 |
  • This gives a very visual way to see what strings match the regular expression and what strings don't
  • 197 |
  • Smooth automatic animations, or animations in steps, with information about the current step
  • 198 |
  • Hover over any of the states, to see an infobox with some example strings that can end up in that state
  • 199 |
  • Pan and zoom the state machine around by dragging the screen and using your mousewheel, to get a better look at large and complicated state machines
  • 200 |
  • A semi-in-depth help menu you are looking at right now!
  • 201 |
202 |
203 |
204 |
205 |
206 |
207 |

Welcome!

208 |

This tool is used to visualize which strings exactly match
a regular expression and which don't, using finite state machines

209 |

You can start playing around now by pressing "Example expression" or by typing in your own regular expression

210 |

Press the "help" button at any time for more information about the application

211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 1 219 | 220 | 221 | 222 | 223 | 224 | 225 | 2 226 | 227 | 228 | 229 | 230 | 231 | 232 | Start! 233 | 234 | 235 |
236 | 237 | 238 | 239 | -------------------------------------------------------------------------------- /js/animation.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /* 4 | Wrapper function for all main animations. Takes a animation function in as argument 5 | and keeps a queue containing the next animation steps, that either get triggered 6 | automatically, or with the animation step button if auto_animation is off 7 | */ 8 | function animationWrap(animationFunction, args, step) { 9 | in_animation = true; 10 | var queue = []; // Queue containing the next animation steps that are ready to go 11 | queue.notify = function(message) { // Custom queue function to notify queue when a new element gets added 12 | if (queue == null) // Animation was skipped during this step, stop 13 | return; 14 | if (auto_animation) 15 | (queue.shift())(); // Execute the first animation in the queue 16 | else { // Not auto animnation, wait for click 17 | if (message != undefined) 18 | $("#step_message").html("Next step: " + message); 19 | $("#options_button").one("click", function() { 20 | $("#step_message").html(""); 21 | (queue.shift())() 22 | }); 23 | } 24 | } 25 | queue.done = $.Deferred(); // Gets resolved when last step of animation is done 26 | $.when(queue.done).then(function() { 27 | in_animation = false; 28 | $("#FA animate").remove(); // Delete the used animation objects 29 | $("#FA animateTransform").remove(); 30 | if (step <= 4) 31 | $("#step_message").html(""); 32 | $("#skip_button").unbind(); // Remove old event handlers 33 | $("#options_button").unbind(); 34 | $("#skip_button").css("visibility", "hidden"); 35 | queue = null; 36 | }) 37 | $("#step_message").html(""); 38 | args.push(queue); 39 | animationFunction.apply(this, args); // Start animation function 40 | $("#skip_button").css("visibility", "visible"); 41 | $("#skip_button").click(function() { 42 | skipAnimation(step, queue); 43 | }) 44 | return queue.done; 45 | } 46 | 47 | function skipAnimation(step, queue) { 48 | if (step == 5 || step == 6) { 49 | $("#graphdouble").remove(); 50 | if (step == 5) { 51 | doubleGraph($("#graph0"), "#31eb37", false); // Green 52 | $("#step_message").html("The string matches the regular expression!"); 53 | } 54 | else { 55 | doubleGraph($("#graph0"), "#e61515", false); // Red 56 | $("#step_message").html("The string doesn't match the regular expression!"); 57 | } 58 | queue.done.resolve(); 59 | return; 60 | } 61 | $.when(instance.FAstrings[step]).then(function(element) { 62 | var FA = step == 0 ? instance.NFAl 63 | : step == 2 ? instance.NFA 64 | : step == 3 ? instance.DFA 65 | : step == 4 ? instance.DFAm 66 | : undefined; 67 | instance.FAobj[step] = FAToHTML(FA, element); 68 | $("#FA").html(instance.FAobj[step].svg).promise().then(function() { 69 | updateScale($("#FA").children()[0]); 70 | }) 71 | queue.done.resolve(); 72 | }) 73 | } 74 | 75 | // Small intro animation (moving header up, not start popup path) 76 | function introAnimation() { 77 | $(".helpbutton2, .examplebutton").css("border", "none") 78 | $(".beginbuttons").animate({ 79 | 'height': '0', 80 | 'opacity': '0' 81 | }, 700).promise().then(function() { 82 | $(".beginbuttons").css("display", "none"); 83 | $("#convert, #regex").css("margin", "10px auto"); 84 | }) 85 | return $("#header").animate({ 86 | 'height': '110px' 87 | }, 700).promise().then(function() { 88 | $("#line").css({ 89 | display: 'block' 90 | }); 91 | $("#FA").css({ 92 | display: 'flex' 93 | }); 94 | }).then(function() { 95 | $("#line").animate({ 96 | 'opacity': '1' 97 | }, 200); 98 | $("#help").animate({ 99 | 'opacity': '1' 100 | }, 200); 101 | $("#options").animate({ 102 | 'opacity': '1' 103 | }, 200); 104 | $("#next_step").animate({ 105 | 'opacity': '1' 106 | }, 200); 107 | return $("#previous_step").animate({ 108 | 'opacity': '1' 109 | }, 200).promise(); 110 | }); 111 | } 112 | 113 | function startPopupAnimation() { // Start popup animation 1, 2, start 114 | $("#pa1").animate({ 115 | "stroke-dashoffset": "0" 116 | }, 600).promise().then(function() { 117 | $("#an1")[0].beginElement(); 118 | $("#popupanim").delay(200).promise().then(function() { 119 | $("#cp11, #cp12").animate({ 120 | "stroke-dashoffset": "0" 121 | }, 400).promise().then(function() { 122 | $("#t1").animate({"opacity": 1}, 300); 123 | $("#pa2").animate({ 124 | "stroke-dashoffset": "0" 125 | }, 600).promise().then(function() { 126 | $("#an2")[0].beginElement(); 127 | $("#popupanim").delay(200).promise().then(function() { 128 | $("#cp21, #cp22").animate({ 129 | "stroke-dashoffset": "0" 130 | }, 400).promise().then(function() { 131 | $("#t2").animate({"opacity": 1}, 300); 132 | $("#pa3").animate({ 133 | "stroke-dashoffset": "0" 134 | }, 600).promise().then(function() { 135 | $("#an3")[0].beginElement(); 136 | $("#popupanim").delay(200).promise().then(function() { 137 | $("#cp31, #cp32").animate({ 138 | "stroke-dashoffset": "0" 139 | }, 700).promise().then(function() { 140 | $("#t3").animate({"opacity": 1}, 300); 141 | $("#popupanim").css("pointer-events", "all"); 142 | }) 143 | }) 144 | }) 145 | }) 146 | }) 147 | }) 148 | }) 149 | }) 150 | }) 151 | } 152 | 153 | 154 | /* 155 | Functions for showing an FA (animating in) 156 | */ 157 | function showFA(FAobj, queue) { 158 | var timeper = Math.max(Math.min(4000 / FAobj.states.length, 500), 250); // Animation time between 250 and 500 ms 159 | 160 | return showEdge(FAobj.start[0], timeper).then(function() { // Show starting edge 161 | queue.push(function() { 162 | showFAParts(FAobj, [FAobj.start[1].toString()], [], [$("polygon", FAobj.start[0])[0].points[1]], queue) 163 | }); 164 | queue.notify("Show state(s) " + FAobj.start[1] + " and outgoing edges" ); 165 | }); 166 | } 167 | 168 | function showFAParts(FAobj, curstates, visited, entrypoints, queue) { // Show the states in curstates and their outgoing edges 169 | // Entrypoints are the coordinates at the tips of arrow of the edges that made 170 | // curstates. These are needed for the precise animation of the circles 171 | var timeper = Math.max(Math.min(4000 / FAobj.states.length, 500), 250); // Animation time between 250 and 500 ms 172 | var nextentrypoints = []; 173 | var nextstates = []; 174 | var p1, p2, p3; 175 | 176 | for (var i in curstates) { 177 | p1 = showState(FAobj.states[curstates[i]], entrypoints[i], timeper); 178 | } 179 | p2 = $.when(p1).then(function() { 180 | for (var i in curstates) { 181 | for (var to in FAobj.edges[curstates[i]]) { 182 | p3 = showEdge(FAobj.edges[curstates[i]][to][1], timeper); 183 | if (!curstates.includes(to) && !visited.includes(to) && !nextstates.includes(to)) { 184 | nextstates.push(to); 185 | nextentrypoints.push($("polygon", FAobj.edges[curstates[i]][to][1])[0].points[1]); 186 | } 187 | } 188 | /*for (var symbol in FAobj.edges[curstates[i]]) { 189 | FAobj.edges[curstates[i]][symbol].forEach(function(val) { 190 | p2 = showEdge(val[1], timeper); 191 | if (!curstates.includes(val[0]) && !visited.includes(val[0]) && !nextstates.includes(val[0])) { 192 | nextstates.push(val[0]); 193 | nextentrypoints.push($("polygon", val[1])[0].points[1]); 194 | } 195 | }); 196 | }*/ 197 | visited.push(curstates[i]); 198 | } 199 | return p3; 200 | }); 201 | 202 | return $.when(p2).then(function() { 203 | if (nextstates.length == 0) { 204 | queue.done.resolve(); // Notify queue that the animation is finished 205 | return; 206 | } 207 | queue.push(function() { 208 | showFAParts(FAobj, nextstates, visited, nextentrypoints, queue) 209 | }); 210 | queue.notify("Show state(s) " + nextstates.join(",") + " and outgoing edges"); 211 | }); 212 | } 213 | 214 | function showState(state, entrypoint, time) { 215 | var circles = $("ellipse", state); 216 | var cx = parseFloat(circles.attr("cx")); 217 | var cy = parseFloat(circles.attr("cy")); 218 | var distance = Math.sqrt(Math.pow(cx - entrypoint.x, 2) + Math.pow(cy - entrypoint.y, 2)); // Distance from center to entrypoint 219 | var startx, starty, endx, endy; 220 | var color = circles.attr("stroke"); 221 | var strokewidth = circles.attr("stroke-width"); 222 | var p1; 223 | for (var i = 0; i < circles.length; i++) { 224 | var r = circles[i].rx.baseVal.value; 225 | // Arrows slightly stick out into the states, calculate actual entrypoint and endpoint 226 | startx = cx - (cx - entrypoint.x) * r / distance; 227 | starty = cy - (cy - entrypoint.y) * r / distance; 228 | endx = 2 * cx - startx; 229 | endy = 2 * cy - starty; 230 | 231 | var curve1 = document.createElementNS('http://www.w3.org/2000/svg', "path"); 232 | var curve2 = document.createElementNS('http://www.w3.org/2000/svg', "path"); 233 | curve1.setAttribute("fill", "none"); 234 | curve2.setAttribute("fill", "none"); 235 | curve1.setAttribute("stroke", color); 236 | curve2.setAttribute("stroke", color); 237 | curve1.setAttribute("stroke-width", strokewidth); 238 | curve2.setAttribute("stroke-width", strokewidth); 239 | curve1.setAttribute("d", "M" + startx + "," + starty + " A" + r + " " + r + " 0 0 0 " + endx + " " + endy); 240 | curve2.setAttribute("d", "M" + startx + "," + starty + " A" + r + " " + r + " 0 1 1 " + endx + " " + endy); 241 | curve1.setAttribute("class", "ellipsepath"); 242 | curve2.setAttribute("class", "ellipsepath"); 243 | 244 | var len = curve1.getTotalLength(); 245 | $(curve1).css({ 246 | 'stroke-dasharray': len, 247 | 'stroke-dashoffset': len 248 | }); 249 | len = curve2.getTotalLength(); 250 | $(curve2).css({ 251 | 'stroke-dasharray': len, 252 | 'stroke-dashoffset': len 253 | }); 254 | state[0].appendChild(curve1); 255 | state[0].appendChild(curve2); 256 | $(curve1).animate({ 257 | 'stroke-dashoffset': 0 258 | }, time); 259 | p1 = $(curve2).animate({ 260 | 'stroke-dashoffset': 0 261 | }, time).promise(); 262 | } 263 | $.when(p1).then(function(path) { // Remove the animation paths and replace them with the actual circle 264 | $("ellipse", path.parent()).attr("visibility", "visible"); 265 | $("text", path.parent()).animate({ 266 | "opacity": 1 267 | }, 220); 268 | $(".ellipsepath", path.parent()).remove(); 269 | $("animate", state).remove(); // Remove old animation objects 270 | }) 271 | return p1; 272 | } 273 | 274 | function unshowState(state, entrypoint, time) { // Opposite of showState 275 | // (in this case entrypoint is actually the exit point for the next edge) 276 | $("ellipse", state).attr("visibility", "hidden"); // Hide ellipses and replace them with animated paths 277 | var circles = $("ellipse", state); 278 | var cx = parseFloat(circles.attr("cx")); 279 | var cy = parseFloat(circles.attr("cy")); 280 | var distance = Math.sqrt(Math.pow(cx - entrypoint.x, 2) + Math.pow(cy - entrypoint.y, 2)); // Distance from center to entrypoint 281 | var startx, starty, endx, endy; 282 | var color = circles.attr("stroke"); 283 | var strokewidth = circles.attr("stroke-width"); 284 | var p1; 285 | for (var i = 0; i < circles.length; i++) { 286 | var r = circles[i].rx.baseVal.value; 287 | // Arrows slightly stick out into the states, calculate actual entrypoint and endpoint 288 | startx = cx + (cx - entrypoint.x) * r / distance; 289 | starty = cy + (cy - entrypoint.y) * r / distance; 290 | endx = 2 * cx - startx; 291 | endy = 2 * cy - starty; 292 | 293 | var curve1 = document.createElementNS('http://www.w3.org/2000/svg', "path"); 294 | var curve2 = document.createElementNS('http://www.w3.org/2000/svg', "path"); 295 | curve1.setAttribute("fill", "none"); 296 | curve2.setAttribute("fill", "none"); 297 | curve1.setAttribute("stroke", color); 298 | curve2.setAttribute("stroke", color); 299 | curve1.setAttribute("stroke-width", strokewidth); 300 | curve2.setAttribute("stroke-width", strokewidth); 301 | curve1.setAttribute("d", "M" + startx + "," + starty + " A" + r + " " + r + " 0 0 0 " + endx + " " + endy); 302 | curve2.setAttribute("d", "M" + startx + "," + starty + " A" + r + " " + r + " 0 1 1 " + endx + " " + endy); 303 | curve1.setAttribute("class", "ellipsepath"); 304 | curve2.setAttribute("class", "ellipsepath"); 305 | 306 | var len = curve1.getTotalLength(); 307 | $(curve1).css({ 308 | 'stroke-dasharray': len, 309 | 'stroke-dashoffset': 0 310 | }); 311 | len = curve2.getTotalLength(); 312 | $(curve2).css({ 313 | 'stroke-dasharray': len, 314 | 'stroke-dashoffset': 0 315 | }); 316 | state[0].appendChild(curve1); 317 | state[0].appendChild(curve2); 318 | $(curve1).animate({ 319 | 'stroke-dashoffset': -len 320 | }, time); 321 | p1 = $(curve2).animate({ 322 | 'stroke-dashoffset': -len 323 | }, time).promise(); 324 | } 325 | $.when(p1).then(function(path) { // Remove the animation paths 326 | $(".ellipsepath", path.parent()).remove(); 327 | $("animate", state).remove(); // Remove old animation objects 328 | }) 329 | return p1; 330 | } 331 | 332 | function showEdge(edge, time) { 333 | var path = $("path", edge); 334 | var pol = $("polygon", edge); 335 | var length = path[0].getTotalLength(); 336 | return path.css("stroke-dashoffset", length).animate({ 337 | 'stroke-dashoffset': 0 338 | }, length / (length + 10) * time).promise().then(function() { 339 | var points = pol[0].points; 340 | var anim = document.createElementNS('http://www.w3.org/2000/svg', "animate"); 341 | var attrs = { 342 | attributeName: "points", 343 | attributeType: "XML", 344 | begin: "indefinite", 345 | dur: (10 / (length + 10) * time) + "ms", 346 | fill: "freeze", 347 | from: points[0].x + " " + points[0].y + " " + points[0].x + " " + points[0].y + " " + points[2].x + " " + points[2].y + " " + points[2].x + " " + points[2].y + " " + points[0].x + " " + points[0].y + " ", 348 | to: points[0].x + " " + points[0].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[2].x + " " + points[2].y + " " + points[0].x + " " + points[0].y + " " 349 | }; 350 | for (var k in attrs) 351 | anim.setAttribute(k, attrs[k]); 352 | pol.attr("visibility", "visible"); 353 | pol[0].appendChild(anim); 354 | $("animate", pol)[0].beginElement(); 355 | return edge.delay(10 / (length + 10) * time).promise().then(function() { // Artifical delay, same time as animation time 356 | $("text", edge).animate({"opacity": 1}, 220); 357 | $("animate", edge).remove(); // Remove old animation objects 358 | }) 359 | }) 360 | } 361 | 362 | function showEdge2(edge, time) { // Show edge the other way around (first polygon then path) 363 | var path = $("path", edge); 364 | var pol = $("polygon", edge); 365 | var length = path[0].lengthsaved; 366 | var points = pol[0].points; 367 | var anim = document.createElementNS('http://www.w3.org/2000/svg', "animate"); 368 | var attrs = { 369 | attributeName: "points", 370 | attributeType: "XML", 371 | begin: "indefinite", 372 | dur: (10 / (length + 10) * time) + "ms", 373 | fill: "freeze", 374 | from: points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " ", 375 | to: points[0].x + " " + points[0].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[2].x + " " + points[2].y + " " + points[0].x + " " + points[0].y + " " 376 | }; 377 | for (var k in attrs) 378 | anim.setAttribute(k, attrs[k]); 379 | pol.attr("visibility", "visible"); 380 | pol[0].appendChild(anim); 381 | $("animate", pol)[0].beginElement(); 382 | return edge.delay(10 / (length + 10) * time).promise().then(function() { // Artifical delay, same time as animation time 383 | return path.css("stroke-dashoffset", -length).animate({ 384 | 'stroke-dashoffset': 0 385 | }, length / (length + 10) * time).promise().then(function() { 386 | $("text", edge).animate({"opacity": 1}, 220); 387 | $("animate", edge).remove(); // Remove old animation objects 388 | }); 389 | }) 390 | } 391 | 392 | function unshowEdge(edge, time) { // Opposite of showEdge. First animate out polygon, then path 393 | var path = $("path", edge); 394 | var pol = $("polygon", edge); 395 | var length = path[0].lengthsaved; 396 | var points = pol[0].points; 397 | var anim = document.createElementNS('http://www.w3.org/2000/svg', "animate"); 398 | var attrs = { 399 | attributeName: "points", 400 | attributeType: "XML", 401 | begin: "indefinite", 402 | dur: (10 / (length + 10) * time) + "ms", 403 | fill: "freeze", 404 | from: points[0].x + " " + points[0].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[2].x + " " + points[2].y + " " + points[0].x + " " + points[0].y + " ", 405 | to: points[0].x + " " + points[0].y + " " + points[0].x + " " + points[0].y + " " + points[2].x + " " + points[2].y + " " + points[2].x + " " + points[2].y + " " + points[0].x + " " + points[0].y + " " 406 | }; 407 | for (var k in attrs) 408 | anim.setAttribute(k, attrs[k]); 409 | pol[0].appendChild(anim); 410 | $("animate", pol)[0].beginElement(); 411 | return edge.delay(10 / (length + 10) * time).promise().then(function() { // Artifical delay, same time as animation time 412 | pol.attr("visibility", "hidden"); 413 | return path.css("stroke-dashoffset", 0).animate({ 414 | 'stroke-dashoffset': length 415 | }, length / (length + 10) * time).promise().then(function() { 416 | $("text", edge).animate({"opacity": 0}, 220); 417 | $("animate", edge).remove(); // Remove old animation objects 418 | }); 419 | }) 420 | } 421 | 422 | function unshowEdge2(edge, time) { // Opposite of showEdge. First animate out path, then polygon 423 | var path = $("path", edge); 424 | var pol = $("polygon", edge); 425 | var length = path[0].getTotalLength(); 426 | return path.css("stroke-dashoffset", 0).animate({ 427 | 'stroke-dashoffset': -length 428 | }, length / (length + 10) * time).promise().then(function() { 429 | var points = pol[0].points; 430 | var anim = document.createElementNS('http://www.w3.org/2000/svg', "animate"); 431 | var attrs = { 432 | attributeName: "points", 433 | attributeType: "XML", 434 | begin: "indefinite", 435 | dur: (10 / (length + 10) * time) + "ms", 436 | fill: "freeze", 437 | from: points[0].x + " " + points[0].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[2].x + " " + points[2].y + " " + points[0].x + " " + points[0].y + " ", 438 | to: points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " + points[1].x + " " + points[1].y + " " 439 | }; 440 | for (var k in attrs) 441 | anim.setAttribute(k, attrs[k]); 442 | pol[0].appendChild(anim); 443 | $("animate", pol)[0].beginElement(); 444 | return edge.delay(10 / (length + 10) * time).promise().then(function() { // Artifical delay, same time as animation time 445 | pol.attr("visibility", "hidden"); 446 | $("animate", edge).remove(); // Remove old animation objects 447 | }) 448 | }) 449 | } 450 | 451 | /* 452 | Convert NFAl to NFA by removing l-transitions 453 | (and updating accepting states and removing unreachable states) 454 | */ 455 | function animToNFAStart(FAobjs, queue) { // Starting animations: Rearrange and add accepting states 456 | if (FAobjs[1] == undefined || FAobjs[1].newedges == undefined) { 457 | $("#step_message").html("The automaton doesn't contain any λ-transitions. Continuing.."); 458 | FAobjs[2].svg.attr("viewBox", FAobjs[0].svg.attr("viewBox")); 459 | FAobjs[2].svg.children().attr("transform", FAobjs[0].svg.children().attr("transform")); 460 | $("#FA").html(FAobjs[2].svg).promise().then(function() { 461 | updateScale($("#FA").children()[0]); 462 | }) 463 | $("#FA").delay(1000).promise().then(function() { 464 | queue.done.resolve(); 465 | }) 466 | return; 467 | } 468 | 469 | var newedges = false; 470 | for (var from in FAobjs[1].newedges) { 471 | for (var to in FAobjs[1].newedges[from]) { 472 | if (FAobjs[1].newedges[from][to].length != 0) 473 | newedges = true; 474 | if (newedges) break 475 | } 476 | if (newedges) break 477 | } 478 | 479 | var fMoveo = function() { 480 | return moveo(FAobjs[0], FAobjs[1]); 481 | } 482 | var fAccepting = function() { 483 | var p1; 484 | for (var i = 0; i < FAobjs[1].newaccepting.length; i++) 485 | p1 = $($("ellipse",FAobjs[1].newaccepting[i][1])[1]).animate({"opacity": "1"}, 400).promise(); 486 | return p1; 487 | } 488 | var fReplace = function() { 489 | replaceEdges(FAobjs, 0, queue); 490 | } 491 | 492 | if (FAobjs[1].newaccepting.length == 0) { // No new accepting states, skip that step 493 | if (!newedges) { // No new edges, no need to rearrange 494 | FAobjs[1].svg.attr("viewBox", FAobjs[0].svg.attr("viewBox")); 495 | FAobjs[1].svg.children().attr("transform", FAobjs[0].svg.children().attr("transform")); 496 | $("#FA").html(FAobjs[1].svg).promise().then(function() { 497 | updateScale($("#FA").children()[0]); 498 | }) 499 | fReplace(); 500 | } 501 | else { // Has new edges, rearrange first 502 | queue.push(function() { 503 | fMoveo().then(fReplace); 504 | }) 505 | queue.notify("Rearrange graph to make space for the new edges"); 506 | } 507 | } 508 | else { // Does have new accepting states 509 | if (!newedges) { // No new edges, no need to rearrange 510 | queue.push(function() { 511 | FAobjs[1].svg.attr("viewBox", FAobjs[0].svg.attr("viewBox")); 512 | FAobjs[1].svg.children().attr("transform", FAobjs[0].svg.children().attr("transform")); 513 | $("#FA").html(FAobjs[1].svg).promise().then(function() { 514 | updateScale($("#FA").children()[0]); 515 | }) 516 | fAccepting().then(fReplace); 517 | }) 518 | var str = ""; 519 | for (var i = 0; i < FAobjs[1].newaccepting.length-1; i++) 520 | str += FAobjs[1].newaccepting[i][0] + ", "; 521 | str += FAobjs[1].newaccepting[FAobjs[1].newaccepting.length-1][0]; 522 | queue.notify("Mark all states that can reach an accepting state using only λ-transitions as accepting (states " + str + ")"); 523 | } 524 | else { // Has new edges, rearrange first 525 | queue.push(function() { 526 | fMoveo().then(function() { 527 | queue.push(function() { 528 | fAccepting().then(fReplace); 529 | }) 530 | var str = ""; 531 | for (var i = 0; i < FAobjs[1].newaccepting.length-1; i++) 532 | str += FAobjs[1].newaccepting[i][0] + ", "; 533 | str += FAobjs[1].newaccepting[FAobjs[1].newaccepting.length-1][0]; 534 | queue.notify("Mark all states that can reach an accepting state using only λ-transitions as accepting (states " + str + ")"); 535 | }) 536 | }) 537 | queue.notify("Rearrange graph to make space for the new edges"); 538 | } 539 | } 540 | } 541 | 542 | function animToNFAFinish(FAobjs, queue) { // Finishing animations: Remove unreachable states and rearrange 543 | if (FAobjs[1].unreachable.length == 0) { // No unreachable states, skip that step 544 | queue.push(function() { 545 | moveo(FAobjs[1], FAobjs[2]).then(function() { 546 | queue.done.resolve(); 547 | }) 548 | }) 549 | queue.notify("Rearrange for clarity") 550 | } 551 | else { 552 | queue.push(function() { 553 | var p1; 554 | for (var from in FAobjs[1].edges) { 555 | for (var to in FAobjs[1].edges[from]) { 556 | for (var i = 0; i < FAobjs[1].unreachable.length; i++) { 557 | if (from == FAobjs[1].unreachable[i][0] || to == FAobjs[1].unreachable[i][0]) { 558 | unshowEdge(FAobjs[1].edges[from][to][1],200); 559 | break; 560 | } 561 | } 562 | } 563 | } 564 | for (var i = 0; i < FAobjs[1].unreachable.length; i++) { 565 | p1 = FAobjs[1].unreachable[i][1].delay(200).promise().then(function() { 566 | return this.animate({"opacity": 0}, 300).promise(); 567 | }) 568 | } 569 | $.when(p1).then(function() { 570 | queue.push(function() { 571 | moveo(FAobjs[1], FAobjs[2], FAobjs[1].unreachable).then(function() { 572 | queue.done.resolve(); 573 | }) 574 | }) 575 | queue.notify("Rearrange for clarity") 576 | }) 577 | }) 578 | 579 | var str = ""; 580 | for (var i = 0; i < FAobjs[1].unreachable.length-1; i++) 581 | str += FAobjs[1].unreachable[i][0] + ", "; 582 | str += FAobjs[1].unreachable[FAobjs[1].unreachable.length-1][0]; 583 | queue.notify("Remove unreachable states from the starting state (remove states " + str + ")") 584 | } 585 | } 586 | 587 | function replaceEdges(FAobjs, state, queue) { // Main animations: Replace the lambda transitions 588 | var FAobj = FAobjs[1]; 589 | if (FAobj.states.length == state) { // Reached last state, play finishing animation 590 | animToNFAFinish(FAobjs, queue); 591 | return; 592 | } 593 | var hasledges = false; 594 | for (var to in FAobj.edges[state]) { 595 | if (FAobj.edges[state][to][0].includes("0")) { 596 | hasledges = true; 597 | break; 598 | } 599 | } 600 | if (!hasledges) { 601 | replaceEdges(FAobjs, state+1, queue); // Doesnt have ledges, go to next state 602 | return; 603 | } 604 | for (var to in FAobj.edges[state]) { 605 | if (FAobj.edges[state][to][0].includes("0")) { 606 | var edge = FAobj.edges[state][to][1]; // Mark lambda edges red to be removed in the next step 607 | var path = $("path", edge); 608 | var pol = $("polygon", edge); 609 | path.attr("stroke", "#ff0000"); 610 | pol.attr("fill", "#ff0000"); 611 | pol.attr("stroke", "#ff0000"); 612 | $("text", edge).attr("fill", "#ff0000"); 613 | } 614 | } 615 | queue.push(function() { 616 | var p1; 617 | for (var to in FAobj.edges[state]) { 618 | if (FAobj.edges[state][to][0].includes("0")) { 619 | var edge = FAobj.edges[state][to]; 620 | edge[0].splice(edge[0].indexOf("0"), 1); 621 | if (edge[0].length == 0) { // Transition only consists of lambda, can remove it entirely 622 | p1 = unshowEdge(edge[1], 1000); // Unshow the edges 623 | } 624 | else { // Transition also contains other symbols, just change text 625 | $("text", edge[1]).text(edge[0].join(",")); 626 | var te = $("text", edge[1])[0]; 627 | var index = te.innerHTML.indexOf("0"); 628 | if (index != -1) 629 | te.innerHTML = te.innerHTML.substr(0, index) + "λ" + te.innerHTML.substr(index+1); 630 | } 631 | } 632 | } 633 | var showEdgeAndText = function(edge) { 634 | return showEdge(edge[1], 1000).then(function() { 635 | $("text", edge[1]).text(edge[0].join(",")); // Set new text of edge 636 | var te = $("text", edge[1])[0]; 637 | var index = te.innerHTML.indexOf("0"); 638 | if (index != -1) 639 | te.innerHTML = te.innerHTML.substr(0, index) + "λ" + te.innerHTML.substr(index+1); 640 | }); 641 | } 642 | 643 | $.when(p1).then(function() { 644 | var p2; 645 | for (var to in FAobj.newedges[state]) { 646 | if (FAobj.newedges[state][to].length == 0) 647 | continue; 648 | var edge = FAobj.edges[state][to]; 649 | var path = $("path", edge[1]); // Make edge black again if previously colored red 650 | var pol = $("polygon", edge[1]); 651 | path.attr("stroke", "#000000"); 652 | pol.attr("fill", "#000000"); 653 | pol.attr("stroke", "#000000"); 654 | $("text", edge[1]).attr("fill", "#000000"); 655 | edge[0] = edge[0].concat(FAobj.newedges[state][to]); 656 | if (edge[0].length == FAobj.newedges[state][to].length) { // Transition didnt exist yet, show it 657 | p2 = showEdgeAndText(edge); 658 | } 659 | } 660 | return p2; 661 | }).then(function() { 662 | replaceEdges(FAobjs, state+1, queue); 663 | }) 664 | }) 665 | queue.notify("Replace λ-transitions from state " + state + " with corresponding non-λ-transitions") 666 | } 667 | 668 | function moveo(FAold, FAnew, unreachable) { // Animate an automaton to another automaton with the same states and edges, possibly with shifted states 669 | // Compute which old state numbers correspond to which new state numbers using a list of the unreachable states from the old automaton 670 | var stepdown = []; 671 | for (var i = 0; i < FAold.states.length; i++) 672 | stepdown[i] = 0 673 | if (unreachable != undefined) { 674 | for (var i = 0; i < unreachable.length; i++) { 675 | for (var j = unreachable[i][0]; j < FAold.states.length; j++) { 676 | stepdown[j]++; 677 | } 678 | } 679 | } 680 | var newtoold = [] 681 | for (var i = 0; i < FAnew.states.length; i++) { 682 | var j = 0; 683 | for (; j < FAold.states.length; j++) { 684 | if (j-stepdown[j]==i) { 685 | newtoold[i] = j 686 | break; 687 | } 688 | } 689 | } 690 | 691 | $("#graph0 text", FAnew.svg).attr("visibility", "hidden"); // Hide all text temporarily 692 | var edges = $(".edge", FAnew); 693 | var states = $(".node", FAnew); 694 | $("text", FAold.svg).finish(); // Finish all ongoing text animations instantly 695 | return $("#graph0 text", FAold.svg).animate({ 696 | "opacity": 0 697 | }, 300).promise().then(function() { 698 | $("#FA").html(FAnew.svg).promise().then(function() { // Replace with actual DFAm 699 | updateScale($("#FA").children()[0]); 700 | }) 701 | // Animate transform translate to new value 702 | var anim = document.createElementNS('http://www.w3.org/2000/svg', "animateTransform"); 703 | var attrs = { 704 | attributeName: "transform", 705 | attributeType: "XML", 706 | type: "translate", 707 | begin: "indefinite", 708 | fill: "freeze", 709 | from: FAold.svg.children().eq(0).attr("transform").match(/translate\((.*)\)/)[1], 710 | to: FAnew.svg.children().eq(0).attr("transform").match(/translate\((.*)\)/)[1], 711 | dur: "1s" 712 | } 713 | for (var k in attrs) 714 | anim.setAttribute(k, attrs[k]); 715 | FAnew.svg.children()[0].appendChild(anim); 716 | anim.beginElement(); 717 | // Animate viewbox to new value 718 | var anim2 = document.createElementNS('http://www.w3.org/2000/svg', "animate"); 719 | var attrs2 = { 720 | attributeName: "viewBox", 721 | attributeType: "XML", 722 | begin: "indefinite", 723 | fill: "freeze", 724 | from: FAold.svg.attr("viewBox"), 725 | to: FAnew.svg.attr("viewBox"), 726 | dur: "1s" 727 | } 728 | for (var k in attrs2) 729 | anim2.setAttribute(k, attrs2[k]); 730 | FAnew.svg[0].appendChild(anim2); 731 | anim2.beginElement(); 732 | $("text", FAold.svg).css("opacity", 1) 733 | for (var i = 0; i < FAnew.states.length; i++) { 734 | var oldi = newtoold[i]; 735 | moveState(FAnew.states[i], 736 | [$("ellipse", FAold.states[oldi]).attr("cx"), $("ellipse", FAold.states[oldi]).attr("cy")], 737 | [$("ellipse", FAnew.states[i]).attr("cx"), $("ellipse", FAnew.states[i]).attr("cy")], 1000); 738 | for (var to in FAnew.edges[i]) { 739 | var edgeNew = FAnew.edges[i][to]; 740 | var edgeOld = FAold.edges[oldi][newtoold[to]]; 741 | if (edgeOld != undefined && edgeNew != undefined && ArrayEquals(edgeOld[0], edgeNew[0])) 742 | moveEdge(edgeNew[1], 743 | [$("path", edgeOld[1]).attr("d"), $("polygon", edgeOld[1]).attr("points")], 744 | [$("path", edgeNew[1]).attr("d"), $("polygon", edgeNew[1]).attr("points")], 1000); 745 | } 746 | } 747 | moveEdge(FAnew.start[0], 748 | [$("path", FAold.start[0]).attr("d"), $("polygon", FAold.start[0]).attr("points")], 749 | [$("path", FAnew.start[0]).attr("d"), $("polygon", FAnew.start[0]).attr("points")], 1000); 750 | return $("#FA svg").delay(1000).promise().then(function() { 751 | $("#graph0 text", FAnew.svg).attr("visibility", "visible"); // Reshow text 752 | for (var i = 0; i < FAnew.edges.length; i++) { // Animate in text 753 | for (var to in FAnew.edges[i]) 754 | if (FAnew.edges[i][to][0].length != 0) 755 | $("text", FAnew.edges[i][to][1]).css("opacity",0).animate({"opacity": 1}, 300); 756 | $("text", FAnew.states[i]).css("opacity",0).animate({"opacity": 1}, 300); 757 | } 758 | $("#FA animate").remove(); // Delete the used animation objects 759 | }); 760 | }) 761 | } 762 | function moveEdge(edge, from, to, time) { // Animate edge from one position to another. From and to contain the d and points attributes 763 | $("path", edge).css("stroke-dasharray", 0); // Set stroke-dasharray to 0 while animating to avoid invisible parts 764 | // Animating path requires both paths to have same amount of segments. Check difference and add empty segments if necessary 765 | var diff = from[0].split(/[\s,CMc]+/).length - to[0].split(/[\s,CMc]+/).length; 766 | if (diff % 6 != 0) 767 | console.log("??"); 768 | if (diff > 0) 769 | to[0] += "c0,0 0,0 0,0".repeat(diff / 6); 770 | else 771 | from[0] += "c0,0 0,0 0,0".repeat(-diff / 6); 772 | var anim = document.createElementNS('http://www.w3.org/2000/svg', "animate"); 773 | var attrs = { 774 | attributeName: "d", 775 | attributeType: "XML", 776 | begin: "indefinite", 777 | dur: time + "ms", 778 | fill: "freeze", 779 | from: from[0], 780 | to: to[0] 781 | }; 782 | for (var k in attrs) 783 | anim.setAttribute(k, attrs[k]); 784 | $("path", edge)[0].appendChild(anim); 785 | anim.beginElement(); 786 | // Animate path polygons (arrow points) from old to new position 787 | var anim2 = document.createElementNS('http://www.w3.org/2000/svg', "animate"); 788 | var attrs2 = { 789 | attributeName: "points", 790 | attributeType: "XML", 791 | begin: "indefinite", 792 | dur: time + "ms", 793 | fill: "freeze", 794 | from: from[1], 795 | to: to[1] 796 | }; 797 | for (var k in attrs) 798 | anim2.setAttribute(k, attrs2[k]); 799 | $("polygon", edge)[0].appendChild(anim2); 800 | anim2.beginElement(); 801 | $("path", edge).delay(time).promise().then(function() { 802 | this[0].lengthsaved = this[0].getTotalLength(); 803 | this.css("stroke-dasharray", this[0].lengthsaved); // Set stroke-dasharray back to original value 804 | }) 805 | return $(edge).delay(time).promise(); 806 | } 807 | 808 | function moveState(state, from, to, time) { // Animate state from one position to another. From and to contain the cx and cy attributes 809 | for (var j = 0; j < $("ellipse", state).length; j++) { 810 | // Animate ellipses from old to new position 811 | var anim = document.createElementNS('http://www.w3.org/2000/svg', "animate"); 812 | var attrs = { 813 | attributeName: "cx", 814 | attributeType: "XML", 815 | begin: "indefinite", 816 | dur: time + "ms", 817 | fill: "freeze", 818 | from: from[0], 819 | to: to[0] 820 | }; 821 | var anim2 = document.createElementNS('http://www.w3.org/2000/svg', "animate"); 822 | var attrs2 = { 823 | attributeName: "cy", 824 | attributeType: "XML", 825 | begin: "indefinite", 826 | dur: time + "ms", 827 | fill: "freeze", 828 | from: from[1], 829 | to: to[1] 830 | }; 831 | for (var k in attrs) 832 | anim.setAttribute(k, attrs[k]); 833 | for (var k in attrs2) 834 | anim2.setAttribute(k, attrs2[k]); 835 | $("ellipse", state)[j].appendChild(anim); 836 | $("ellipse", state)[j].appendChild(anim2); 837 | anim.beginElement(); 838 | anim2.beginElement(); 839 | } 840 | return $(state).delay(time).promise(); 841 | } 842 | 843 | /* 844 | Convert NFA to DFA using subset construction 845 | (Make a splitscreen, and state by state build the DFA) 846 | */ 847 | function animToDFAStart(instance, queue) { 848 | $("#FA svg").animate({"width": "40%"}, 600).promise().then(function() { 849 | updateScale($("#FA svg")[0]); 850 | //$("#FA").append("
New state nr.Corresponding old states
"); 851 | $("#FA").append("
New state nr.
Corresponding old states
"); 852 | instance.FAobj[3].svg.css("width", "40%"); 853 | $("#FA").append(instance.FAobj[3].svg).promise().then(function() { 854 | updateScale($("#FA svg")[1]); 855 | }); 856 | queue.push(function() { 857 | $("#table_body").append("
0
" + instance.DFA.statescor[0].join(",") + "
"); 858 | showEdge(instance.FAobj[3].start[0], 300).then(function() { 859 | showState(instance.FAobj[3].states[0], $("polygon", instance.FAobj[3].start[0])[0].points[1], 300).then(function() { 860 | animToDFAMain(instance, queue, 0, 0, [0]); 861 | }) 862 | }) 863 | }) 864 | queue.notify("Create a new starting state, that corresponds to old stating state"); 865 | }) 866 | } 867 | 868 | function animToDFAMain(instance, queue, curstate, curto, shown) { 869 | var NFAobj = instance.FAobj[2]; 870 | var DFAobj = instance.FAobj[3]; 871 | if (curto == DFAobj.states.length) { // Done with all edges, go to next state 872 | if (curstate == DFAobj.states.length-1) { // Just did last state, finish 873 | queue.push(function() { 874 | $("#FA").children().eq(0).animate({"width": 0}, 400).promise().then(function() { 875 | this.remove(); 876 | }) 877 | $("#FA").children().eq(1).css("min-width", 0).animate({"width": 0}, 400).promise().then(function() { 878 | this.remove(); 879 | }) 880 | $("#FA").children().eq(2).animate({"width": "100%"}, 400).promise().then(function() { 881 | updateScale(this[0]); 882 | }) 883 | queue.done.resolve(); 884 | }) 885 | queue.notify("Finished. Click animation step one last time when you are ready to get rid of the table"); 886 | return; 887 | } 888 | else { // More states to go 889 | animToDFAMain(instance, queue, curstate+1, 0, shown); 890 | return; 891 | } 892 | } 893 | 894 | if (DFAobj.edges[curstate][curto] == undefined) { // No edge from state to curto 895 | animToDFAMain(instance, queue, curstate, curto+1, shown); 896 | return; 897 | } 898 | 899 | for (var oldfrom of instance.DFA.statescor[curstate]) { 900 | for (var oldto of instance.DFA.statescor[curto]) { 901 | if (NFAobj.edges[oldfrom][oldto] == undefined) 902 | continue; 903 | var edge = NFAobj.edges[oldfrom][oldto][1]; // Mark NFA edges red for clarity 904 | var path = $("path", edge); 905 | var pol = $("polygon", edge); 906 | path.attr("stroke", "#ff0000"); 907 | pol.attr("fill", "#ff0000"); 908 | pol.attr("stroke", "#ff0000"); 909 | $("text", edge).attr("fill", "#ff0000"); 910 | } 911 | } 912 | 913 | queue.push(function() { 914 | if (!shown.includes(curto)) { 915 | if (instance.DFA.statescor[curto].length == 0) 916 | $("#table_body").append("
" + curto + "
None
"); 917 | else 918 | $("#table_body").append("
" + curto + "
" + instance.DFA.statescor[curto].join(",") + "
"); 919 | } 920 | showEdge(DFAobj.edges[curstate][curto][1], 300).then(function() { 921 | var p1; 922 | if (!shown.includes(curto)) {// State hasnt been shown yet, do it now 923 | p1 = showState(DFAobj.states[curto], $("polygon", DFAobj.edges[curstate][curto][1])[0].points[1], 300); 924 | shown.push(curto); 925 | } 926 | $.when(p1).then(function() { 927 | for (var oldfrom of instance.DFA.statescor[curstate]) { 928 | for (var oldto of instance.DFA.statescor[curto]) { 929 | if (NFAobj.edges[oldfrom][oldto] == undefined) 930 | continue; 931 | var edge = NFAobj.edges[oldfrom][oldto][1]; // Make NFA edges black again for clarity 932 | var path = $("path", edge); 933 | var pol = $("polygon", edge); 934 | path.attr("stroke", "#000000"); 935 | pol.attr("fill", "#000000"); 936 | pol.attr("stroke", "#000000"); 937 | $("text", edge).attr("fill", "#000000"); 938 | } 939 | } 940 | animToDFAMain(instance, queue, curstate, curto+1, shown); 941 | }) 942 | }) 943 | }) 944 | if (instance.DFA.statescor[curstate].length == 0) { 945 | var notifymsg = "Starting from the 'empty' new state " + curstate + ", corresponding to no old states, using any symbol, we still can't reach any old states." 946 | } 947 | else { 948 | var notifymsg = "Starting from old "; 949 | if (instance.DFA.statescor[curstate].length == 1) 950 | notifymsg += "state " + instance.DFA.statescor[curstate][0] + " (new state " + curstate + "), "; 951 | else 952 | notifymsg += "states " + instance.DFA.statescor[curstate].join(",") + " (new state " + curstate + "), "; 953 | if (DFAobj.edges[curstate][curto][0].length == 1) 954 | notifymsg += "using the symbol " + DFAobj.edges[curstate][curto][0][0] + ", "; 955 | else 956 | notifymsg += "using one of the symbols " + DFAobj.edges[curstate][curto][0].join(",") + ", "; 957 | if (instance.DFA.statescor[curto].length == 0) 958 | notifymsg += "we can reach no old states"; 959 | else if (instance.DFA.statescor[curto].length == 1) 960 | notifymsg += "we can reach old state " + instance.DFA.statescor[curto][0]; 961 | else 962 | notifymsg += "we can reach old states " + instance.DFA.statescor[curto].join(","); 963 | if (!shown.includes(curto)) 964 | notifymsg += ". This is not a new state yet, so create it."; 965 | else 966 | notifymsg += " (new state " + curto + ")"; 967 | } 968 | queue.notify(notifymsg); 969 | } 970 | 971 | /* 972 | Convert DFA to DFAm (minimal DFA) by merging states that are equivalent 973 | */ 974 | function animToDFAmStart(instance, queue) { // Start of DFA to DFAm animation: move viewbox 975 | if (instance.DFA.states == instance.DFAm.states) { // DFA was already minimal 976 | $("#step_message").html("The automaton was already minimal. Continuing.."); 977 | var DFAobj = instance.FAobj[3]; 978 | var DFAmobj = instance.FAobj[4]; 979 | DFAmobj.svg.attr("viewBox", DFAobj.svg.attr("viewBox")); // Change viewbox & transform to new value 980 | DFAmobj.svg.children().attr("transform", DFAobj.svg.children().attr("transform")); 981 | $("#FA").html(DFAmobj.svg).promise().then(function() { // Replace with actual DFAm 982 | updateScale($("#FA").children()[0]); 983 | }) 984 | $("#FA").delay(1000).promise().then(function() { 985 | queue.done.resolve(); 986 | }) 987 | } 988 | else { 989 | $(".edge text").animate({"opacity": 0}, 200); // Temporarily remove text from edges during animation for clarity 990 | animToDFAmMain(instance, queue, 0); 991 | } 992 | } 993 | 994 | function animToDFAmMain(instance, queue, curstate) { // Main DFA to DFAm animation: merge states 995 | var DFAobj = instance.FAobj[3]; 996 | var DFAmobj = instance.FAobj[4]; 997 | if (curstate == instance.DFAm.states) { // Just did last state, finish 998 | DFAmobj.svg.attr("viewBox", DFAobj.svg.attr("viewBox")); // Change viewbox & transform to new value 999 | DFAmobj.svg.children().attr("transform", DFAobj.svg.children().attr("transform")); 1000 | $("#graph0 text", DFAmobj.svg).css("opacity", 0).animate({"opacity": 1}, 200); // Animate in text 1001 | $("#FA").html(DFAmobj.svg).promise().then(function() { // Replace with actual DFAm 1002 | updateScale($("#FA").children()[0]); 1003 | }) 1004 | queue.done.resolve(); 1005 | return; 1006 | } 1007 | // Mark the states and edges to be moved as red 1008 | for (var oldstate of instance.DFAm.statescor[curstate]) { 1009 | var stateobj = DFAobj.states[oldstate]; 1010 | $("ellipse", stateobj).css("stroke", "#ff0000"); 1011 | $("text", stateobj).css("fill", "#ff0000"); 1012 | for (var oldto in DFAobj.edges[oldstate]) { 1013 | var edge = DFAobj.edges[oldstate][oldto][1]; 1014 | $("path", edge).css("stroke", "#ff0000"); 1015 | $("polygon", edge).css("stroke", "#ff0000"); 1016 | $("polygon", edge).css("fill", "#ff0000"); 1017 | } 1018 | for (var oldfrom in DFAobj.edges) { 1019 | if (DFAobj.edges[oldfrom][oldstate] == undefined) 1020 | continue; 1021 | var edge = DFAobj.edges[oldfrom][oldstate][1]; 1022 | $("path", edge).css("stroke", "#ff0000"); 1023 | $("polygon", edge).css("stroke", "#ff0000"); 1024 | $("polygon", edge).css("fill", "#ff0000"); 1025 | } 1026 | } 1027 | queue.push(function() { 1028 | var newobj = DFAmobj.states[curstate]; 1029 | for (var oldstate of instance.DFAm.statescor[curstate]) { 1030 | var oldobj = DFAobj.states[oldstate]; 1031 | moveState(oldobj, // Move states to new position 1032 | [$("ellipse", oldobj).attr("cx"), $("ellipse", oldobj).attr("cy")], 1033 | [$("ellipse", newobj).attr("cx"), $("ellipse", newobj).attr("cy")], 600 1034 | ); 1035 | $("text", oldobj).css("opacity", 0); // Temporarily get rid of state nrs (they change) 1036 | if (oldstate == instance.DFA.start) { // Move starting edge 1037 | var d1 = $("path", DFAobj.start[0]).attr("d"); 1038 | var d2 = $("path", DFAmobj.start[0]).attr("d"); 1039 | var p1 = $("polygon", DFAobj.start[0]).attr("points"); 1040 | var p2 = $("polygon", DFAmobj.start[0]).attr("points"); 1041 | moveEdge(DFAobj.start[0], [d1, p1], [d2, p2], 600); 1042 | } 1043 | // Next: Move edge from one position to another. 1044 | // The exact curve is not given, since either the starting state or 1045 | // ending state moves and the other doesn't. Solution: 1046 | // For outgoing states, transform to new edge, rotated such that endpoint 1047 | // Coincides with old ending state. 1048 | // For incoming states, rotate old edge such that endpoint coincides 1049 | // with new ending state 1050 | for (var oldto in DFAobj.edges[oldstate]) { 1051 | var oldfrom = oldstate; 1052 | var newfrom = curstate; 1053 | oldto = parseInt(oldto); 1054 | var newto; 1055 | for (var i = 0; i < DFAmobj.states.length; i++) { 1056 | if (instance.DFAm.statescor[i].includes(oldto)) { 1057 | newto = i; 1058 | break; 1059 | } 1060 | } 1061 | 1062 | var oldedge = DFAobj.edges[oldfrom][oldto][1]; 1063 | var newedge = DFAmobj.edges[newfrom][newto][1]; 1064 | var d1 = $("path", oldedge).attr("d"); 1065 | var d2 = $("path", newedge).attr("d"); 1066 | var p1 = $("polygon", oldedge).attr("points"); 1067 | var p2 = $("polygon", newedge).attr("points"); 1068 | if (oldedge.halved) { 1069 | moveEdge(oldedge, [d1, p1], [d2, p2], 600); 1070 | } 1071 | else { 1072 | var d1split = d1.split(/[\s,CMc]+/); 1073 | var d2split = d2.split(/[\s,CMc]+/); 1074 | var origin = {"x": parseFloat(d2split[1]), "y": parseFloat(d2split[2])}; // Origin point to rotate and scale curve around 1075 | var oldcenter = {"x": $("ellipse", DFAobj.states[oldto])[0].cx.baseVal.value, "y": $("ellipse", DFAobj.states[oldto])[0].cy.baseVal.value}; 1076 | var newcenter = {"x": $("ellipse", DFAmobj.states[newto])[0].cx.baseVal.value, "y": $("ellipse", DFAmobj.states[newto])[0].cy.baseVal.value}; 1077 | var newentrypoint = {"x": $("polygon", newedge)[0].points[1].x, "y": $("polygon", newedge)[0].points[1].y} 1078 | var centerangle = Math.atan2(oldcenter.y-origin.y,oldcenter.x-origin.x) - Math.atan2(newcenter.y-origin.y,newcenter.x-origin.x); 1079 | var entryangle = Math.atan2(newentrypoint.y-newcenter.y,newentrypoint.x-newcenter.x); 1080 | var oldentryunit = {"x": Math.cos(centerangle+entryangle), "y": Math.sin(centerangle+entryangle)}; 1081 | var r = $("ellipse", DFAobj.states[oldto])[0].rx.baseVal.value; 1082 | var oldend = {"x": oldcenter.x + oldentryunit.x * (r + 10), "y": oldcenter.y + oldentryunit.y * (r + 10)}; 1083 | var newend = {"x": parseFloat(d2split[d2split.length-2]), "y": parseFloat(d2split[d2split.length-1])}; 1084 | 1085 | var scale = Math.sqrt(Math.pow(oldend.x-origin.x,2)+Math.pow(oldend.y-origin.y,2)) 1086 | / Math.sqrt(Math.pow(newend.x-origin.x,2)+Math.pow(newend.y-origin.y,2)); // Ratio of distances to origin point 1087 | var angle = Math.atan2(oldend.y-origin.y,oldend.x-origin.x) 1088 | - Math.atan2(newend.y-origin.y,newend.x-origin.x); // Angle between 2 end points as seen from the origin 1089 | var newd = "M" + d2split[1] + "," + d2split[2] + "C"; // Will contain the new "d" attribute 1090 | var newdarray = []; 1091 | for (var i = 3; i < d2split.length; i+=2) { 1092 | var newpoint = rotateScale(parseFloat(d2split[i]), parseFloat(d2split[i+1]), angle, scale, origin); 1093 | newdarray.push(newpoint.x + "," + newpoint.y); 1094 | } 1095 | newd += newdarray.join(" "); 1096 | var newpoints = []; // Will contan the new "points" attribute 1097 | newpoints[0] = (oldend.x - oldentryunit.y * 3.5) + "," + (oldend.y + oldentryunit.x * 3.5); 1098 | newpoints[1] = (oldend.x - oldentryunit.x * 10.4) + "," + (oldend.y - oldentryunit.y * 10.4); 1099 | newpoints[2] = (oldend.x + oldentryunit.y * 3.5) + "," + (oldend.y - oldentryunit.x * 3.5); 1100 | newpoints[3] = newpoints[0]; 1101 | newpoints = newpoints.join(" "); 1102 | moveEdge(DFAobj.edges[oldfrom][oldto][1], [d1, p1], [newd, newpoints], 600); 1103 | DFAobj.edges[oldfrom][oldto][1].halved = true; 1104 | } 1105 | } 1106 | for (var oldfrom in DFAobj.edges) { 1107 | for (var oldto in DFAobj.edges[oldfrom]) { 1108 | oldto = parseInt(oldto); 1109 | if (oldto != oldstate) 1110 | continue; 1111 | oldfrom = parseInt(oldfrom); 1112 | var newto = curstate; 1113 | var newfrom; 1114 | for (var i = 0; i < DFAmobj.states.length; i++) { 1115 | if (instance.DFAm.statescor[i].includes(oldfrom)) { 1116 | newfrom = i; 1117 | break; 1118 | } 1119 | } 1120 | 1121 | var oldedge = DFAobj.edges[oldfrom][oldto][1]; 1122 | var newedge = DFAmobj.edges[newfrom][newto][1]; 1123 | var d1 = $("path", oldedge).attr("d"); 1124 | var d2 = $("path", newedge).attr("d"); 1125 | var p1 = $("polygon", oldedge).attr("points"); 1126 | var p2 = $("polygon", newedge).attr("points"); 1127 | if (oldedge.halved) { 1128 | moveEdge(oldedge, [d1, p1], [d2, p2], 600); 1129 | } 1130 | else { 1131 | var d1split = d1.split(/[\s,CMc]+/); 1132 | var d2split = d2.split(/[\s,CMc]+/); 1133 | var origin = {"x": parseFloat(d1split[1]), "y": parseFloat(d1split[2])}; // Origin point to rotate and scale curve around 1134 | var oldcenter = {"x": $("ellipse", DFAobj.states[oldto])[0].cx.baseVal.value, "y": $("ellipse", DFAobj.states[oldto])[0].cy.baseVal.value}; 1135 | var newcenter = {"x": $("ellipse", DFAmobj.states[newto])[0].cx.baseVal.value, "y": $("ellipse", DFAmobj.states[newto])[0].cy.baseVal.value}; 1136 | var oldentrypoint = {"x": $("polygon", oldedge)[0].points[1].x, "y": $("polygon", oldedge)[0].points[1].y} 1137 | var centerangle = Math.atan2(newcenter.y-origin.y,newcenter.x-origin.x) - Math.atan2(oldcenter.y-origin.y,oldcenter.x-origin.x); 1138 | var entryangle = Math.atan2(oldentrypoint.y-oldcenter.y,oldentrypoint.x-oldcenter.x); 1139 | var newentryunit = {"x": Math.cos(centerangle+entryangle), "y": Math.sin(centerangle+entryangle)}; 1140 | var r = $("ellipse", DFAobj.states[newto])[0].rx.baseVal.value; 1141 | var newend = {"x": newcenter.x + newentryunit.x * (r + 10), "y": newcenter.y + newentryunit.y * (r + 10)}; 1142 | var oldend = {"x": parseFloat(d1split[d1split.length-2]), "y": parseFloat(d1split[d1split.length-1])}; 1143 | 1144 | var scale = Math.sqrt(Math.pow(newend.x-origin.x,2)+Math.pow(newend.y-origin.y,2)) 1145 | / Math.sqrt(Math.pow(oldend.x-origin.x,2)+Math.pow(oldend.y-origin.y,2)); // Ratio of distances to origin point 1146 | var angle = Math.atan2(newend.y-origin.y,newend.x-origin.x) 1147 | - Math.atan2(oldend.y-origin.y,oldend.x-origin.x); // Angle between 2 end points as seen from the origin 1148 | var newd = "M" + d1split[1] + "," + d1split[2] + "C"; // Will contain the new "d" attribute 1149 | var newdarray = []; 1150 | for (var i = 3; i < d1split.length; i+=2) { 1151 | var newpoint = rotateScale(parseFloat(d1split[i]), parseFloat(d1split[i+1]), angle, scale, origin); 1152 | newdarray.push(newpoint.x + "," + newpoint.y); 1153 | } 1154 | newd += newdarray.join(" "); 1155 | var newpoints = []; // Will contan the new "points" attribute 1156 | newpoints[0] = (newend.x - newentryunit.y * 3.5) + "," + (newend.y + newentryunit.x * 3.5); 1157 | newpoints[1] = (newend.x - newentryunit.x * 10.4) + "," + (newend.y - newentryunit.y * 10.4); 1158 | newpoints[2] = (newend.x + newentryunit.y * 3.5) + "," + (newend.y - newentryunit.x * 3.5); 1159 | newpoints[3] = newpoints[0]; 1160 | newpoints = newpoints.join(" "); 1161 | moveEdge(DFAobj.edges[oldfrom][oldto][1], [d1, p1], [newd, newpoints], 600); 1162 | DFAobj.edges[oldfrom][oldto][1].halved = true; 1163 | } 1164 | } 1165 | } 1166 | } 1167 | $(document).delay(600).promise().then(function() { // Wait until animations are finished until pushing next step 1168 | for (var oldstate of instance.DFAm.statescor[curstate]) { // Make moved states and edges green 1169 | var stateobj = DFAobj.states[oldstate]; 1170 | $("ellipse", stateobj).css("stroke", "#003300"); 1171 | $("text", stateobj).css("fill", "#003300"); 1172 | for (var oldto in DFAobj.edges[oldstate]) { 1173 | var edge = DFAobj.edges[oldstate][oldto][1]; 1174 | $("path", edge).css("stroke", "#003300"); 1175 | $("polygon", edge).css("stroke", "#003300"); 1176 | $("polygon", edge).css("fill", "#003300"); 1177 | } 1178 | for (var oldfrom in DFAobj.edges) { 1179 | if (DFAobj.edges[oldfrom][oldstate] == undefined) 1180 | continue; 1181 | var edge = DFAobj.edges[oldfrom][oldstate][1]; 1182 | $("path", edge).css("stroke", "#003300"); 1183 | $("polygon", edge).css("stroke", "#003300"); 1184 | $("polygon", edge).css("fill", "#003300"); 1185 | } 1186 | } 1187 | animToDFAmMain(instance, queue, curstate+1); 1188 | }) 1189 | }) 1190 | if (instance.DFAm.statescor[curstate].length == 1) { 1191 | queue.notify("State " + instance.DFAm.statescor[curstate][0] + " cannot be merged with any states. Move it to its new position"); 1192 | } 1193 | else { 1194 | queue.notify("States " + instance.DFAm.statescor[curstate].join(", ") + " are equivalent and can be merged"); 1195 | } 1196 | } 1197 | 1198 | function rotateScale(x, y, angle, scale, origin) { // Rotate point (x,y) angle around origin and scale scale 1199 | var sin = Math.sin(angle); 1200 | var cos = Math.cos(angle); 1201 | x -= origin.x; // Translate origin point to (0,0) (sortof) 1202 | y -= origin.y; 1203 | var newx = (x * cos - y * sin) * scale; // Rotate and scale 1204 | var newy = (x * sin + y * cos) * scale; 1205 | newx += origin.x; // Translate origin point back 1206 | newy += origin.y; 1207 | return {"x": newx, "y": newy}; 1208 | } 1209 | 1210 | /* 1211 | Animation for checking if string matches regular expression 1212 | */ 1213 | function animCheckMatchStart(FAobj, matches, string, queue) { // Start by marking the starting state 1214 | queue.push(function() { 1215 | doubleGraph($("#graph0"), "#30cfc9", true); // Create a duplicate graph on top of the other one to be animated 1216 | var id = "#" + FAobj.start[0].attr("id") + "double"; 1217 | var id2 = "#node" + (FAobj.start[1]+2) + "double"; 1218 | showEdge($(id), 500).then(function() { 1219 | unshowEdge2($(id), 500); 1220 | var entry = $("polygon", $(id))[0].points[1]; 1221 | showState($(id2), entry, 500).then(function() { 1222 | $(id2).delay(10).promise().then(function() { 1223 | animCheckMatchMain(FAobj, matches, string, 0, queue); 1224 | }) 1225 | }); 1226 | }) 1227 | }) 1228 | queue.notify("Go to the starting state"); 1229 | } 1230 | 1231 | function animCheckMatchFinish(FAobj, matches, lastentry, queue) { // Finished: Color the whole thing green/red if matched/not matched 1232 | queue.push(function() { 1233 | $("#graphdouble").remove(); // Remove blue double graph, create a new one 1234 | if (matches.matched) 1235 | doubleGraph($("#graph0"), "#31eb37", true); // Green 1236 | else 1237 | doubleGraph($("#graph0"), "#e61515", true); // Red 1238 | animCheckMatchMain2(FAobj, [matches.passed[matches.passed.length-1]], [], [lastentry], queue).then(function() { 1239 | if (matches.matched) 1240 | $("#step_message").html("The string matches the regular expression!"); 1241 | else 1242 | $("#step_message").html("The string doesn't match the regular expression!"); 1243 | }) 1244 | }) 1245 | if (matches.matched) 1246 | queue.notify("After reading the entire string, we ended up in an accepting state (" + matches.passed[matches.passed.length-1] + "). Therefore the string matches the regular expression"); 1247 | else 1248 | queue.notify("After reading the entire string, we ended up in an non-accepting state (" + matches.passed[matches.passed.length-1] + "). Therefore the string does not match the regular expression"); 1249 | } 1250 | 1251 | function animCheckMatchMain(FAobj, matches, string, index, queue) { 1252 | queue.push(function() { 1253 | var nextstatenr = matches.passed[index+1]; 1254 | var previousstatenr = matches.passed[index]; 1255 | var nextstate = $("#node" + (nextstatenr+2) + "double"); 1256 | var previousstate = $("#node" + (previousstatenr+2) + "double"); 1257 | var edgeid = FAobj.edges[previousstatenr][nextstatenr][1].attr("id") + "double"; 1258 | var edge = $("#" + edgeid); 1259 | var entry = $("polygon", edge)[0].points[1]; 1260 | if (previousstatenr != nextstatenr) 1261 | unshowState(previousstate, $("path", edge)[0].getPointAtLength(0), 500); 1262 | showEdge(edge, 500).then(function() { 1263 | if (previousstatenr != nextstatenr) 1264 | showState(nextstate, entry, 500); 1265 | edge.delay(10).promise().then(function() { 1266 | unshowEdge2(edge, 500).then(function() { 1267 | nextstate.delay(10).promise().then(function() { 1268 | if (index == string.length-1) { // Just did last step, go to finishing animation 1269 | animCheckMatchFinish(FAobj, matches, $("polygon", edge)[0].points[1], queue); 1270 | return; 1271 | } 1272 | animCheckMatchMain(FAobj, matches, string, index+1, queue); 1273 | }) 1274 | }) 1275 | }) 1276 | }) 1277 | }) 1278 | queue.notify("Read character " + string[index] + ". Follow edge to state " + matches.passed[index+1]); 1279 | } 1280 | 1281 | function animCheckMatchMain2(FAobj, curstates, visited, entrypoints, queue) { // Recursive function from finish function: color green or red 1282 | var timeper = Math.max(Math.min(2000 / FAobj.states.length, 250), 125); // Animation time between 125 and 250 ms 1283 | var nextentrypoints = []; 1284 | var nextstates = []; 1285 | var p1, p2, p3; 1286 | 1287 | for (var i in curstates) { 1288 | var id = "#node" + (curstates[i]+2) + "double"; 1289 | p1 = showState($(id), entrypoints[i], timeper); 1290 | } 1291 | p2 = $.when(p1).then(function() { 1292 | for (var i in curstates) { 1293 | if (curstates[i] == FAobj.start[1]) { 1294 | var id = "#" + FAobj.start[0].attr("id") + "double"; 1295 | p3 = showEdge2($(id), timeper); 1296 | } 1297 | for (var to in FAobj.edges[curstates[i]]) { 1298 | to = parseInt(to); 1299 | if (visited.includes(to)) 1300 | continue; 1301 | var id = "#" + FAobj.edges[curstates[i]][to][1].attr("id") + "double"; 1302 | p3 = showEdge($(id), timeper); 1303 | if (!curstates.includes(to) && !visited.includes(to) && !nextstates.includes(to)) { 1304 | nextstates.push(to); 1305 | nextentrypoints.push($("polygon", $(id))[0].points[1]); 1306 | } 1307 | } 1308 | for (var from in FAobj.edges) { 1309 | from = parseInt(from); 1310 | if (FAobj.edges[from][curstates[i]] == undefined || visited.includes(from) || curstates.includes(from)) 1311 | continue; 1312 | var id = "#" + FAobj.edges[from][curstates[i]][1].attr("id") + "double"; 1313 | p3 = showEdge2($(id), timeper); 1314 | if (!curstates.includes(from) && !visited.includes(from) && !nextstates.includes(from)) { 1315 | nextstates.push(from); 1316 | nextentrypoints.push($("path", $(id))[0].getPointAtLength(0)); 1317 | } 1318 | } 1319 | } 1320 | return p3; 1321 | }); 1322 | 1323 | return $.when(p2).then(function() { 1324 | for (var i in curstates) 1325 | visited.push(curstates[i]); 1326 | if (nextstates.length == 0) { 1327 | queue.done.resolve(); // Notify queue that the animation is finished 1328 | return; 1329 | } 1330 | animCheckMatchMain2(FAobj, nextstates, visited, nextentrypoints, queue); 1331 | }); 1332 | } 1333 | 1334 | function doubleGraph(graph, color, invis) { // Make a duplicate of the graph with different ids and class names for animating over the other graph 1335 | var g = document.createElementNS('http://www.w3.org/2000/svg', "g"); 1336 | g.setAttribute("id", "graphdouble"); 1337 | g.setAttribute("transform", graph.attr("transform")); 1338 | graph.after(g); 1339 | var states = $(".node", graph); 1340 | var edges = $(".edge", graph); 1341 | for (var state of states) { 1342 | var statecopy = state.cloneNode(false); 1343 | statecopy.setAttribute("id", state.id + "double"); 1344 | $(statecopy).removeClass("node").addClass("nodedouble"); 1345 | var ellipsecopy = $("ellipse", state)[0].cloneNode(false); 1346 | ellipsecopy.setAttribute("fill", "none"); 1347 | ellipsecopy.setAttribute("stroke", color); 1348 | ellipsecopy.setAttribute("stroke-width", "1.7px"); 1349 | statecopy.appendChild(ellipsecopy); 1350 | if ($("ellipse", state).length == 2) {// Accepting state 1351 | var ellipsecopy2 = $("ellipse", state)[1].cloneNode(false); 1352 | ellipsecopy2.setAttribute("fill", "none"); 1353 | ellipsecopy2.setAttribute("stroke", color); 1354 | ellipsecopy2.setAttribute("stroke-width", "1.7px"); 1355 | statecopy.appendChild(ellipsecopy2); 1356 | } 1357 | g.appendChild(statecopy); 1358 | } 1359 | for (var edge of edges) { 1360 | var edgecopy = edge.cloneNode(false); 1361 | edgecopy.setAttribute("id", edge.id + "double"); 1362 | $(edgecopy).removeClass("edge").addClass("edgedouble"); 1363 | var pathcopy = $("path", edge)[0].cloneNode(false); 1364 | var polcopy = $("polygon", edge)[0].cloneNode(false); 1365 | pathcopy.setAttribute("stroke", color); 1366 | pathcopy.setAttribute("stroke-width", "1.7px"); 1367 | pathcopy.lengthsaved = $("path", edge)[0].lengthsaved; 1368 | $(pathcopy).css('stroke-dasharray', pathcopy.lengthsaved); 1369 | if (invis) 1370 | $(pathcopy).css('stroke-dashoffset', pathcopy.lengthsaved); 1371 | polcopy.setAttribute("stroke", color); 1372 | polcopy.setAttribute("fill", color); 1373 | edgecopy.appendChild(pathcopy); 1374 | edgecopy.appendChild(polcopy); 1375 | g.appendChild(edgecopy); 1376 | } 1377 | if (invis) // Start off as invisible 1378 | $("polygon, ellipse", g).attr("visibility", "hidden") 1379 | } 1380 | -------------------------------------------------------------------------------- /js/back.js: -------------------------------------------------------------------------------- 1 | /* 2 | ------ 3 | File containing all the backend algorithms 4 | Parsing expression, constructing NFAl, converting it to 5 | NFA, DFA and DFAm, simplifying automata and other utils 6 | ------ 7 | */ 8 | 9 | "use strict"; 10 | 11 | /* 12 | Parsing regular expressions 13 | Using standard precedence order: parenthesis > star > concat > or 14 | Return an operation tree 15 | */ 16 | function parseRegex(stringo) { 17 | var stream = { 18 | string: stringo, 19 | pos: 0, 20 | cur: (function() { return this.string[this.pos]; }), 21 | done: (function() { return this.pos == this.string.length; }) 22 | }; 23 | try { 24 | var tree = parseExpr(stream); 25 | if (!stream.done()) 26 | throw "Non-empty stream"; 27 | console.log(tree); 28 | return tree; 29 | } 30 | catch (err) { 31 | return -1; 32 | } 33 | } 34 | 35 | function parseExpr(stream) { 36 | var terms = [] 37 | while (!stream.done()) { 38 | terms.push(parseTerm(stream)); 39 | if (!stream.done() && stream.cur() == '|') 40 | stream.pos++; 41 | else 42 | break; 43 | } 44 | if (terms.length == 0) 45 | throw "Empty expression"; 46 | else if (terms.length == 1) 47 | return terms[0]; 48 | else { 49 | return { 50 | type: "or", 51 | value: terms 52 | } 53 | } 54 | } 55 | 56 | function parseTerm(stream) { 57 | var concats = [] 58 | while (!stream.done()) { 59 | var concat = parseConcat(stream); 60 | if (concat != undefined) { 61 | if (concat.length == 2) { // In case of +, count as 2 concatsx 62 | concats.push(concat[0]); 63 | concats.push(concat[1]); 64 | } 65 | else { 66 | concats.push(concat); 67 | } 68 | } 69 | else 70 | break; 71 | } 72 | if (concats.length == 0) 73 | throw "Empty term"; 74 | else if (concats.length == 1) 75 | return concats[0]; 76 | else { 77 | return { 78 | type: "concat", 79 | value: concats 80 | } 81 | } 82 | } 83 | 84 | function parseConcat(stream) { 85 | var atom = parseAtom(stream); 86 | if (atom != undefined && !stream.done() && stream.cur() == "*") { 87 | stream.pos++; 88 | return { 89 | type: "star", 90 | value: atom 91 | } 92 | } 93 | else if (atom != undefined && !stream.done() && stream.cur() == "+") { 94 | stream.pos++; 95 | return [ // At least 1, concat of atom and atom* 96 | atom, { 97 | type: "star", 98 | value: atom 99 | } 100 | ] 101 | } 102 | else { 103 | return atom; 104 | } 105 | } 106 | 107 | function parseAtom(stream) { 108 | if (stream.done()) 109 | throw "Missing atom"; 110 | else if (stream.cur().toUpperCase() != stream.cur().toLowerCase()) { // Is letter 111 | stream.pos++; 112 | return { 113 | type: "letter", 114 | value: stream.string[stream.pos-1] 115 | } 116 | } 117 | else if (stream.cur() == '0') { 118 | stream.pos++; 119 | return { 120 | type: "lambda", 121 | value: undefined 122 | } 123 | } 124 | else if (stream.cur() == '(') { 125 | stream.pos++; 126 | var expr = parseExpr(stream); 127 | if (!stream.done() && stream.cur() == ')') { 128 | stream.pos++; 129 | return expr; 130 | } 131 | else { 132 | throw "Missing )" 133 | } 134 | } 135 | else { 136 | return undefined; 137 | } 138 | } 139 | 140 | /* 141 | Functions for NFAl: Construct NFAl from regular expression tree 142 | and a function to remove states with only 1 incoming or outgoing lambda transition 143 | */ 144 | function constructNFAl(tree) { 145 | var NFAl = { // Initialize as empty NFA 146 | alphabet: new Set([]), // Set containing letters used in expression 147 | states: 0, // Amount of states 148 | start: 0, // Index of starting state 149 | edges: [], // List of map of outgoing edges for each state 150 | outgoing: [], // Amount of outgoing edges for each state 151 | incoming: [], // Amount of incoming edges for each state 152 | accepting: new Set([]) // Set of accepting states 153 | } 154 | 155 | switch(tree.type) { 156 | case "or": 157 | NFAl.states++; // Add starting state 158 | var oldaccepting = new Set([]); 159 | for (var i = 0; i < tree.value.length; i++) { 160 | var NFAltemp = constructNFAl(tree.value[i]); // Build NFA's for all parts 161 | NFAltemp.alphabet.forEach(function(val) { 162 | NFAl.alphabet.add(val); 163 | }); 164 | mergeEdges(NFAl, NFAltemp); 165 | addTransition(NFAl, 0, NFAltemp.start+NFAl.states, "0"); 166 | oldaccepting.add(NFAltemp.accepting.values().next().value + NFAl.states); 167 | NFAl.states += NFAltemp.states; 168 | } 169 | NFAl.states++; // Add accepting state 170 | oldaccepting.forEach(function(val) { 171 | addTransition(NFAl, val, NFAl.states-1, "0"); 172 | }); 173 | NFAl.accepting.add(NFAl.states-1); 174 | return NFAl; 175 | 176 | case "concat": 177 | for (var i = 0; i < tree.value.length; i++) { 178 | var NFAltemp = constructNFAl(tree.value[i]); // Build NFA's for all parts 179 | NFAltemp.alphabet.forEach(function(val) { 180 | NFAl.alphabet.add(val); 181 | }); 182 | mergeEdges(NFAl, NFAltemp); 183 | if (i == 0) 184 | NFAl.start = NFAltemp.start; 185 | else 186 | addTransition(NFAl,prev,NFAltemp.start+NFAl.states,"0"); 187 | if (i == tree.value.length-1) 188 | NFAl.accepting.add(NFAltemp.accepting.values().next().value+NFAl.states); 189 | var prev = NFAltemp.accepting.values().next().value+NFAl.states; 190 | NFAl.states += NFAltemp.states; 191 | } 192 | return NFAl 193 | 194 | case "star": 195 | var NFAltemp = constructNFAl(tree.value); // Build NFA for part to be starred 196 | addTransition(NFAltemp,NFAltemp.states,NFAltemp.start,"0"); 197 | addTransition(NFAltemp,NFAltemp.accepting.values().next().value,NFAltemp.states,"0"); 198 | NFAltemp.states++; 199 | NFAltemp.accepting = new Set([NFAltemp.states-1]); 200 | NFAltemp.start = NFAltemp.states-1; 201 | return NFAltemp; 202 | 203 | case "letter": 204 | NFAl.alphabet.add(tree.value); 205 | NFAl.states = 2; 206 | addTransition(NFAl,0,1,tree.value); 207 | NFAl.accepting.add(1); 208 | return NFAl; 209 | 210 | case "lambda": 211 | NFAl.states = 1; 212 | NFAl.accepting.add(0); 213 | return NFAl; 214 | 215 | default: 216 | console.log("Unknown type"); 217 | } 218 | 219 | } 220 | 221 | // Merge edges and incoming and outgoing arrays of two NFAl's 222 | function mergeEdges(orig, temp) { 223 | for (var i = 0; i < temp.states; i++) { 224 | if (temp.edges[i] != undefined) { 225 | for (var symbol in temp.edges[i]) { 226 | temp.edges[i][symbol].forEach(function(value) { 227 | addTransition(orig,i+orig.states, value+orig.states, symbol); 228 | }); 229 | } 230 | } 231 | 232 | } 233 | } 234 | 235 | // Find and remove trivial states (states that have only 1 outgoing or 1 incoming lambda-edge) 236 | function trivialStates(NFAl) { 237 | for (var i = NFAl.states-1; i >= 0; i--) { 238 | var removed = false; 239 | if (NFAl.outgoing[i] == 1) { 240 | for (var symbol in NFAl.edges[i]) { 241 | if (symbol == "0" && !NFAl.edges[i][symbol].has(i) && NFAl.edges[i][symbol] != undefined && NFAl.edges[i][symbol].size != 0) { 242 | removeTrivialState(NFAl, i, NFAl.edges[i][symbol].values().next().value, false); 243 | removed = true; 244 | break; 245 | } 246 | } 247 | } 248 | if (NFAl.incoming[i] == 1 && !removed) { 249 | for (var j = 0; j < NFAl.states; j++) { 250 | if (removed) 251 | break; 252 | if (NFAl.edges[j] == undefined || NFAl.edges[j]["0"] == undefined) 253 | continue; 254 | var it = NFAl.edges[j]["0"].values(); 255 | for (var val = it.next().value; val !== undefined; val = it.next().value) { 256 | if (val == i) { 257 | removeTrivialState(NFAl, i, j, true); 258 | removed = true; 259 | break; 260 | } 261 | }; 262 | } 263 | } 264 | } 265 | return NFAl; 266 | } 267 | 268 | function removeTrivialState(NFAl, state, tofr, tag) { 269 | if (NFAl.accepting.has(state) || NFAl.start == state) 270 | return; 271 | if (!tag) { // Trivial outgoing 272 | for (var i = 0; i < NFAl.states; i++) { 273 | for (var symbol in NFAl.edges[i]) { 274 | NFAl.edges[i][symbol].forEach(function(value) { 275 | if (value == state) { 276 | addTransition(NFAl, i, tofr, symbol); 277 | removeTransition(NFAl, i, state, symbol); 278 | } 279 | }); 280 | } 281 | } 282 | removeTransition(NFAl, state, tofr, "0"); 283 | } 284 | else { // Trivial incoming 285 | for (var symbol in NFAl.edges[state]) { 286 | NFAl.edges[state][symbol].forEach(function(value) { 287 | addTransition(NFAl, tofr, value, symbol); 288 | removeTransition(NFAl, state, value, symbol); 289 | }); 290 | } 291 | removeTransition(NFAl, tofr, state, "0"); 292 | } 293 | 294 | removeState(NFAl, state); 295 | // Update edges array number now that a state has been deleted 296 | /*for (var i = 0; i < NFAl.states; i++) { 297 | for (var symbol in NFAl.edges[i]) { 298 | var newSet = new Set([]); 299 | NFAl.edges[i][symbol].forEach(function(value) { 300 | if (value >= state) 301 | newSet.add(value-1); 302 | else 303 | newSet.add(value); 304 | }); 305 | NFAl.edges[i][symbol] = newSet; 306 | } 307 | } 308 | 309 | if (NFAl.start >= state) 310 | NFAl.start--; 311 | 312 | var newaccepting = new Set([]); 313 | var it = NFAl.accepting.values(); 314 | for (var val = it.next().value; val !== undefined; val = it.next().value) { 315 | if (val >= state) 316 | newaccepting.add(val-1); 317 | else 318 | newaccepting.add(val); 319 | } 320 | NFAl.accepting = newaccepting; 321 | 322 | for (var i = state; i < NFAl.states; i++) { 323 | NFAl.edges[i] = NFAl.edges[i+1]; 324 | NFAl.outgoing[i] = NFAl.outgoing[i+1]; 325 | NFAl.incoming[i] = NFAl.incoming[i+1]; 326 | } 327 | NFAl.states--;*/ 328 | } 329 | 330 | /* 331 | Functions for NFA: Construct NFA from NFAl by removing lambda transitions, 332 | a function to compute lambda closure of states and a function for removing unreachable states 333 | */ 334 | function removelTransitions(NFAl) { 335 | var closure = lClosure(NFAl); // Calculate which states can be reached from which states using only lambda transitions 336 | NFAl.ledges = new Set(); 337 | NFAl.newedges = new Set(); 338 | NFAl.normaledges = new Set(); 339 | NFAl.newaccepting = []; 340 | for (var i = 0; i < NFAl.states; i++) { 341 | if (NFAl.edges[i] == undefined) 342 | continue; 343 | for (var symbol in NFAl.edges[i]) { 344 | if (NFAl.edges[i][symbol] != undefined) { 345 | NFAl.edges[i][symbol].forEach(function(val) { 346 | if (symbol != "0") 347 | NFAl.normaledges.add([i,val,symbol]); 348 | else { 349 | NFAl.ledges.add([i,val,"0"]); 350 | } 351 | }); 352 | } 353 | } 354 | } 355 | for (var i = 0; i < NFAl.states; i++) { 356 | closure[i].forEach(function(val) { 357 | if (NFAl.edges[val] != undefined) { 358 | for (var symbol in NFAl.edges[val]) { 359 | if (symbol != "0" && NFAl.edges[val][symbol] != undefined) { 360 | NFAl.edges[val][symbol].forEach(function(val2) { 361 | if (NFAl.edges[i] == undefined || NFAl.edges[i][symbol] == undefined || !NFAl.edges[i][symbol].has(val2)) { 362 | if (addTransition(NFAl, i, val2, symbol)) 363 | NFAl.newedges.add([i,val2,symbol]); 364 | } 365 | }); 366 | } 367 | } 368 | } 369 | if (NFAl.accepting.has(val) && !NFAl.accepting.has(i)) { 370 | NFAl.accepting.add(i); 371 | NFAl.newaccepting.push(i); 372 | } 373 | }); 374 | } 375 | for (var i = 0; i < NFAl.states; i++) { 376 | if (NFAl.edges[i] == undefined || NFAl.edges[i]["0"] == undefined) 377 | continue; 378 | NFAl.edges[i]["0"].forEach(function(val) { 379 | removeTransition(NFAl, i, val, "0"); 380 | }); 381 | } 382 | return NFAl; 383 | } 384 | 385 | function lClosure(NFAl) { 386 | var closure = [] 387 | for (var i = NFAl.states-1; i >= 0; i--) { 388 | closure[i] = new Set([i]); // State can reach itself using only lambda transitions 389 | closure[i].forEach(function(val) { 390 | if (NFAl.edges[val] != undefined && NFAl.edges[val]["0"] != undefined) { 391 | NFAl.edges[val]["0"].forEach(function(val2) { 392 | if (val2 > i) { 393 | closure[val2].forEach(function(val3) { 394 | closure[i].add(val3); 395 | }); 396 | } 397 | else { 398 | closure[i].add(val2); 399 | } 400 | }); 401 | } 402 | }); 403 | } 404 | return closure; 405 | } 406 | 407 | function removeUnreachable(NFA, NFAold) { // Remove unreachable states and their edges, and lower state numbers 408 | var reachable = new Set([NFA.start]); // Start state is reachable 409 | NFAold.unreachable = []; 410 | reachable.forEach(function(val) { 411 | if (NFA.edges[val] == undefined) 412 | return; 413 | for (var symbol in NFA.edges[val]) { 414 | NFA.edges[val][symbol].forEach(function(val2) { 415 | reachable.add(val2); 416 | }) 417 | } 418 | }) 419 | for (var i = NFA.states-1; i >= 0; i--) { 420 | if (!reachable.has(i)) { 421 | removeState(NFA, i); 422 | NFAold.unreachable.push(i); 423 | } 424 | } 425 | return NFA; 426 | } 427 | 428 | /* 429 | Functions for DFA 430 | */ 431 | function subsetConstruction(NFA) { // Convert a NFA to DFA by using the subset construction 432 | var DFA = { 433 | alphabet: new Set(NFA.alphabet), // Set containing letters used in expression 434 | states: 1, // Amount of states 435 | start: 0, // Index of starting state 436 | edges: [], // List of map of outgoing edges for each state 437 | outgoing: [], // Amount of outgoing edges for each state 438 | incoming: [], // Amount of incoming edges for each state 439 | accepting: new Set(), // Set of accepting states 440 | statescor: [] // Array that keeps track of what old states correspond to a new state 441 | } 442 | DFA.statescor[0] = [NFA.start]; // The only state corresponding to the new starting state is the old starting state 443 | if (NFA.accepting.has(NFA.start)) 444 | DFA.accepting.add(0); 445 | var reachable = []; // States reachable from a current list of states using a certain symbol 446 | for (var i = 0; i < DFA.states; i++) { 447 | DFA.alphabet.forEach(function(symbol) { 448 | reachable = []; // Initialize reachable as empty for each symbol 449 | for (var j = 0; j < DFA.statescor[i].length; j++) { 450 | if (NFA.edges[DFA.statescor[i][j]] != undefined && NFA.edges[DFA.statescor[i][j]][symbol] != undefined) { 451 | NFA.edges[DFA.statescor[i][j]][symbol].forEach(function(val) { 452 | if (!reachable.includes(val)) 453 | reachable.push(val); 454 | }) 455 | } 456 | } 457 | reachable.sort(function(a, b) { 458 | return a-b; 459 | }); 460 | var index = ArrayHasArray(DFA.statescor, reachable); 461 | if (index != -1) { // State corresponding to exactly these elements already exists 462 | addTransition(DFA, i, index, symbol); 463 | } 464 | else { // State corresponding to exactly these elements doesn't yet exist: create it 465 | var isaccepting = false; // New state is accepting if reachable contains an accepting state from the NFA 466 | for (var j = 0; j < reachable.length; j++) { 467 | if (NFA.accepting.has(reachable[j])) 468 | isaccepting = true; 469 | } 470 | if (isaccepting) { 471 | DFA.accepting.add(DFA.states); 472 | } 473 | addTransition(DFA, i, DFA.states, symbol); 474 | DFA.statescor[DFA.states] = reachable; 475 | DFA.states++; 476 | } 477 | }) 478 | } 479 | return DFA; 480 | } 481 | 482 | /* 483 | Functions for DFAm 484 | */ 485 | function minimizeDFA(DFA) { // Minimize a DFA by computing which states are equivalent and combining them 486 | var equivalent = []; // 2d array of booleans indicating if state i and j are equivalent 487 | for (var i = 0; i < DFA.states; i++) { 488 | equivalent[i] = []; 489 | for (var j = i; j < DFA.states; j++) { 490 | // Start by marking states equivalent if they are both accepting or non-accepting 491 | if ((DFA.accepting.has(i) && DFA.accepting.has(j)) || (!DFA.accepting.has(i) && !DFA.accepting.has(j))) 492 | equivalent[i][j] = true; 493 | else 494 | equivalent[i][j] = false; 495 | } 496 | } 497 | while (true) { // Continue marking rounds until we have a round where nothing is marked 498 | var marked = false; // Have we marked something this round? 499 | for (var i = 0; i < DFA.states; i++) { 500 | for (var j = i+1; j < DFA.states; j++) { 501 | if (equivalent[i][j]) { 502 | DFA.alphabet.forEach(function(symbol) { 503 | if (!equivalent[i][j]) 504 | return; 505 | var a = DFA.edges[i][symbol].values().next().value; 506 | var b = DFA.edges[j][symbol].values().next().value; 507 | if (!equivalent[Math.min(a,b)][Math.max(a,b)]) { 508 | equivalent[i][j] = false; 509 | marked = true; 510 | } 511 | }) 512 | } 513 | } 514 | } 515 | if (!marked) // Nothing marked this round, stop 516 | break; 517 | } 518 | 519 | var DFAm = { 520 | alphabet: new Set(DFA.alphabet), // Set containing letters used in expression 521 | states: 0, // Amount of states 522 | edges: [], // List of map of outgoing edges for each state 523 | outgoing: [], // Amount of outgoing edges for each state 524 | incoming: [], // Amount of incoming edges for each state 525 | accepting: new Set(), // Set of accepting states 526 | statescor: [] 527 | } 528 | 529 | var taken = []; 530 | for (var i = 0; i < DFA.states; i++) { 531 | if (taken.includes(i)) // State already got assigned to a new state 532 | continue; 533 | if (DFA.accepting.has(i)) // New state is accepting iff old states were accepting 534 | DFAm.accepting.add(DFAm.states); 535 | DFAm.statescor[DFAm.states] = []; 536 | for (var j = i; j < DFA.states; j++) { 537 | if (equivalent[i][j]) { 538 | DFAm.statescor[DFAm.states].push(j); 539 | taken.push(j); 540 | } 541 | } 542 | if (DFAm.statescor[DFAm.states].includes(DFA.start)) 543 | DFAm.start = DFAm.states; // New starting state corresponds to old starting state 544 | DFAm.states++; 545 | } 546 | 547 | for (var i = 0; i < DFAm.states; i++) { // Add the new transitions 548 | DFAm.alphabet.forEach(function(symbol) { 549 | var toOld = DFA.edges[DFAm.statescor[i][0]][symbol].values().next().value; 550 | var toNew; 551 | for (var j = 0; j < DFAm.states; j++) { 552 | if (DFAm.statescor[j].includes(toOld)) { 553 | toNew = j; 554 | break; 555 | } 556 | } 557 | addTransition(DFAm, i, toNew, symbol); 558 | }) 559 | } 560 | return DFAm; 561 | } 562 | 563 | 564 | /* 565 | General functions 566 | */ 567 | function generateAutomata(regex) { // Create all the various automata 568 | instance = {}; 569 | instance.NFAl = trivialStates(constructNFAl(parseRegex(regex_global))); 570 | instance.NFATransition = removelTransitions(deepCopyAutomaton(instance.NFAl)); 571 | instance.NFA = removeUnreachable(deepCopyAutomaton(instance.NFATransition), instance.NFATransition); 572 | instance.DFA = subsetConstruction(instance.NFA); 573 | instance.DFAm = minimizeDFA(instance.DFA); 574 | generateExampleStrings(instance); 575 | instance.FAobj = []; 576 | instance.FAstrings = []; 577 | } 578 | 579 | function generateExampleStrings(instance) { // Generate some example strings for each state in each automaton 580 | for (var FA of [instance.NFAl, instance.NFA, instance.DFA, instance.DFAm]) { 581 | FA.examplestrings = []; 582 | for (var i = 0; i < FA.states; i++) 583 | FA.examplestrings[i] = []; 584 | FA.examplestrings[FA.start].push(""); // The starting state contains the empty string (lambda) 585 | var indexes = []; 586 | indexes[FA.start] = 0; 587 | nextExampleStrings(FA, [FA.start], indexes); 588 | } 589 | } 590 | 591 | function nextExampleStrings(FA, states, startingindexes) { 592 | var oldlength = []; 593 | for (var i = 0; i < FA.states; i++) 594 | oldlength[i] = FA.examplestrings[i].length; 595 | for (var state of states) { 596 | for (var i = startingindexes[state]; i < oldlength[state]; i++) { 597 | for (var symbol in FA.edges[state]) { 598 | if (FA.edges[state][symbol] == undefined) 599 | continue; 600 | FA.edges[state][symbol].forEach(function(to) { 601 | if (FA.examplestrings[to].length == 25) // Allow a maximum of 25 example strings per state 602 | return; 603 | if (symbol != '0' && !FA.examplestrings[to].includes(FA.examplestrings[state][i] + symbol)) 604 | FA.examplestrings[to].push(FA.examplestrings[state][i] + symbol); 605 | else if (symbol == '0' && !FA.examplestrings[to].includes(FA.examplestrings[state][i])) 606 | FA.examplestrings[to].push(FA.examplestrings[state][i]); 607 | }) 608 | } 609 | } 610 | } 611 | var newstates = []; // States to be checked next round 612 | for (var i = 0; i < FA.states; i++) { 613 | if (FA.examplestrings[i].length > oldlength[i]) // New strings have been added, check next round 614 | newstates.push(i); 615 | } 616 | if (newstates.length == 0) { // No new strings added this round, stop 617 | return; 618 | } 619 | nextExampleStrings(FA, newstates, oldlength); 620 | } 621 | 622 | function stringMatches(DFA, string) { // Check if string is accepted by the DFA, return array of passed states 623 | var passed = [DFA.start]; 624 | var cur = DFA.start; 625 | for (var i = 0; i < string.length; i++) { 626 | cur = DFA.edges[cur][string[i]].values().next().value; 627 | passed.push(cur); 628 | } 629 | return {"matched": DFA.accepting.has(cur), "passed": passed}; 630 | } 631 | 632 | function addTransition(FA, from, to, symbol) { 633 | if (FA.edges[from] == undefined) 634 | FA.edges[from] = {}; 635 | if (FA.edges[from][symbol] == undefined) 636 | FA.edges[from][symbol] = new Set([to]); 637 | else { 638 | if (FA.edges[from][symbol].has(to)) 639 | return false; // Transition already exists 640 | FA.edges[from][symbol].add(to); 641 | } 642 | if (FA.outgoing[from] == undefined) 643 | FA.outgoing[from] = 1; 644 | else 645 | FA.outgoing[from]++; 646 | if (FA.incoming[to] == undefined) 647 | FA.incoming[to] = 1; 648 | else 649 | FA.incoming[to]++; 650 | return true; 651 | } 652 | 653 | function removeTransition(FA, from, to, symbol) { 654 | if (FA.edges[from][symbol].has(to)) { 655 | FA.outgoing[from]--; 656 | FA.incoming[to]--; 657 | FA.edges[from][symbol].delete(to); 658 | } 659 | } 660 | 661 | function removeState(FA, state) { // Remove a state, corresponding edges, and lower state numbers 662 | if (FA.edges[state] != undefined) { 663 | for (var symbol in FA.edges[state]) { 664 | FA.edges[state][symbol].forEach(function(val) { 665 | removeTransition(FA, state, val, symbol); 666 | }) 667 | } 668 | } 669 | 670 | // Update edges array number now that a state has been deleted 671 | for (var i = 0; i < FA.states; i++) { 672 | if (FA.edges[i] == undefined) 673 | continue; 674 | for (var symbol in FA.edges[i]) { 675 | FA.edges[i][symbol].forEach(function(val) { 676 | if (val == state) 677 | removeTransition(FA, i, state, symbol); 678 | }) 679 | } 680 | for (var symbol in FA.edges[i]) { 681 | var newSet = new Set([]); 682 | FA.edges[i][symbol].forEach(function(value) { 683 | if (value >= state) 684 | newSet.add(value-1); 685 | else 686 | newSet.add(value); 687 | }); 688 | FA.edges[i][symbol] = newSet; 689 | } 690 | } 691 | if (FA.start >= state) 692 | FA.start--; 693 | var newaccepting = new Set([]); 694 | var it = FA.accepting.values(); 695 | for (var val = it.next().value; val !== undefined; val = it.next().value) { 696 | if (val >= state) 697 | newaccepting.add(val-1); 698 | else 699 | newaccepting.add(val); 700 | } 701 | FA.accepting = newaccepting; 702 | for (var i = state; i < FA.states; i++) { 703 | FA.edges[i] = FA.edges[i+1]; 704 | FA.outgoing[i] = FA.outgoing[i+1]; 705 | FA.incoming[i] = FA.incoming[i+1]; 706 | } 707 | FA.states--; 708 | } 709 | 710 | function deepCopyAutomaton(FA) { 711 | var copy = { 712 | alphabet: new Set(FA.alphabet), // Set containing letters used in expression 713 | states: FA.states, // Amount of states 714 | start: FA.start, // Index of starting state 715 | edges: [], // List of map of outgoing edges for each state 716 | outgoing: Array.from(FA.outgoing), // Amount of outgoing edges for each state 717 | incoming: Array.from(FA.incoming), // Amount of incoming edges for each state 718 | accepting: new Set(FA.accepting) // Set of accepting states 719 | } 720 | for (var i = 0; i < FA.states; i++) { 721 | if (FA.edges[i] != undefined) { 722 | copy.edges[i] = {}; 723 | for (var symbol in FA.edges[i]) { 724 | copy.edges[i][symbol] = new Set(FA.edges[i][symbol]); 725 | } 726 | } 727 | } 728 | return copy; 729 | } 730 | 731 | function SetHasArray(set, array) { // Check if set contains a certain array 732 | var found = false; 733 | set.forEach(function(val) { 734 | if (!found) { 735 | for (var i = 0; i < array.length; i++) { 736 | if (array[i] != val[i]) 737 | return; 738 | } 739 | found = true; 740 | } 741 | }) 742 | return found; 743 | } 744 | 745 | function ArrayHasArray(array, sub) { // Check if array contains a certain subarray 746 | for (var i = 0; i < array.length; i++) { 747 | if (ArrayEquals(array[i], sub)) { 748 | return i; 749 | } 750 | } 751 | return -1; 752 | } 753 | 754 | function ArrayEquals(arr1, arr2) { // Check if two arrays are the same 755 | if (arr1.length != arr2.length) 756 | return false; 757 | for (var i = 0; i < arr1.length; i++) 758 | if (arr1[i] != arr2[i]) 759 | return false; 760 | return true; 761 | } 762 | -------------------------------------------------------------------------------- /js/front.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var auto_animation = true; 3 | var in_animation = false; // If an animation is currently active 4 | var startscreen = true; // If the home screen is shown 5 | var regex_global = ""; 6 | var instance; 7 | var viz = new Viz({ workerURL: "js/full.render.js" }); // Load viz renderer 8 | 9 | $(document).ready(function(){ 10 | var test = browserTest(); // Check if some neccessary features are available in the users browser 11 | if (!test) { 12 | return; // Stop program, features not supported 13 | } 14 | loadSvgInteraction(); // Load panning and zooming events 15 | $("#regex").on("input", onRegexChange); 16 | $("#convert").click(onConvertRegex); 17 | $(".stepbutton").click(clickStepButton); 18 | 19 | $(".helpbutton").click(function() { 20 | $(".helpmenu").toggleClass("helpmenu_selected"); 21 | }) 22 | $(".helpexit").click(function() { 23 | $(".helpmenu").removeClass("helpmenu_selected"); 24 | }) 25 | $(".helpsectionbutton").click(function() { // Scroll to the clicked section 26 | $(".helpcontent").stop().animate({ 27 | scrollTop: $(this.getAttribute("section"))[0].offsetTop 28 | }, 600); 29 | }) 30 | $(".examplebutton").click(function() { 31 | $("#regex").val("a*(b+a|de*)*a*"); // Example regular expression 32 | $("#regex").trigger("input"); 33 | }) 34 | $("#startrect").click(function() { 35 | $("#startpopup").css("display", "none"); 36 | }) 37 | $("#options_check").change(function() { 38 | auto_animation = this.checked; 39 | if (auto_animation) { 40 | $("#options_button").attr("disabled", true); 41 | $("#options_button").trigger("click"); // Auto animation is now on, trigger one 42 | } // last click to clear the current animation queue 43 | else 44 | $("#options_button").attr("disabled", false); 45 | }); 46 | $("#startpopup").animate({"opacity": 1}, 500).promise().then(function() { // Animate in starting popup 47 | startPopupAnimation(); // Popup path animation 48 | }); 49 | $("#regex").trigger("input"); // If the input has been autofilled, check if the expression is correct at the start 50 | }); 51 | 52 | function browserTest() { 53 | try { 54 | var polygontest = document.createElementNS('http://www.w3.org/2000/svg', "polygon"); 55 | polygontest.setAttribute("points", "1,1 2,2 1,1"); 56 | var point = polygontest.points[0].x; 57 | if (point == undefined) 58 | throw "Unsupported SVG operations"; 59 | return true; 60 | } 61 | catch(e) { 62 | var errstring = "The browser you are using does not support some svg features this site relies on. This site is built for desktops and we recommended using Chrome, but Firefox, Safari and Opera should also work."; 63 | alert(errstring); 64 | $("body").html(errstring); 65 | return false; 66 | } 67 | } 68 | 69 | function onRegexChange() { 70 | var parsed = parseRegex($(this).val()); 71 | if ($(this).val() == "" || $(this).val() == undefined) { 72 | $("#convert").attr("disabled", true); 73 | $(this).removeClass("valid").removeClass("invalid"); 74 | $(".greenPath").removeClass("enabled"); 75 | $(".redPath").removeClass("enabled"); 76 | } 77 | else if (parsed === -1) { 78 | $("#convert").attr("disabled", true); 79 | $(this).removeClass("valid").addClass("invalid"); 80 | $(".greenPath").removeClass("enabled"); 81 | $(".redPath").addClass("enabled"); 82 | } 83 | else { 84 | $("#convert").attr("disabled", false); 85 | $(this).removeClass("invalid").addClass("valid"); 86 | $(".greenPath").addClass("enabled"); 87 | $(".redPath").removeClass("enabled"); 88 | } 89 | } 90 | 91 | function onConvertRegex() { 92 | var val = $("#regex").val() 93 | var parsed = parseRegex(val); 94 | if (parsed === -1 || val == "" || val == undefined || in_animation) // Invalid expression or animation in progress 95 | return; 96 | 97 | in_animation = true; 98 | $("#convert").attr("disabled", true).animate({"opacity": 0}, 700); 99 | $(".step").css("display", "none"); 100 | regex_global = $("#regex").val(); 101 | var p1; 102 | if (startscreen) { // First time making FA, show intro animation 103 | startscreen = false; 104 | p1 = introAnimation() 105 | } 106 | else { // Not first time making FA, first get rid of old one 107 | $("#FA svg").addClass('disappear'); 108 | p1 = $("#FA svg").delay(900); // Artificial delay, animation takes 0.9s 109 | $.when(p1).then(function() { 110 | $(this).remove(); 111 | }) 112 | } 113 | 114 | generateAutomata(); 115 | instance.FAstrings[0] = viz.renderSVGElement(toDotString(instance.NFAl, 0)); 116 | if (instance.NFATransition.ledges.size == 0) { 117 | instance.FAstrings[1] = instance.FAstrings[0]; 118 | instance.FAstrings[2] = instance.FAstrings[0]; 119 | } 120 | else { 121 | instance.FAstrings[1] = viz.renderSVGElement(toDotString(instance.NFATransition, 1)); 122 | instance.FAstrings[2] = viz.renderSVGElement(toDotString(instance.NFA, 0)); 123 | } 124 | instance.FAstrings[3] = viz.renderSVGElement(toDotString(instance.DFA, 0)); 125 | if (instance.DFA.states == instance.DFAm.states) // DFA was already minimal 126 | instance.FAstrings[4] = instance.FAstrings[3] 127 | else // DFA was not yet minimal. Create new svg for minimal DFA 128 | instance.FAstrings[4] = viz.renderSVGElement(toDotString(instance.DFAm, 0)); 129 | $.when(p1).then(function() { 130 | return showFAWrap(0); 131 | }).then(function() { 132 | $("#convert").attr("disabled", false).animate({"opacity": 1}, 700); 133 | showStepButtons(1); 134 | }) 135 | } 136 | 137 | /* 138 | Wrapper functions for the main animations: 139 | - Show NFAl, NFA, DFA or DFAm 140 | - Convert NFAl to NFA to DFA to DFAm 141 | - Check if string matches regular expression 142 | */ 143 | function showFAWrap(type) { // type=0: NFAl, 2: NFA, 3: DFA, 4: DFAm 144 | in_animation = true; 145 | return $.when(instance.FAstrings[type]).then(function(element) { // Element contains the svg element 146 | var FA = type == 0 ? instance.NFAl 147 | : type == 2 ? instance.NFA 148 | : type == 3 ? instance.DFA 149 | : instance.DFAm; 150 | instance.FAobj[type] = FAToHTML(FA, element); 151 | makeInvisible(instance.FAobj[type]); 152 | $("#FA").html(instance.FAobj[type].svg); 153 | updateScale($("#FA").children()[0]); 154 | return animationWrap(showFA, [instance.FAobj[type]], type); 155 | }) 156 | } 157 | 158 | function toNFA() { 159 | in_animation = true; 160 | return $.when(instance.FAstrings[1], instance.FAstrings[2]).then(function(element1, element2) { // Check if needed svg's have been rendered 161 | if (instance.NFATransition.ledges.size != 0) { 162 | instance.FAobj[1] = FAToHTML(instance.NFATransition, element1, 1); 163 | hideNewEdges(instance.FAobj[1]); 164 | } 165 | else { 166 | instance.FAobj[1] = undefined; 167 | } 168 | instance.FAobj[2] = FAToHTML(instance.NFA, element2); 169 | return animationWrap(animToNFAStart, [instance.FAobj], 2).then(function() { 170 | updateScale($("#FA").children()[0]); 171 | }) 172 | }) 173 | } 174 | 175 | function toDFA() { 176 | in_animation = true; 177 | return $.when(instance.FAstrings[3]).then(function(element) { // Check if needed svg has been rendered 178 | instance.FAobj[3] = FAToHTML(instance.DFA, element); 179 | makeInvisible(instance.FAobj[3]); 180 | return animationWrap(animToDFAStart, [instance], 3).then(function() { 181 | updateScale($("#FA").children()[0]); 182 | }) 183 | }) 184 | } 185 | 186 | function toDFAm() { 187 | in_animation = true; 188 | return $.when(instance.FAstrings[4]).then(function(element) { // Check if needed svg has been rendered 189 | instance.FAobj[4] = FAToHTML(instance.DFAm, element); 190 | return animationWrap(animToDFAmStart, [instance], 4).then(function() { 191 | updateScale($("#FA").children()[0]); 192 | }) 193 | }) 194 | } 195 | 196 | function checkMatch(string) { 197 | in_animation = true; 198 | $("#graphdouble").remove(); // Remove old graph double 199 | var match = stringMatches(instance.DFAm, string) 200 | return animationWrap(animCheckMatchStart, [instance.FAobj[4], match, string], match.matched ? 5 : 6).then(function() { 201 | var double = $("#graphdouble"); 202 | double.delay(3000).promise().then(function() { // If the graph double is still there after 3 seconds, remove it 203 | double.remove(); 204 | $("#step_message").html(""); 205 | }) 206 | }); 207 | } 208 | 209 | function updateScale(element) { 210 | if (element.viewBox.baseVal.width * $(element).height() < element.viewBox.baseVal.height * $(element).width()) { 211 | element.scale = element.viewBox.baseVal.height / $(element).height(); 212 | element.scaledir = 0; 213 | } 214 | else { 215 | element.scale = element.viewBox.baseVal.width / $(element).width(); 216 | element.scaledir = 1; 217 | } 218 | } 219 | 220 | function loadSvgInteraction() { 221 | var holdingNode = false; 222 | var panning = false; 223 | var holdingIndex; // Value of the state currently being held 224 | var oldX, oldY; 225 | var nodeSelected = false; 226 | var infobox, state, statedouble; 227 | 228 | $(document).mouseup(function() { 229 | holdingNode = false; 230 | panning = false; 231 | }) 232 | 233 | $(document).on("mousedown", "#FA svg", function(e) { 234 | panning = true; 235 | oldX = e.clientX; 236 | oldY = e.clientY; 237 | }) 238 | 239 | $(document).on("mousemove", "#FA svg", function(e) { 240 | if (panning) { 241 | this.viewBox.baseVal.x -= (e.clientX-oldX)*this.scale 242 | this.viewBox.baseVal.y -= (e.clientY-oldY)*this.scale; 243 | oldX = e.clientX; 244 | oldY = e.clientY; 245 | } 246 | }) 247 | 248 | $("#FA").on("wheel mousewheel", "svg", function(e) { 249 | e.preventDefault(); // Don't also scroll the window 250 | var delta; 251 | if (e.originalEvent.wheelDelta !== undefined) 252 | delta = e.originalEvent.wheelDelta; 253 | else 254 | delta = e.originalEvent.deltaY * -1; 255 | var mat = this.getScreenCTM().inverse(); 256 | var mouseX = e.clientX*mat.a+e.clientY*mat.c+mat.e; 257 | var mouseY = e.clientX*mat.b+e.clientY*mat.d+mat.f; 258 | var zoomscale; 259 | if (this.scaledir) { 260 | if (delta > 0 && this.viewBox.baseVal.width > 50) 261 | zoomscale = (this.viewBox.baseVal.width-50)/this.viewBox.baseVal.width; 262 | else if (delta < 0) 263 | zoomscale = (this.viewBox.baseVal.width+50)/this.viewBox.baseVal.width; 264 | else 265 | return; 266 | } 267 | else { 268 | if (delta > 0 && this.viewBox.baseVal.height > 50) 269 | zoomscale = (this.viewBox.baseVal.height-50)/this.viewBox.baseVal.height; 270 | else if (delta < 0) 271 | zoomscale = (this.viewBox.baseVal.height+50)/this.viewBox.baseVal.height; 272 | else 273 | return; 274 | } 275 | this.viewBox.baseVal.width *= zoomscale; 276 | this.viewBox.baseVal.height *= zoomscale; 277 | this.viewBox.baseVal.x = mouseX - (mouseX-this.viewBox.baseVal.x)*zoomscale; 278 | this.viewBox.baseVal.y = mouseY - (mouseY-this.viewBox.baseVal.y)*zoomscale; 279 | updateScale(this); 280 | }) 281 | 282 | $(window).on("resize", function(e) { // Update scale when window gets resized 283 | var FAs = $("#FA svg"); 284 | for (var FA of FAs) 285 | updateScale(FA); 286 | }) 287 | 288 | $(document).on("mouseenter mousemove", ".node", function(e) { 289 | if (!nodeSelected && !in_animation) { 290 | nodeSelected = true; 291 | infobox = $("#infobox" + this.id); 292 | state = this; 293 | statedouble = document.getElementById(state.id + "double") 294 | $("#graph0").prepend($("#graph0").children().filter(".edge")); // Move edges to background 295 | $(".info_path", infobox).addClass("info_path_selected"); 296 | $(".info_text", infobox).addClass("info_text_selected") 297 | $(".info_rect", state).addClass("info_rect_selected"); 298 | $("ellipse", state).eq(0).addClass("selected_big"); 299 | if (statedouble) 300 | $("ellipse", statedouble).eq(0).addClass("selected_big"); 301 | if ($("ellipse", state).length == 2) { // Accepting state, 2 ellipses 302 | $("ellipse", state).eq(1).addClass("selected_small"); 303 | if (statedouble) 304 | $("ellipse", statedouble).eq(1).addClass("selected_small"); 305 | } 306 | } 307 | }) 308 | 309 | $(document).on("mouseleave", ".node", function(e) { 310 | if (nodeSelected) { 311 | nodeSelected = false; 312 | $(".info_path", infobox).removeClass("info_path_selected"); 313 | $(".info_text", infobox).removeClass("info_text_selected"); 314 | $(".info_rect", state).removeClass("info_rect_selected"); 315 | $("ellipse", state).eq(0).removeClass("selected_big"); 316 | if (statedouble) 317 | $("ellipse", statedouble).eq(0).removeClass("selected_big"); 318 | if ($("ellipse", state).length == 2) { // Accepting state, 2 ellipses 319 | $("ellipse", state).eq(1).removeClass("selected_small"); 320 | if (statedouble) 321 | $("ellipse", statedouble).eq(1).removeClass("selected_small"); 322 | } 323 | $(document).delay(1500).promise().then(function() { 324 | // If no state selected after transition is done (1.5s), move edges to foreground again 325 | if (!nodeSelected) 326 | $("#graph0").append($("#graph0").children().filter(".edge")); 327 | }) 328 | } 329 | }) 330 | } 331 | 332 | 333 | function makeInvisible(FAobj) { // Make all nodes, edges, text hidden 334 | $("#graph0 ellipse", FAobj.svg).attr("visibility", "hidden"); 335 | $("#graph0 text", FAobj.svg).css({"opacity": 0}); 336 | 337 | var edges = $(".edge", FAobj.svg); 338 | for (var i = 0; i < edges.length; i++) { 339 | var path = $("path", edges[i]); 340 | path.css('stroke-dashoffset', path[0].lengthsaved); 341 | $("polygon", edges[i]).attr("visibility", "hidden"); 342 | } 343 | /*for (var i = 0; i < FAobj.edges.length; i++) { 344 | for (var symbol in FAobj.edges[i]) { 345 | FAobj.edges[i][symbol].forEach(function(val) { 346 | var path = $("path", val[1]); 347 | path.css({'stroke-dasharray': path[0].getTotalLength(), 'stroke-dashoffset': path[0].getTotalLength()}); 348 | $("polygon", val[1]).attr("visibility", "hidden"); 349 | }); 350 | } 351 | } 352 | var start = $("path", FAobj.start[0]); 353 | start.css({'stroke-dasharray': start[0].getTotalLength(), 'stroke-dashoffset': start[0].getTotalLength()}); 354 | $("polygon", FAobj.start).attr("visibility", "hidden");*/ 355 | } 356 | 357 | function hideNewEdges(FAobj) { 358 | for (var from in FAobj.newedges) { 359 | for (var to in FAobj.newedges[from]) { 360 | var edge = FAobj.edges[from][to][1]; 361 | if (FAobj.edges[from][to][0].length == 0) { 362 | var path = $("path", edge); 363 | path.css('stroke-dashoffset', path[0].lengthsaved); 364 | $("polygon", edge).attr("visibility", "hidden"); 365 | $("text", edge).css("opacity", 0); 366 | } 367 | else { 368 | $("text", edge).text(FAobj.edges[from][to][0].join(",")); 369 | var te = $("text", edge)[0]; 370 | var index = te.innerHTML.indexOf("0"); 371 | if (index != -1) 372 | te.innerHTML = te.innerHTML.substr(0, index) + "λ" + te.innerHTML.substr(index+1); 373 | } 374 | } 375 | } 376 | } 377 | 378 | /*function replaceLNewEdges(FAobj) { 379 | for (var from in FAobj.edges) { 380 | for (var to in FAobj.edges[from]) { 381 | var index = FAobj.edges[from][to][0].indexOf("0"); 382 | if (index != -1) 383 | FAobj.edges[from][to][0].splice(index, 1); 384 | FAobj.edges[from][to][0] = FAobj.edges[from][to][0].concat(FAobj.newedges[from][to]); 385 | } 386 | } 387 | }*/ 388 | 389 | function toDotString(FA, flag) { // Generate a dotstring for a FA 390 | // flag is 1 if this is for the combination of NFAl and NFAl 391 | var dotstring = "digraph automaton {\n" 392 | dotstring += "rankdir=LR\n" 393 | dotstring += "qi [shape=point, style=invis]; qi\n" 394 | for (var i = 0; i < FA.states; i++) { 395 | dotstring += "node [shape=circle] " + i + ";\n"; 396 | } 397 | dotstring += "qi -> " + FA.start + "[style=bold];\n"; 398 | if (!flag) { 399 | for (var i = 0; i < FA.edges.length; i++) { 400 | if (FA.edges[i] != undefined) { 401 | var todict = {}; 402 | for (var symbol in FA.edges[i]) { 403 | FA.edges[i][symbol].forEach(function(val) { 404 | if (todict[val] == undefined) 405 | todict[val] = []; 406 | todict[val].push(symbol) 407 | }); 408 | /*FA.edges[i][symbol].forEach(function(val) { 409 | dotstring += i + " -> " + val + " [label = \"" + symbol + "\"];\n"; 410 | });*/ 411 | } 412 | for (var key in todict) { 413 | dotstring += i + " -> " + key + " [label = \"" + todict[key].join(",") + "\"];\n"; 414 | } 415 | } 416 | } 417 | } 418 | else { 419 | var todict = {}; 420 | var builddict = function(arr) { 421 | if (todict[arr[0]] == undefined) 422 | todict[arr[0]] = {}; 423 | if (todict[arr[0]][arr[1]] == undefined) 424 | todict[arr[0]][arr[1]] = []; 425 | todict[arr[0]][arr[1]].push(arr[2]); 426 | } 427 | FA.normaledges.forEach(function(arr) { 428 | builddict(arr); 429 | //dotstring += arr[0] + " -> " + arr[1] + " [label = \"" + arr[2] + "\"];\n"; 430 | }) 431 | FA.newedges.forEach(function(arr) { 432 | builddict(arr); 433 | //dotstring += arr[0] + " -> " + arr[1] + " [label = \"" + arr[2] + "\"];\n"; 434 | }) 435 | FA.ledges.forEach(function(arr) { 436 | builddict(arr); 437 | //dotstring += arr[0] + " -> " + arr[1] + " [label = \"" + arr[2] + "\"];\n"; 438 | }) 439 | for (var from in todict) 440 | for (var to in todict[from]) 441 | dotstring += from + " -> " + to + " [label = \"" + todict[from][to].join(",") + "\"];\n"; 442 | } 443 | dotstring += "}"; 444 | return dotstring; 445 | } 446 | 447 | function FAToHTML(FA, HTML, flag) { // From a FA and HTML (svg) code return an object containing reference to the state and edge elements 448 | var retObj = { // Object to be returned 449 | edges: [], 450 | states: [] 451 | } 452 | for (var i = 0; i < FA.states; i++) 453 | retObj.edges[i] = {}; 454 | 455 | // Create a new svg element and paste the svg content in it, because the svg 456 | // element created by viz didn't work properly with animations in some browsers 457 | var realsvg = document.createElementNS('http://www.w3.org/2000/svg', "svg"); 458 | realsvg.setAttribute("width", HTML.attributes.width.value); 459 | realsvg.setAttribute("height", HTML.attributes.height.value); 460 | realsvg.setAttribute("viewBox", HTML.attributes.viewBox.value); 461 | realsvg.setAttribute("xmlns", HTML.attributes.xmlns.value); 462 | realsvg.setAttribute("xmlns:xlink", HTML.attributes["xmlns:xlink"].value); 463 | $(realsvg).append($(HTML.cloneNode(true)).children()[0]); 464 | 465 | var elements = $(realsvg); 466 | retObj.svg = elements; 467 | retObj.start = [$("#edge1", elements), FA.start]; 468 | 469 | var g = document.createElementNS('http://www.w3.org/2000/svg', "g"); 470 | g.setAttribute("id", "infoboxes"); 471 | g.setAttribute("transform", retObj.svg.children().eq(0).attr("transform")); 472 | retObj.svg.append(g); 473 | var appendAndTruncate = function(examplestring, tspan, text) { 474 | $(tspan).appendTo(text).promise().then(function() { 475 | tspan.innerHTML = examplestring; 476 | // Truncate string if too long 477 | if (tspan.getSubStringLength(0, examplestring.length) > 95) { 478 | var newlength = examplestring.length-1; 479 | tspan.innerHTML = examplestring.substr(0, newlength); 480 | while (tspan.getSubStringLength(0, newlength) > 90 && newlength > 1) { 481 | newlength--; 482 | tspan.innerHTML = examplestring.substr(0, newlength); 483 | } 484 | tspan.innerHTML += "..."; 485 | } 486 | }) 487 | } 488 | 489 | for (var i = 0; i < FA.states; i++) { 490 | retObj.states[i] = $("#node" + (i+2), elements); // State 0 has id #node2 etc.. 491 | if (FA.accepting.has(i)) { // Add inner circle to accepting states 492 | var circle = document.createElementNS('http://www.w3.org/2000/svg', "ellipse"); 493 | circle.setAttribute("stroke", "#000000"); 494 | circle.setAttribute("cx", $("ellipse", retObj.states[i]).attr("cx")); 495 | circle.setAttribute("cy", $("ellipse", retObj.states[i]).attr("cy")); 496 | circle.setAttribute("rx", $("ellipse", retObj.states[i]).attr("rx")-3); 497 | circle.setAttribute("ry", $("ellipse", retObj.states[i]).attr("ry")-3); 498 | $("ellipse", retObj.states[i]).after(circle); 499 | } 500 | if (!flag) { // Add hover info box to states 501 | var subg = document.createElementNS('http://www.w3.org/2000/svg', "g"); 502 | subg.setAttribute("id", "infobox" + retObj.states[i].attr("id")); 503 | subg.setAttribute("class", "info_box") 504 | g.appendChild(subg); 505 | var cx = $("ellipse", retObj.states[i]).attr("cx"); 506 | var cy = $("ellipse", retObj.states[i]).attr("cy"); 507 | var path = document.createElementNS('http://www.w3.org/2000/svg', "path"); 508 | path.setAttribute("stroke", "#000000"); 509 | path.setAttribute("fill", "#eeeeee"); 510 | path.setAttribute("d", "M" + (cx-23) + "," + cy + " l0,-75 100,0 0,52 -77,0 a23 23 0 0 0 -23 23"); 511 | path.setAttribute("class", "info_path"); 512 | var len = path.getTotalLength(); 513 | path.style["stroke-dasharray"] = len; 514 | path.style["stroke-dashoffset"] = len; 515 | subg.append(path); 516 | var text = document.createElementNS('http://www.w3.org/2000/svg', "text"); 517 | text.setAttribute("x", cx-20); 518 | text.setAttribute("y", cy-64); 519 | text.setAttribute("fill", "#000000"); 520 | text.classList.add("info_text"); 521 | subg.append(text); 522 | var texthead = document.createElementNS('http://www.w3.org/2000/svg', "tspan"); 523 | texthead.setAttribute("x", cx-20); 524 | texthead.setAttribute("dy", "0"); 525 | texthead.classList.add("info_text_big"); 526 | texthead.innerHTML = "State " + i; 527 | text.append(texthead); 528 | // Get (a maximum of) 4 random strings from the examplestrings array 529 | var examplestrings = []; 530 | if (FA.examplestrings[i].length <= 4) 531 | examplestrings = FA.examplestrings[i]; 532 | else { 533 | // Always add the first found examplestring 534 | examplestrings.push(FA.examplestrings[i][0]); 535 | while (examplestrings.length < 4) { 536 | var index = Math.floor(Math.random()*FA.examplestrings[i].length); 537 | if (examplestrings.indexOf(FA.examplestrings[i][index]) == -1) 538 | examplestrings.push(FA.examplestrings[i][index]); 539 | } 540 | examplestrings.sort(function(a,b) { 541 | return a.length - b.length; // Ascending in length 542 | }) 543 | } 544 | var textexample = document.createElementNS('http://www.w3.org/2000/svg', "tspan"); 545 | textexample.setAttribute("x", cx-20); 546 | textexample.setAttribute("dy", "7.8px"); 547 | textexample.classList.add("info_text_small"); 548 | textexample.innerHTML = "Example strings:"; 549 | $(textexample).appendTo(text); 550 | for (var examplestring of examplestrings) { 551 | var textexample = document.createElementNS('http://www.w3.org/2000/svg', "tspan"); 552 | textexample.setAttribute("x", cx-20); 553 | textexample.setAttribute("dy", "7.5px"); 554 | textexample.classList.add("info_text_small"); 555 | if (examplestring.length == 0) { // Empty string 556 | textexample.innerHTML = "λ (Empty string)"; 557 | $(textexample).appendTo(text); 558 | } 559 | else { 560 | appendAndTruncate(examplestring, textexample, text); 561 | } 562 | } 563 | var rect = document.createElementNS('http://www.w3.org/2000/svg', "rect"); // Invisible rectangle to detect hovering 564 | rect.setAttribute("x", cx-23); 565 | rect.setAttribute("y", cy-75); 566 | rect.setAttribute("width", "100"); 567 | rect.setAttribute("height", "75"); 568 | rect.setAttribute("fill", "rgba(0,0,0,0)"); 569 | rect.setAttribute("stroke", "rgba(0,0,0,0)"); 570 | rect.setAttribute("class", "info_rect"); 571 | retObj.states[i].append(rect); 572 | } 573 | } 574 | 575 | var edges = $(".edge", elements); 576 | if (!flag) { // !flag: normal FA 577 | for (var i = 1; i < edges.length; i++) { 578 | var title = $("title", edges[i]).text().split("->"); 579 | var from = parseInt(title[0]); 580 | var to = parseInt(title[1]); 581 | var symbols = $("text", edges[i]).text().split(","); 582 | if (retObj.edges[from][to] == undefined) 583 | retObj.edges[from][to] = [[], $(edges[i])]; 584 | retObj.edges[from][to][0] = retObj.edges[from][to][0].concat(symbols) 585 | } 586 | } 587 | else { // flag: combination of NFAl and NFA 588 | retObj.newedges = []; 589 | retObj.newaccepting = []; 590 | retObj.unreachable = []; 591 | for (var i = 0; i < FA.states; i++) 592 | retObj.newedges[i] = {}; 593 | for (var i = 0; i < FA.newaccepting.length; i++) { 594 | retObj.newaccepting.push([FA.newaccepting[i], retObj.states[FA.newaccepting[i]]]); 595 | // Temporarily hide the inner circle, to be added back in the toNFA animation 596 | $($("ellipse", retObj.newaccepting[i][1])[1]).css("opacity", "0"); 597 | } 598 | for (var i = 0; i < FA.unreachable.length; i++) 599 | retObj.unreachable.push([FA.unreachable[i], retObj.states[FA.unreachable[i]]]); 600 | 601 | for (var i = 1; i < edges.length; i++) { 602 | var title = $("title", edges[i]).text().split("->"); 603 | var from = parseInt(title[0]); 604 | var to = parseInt(title[1]); 605 | var symbols = $("text", edges[i]).text().split(","); 606 | retObj.edges[from][to] = [[], $(edges[i])]; 607 | retObj.newedges[from][to] = []; 608 | for (var index in symbols) { 609 | if (SetHasArray(FA.newedges, [from,to,symbols[index]])) 610 | retObj.newedges[from][to].push(symbols[index]); 611 | else 612 | retObj.edges[from][to][0].push(symbols[index]); 613 | } 614 | } 615 | } 616 | for (var i = 0; i < edges.length; i++) { // Set stroke-dasharray for all edges 617 | $("path", edges[i])[0].lengthsaved = $("path", edges[i])[0].getTotalLength() 618 | $("path", edges[i]).css("stroke-dasharray", $("path", edges[i])[0].lengthsaved); 619 | } 620 | $("text", edges).each(function() { 621 | var index = this.innerHTML.indexOf("0"); 622 | if (index != -1) 623 | this.innerHTML = this.innerHTML.substr(0, index) + "λ" + this.innerHTML.substr(index+1); 624 | }) 625 | $("title", elements).remove(); // Remove all the title elements (hovering indicator) 626 | $("ellipse", elements).attr("fill", "#eeeeee"); 627 | elements.children().children("polygon").remove(); // Remove the background fill 628 | elements.children().contents().each(function() { 629 | if (this.nodeType == Node.COMMENT_NODE || this.nodeType == Node.TEXT_NODE) { 630 | $(this).remove(); // Remove comments from the html 631 | } 632 | }) 633 | return retObj; 634 | } 635 | 636 | function showStepButtons(step) { 637 | var textl = "", textr= ""; 638 | switch (step) { 639 | case 1: 640 | textl = "Replay
animation"; 641 | textr = "Remove
λ-transitions"; 642 | break; 643 | case 2: 644 | textl = "Revert to
NFA-λ"; 645 | textr = "Remove non-
determinism"; 646 | break; 647 | case 3: 648 | textl = "Revert to
NFA"; 649 | textr = "Minimize"; 650 | break; 651 | case 4: 652 | textl = "Revert to
DFA" 653 | } 654 | $(".step").finish(); 655 | if (step < 4) { 656 | $("#stepr .steptext").html(textr); 657 | $("#stepr").attr("step", step); 658 | $("#stepr").css({"display": "flex", "opacity": 0}).animate({"opacity": 1}, 800); 659 | } 660 | else { // Step == 4 661 | $("#stepr").css("display", "none"); 662 | $("#stepmatch .stepbutton").attr("step", 4); 663 | $("#stepmatch").css({"display": "block", "opacity": 0}).animate({"opacity": 1}, 800); 664 | } 665 | $("#stepl .steptext").html(textl); 666 | $("#stepl").attr("step", step+4); 667 | $("#stepl").css({"display": "flex", "opacity": 0}).animate({"opacity": 1}, 800); 668 | $("#convert").attr("disabled", false).animate({"opacity": 1}, 800); 669 | } 670 | 671 | function clickStepButton() { 672 | if (in_animation) 673 | return; // Animation already in progress: stop 674 | if (this.getAttribute("step") == "4") { 675 | var string = $("#matchinput").val(); 676 | for (var i = 0; i < string.length; i++) { 677 | if (!instance.DFAm.alphabet.has(string[i])) { // String contains letter not in alphabet 678 | $("#step_message").html("String may only contain characters that are present in the automaton"); 679 | return; 680 | } 681 | } 682 | } 683 | $(".step, #convert").finish().attr("disabled", true).animate({"opacity": 0}, 100).promise().then(function() { 684 | if (in_animation) 685 | $(".step").css("display", "none"); 686 | }) 687 | switch (this.getAttribute("step")) { 688 | case "1": 689 | toNFA().then(function() { showStepButtons(2); }); 690 | break; 691 | case "2": 692 | toDFA().then(function() { showStepButtons(3); }); 693 | break; 694 | case "3": 695 | toDFAm().then(function() { showStepButtons(4); }); 696 | break; 697 | case "4": 698 | checkMatch(string).then(function() { showStepButtons(4); }); 699 | break; 700 | case "5": 701 | case "6": 702 | showFAWrap(0).then(function() { showStepButtons(1); }); 703 | break; 704 | case "7": 705 | showFAWrap(2).then(function() { showStepButtons(2); }); 706 | break; 707 | case "8": 708 | showFAWrap(3).then(function() { showStepButtons(3); }); 709 | break; 710 | } 711 | } 712 | -------------------------------------------------------------------------------- /js/viz.js: -------------------------------------------------------------------------------- 1 | /* 2 | Viz.js 2.1.2 (Graphviz 2.40.1, Expat 2.2.5, Emscripten 1.37.36) 3 | Copyright (c) 2014-2018 Michael Daines 4 | Licensed under MIT license 5 | 6 | This distribution contains other software in object code form: 7 | 8 | Graphviz 9 | Licensed under Eclipse Public License - v 1.0 10 | http://www.graphviz.org 11 | 12 | Expat 13 | Copyright (c) 1998, 1999, 2000 Thai Open Source Software Center Ltd and Clark Cooper 14 | Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Expat maintainers. 15 | Licensed under MIT license 16 | http://www.libexpat.org 17 | 18 | zlib 19 | Copyright (C) 1995-2013 Jean-loup Gailly and Mark Adler 20 | http://www.zlib.net/zlib_license.html 21 | */ 22 | (function (global, factory) { 23 | typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : 24 | typeof define === 'function' && define.amd ? define(factory) : 25 | (global.Viz = factory()); 26 | }(this, (function () { 'use strict'; 27 | 28 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { 29 | return typeof obj; 30 | } : function (obj) { 31 | return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; 32 | }; 33 | 34 | var classCallCheck = function (instance, Constructor) { 35 | if (!(instance instanceof Constructor)) { 36 | throw new TypeError("Cannot call a class as a function"); 37 | } 38 | }; 39 | 40 | var createClass = function () { 41 | function defineProperties(target, props) { 42 | for (var i = 0; i < props.length; i++) { 43 | var descriptor = props[i]; 44 | descriptor.enumerable = descriptor.enumerable || false; 45 | descriptor.configurable = true; 46 | if ("value" in descriptor) descriptor.writable = true; 47 | Object.defineProperty(target, descriptor.key, descriptor); 48 | } 49 | } 50 | 51 | return function (Constructor, protoProps, staticProps) { 52 | if (protoProps) defineProperties(Constructor.prototype, protoProps); 53 | if (staticProps) defineProperties(Constructor, staticProps); 54 | return Constructor; 55 | }; 56 | }(); 57 | 58 | var _extends = Object.assign || function (target) { 59 | for (var i = 1; i < arguments.length; i++) { 60 | var source = arguments[i]; 61 | 62 | for (var key in source) { 63 | if (Object.prototype.hasOwnProperty.call(source, key)) { 64 | target[key] = source[key]; 65 | } 66 | } 67 | } 68 | 69 | return target; 70 | }; 71 | 72 | var WorkerWrapper = function () { 73 | function WorkerWrapper(worker) { 74 | var _this = this; 75 | 76 | classCallCheck(this, WorkerWrapper); 77 | 78 | this.worker = worker; 79 | this.listeners = []; 80 | this.nextId = 0; 81 | 82 | this.worker.addEventListener('message', function (event) { 83 | var id = event.data.id; 84 | var error = event.data.error; 85 | var result = event.data.result; 86 | 87 | _this.listeners[id](error, result); 88 | delete _this.listeners[id]; 89 | }); 90 | } 91 | 92 | createClass(WorkerWrapper, [{ 93 | key: 'render', 94 | value: function render(src, options) { 95 | var _this2 = this; 96 | 97 | return new Promise(function (resolve, reject) { 98 | var id = _this2.nextId++; 99 | 100 | _this2.listeners[id] = function (error, result) { 101 | if (error) { 102 | reject(new Error(error.message, error.fileName, error.lineNumber)); 103 | return; 104 | } 105 | resolve(result); 106 | }; 107 | 108 | _this2.worker.postMessage({ id: id, src: src, options: options }); 109 | }); 110 | } 111 | }]); 112 | return WorkerWrapper; 113 | }(); 114 | 115 | var ModuleWrapper = function ModuleWrapper(module, render) { 116 | classCallCheck(this, ModuleWrapper); 117 | 118 | var instance = module(); 119 | this.render = function (src, options) { 120 | return new Promise(function (resolve, reject) { 121 | try { 122 | resolve(render(instance, src, options)); 123 | } catch (error) { 124 | reject(error); 125 | } 126 | }); 127 | }; 128 | }; 129 | 130 | // https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding 131 | 132 | 133 | function b64EncodeUnicode(str) { 134 | return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) { 135 | return String.fromCharCode('0x' + p1); 136 | })); 137 | } 138 | 139 | function defaultScale() { 140 | if ('devicePixelRatio' in window && window.devicePixelRatio > 1) { 141 | return window.devicePixelRatio; 142 | } else { 143 | return 1; 144 | } 145 | } 146 | 147 | function svgXmlToImageElement(svgXml) { 148 | var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, 149 | _ref$scale = _ref.scale, 150 | scale = _ref$scale === undefined ? defaultScale() : _ref$scale, 151 | _ref$mimeType = _ref.mimeType, 152 | mimeType = _ref$mimeType === undefined ? "image/png" : _ref$mimeType, 153 | _ref$quality = _ref.quality, 154 | quality = _ref$quality === undefined ? 1 : _ref$quality; 155 | 156 | return new Promise(function (resolve, reject) { 157 | var svgImage = new Image(); 158 | 159 | svgImage.onload = function () { 160 | var canvas = document.createElement('canvas'); 161 | canvas.width = svgImage.width * scale; 162 | canvas.height = svgImage.height * scale; 163 | 164 | var context = canvas.getContext("2d"); 165 | context.drawImage(svgImage, 0, 0, canvas.width, canvas.height); 166 | 167 | canvas.toBlob(function (blob) { 168 | var image = new Image(); 169 | image.src = URL.createObjectURL(blob); 170 | image.width = svgImage.width; 171 | image.height = svgImage.height; 172 | 173 | resolve(image); 174 | }, mimeType, quality); 175 | }; 176 | 177 | svgImage.onerror = function (e) { 178 | var error; 179 | 180 | if ('error' in e) { 181 | error = e.error; 182 | } else { 183 | error = new Error('Error loading SVG'); 184 | } 185 | 186 | reject(error); 187 | }; 188 | 189 | svgImage.src = 'data:image/svg+xml;base64,' + b64EncodeUnicode(svgXml); 190 | }); 191 | } 192 | 193 | function svgXmlToImageElementFabric(svgXml) { 194 | var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, 195 | _ref2$scale = _ref2.scale, 196 | scale = _ref2$scale === undefined ? defaultScale() : _ref2$scale, 197 | _ref2$mimeType = _ref2.mimeType, 198 | mimeType = _ref2$mimeType === undefined ? 'image/png' : _ref2$mimeType, 199 | _ref2$quality = _ref2.quality, 200 | quality = _ref2$quality === undefined ? 1 : _ref2$quality; 201 | 202 | var multiplier = scale; 203 | 204 | var format = void 0; 205 | if (mimeType == 'image/jpeg') { 206 | format = 'jpeg'; 207 | } else if (mimeType == 'image/png') { 208 | format = 'png'; 209 | } 210 | 211 | return new Promise(function (resolve, reject) { 212 | fabric.loadSVGFromString(svgXml, function (objects, options) { 213 | // If there's something wrong with the SVG, Fabric may return an empty array of objects. Graphviz appears to give us at least one element back even given an empty graph, so we will assume an error in this case. 214 | if (objects.length == 0) { 215 | reject(new Error('Error loading SVG with Fabric')); 216 | } 217 | 218 | var element = document.createElement("canvas"); 219 | element.width = options.width; 220 | element.height = options.height; 221 | 222 | var canvas = new fabric.Canvas(element, { enableRetinaScaling: false }); 223 | var obj = fabric.util.groupSVGElements(objects, options); 224 | canvas.add(obj).renderAll(); 225 | 226 | var image = new Image(); 227 | image.src = canvas.toDataURL({ format: format, multiplier: multiplier, quality: quality }); 228 | image.width = options.width; 229 | image.height = options.height; 230 | 231 | resolve(image); 232 | }); 233 | }); 234 | } 235 | 236 | var Viz = function () { 237 | function Viz() { 238 | var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {}, 239 | workerURL = _ref3.workerURL, 240 | worker = _ref3.worker, 241 | Module = _ref3.Module, 242 | render = _ref3.render; 243 | 244 | classCallCheck(this, Viz); 245 | 246 | if (typeof workerURL !== 'undefined') { 247 | this.wrapper = new WorkerWrapper(new Worker(workerURL)); 248 | } else if (typeof worker !== 'undefined') { 249 | this.wrapper = new WorkerWrapper(worker); 250 | } else if (typeof Module !== 'undefined' && typeof render !== 'undefined') { 251 | this.wrapper = new ModuleWrapper(Module, render); 252 | } else if (typeof Viz.Module !== 'undefined' && typeof Viz.render !== 'undefined') { 253 | this.wrapper = new ModuleWrapper(Viz.Module, Viz.render); 254 | } else { 255 | throw new Error('Must specify workerURL or worker option, Module and render options, or include one of full.render.js or lite.render.js after viz.js.'); 256 | } 257 | } 258 | 259 | createClass(Viz, [{ 260 | key: 'renderString', 261 | value: function renderString(src) { 262 | var _ref4 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}, 263 | _ref4$format = _ref4.format, 264 | format = _ref4$format === undefined ? 'svg' : _ref4$format, 265 | _ref4$engine = _ref4.engine, 266 | engine = _ref4$engine === undefined ? 'dot' : _ref4$engine, 267 | _ref4$files = _ref4.files, 268 | files = _ref4$files === undefined ? [] : _ref4$files, 269 | _ref4$images = _ref4.images, 270 | images = _ref4$images === undefined ? [] : _ref4$images, 271 | _ref4$yInvert = _ref4.yInvert, 272 | yInvert = _ref4$yInvert === undefined ? false : _ref4$yInvert, 273 | _ref4$nop = _ref4.nop, 274 | nop = _ref4$nop === undefined ? 0 : _ref4$nop; 275 | 276 | for (var i = 0; i < images.length; i++) { 277 | files.push({ 278 | path: images[i].path, 279 | data: '\n\n' 280 | }); 281 | } 282 | 283 | return this.wrapper.render(src, { format: format, engine: engine, files: files, images: images, yInvert: yInvert, nop: nop }); 284 | } 285 | }, { 286 | key: 'renderSVGElement', 287 | value: function renderSVGElement(src) { 288 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 289 | 290 | return this.renderString(src, _extends({}, options, { format: 'svg' })).then(function (str) { 291 | var parser = new DOMParser(); 292 | return parser.parseFromString(str, 'image/svg+xml').documentElement; 293 | }); 294 | } 295 | }, { 296 | key: 'renderImageElement', 297 | value: function renderImageElement(src) { 298 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 299 | var scale = options.scale, 300 | mimeType = options.mimeType, 301 | quality = options.quality; 302 | 303 | 304 | return this.renderString(src, _extends({}, options, { format: 'svg' })).then(function (str) { 305 | if ((typeof fabric === 'undefined' ? 'undefined' : _typeof(fabric)) === "object" && fabric.loadSVGFromString) { 306 | return svgXmlToImageElementFabric(str, { scale: scale, mimeType: mimeType, quality: quality }); 307 | } else { 308 | return svgXmlToImageElement(str, { scale: scale, mimeType: mimeType, quality: quality }); 309 | } 310 | }); 311 | } 312 | }, { 313 | key: 'renderJSONObject', 314 | value: function renderJSONObject(src) { 315 | var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; 316 | var format = options.format; 317 | 318 | 319 | if (format !== 'json' || format !== 'json0') { 320 | format = 'json'; 321 | } 322 | 323 | return this.renderString(src, _extends({}, options, { format: format })).then(function (str) { 324 | return JSON.parse(str); 325 | }); 326 | } 327 | }]); 328 | return Viz; 329 | }(); 330 | 331 | return Viz; 332 | 333 | }))); 334 | --------------------------------------------------------------------------------