├── LICENSE ├── README.md ├── favicon-32.png ├── index.html ├── lua-patterns-logo.png ├── pm.js └── styles.css /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Spar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lua Patterns Viewer 2 | A tool for inspecting, analyzing and learning Lua patterns. 3 | Inspired by https://regexr.com/ and https://regex101.com/. 4 | Any help, feedback, suggestions, criticism are welcome. 5 | 6 | ## Visit the page: https://gitspartv.github.io/lua-patterns/ 7 | 8 | [![HitCount](http://hits.dwyl.com/GitSparTV/lua-patterns.svg?style=flat)](http://hits.dwyl.com/GitSparTV/lua-patterns) -------------------------------------------------------------------------------- /favicon-32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSparTV/lua-patterns/a2b041ceeb491f263c6ae71c83ff5cec5bdc7478/favicon-32.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 17 | 18 | 19 | 20 | 21 | Lua Patterns Viewer 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
37 | Hide 38 |
Logo
40 |

Lua Patterns Viewer 1.1.5
Lua Manual

42 | 43 |


Unimplemented:
- Reference Manual
- Match test
- Lua backslash escapes
- Unified token colors
- Proper explanations

44 |
45 |


Settings:

46 | 47 | 62 | 63 |
64 |

Made by Spar.
GitHub

Analytics:
analytics counter

66 |
67 | open info 68 |
69 |

Lua Patterns Viewer

70 |

👋 Hi! Do you like this site? Answer a few questions in this Google form to improve this tool!

71 |
72 |
73 | 94 |
95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /lua-patterns-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GitSparTV/lua-patterns/a2b041ceeb491f263c6ae71c83ff5cec5bdc7478/lua-patterns-logo.png -------------------------------------------------------------------------------- /pm.js: -------------------------------------------------------------------------------- 1 | const TOK = Object.freeze({ 2 | START: 0, // ^ 3 | END: 1, // $ 4 | ANY: 2, // . 5 | ZEROORMORE: 3, // * 6 | ONEORMORE: 4, // + 7 | ZEROORMORELAZY: 5, // - 8 | ZEROORONE: 6, // ? 9 | CHAR: 7, // literal 10 | LPAR: 8, // ( 11 | RPAR: 9, // ) 12 | ESCAPED: 10, // %escaped 13 | LBRACKET: 11, // [ 14 | RBRACKET: 12, // ] 15 | INVERSE: 13, // ^ in set 16 | CLASS: 14, // %class 17 | CAPTUREREF: 15, // %1 18 | BALANCED: 16, // %b 19 | FRONTIER: 17, // %f 20 | ERROR: 18, // error 21 | }) 22 | const TokToStr = [ 23 | "START", 24 | "END", 25 | "ANY", 26 | "ZEROORMORE", 27 | "ONEORMORE", 28 | "ZEROORMORELAZY", 29 | "ZEROORONE", 30 | "CHAR", 31 | "LPAR", 32 | "RPAR", 33 | "ESCAPED", 34 | "LBRACKET", 35 | "RBRACKET", 36 | "INVERSE", 37 | "CLASS", 38 | "CAPTUREREF", 39 | "BALANCED", 40 | "FRONTIER", 41 | ] 42 | 43 | // print = console.log 44 | print = function () { } 45 | 46 | class Token { 47 | constructor(tk, str) { 48 | this.type = tk 49 | this.string = str 50 | } 51 | } 52 | 53 | class Lexer { 54 | constructor(str) { 55 | this.input = str 56 | this.end = str.length 57 | this.last = str.length - 1 58 | this.tokens = [] 59 | this.current = str.charAt(0) 60 | this.caret = 0 61 | } 62 | 63 | Next() { 64 | this.current = this.input.charAt(++this.caret) 65 | } 66 | 67 | Lookahead() { 68 | return this.input.charAt(this.caret + 1) 69 | } 70 | 71 | CheckNext(char) { 72 | if (this.current === char) { 73 | this.Next() 74 | return true 75 | } 76 | return false 77 | } 78 | 79 | IsEnd() { 80 | return this.caret >= this.end 81 | } 82 | 83 | IsLast() { 84 | return this.caret === this.last 85 | } 86 | 87 | AddToken(type, info) { 88 | this.tokens.push(new Token(type, info)) 89 | } 90 | 91 | Sub(a, b) { 92 | return this.input.substring(a, b) 93 | } 94 | } 95 | 96 | function MatchClass(char) { 97 | switch (char.toLowerCase()) { 98 | case 'a': case 'c': case 'd': case 'g': case 'l': 99 | case 'p': case 's': case 'u': case 'w': case 'x': case 'z': 100 | return true; 101 | default: 102 | return false; 103 | } 104 | } 105 | 106 | function ReadQuantity(lex) { 107 | switch (lex.current) { 108 | case "+": 109 | lex.AddToken(TOK.ONEORMORE) 110 | lex.Next() 111 | break 112 | case "-": 113 | lex.AddToken(TOK.ZEROORMORELAZY) 114 | lex.Next() 115 | break 116 | case "*": 117 | lex.AddToken(TOK.ZEROORMORE) 118 | lex.Next() 119 | break 120 | case "?": 121 | lex.AddToken(TOK.ZEROORONE) 122 | lex.Next() 123 | break 124 | } 125 | } 126 | 127 | function ReadEscape(lex) { 128 | if (lex.IsEnd()) { lex.AddToken(TOK.ERROR, "Unfinished escape. Finish \"%\" with class or escape character or use \"%%\" to match \"%\" as character."); throw new Error("") } 129 | if (MatchClass(lex.current)) { 130 | lex.AddToken(TOK.CLASS, lex.current) 131 | } else { 132 | lex.AddToken(TOK.ESCAPED, lex.current) 133 | } 134 | lex.Next() 135 | } 136 | 137 | function ReadSet(lex) { 138 | lex.AddToken(TOK.LBRACKET) 139 | lex.Next() 140 | if (lex.CheckNext("^")) lex.AddToken(TOK.INVERSE); 141 | do { 142 | if (lex.IsEnd()) { lex.AddToken(TOK.ERROR, "Missing \"]\" to close set."); throw new Error("") } 143 | if (lex.current === "%" && lex.caret < lex.end) { 144 | lex.Next() 145 | ReadEscape(lex) 146 | } else { 147 | lex.AddToken(TOK.CHAR, lex.current) 148 | lex.Next() 149 | } 150 | } while (lex.current != "]") 151 | lex.AddToken(TOK.RBRACKET) 152 | lex.Next() 153 | } 154 | 155 | function PatternsLex(input) { 156 | const lex = new Lexer(input) 157 | try { 158 | if (lex.CheckNext("^")) { 159 | lex.AddToken(TOK.START) 160 | } 161 | print("Str", input) 162 | print("Len", input.length) 163 | while (!lex.IsEnd()) { 164 | switch (lex.current) { 165 | case "(": 166 | print("(") 167 | lex.AddToken(TOK.LPAR) 168 | lex.Next() 169 | break 170 | case ")": 171 | print(")") 172 | lex.AddToken(TOK.RPAR) 173 | lex.Next() 174 | break 175 | case "$": 176 | print("$") 177 | if (lex.IsLast()) { 178 | lex.AddToken(TOK.END) 179 | lex.Next() 180 | } else { 181 | lex.AddToken(TOK.CHAR, lex.current) 182 | lex.Next() 183 | ReadQuantity(lex) 184 | } 185 | break 186 | case "%": 187 | lex.Next() 188 | print("%", lex.current) 189 | switch (lex.current) { 190 | case "b": 191 | if (lex.caret + 2 >= lex.end) { lex.AddToken(TOK.ERROR, "Missing characters for \"%b\" pattern. Example: \"%b()\"."); throw new Error(""); } 192 | lex.AddToken(TOK.BALANCED, lex.Sub(lex.caret + 1, lex.caret + 3)) 193 | lex.Next() 194 | lex.Next() 195 | lex.Next() 196 | break 197 | case "f": 198 | lex.Next() 199 | if (lex.current != "[") { lex.AddToken(TOK.ERROR, "Missing \"[\" after \"%f\" in pattern. Example: \"%f[%w]\"."); throw new Error(""); } 200 | lex.AddToken(TOK.FRONTIER) 201 | ReadSet(lex) 202 | break 203 | case '0': case '1': case '2': case '3': 204 | case '4': case '5': case '6': case '7': 205 | case '8': case '9': 206 | lex.AddToken(TOK.CAPTUREREF, lex.current) 207 | lex.Next() 208 | break 209 | default: 210 | ReadEscape(lex) 211 | ReadQuantity(lex) 212 | break 213 | } 214 | break 215 | case "[": 216 | print("[") 217 | ReadSet(lex) 218 | ReadQuantity(lex) 219 | break 220 | case ".": 221 | print(".") 222 | lex.AddToken(TOK.ANY) 223 | lex.Next() 224 | ReadQuantity(lex) 225 | break 226 | default: 227 | print("char", lex.current) 228 | lex.AddToken(TOK.CHAR, lex.current) 229 | lex.Next() 230 | ReadQuantity(lex) 231 | break 232 | } 233 | } 234 | } 235 | catch (e) { 236 | if (e.message.length > 0) { 237 | print(e.name + ": " + e.message) 238 | lex.AddToken(TOK.ERROR, "Lexer error: " + e.message) 239 | } 240 | } 241 | 242 | return lex.tokens 243 | } 244 | 245 | const PAT = Object.freeze({ 246 | ERROR: 0, 247 | CHARS: 1, 248 | QUANTIFIER: 2, 249 | ANY: 3, 250 | START: 4, 251 | END: 5, 252 | ESCAPED: 6, 253 | CLASS: 7, 254 | CAPTUREREF: 8, 255 | BALANCED: 9, 256 | SET: 10, 257 | CAPTURE: 11, 258 | FRONTIER: 12, 259 | RANGE: 13, 260 | INVERSESET: 14, 261 | POSITION: 15, 262 | WARNING: 16, 263 | NOTE: 17, 264 | SETCHARS: 18, 265 | }) 266 | const PatToStr = [ 267 | "ERROR", 268 | "CHARS", 269 | "QUANTIFIER", 270 | "ANY", 271 | "START", 272 | "END", 273 | "ESCAPED", 274 | "CLASS", 275 | "CAPTUREREF", 276 | "BALANCED", 277 | "SET", 278 | "CAPTURE", 279 | "FRONTIER", 280 | "RANGE", 281 | "INVERSESET", 282 | "POSITION", 283 | "WARNING", 284 | "NOTE", 285 | "SETCHARS", 286 | ] 287 | 288 | class Parser { 289 | constructor(tokens) { 290 | this.tokens = tokens 291 | this.caret = 0 292 | this.last = tokens.length - 1 293 | this.end = tokens.length 294 | this.current = tokens[0] 295 | this.nodes = [] 296 | this.levels = [] 297 | this.captures = [] 298 | this.rem = null 299 | } 300 | 301 | Next() { 302 | this.current = this.tokens[++this.caret] 303 | } 304 | 305 | IsNextQuantifier(type) { 306 | if (this.IsLast()) return false 307 | 308 | let token = this.tokens[this.caret + 1] 309 | switch (token.type) { 310 | case TOK.ZEROORMORE: case TOK.ONEORMORE: case TOK.ZEROORMORELAZY: case TOK.ZEROORONE: 311 | return true 312 | default: 313 | return false 314 | } 315 | } 316 | 317 | IsNextRange() { 318 | if (this.caret + 2 >= this.end) return false 319 | 320 | let token = this.tokens[this.caret + 1] 321 | let token2 = this.tokens[this.caret + 2] 322 | 323 | if (token.type === TOK.CHAR && token.string === "-" && token2.type === TOK.CHAR) return true 324 | 325 | return false 326 | } 327 | 328 | IsNextRPar() { 329 | if (this.IsLast()) return false 330 | return this.tokens[this.caret + 1].type === TOK.RPAR 331 | } 332 | 333 | Add(node) { 334 | if (this.levels.length === 0) { 335 | this.nodes.push(node) 336 | } else { 337 | this.levels[this.levels.length - 1].Add(node) 338 | } 339 | } 340 | 341 | StartCapture() { 342 | let capture = new PatternObject(PAT.CAPTURE, this, this.captures.length + 1) 343 | this.captures.push(capture) 344 | this.levels.push(capture) 345 | } 346 | 347 | EndCapture() { 348 | this.levels.pop() 349 | } 350 | 351 | IsEnd() { 352 | return this.caret >= this.end 353 | } 354 | 355 | IsLast() { 356 | return this.caret === this.last 357 | } 358 | } 359 | 360 | class PatternObject { 361 | constructor(type, parent, text) { 362 | this.type = type 363 | this.text = text 364 | this.children = [] 365 | parent.Add(this) 366 | } 367 | 368 | Add(child) { 369 | this.children.push(child) 370 | } 371 | } 372 | 373 | function CheckQuantifier(par, parent) { 374 | if (par.IsEnd()) return 375 | switch (par.current.type) { 376 | case TOK.ZEROORMORE: case TOK.ONEORMORE: case TOK.ZEROORMORELAZY: case TOK.ZEROORONE: 377 | new PatternObject(PAT.QUANTIFIER, parent, par.current.type) 378 | if (parent.type === PAT.CHARS && parent.text.charCodeAt(0) > 255) new PatternObject(PAT.WARNING, parent, "Character \"" + parent.text + "\" (" + parent.text.charCodeAt(0) + ") is outside ASCII range. It will be interpreted incorrectly (as separate parts of the symbol).") 379 | par.Next() 380 | return true 381 | default: 382 | return 383 | } 384 | } 385 | 386 | function MakeString(par) { 387 | let string = new PatternObject(PAT.CHARS, par, "") 388 | do { 389 | string.text += par.current.string 390 | par.Next() 391 | } while (!par.IsEnd() && par.current.type === TOK.CHAR && !par.IsNextQuantifier()) 392 | 393 | CheckQuantifier(par, string) 394 | } 395 | 396 | function MakeSet(par, parent) { 397 | let set 398 | par.Next() 399 | if (par.current.type === TOK.INVERSE) { set = new PatternObject(PAT.INVERSESET, parent ? parent : par); par.Next() } else { set = new PatternObject(PAT.SET, parent ? parent : par) } 400 | 401 | do { 402 | switch (par.current.type) { 403 | case TOK.CLASS: 404 | new PatternObject(PAT.CLASS, set, par.current.string) 405 | par.Next() 406 | break 407 | case TOK.CHAR: 408 | if (par.IsNextRange()) { 409 | let string = par.current.string 410 | par.Next() 411 | par.Next() 412 | new PatternObject(PAT.RANGE, set, string + par.current.string) 413 | if (string.charCodeAt(0) > 255 || par.current.string.charCodeAt(0) > 255) new PatternObject(PAT.WARNING, set, "Range \"" + string + "\" (" + string.charCodeAt(0) + ") - \"" + par.current.string + "\" (" + par.current.string.charCodeAt(0) + ") is outside ASCII range. It will be interpreted incorrectly (as separate parts of the symbol).") 414 | par.Next() 415 | } else { 416 | let string = new PatternObject(PAT.SETCHARS, set, par.current.string) 417 | if (par.current.string.charCodeAt(0) > 255) new PatternObject(PAT.WARNING, set, "Character \"" + par.current.string + "\" (" + par.current.string.charCodeAt(0) + ") is outside ASCII range. It will be interpreted incorrectly (as separate parts of the symbol).") 418 | par.Next() 419 | } 420 | break 421 | case TOK.ESCAPED: 422 | new PatternObject(PAT.ESCAPED, set, par.current.string) 423 | if (par.current.string.charCodeAt(0) > 255) new PatternObject(PAT.WARNING, set, "Character \"" + par.current.string + "\" (" + par.current.string.charCodeAt(0) + ") is outside ASCII range. It will be interpreted incorrectly (as separate parts of the symbol).") 424 | par.Next() 425 | break 426 | case TOK.ERROR: 427 | new PatternObject(PAT.ERROR, set, par.current.string) 428 | par.Next() 429 | return 430 | default: 431 | console.log("???", par.current.type) 432 | par.Next() 433 | break 434 | } 435 | } while (par.current.type != TOK.RBRACKET) 436 | par.Next() 437 | 438 | CheckQuantifier(par, set) 439 | } 440 | 441 | function PatternsParse(tokens) { 442 | const par = new Parser(tokens) 443 | try { 444 | while (!par.IsEnd()) { 445 | switch (par.current.type) { 446 | case TOK.ANY: 447 | { 448 | print(".") 449 | let obj = new PatternObject(PAT.ANY, par) 450 | par.Next() 451 | CheckQuantifier(par, obj) 452 | if (obj.children[0] && obj.children[0].type === PAT.QUANTIFIER && (obj.children[0].text === TOK.ZEROORMORE || obj.children[0].text === TOK.ONEORMORE)) { 453 | new PatternObject(PAT.NOTE, obj, "Matching any characters with \"+\" or \"*\" quantifiers may fail your pattern. They match the longest sequence, meaning anything after this pattern won't be matched.") 454 | } 455 | } 456 | break 457 | case TOK.CHAR: 458 | { 459 | print("char") 460 | MakeString(par) 461 | } 462 | break 463 | case TOK.ESCAPED: 464 | { 465 | print("escaped") 466 | let obj = new PatternObject(PAT.ESCAPED, par, par.current.string) 467 | if (par.current.string.charCodeAt(0) > 255) new PatternObject(PAT.WARNING, obj, "Character \"" + par.current.string + "\" (" + par.current.string.charCodeAt(0) + ") is outside ASCII range. It will be interpreted incorrectly (as separate parts of the symbol).") 468 | par.Next() 469 | CheckQuantifier(par, obj) 470 | } 471 | break 472 | case TOK.LBRACKET: 473 | { 474 | print("[") 475 | MakeSet(par) 476 | } 477 | break 478 | case TOK.CLASS: 479 | { 480 | print("class") 481 | let obj = new PatternObject(PAT.CLASS, par, par.current.string) 482 | par.Next() 483 | CheckQuantifier(par, obj) 484 | } 485 | break 486 | case TOK.LPAR: 487 | { 488 | print("(") 489 | if (par.IsNextRPar()) { 490 | par.captures.push(new PatternObject(PAT.POSITION, par, par.captures.length + 1)) 491 | par.Next() 492 | par.Next() 493 | } else { 494 | par.StartCapture() 495 | } 496 | par.Next() 497 | } 498 | break 499 | case TOK.RPAR: 500 | { 501 | print(")") 502 | par.EndCapture() 503 | par.Next() 504 | } 505 | break 506 | case TOK.CAPTUREREF: 507 | { 508 | print("Captureref") 509 | let obj = new PatternObject(PAT.CAPTUREREF, par, par.current.string) 510 | if (par.current.string === "0") { 511 | new PatternObject(PAT.NOTE, obj, "Reference for capture #0 is available only in string.gsub.") 512 | } else if (par.captures.length < par.current.string) { 513 | new PatternObject(PAT.WARNING, obj, "Reference for capture #" + par.current.string + " is not found.") 514 | } else if (par.captures[par.current.string - 1].type === PAT.POSITION) { 515 | new PatternObject(PAT.NOTE, obj, "References for position captures are available only in string.gsub.") 516 | } 517 | par.Next() 518 | } 519 | break 520 | case TOK.BALANCED: 521 | { 522 | print("balanced") 523 | new PatternObject(PAT.BALANCED, par, par.current.string) 524 | par.Next() 525 | } 526 | break 527 | case TOK.FRONTIER: 528 | { 529 | print("%f") 530 | let f = new PatternObject(PAT.FRONTIER, par) 531 | par.Next() 532 | MakeSet(par, f) 533 | } 534 | break 535 | case TOK.ERROR: 536 | { 537 | new PatternObject(PAT.ERROR, par, par.current.string) 538 | par.Next() 539 | } 540 | break 541 | case TOK.START: 542 | { 543 | print("^") 544 | new PatternObject(PAT.START, par) 545 | par.Next() 546 | } 547 | break 548 | case TOK.END: 549 | { 550 | print("$") 551 | new PatternObject(PAT.END, par) 552 | par.Next() 553 | } 554 | break 555 | default: 556 | { 557 | print("unknown", par.current) 558 | new PatternObject(PAT.ERROR, par, "Unknown pattern, check console.") 559 | par.Next() 560 | } 561 | break 562 | } 563 | } 564 | if (par.levels.length != 0) throw new Error("Unfinished capture #" + par.captures.length + ". \")\" is missing.") 565 | } catch (e) { 566 | console.log(e.name + ": " + e.message) 567 | par.levels.length = 0 568 | new PatternObject(PAT.ERROR, par, "Parser error: " + e.message) 569 | } 570 | 571 | return par.nodes 572 | } 573 | 574 | let basediv = null 575 | function CreateDiv(type, parent, text, name, description) { 576 | let element = document.createElement("div"); 577 | let p = document.createElement("a") 578 | p.className = "input" 579 | p.appendChild(document.createTextNode(text)) 580 | let nname = document.createElement("a") 581 | nname.className = "name" 582 | nname.appendChild(document.createTextNode(name)) 583 | let ndescription = document.createElement("a") 584 | ndescription.className = "description" 585 | ndescription.appendChild(document.createTextNode(description)) 586 | element.classList.add("token", ...type.split(" ")) 587 | element.appendChild(p) 588 | element.appendChild(nname) 589 | element.appendChild(ndescription) 590 | parent.appendChild(element) 591 | 592 | return element 593 | } 594 | 595 | function CleanBaseDiv() { 596 | while (basediv.firstChild) { 597 | basediv.removeChild(basediv.firstChild) 598 | } 599 | } 600 | 601 | const PAT_QUANTIFIER_NAMES = Object.freeze({ 602 | [TOK.ZEROORMORE]: ["*", "zero or more", "Allows to match the pattern zero or more times. This will match the longest sequence."], 603 | [TOK.ONEORMORE]: ["+", "one or more", "Allows to match the pattern one or more times. This will match the longest sequence."], 604 | [TOK.ZEROORMORELAZY]: ["-", "lazy zero or more", "Allows to match the pattern zero or more times. This will match the shortest sequence."], 605 | [TOK.ZEROORONE]: ["?", "zero or one", "Allows to match the pattern zero or one time."], 606 | }) 607 | 608 | const PAT_CLASS_NAMES = Object.freeze({ 609 | ["a"]: ["Letters", "all letters (Equivalent to [a-zA-Z])"], 610 | ["A"]: ["Not letters", "all non-letters (Equivalent to [^a-zA-Z])"], 611 | ["c"]: ["Controls", "all control characters (Such as \"\\t\", \"\\n\", \"\\r\", etc.) (Equivalent to [\\0-\\31])"], 612 | ["C"]: ["Not Controls", "all non-control characters (Equivalent to [^\\0-\\31])"], 613 | ["d"]: ["Digits", "all digits (Equivalent to [0-9])"], 614 | ["D"]: ["Not digits", "all non-digits (Equivalent to [^0-9])"], 615 | ["g"]: ["Printable", "all printable characters except space (Equivalent to [\\33-\\126])"], 616 | ["G"]: ["Not printable", "all non-printable characters including space (Equivalent to [^\\33-\\126])"], 617 | ["l"]: ["Lowercase", "all lowercase letters (Equivalent to [a-z])"], 618 | ["L"]: ["Not lowercase", "all non-lowercase letters (Equivalent to [^a-z])"], 619 | ["p"]: ["Punctuations", "all punctuation characters (Equivalent to [!\"#$%%&'()*+,%-./:;<=>?@[\\%]^_`{|}~])"], 620 | ["P"]: ["Not punctuations", "all non-punctuation characters (Equivalent to [^!\"#$%%&'()*+,%-./:;<=>?@[\\%]^_`{|}~])"], 621 | ["s"]: ["Spaces", "all white-space characters (Equivalent to [ \\t\\n\\v\\f\\r])"], 622 | ["S"]: ["Not spaces", "all non-white-space characters (Equivalent to [^ \\t\\n\\v\\f\\r])"], 623 | ["u"]: ["Uppercase", "all uppercase letters (Equivalent to [A-Z])"], 624 | ["U"]: ["Not uppercase", "all non-uppercase letters (Equivalent to [^A-Z])"], 625 | ["w"]: ["Alphanumerics", "all digits, lowercase and uppercase letters (Equivalent to [a-zA-Z0-9])"], 626 | ["W"]: ["Not alphanumerics", "all non-digits, non-lowercase and non-uppercase letters (Equivalent to [^a-zA-Z0-9])"], 627 | ["x"]: ["Hexadecimals", "all hexadecimal digits (Equivalent to [0-9a-fA-F]"], 628 | ["X"]: ["Not hexadecimals", "all non-hexadecimal digits (Equivalent to [^0-9a-fA-F]"], 629 | ["z"]: ["\\0 byte", "NULL character (0). Deprecated in Lua 5.2.0, use \"\\0\" as regular character."], 630 | ["Z"]: ["Not \\0 byte", "non-NULL character (0). Deprecated in Lua 5.2.0, use \"[^\\0]\" as regular character."], 631 | }) 632 | 633 | function PatternsShow(nodes, parent) { 634 | try { 635 | for (let node of nodes) { 636 | switch (node.type) { 637 | case PAT.CHARS: 638 | { 639 | let len = node.text.length 640 | let element = CreateDiv("char", parent, node.text, len > 1 ? "Characters." : "Character.", len > 1 ? ("Matches the characters \"" + node.text + "\" literally.") : ("Matches the character \"" + node.text + "\" literally.")) 641 | PatternsShow(node.children, element) 642 | } 643 | break 644 | case PAT.SETCHARS: 645 | { 646 | let len = node.text.length 647 | let element = CreateDiv("char", parent, node.text, "Character.", (parent.classList.contains("inverse") ? "Doesn't match \"" : "Matches \"") + node.text + "\" literally.") 648 | PatternsShow(node.children, element) 649 | } 650 | break 651 | case PAT.QUANTIFIER: 652 | { 653 | let element = CreateDiv("quantifier", parent, PAT_QUANTIFIER_NAMES[node.text][0], "Quantifier (" + PAT_QUANTIFIER_NAMES[node.text][1] + ").", PAT_QUANTIFIER_NAMES[node.text][2]) 654 | PatternsShow(node.children, element) 655 | } 656 | break 657 | case PAT.ANY: 658 | { 659 | let element = CreateDiv("any", parent, ".", "Any.", "Matches any character. Equivalent to \"[%s%S]\".") 660 | PatternsShow(node.children, element) 661 | } 662 | break 663 | case PAT.ESCAPED: 664 | { 665 | let element = CreateDiv("char", parent, "%" + node.text, "Escaped character.", "Matches the character \"" + node.text + "\" literally.") 666 | if (parent === basediv) { PatternsShow(node.children, element) } 667 | } 668 | break 669 | case PAT.CLASS: 670 | { 671 | let element = CreateDiv("class", parent, "%" + node.text, "Class (" + PAT_CLASS_NAMES[node.text][0] + ").", "Matches " + PAT_CLASS_NAMES[node.text][1] + ".") 672 | if (parent === basediv) { PatternsShow(node.children, element) } 673 | } 674 | break 675 | case PAT.CAPTUREREF: 676 | { 677 | let element = CreateDiv("captureref", parent, "%" + node.text, "Capture reference.", "Matches or returns the same pattern as in referenced capture \"" + node.text + "\".") 678 | PatternsShow(node.children, element) 679 | } 680 | break 681 | case PAT.BALANCED: 682 | { 683 | CreateDiv("balanced", parent, "%b" + node.text, "Balanced match.", "Matches characters starting at \"" + node.text.charAt(0) + "\" until the corresponding \"" + node.text.charAt(1) + "\".") 684 | } 685 | break 686 | case PAT.SET: 687 | { 688 | let element = CreateDiv("set", parent, "[...]", "Set.", "Matches any character from the set:") 689 | PatternsShow(node.children, element) 690 | } 691 | break 692 | case PAT.INVERSESET: 693 | { 694 | let element = CreateDiv("inverse set", parent, "[^...]", "Inverse set.", "Matches any character except:") 695 | PatternsShow(node.children, element) 696 | } 697 | break 698 | case PAT.POSITION: 699 | { 700 | let element = CreateDiv("position", parent, "()", "Position capture #" + node.text + ".", "Captures the position in the string.") 701 | PatternsShow(node.children, element) 702 | } 703 | break 704 | case PAT.CAPTURE: 705 | { 706 | let element = CreateDiv("capture", parent, "(...)", "Capture #" + node.text + ".", "Makes a pattern group to be used for backreferencing or substring output.") 707 | PatternsShow(node.children, element) 708 | } 709 | break 710 | case PAT.FRONTIER: 711 | { 712 | let element = CreateDiv("frontier", parent, "%f", "Frontier.", "Matches any character from the set when the previous character doesn't match it. Can match start and end boundaries of the string.") 713 | PatternsShow(node.children, element) 714 | } 715 | break 716 | case PAT.RANGE: 717 | { 718 | let s = node.text.charAt(0), e = node.text.charAt(1) 719 | let element = CreateDiv("range", parent, s + "-" + e, "Range.", "Matches any character in the range \"" + s + "\" (byte " + s.charCodeAt(0) + ") to \"" + e + "\" (byte " + e.charCodeAt(0) + ").") 720 | if (s > e) { 721 | CreateDiv("warning", element, "?", "Warning.", "The range won't match anything because the start of the range is greater than the end (\"" + s + "\" (" + s.charCodeAt(0) + ") > \"" + e + "\" (" + e.charCodeAt(0) + ")).") 722 | } else if (s === e) { 723 | CreateDiv("note", element, "i", "Note.", "The range has range of one character. Consider using regular char instead.") 724 | } 725 | } 726 | break 727 | case PAT.START: 728 | { 729 | let element = CreateDiv("start", parent, "^", "Start anchor.", "Tells to match the pattern only if it starts from the beginning of the string.") 730 | } 731 | break 732 | case PAT.END: 733 | { 734 | let element = CreateDiv("end", parent, "$", "End anchor.", "Tells to match the pattern only if it ends at the end of the string.") 735 | } 736 | break 737 | case PAT.WARNING: 738 | { 739 | CreateDiv("warning", parent, "?", "Warning.", node.text) 740 | } 741 | break 742 | case PAT.NOTE: 743 | { 744 | CreateDiv("note", parent, "i", "Note.", node.text) 745 | } 746 | break 747 | case PAT.ERROR: 748 | { 749 | CreateDiv("error", parent, "!", "Error.", node.text) 750 | } 751 | break 752 | default: 753 | console.log(node) 754 | CreateDiv("error", basediv, "!", "Error.", "Unknown pattern object (" + node.type + ") [" + PatToStr[node.type] + "]") 755 | break 756 | } 757 | } 758 | } catch (e) { 759 | CreateDiv("error", basediv, "!", "Error.", "Pattern renderer error: " + e.name + ": " + e.message) 760 | } 761 | } 762 | 763 | function PatternsPrint(input) { 764 | const tokens = PatternsLex(input) 765 | const output = PatternsParse(tokens) 766 | basediv = document.getElementById("result") 767 | CleanBaseDiv() 768 | PatternsShow(output, basediv) 769 | 770 | } 771 | -------------------------------------------------------------------------------- /styles.css: -------------------------------------------------------------------------------- 1 | .token { 2 | background: #BCC2DA; 3 | margin: 10px 10px 10px 10px; 4 | max-width: 80vw; 5 | padding: 5px 5px 5px 5px; 6 | border: 0.1px solid #73768363; 7 | overflow-wrap: break-word; 8 | } 9 | 10 | .token.quantifier { 11 | background: #6080ff; 12 | } 13 | 14 | .token.any { 15 | background: #CF7DE7; 16 | } 17 | 18 | .token.start, .token.end { 19 | background: #FFD742; 20 | } 21 | 22 | .token.set { 23 | background: #d6dbec; 24 | } 25 | 26 | .token.frontier { 27 | background: #9d97d4; 28 | } 29 | 30 | .token.balanced { 31 | background: #9f74f0; 32 | } 33 | 34 | .token.capture, .token.position { 35 | background: #a3ff8d; 36 | } 37 | 38 | .token.captureref { 39 | background: #d7ffcd; 40 | } 41 | 42 | .token.class { 43 | background: #A7D0E7; 44 | } 45 | 46 | .token.warning { 47 | background: #fffc62; 48 | } 49 | 50 | .token.note { 51 | background: #62b3ff; 52 | } 53 | 54 | .token.error { 55 | background: #E76262; 56 | } 57 | 58 | /*======================================*/ 59 | #input { 60 | height: min(35px, 8vw); 61 | width: 100%; 62 | font-size: min(20pt, 7vw); 63 | font-family: 'Roboto Mono', monospace; 64 | } 65 | 66 | .token a { 67 | color: #1a1a1a; 68 | } 69 | 70 | .token a.input { 71 | font-family: 'Roboto Mono', monospace; 72 | background: #eee; 73 | font-weight: bold; 74 | margin-right: 5px; 75 | padding: 0px 5px 1px 5px; 76 | white-space: pre; 77 | } 78 | 79 | .token a.name { 80 | font-weight: bold; 81 | margin-right: 3px; 82 | display: inline-block; 83 | } 84 | 85 | .compact .description { 86 | display: none; 87 | } 88 | 89 | .token a.description{ 90 | white-space: pre-wrap; 91 | } 92 | 93 | body { 94 | font-family: 'Roboto', monospace; 95 | font-size: min(12pt, 5vw); 96 | } 97 | 98 | #settings-compact-mode { 99 | margin-left: 20px; 100 | margin-right: -5px; 101 | } 102 | 103 | #leftside { 104 | height: 100%; 105 | width: 250px; 106 | position: fixed; 107 | z-index: 1; 108 | top: 0; 109 | left: 0; 110 | background-color: #4b4b53; 111 | overflow-x: hidden; 112 | padding-top: 20px; 113 | transition: 0.5s; 114 | border-right: 2px solid #222325; 115 | } 116 | 117 | #leftside p, label { 118 | margin: 0px 10px 5px 10px; 119 | color: #ffffff; 120 | white-space: nowrap; 121 | } 122 | 123 | .unselectable { 124 | -webkit-touch-callout: none; 125 | -webkit-user-select: none; 126 | -khtml-user-select: none; 127 | -moz-user-select: none; 128 | -ms-user-select: none; 129 | user-select: none; 130 | } 131 | 132 | #leftside p.center { 133 | margin: 15px 5px 5px 5px; 134 | text-align: center; 135 | } 136 | 137 | .closebtn { 138 | position: absolute; 139 | font-weight: bold; 140 | font-size: 10pt; 141 | color: #f0f0f0; 142 | margin: 5px; 143 | text-decoration: none; 144 | top: 0; 145 | right: 0; 146 | } 147 | 148 | .closebtn:hover { 149 | text-decoration: underline; 150 | } 151 | 152 | #main { 153 | margin-left: 250px; 154 | position: relative; 155 | transition: margin-left 1s; 156 | padding: 0px 10px; 157 | } 158 | --------------------------------------------------------------------------------