├── 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 | [](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 |
40 |
Lua Patterns Viewer 1.1.5Lua Manual
42 |
43 |
Unimplemented: - Reference Manual - Match test - Lua backslash escapes - Unified token colors - Proper explanations
44 |
45 |
Settings:
46 |
47 |
62 |
Compact mode
63 |
64 |
Made by Spar.GitHub Analytics:
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 |
--------------------------------------------------------------------------------