├── .gitignore
├── LICENSE
├── README.md
├── demo
├── index.html
└── styles.css
├── package.json
├── pnpm-lock.yaml
├── pnpm-workspace.yaml
├── src
└── index.js
└── themes
├── default.css
├── nobg.css
├── regexbuddy.css
└── regexpal.css
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /dist
3 | /temp
4 |
5 | # Folder view configuration/cache files
6 | .DS_Store
7 | desktop.ini
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2007-2025 Steven Levithan
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 | # Regex Colorizer 🎨
2 |
3 | [![npm version][npm-version-src]][npm-version-href]
4 | [![bundle][bundle-src]][bundle-href]
5 |
6 | Regex Colorizer is a lightweight library (3.8 kB, with no dependencies) for adding syntax highlighting to your regular expressions in blogs, docs, regex testers, and other tools. It supports the **JavaScript regex flavor** (ES2023) with **web reality**. In other words, it highlights regexes as web browsers actually interpret them.
7 |
8 | The API is simple. Just give the elements that contain your regexes (`pre`, `code`, or whatever) the class `regex`, and call `colorizeAll()`. See more usage examples below.
9 |
10 | Errors are highlighted, along with some edge cases that can cause cross-browser grief. Hover over errors for a description of the problem.
11 |
12 | ## 🧪 Demo
13 |
14 | Try it out on the [**demo page**](https://slevithan.github.io/regex-colorizer/demo/), which also includes more details.
15 |
16 | ## 🕹️ Install and use
17 |
18 | ```sh
19 | npm install regex-colorizer
20 | ```
21 |
22 | ```js
23 | import {colorizeAll, loadStyles} from 'regex-colorizer';
24 | ```
25 |
26 |
27 | Using a CDN and global name
28 |
29 | ```html
30 |
31 |
34 | ```
35 |
36 |
37 | ## 🪧 Examples
38 |
39 | ```js
40 | import {colorizeAll, colorizePattern, loadStyles} from 'regex-colorizer';
41 |
42 | // Don't run this line if you provide your own stylesheet
43 | loadStyles();
44 |
45 | // Highlight all elements with class `regex`
46 | colorizeAll();
47 |
48 | // Or provide a `querySelectorAll` value for elements to highlight
49 | colorizeAll({
50 | selector: '.regex',
51 | });
52 |
53 | // Optionally provide flags
54 | colorizeAll({
55 | // Flags provided in `data-flags` attributes will override this
56 | flags: 'u',
57 | });
58 |
59 | // You can also just get the highlighting HTML for a specific pattern
60 | element.innerHTML = colorizePattern('(?<=\\d)', {
61 | flags: 'u',
62 | });
63 | ```
64 |
65 | In your HTML:
66 |
67 | ```html
68 |
69 | This regex is highlighted inline:
70 | (?<=\d)\p{L}\8.
71 |
72 | And here's the same regex but with different rules from flag u:
73 | (?<=\d)\p{L}\8.
74 |
75 |
Regex Colorizer adds syntax highlighting to your regular expressions in blogs, docs, regex testers, and other tools. It supports the JavaScript regex flavor (ES2023) with web reality. In other words, it highlights regexes as web browsers actually interpret them.
28 |
The API is simple. Just give the elements that contain your regexes (pre, code, or whatever) the class regex, and call colorizeAll(). See more usage examples below.
Quoted string, allowing escaped quotes and backslashes
34 |
(["'])(?:\\?.)*?\1
35 |
36 |
Try it
37 |
38 |
39 |
Type a regex pattern
40 |
41 |
45 |
46 |
47 |
48 |
Themes
49 |
Several themes are available as stylesheets. Select an option to change the highlighting styles for all regexes on this page.
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
You don't need to add a stylesheet to your page to use the default theme. Just run loadStyles().
58 |
The No BG theme avoids background colors as part of highlighting.
59 |
In 2007, RegexPal was the first web-based regex tester with regex syntax highlighting. Regex Colorizer started out by extracting RegexPal's highlighting code into a standalone library. This theme uses all black text, because RegexPal's implementation used highlighted text underneath a textarea with a transparent background. The RegexPal theme doesn't uniquely distinguish the following: escaped literal tokens, backreferences, and character class boundaries.
60 |
OG RegexBuddy's highlighting styles. RegexBuddy inspired RegexPal and many others. The RegexBuddy theme is based on older versions of the app which didn't uniquely distinguish the following: escaped literal tokens, backreferences, character class boundaries, and range-hyphens.
61 |
62 |
63 |
More examples
64 |
Regex with errors
65 |
Errors are highlighted in red, along with some edge cases that can cause cross-browser grief. Hover over errors for a description of the problem.
Regex syntax is complex, so Regex Colorizer's highlighting is contextually aware of things that happen forward and backward in the regex. Here's an example of contextually-aware highlighting of regex syntax:
71 |
72 |
\+ Escapes are backreferences if num <= capturing groups
73 | \1 \8 \9 \10 \k<1> \k<name>
74 | \+ Octals: leading 0 extends length
75 | \11 \100 \1234 \01234 \00123 \0 \00 \00000 \377 \400
76 | \+ Non-octal digits
77 | \18 \80 \90
78 | \+ Leading 0 in character class doesn't extend length
79 | [ \1234 is the same but not \0123; no backrefs \1 ]
80 | \+ Capturing groups can appear after their backrefs
81 | (1)(2)(?<name>3)(4)(5)(6)(7)(8)(9)(10)
82 |
83 |
87 |
88 |
89 |
90 |
Some other examples of contextually-aware highlighting:
91 |
92 |
Named backreferences:\k<name> is only a named backreference if a named capture (with any name) appears in the regex, even if after or enclosing the backreference (so \k<n>(?<n>a) and (?<n>a\k<n>) are valid; the backreferences match empty strings). If there aren't any named captures in the pattern, it's an escaped literal \k<name> and matches the string 'k<name>'. When named backreference behavior is triggered, \k is an error if it's not a complete token (ex: (?<n>)\k), if the name is invalid (ex: (?<n>)\k<1>), or if there's no corresponding named capturing group anywhere in the regex (ex: (?<n>)\k<m>). Flags u and v enforce named backreference mode handling in all cases. And of course, named backreference syntax doesn't apply within character classes ([\k<n>]).
93 |
Hypens in character classes: Hyphens only create ranges when preceded and followed by rangeable tokens in the correct order that aren't themselves part of ranges. Hyphens are literal - characters in other cases. Compare [----] (one range-hyphen), [-----] (still one), and [------] (two). Also handles errors (ex: [z-a] and [\w-z], but [\w-]) and metasequences or escaped literals that are used in valid ranges (ex: [a-\z] and [\b-\cZ], but [\cZ-\b]). Note: Within a character class, \b matches a backspace control character rather than a word boundary. Flag v changes the rules to make any unescaped, non-range hyphens in character classes become errors, and enables -- as the set substraction operator.
94 |
95 |
96 |
Usage
97 |
Here's how to highlight all your regexes like you can see running on this page:
98 |
import {colorizeAll, colorizePattern, loadStyles} from 'regex-colorizer';
99 |
100 | // Don't run this line if you provide your own stylesheet
101 | loadStyles();
102 |
103 | // Highlight all elements with class `regex`
104 | colorizeAll();
105 |
106 | // Or provide a `querySelectorAll` value for elements to highlight
107 | colorizeAll({
108 | selector: '.regex',
109 | });
110 |
111 | // Optionally provide flags
112 | colorizeAll({
113 | // Flags provided in `data-flags` attributes will override this
114 | flags: 'u',
115 | });
116 |
117 | // You can also just get the highlighting HTML for a specific pattern
118 | element.innerHTML = colorizePattern('(?<=\\d)', {
119 | flags: 'u',
120 | });
121 |
122 |
In your HTML:
123 |
<p>
124 | This regex is highlighted inline:
125 | <code class="regex">(?<=\d)\p{L}\8</code>.
126 |
127 | And here's the same regex but with different rules from flag u:
128 | <code class="regex" data-flags="u">(?<=\d)\p{L}\8</code>.
129 | <!-- Can include any valid flags. Ex: data-flags="gimsuyd" -->
130 | </p>
131 |
132 |
Output:
133 |
134 |
135 | This regex is highlighted inline:
136 | (?<=\d)\p{L}\8.
137 |
138 | And here's the same regex but with different rules from flag u:
139 | (?<=\d)\p{L}\8.
140 |
141 |
142 |
143 |
155 |
156 |
157 |
158 |
159 |
195 |
196 |
197 |
198 |
201 |
202 |
203 |
--------------------------------------------------------------------------------
/demo/styles.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | margin: 0;
4 | }
5 |
6 | body {
7 | font-family: Calibri, "Helvetica Neue", sans-serif;
8 | background-color: #c9e4ff;
9 | background: repeating-linear-gradient(45deg, #d6ebff, #d6ebff 3px, #c9e4ff 3px, #c9e4ff 10px);
10 | }
11 |
12 | #content {
13 | background-color: #fff;
14 | max-width: 65rem;
15 | padding: 2.8vw 4.5vw;
16 | margin: 0 auto;
17 | box-shadow: 0 0 0 10px #80c0ff;
18 | }
19 |
20 | h1, h2, ul, p, pre, figure {
21 | margin-bottom: 12px;
22 | }
23 |
24 | h1, h2 {
25 | font-family: Cambria, Cochin, Georgia;
26 | }
27 |
28 | h2 {
29 | margin-top: 18px;
30 | }
31 |
32 | h3 {
33 | font-size: 1em;
34 | margin-top: 4px;
35 | margin-bottom: 2px;
36 | }
37 |
38 | .no-margin-collapse {
39 | margin-bottom: 30px;
40 | }
41 |
42 | div.info {
43 | border-left: 10px solid #80c0ff;
44 | border-radius: 0.375em;
45 | background-color: #d6ebff;
46 | padding: 0.5em;
47 | }
48 |
49 | div.info code {
50 | background-color: #80c0ff;
51 | }
52 |
53 | textarea {
54 | width: 100%;
55 | min-height: 55px;
56 | max-height: 155px;
57 | /* Copy the whitepace/wrapping styles applied to regexes by Regex Colorizer's themes */
58 | white-space: pre-wrap;
59 | word-break: break-all;
60 | overflow-wrap: anywhere;
61 | border-radius: 0.375em;
62 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
63 | transition: box-shadow 0.2s ease;
64 | background-color: #fcfcfc;
65 | }
66 |
67 | textarea:focus {
68 | outline: none;
69 | box-shadow: 0 0 8px #80c0ff;
70 | }
71 |
72 | pre, code, textarea {
73 | font-family: Consolas, "Source Code Pro", Monospace;
74 | font-size: 1em;
75 | }
76 |
77 | pre, code {
78 | border-radius: 0.375em;
79 | }
80 |
81 | pre.regex, textarea {
82 | padding: 0.6em;
83 | }
84 |
85 | .regex {
86 | border: 1px dotted #aaa;
87 | }
88 |
89 | .html-output-example .regex {
90 | border: none;
91 | }
92 |
93 | pre {
94 | display: inline-block;
95 | }
96 |
97 | pre:has(code), code {
98 | background-color: #f3f3f3;
99 | }
100 |
101 | pre:has(code) {
102 | max-width: 100%;
103 | overflow-x: auto;
104 | }
105 |
106 | pre code {
107 | display: block;
108 | padding: 1em;
109 | }
110 |
111 | code {
112 | padding: 0 3px;
113 | }
114 |
115 | code.regex {
116 | padding: 1px;
117 | background-color: inherit;
118 | }
119 |
120 | button {
121 | font-weight: bold;
122 | border-radius: 0.4em;
123 | padding: 0.6em 1em;
124 | margin-right: 4px;
125 | }
126 |
127 | small {
128 | font-size: 0.8em;
129 | }
130 |
131 | footer {
132 | color: #aaa;
133 | margin-top: 40px;
134 | }
135 |
136 | footer a {
137 | color: inherit;
138 | text-decoration: none;
139 | }
140 |
141 | footer a:hover {
142 | color: initial;
143 | }
144 |
145 | footer img, h1 img:hover {
146 | filter: invert(70%);
147 | }
148 |
149 | footer img:hover {
150 | filter: unset;
151 | }
152 |
153 | img {
154 | vertical-align: middle;
155 | }
156 |
157 | figure:has(pre.regex) {
158 | display: inline-block;
159 | border-radius: 0.375em;
160 | background-color: #f9f9f9;
161 | padding: 0.3em;
162 | }
163 |
164 | figure pre.regex {
165 | width: 100%;
166 | background-color: #fff;
167 | margin-bottom: 6px;
168 | }
169 |
170 | footer p:last-of-type, div.info p {
171 | margin-bottom: 0;
172 | }
173 |
174 | .hidden {
175 | display: none;
176 | }
177 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "regex-colorizer",
3 | "version": "1.0.2",
4 | "description": "Highlight regex syntax",
5 | "homepage": "https://slevithan.github.io/regex-colorizer/demo/",
6 | "author": "Steven Levithan",
7 | "license": "MIT",
8 | "type": "module",
9 | "exports": "./src/index.js",
10 | "browser": "./dist/regex-colorizer.min.js",
11 | "scripts": {
12 | "prebuild": "rimraf --glob dist/*",
13 | "build": "esbuild src/index.js --bundle --minify --outfile=dist/regex-colorizer.min.js --global-name=RegexColorizer",
14 | "test": "echo \"Error: no test specified\" && exit 1",
15 | "prepare": "pnpm build"
16 | },
17 | "files": [
18 | "dist",
19 | "src"
20 | ],
21 | "repository": {
22 | "type": "git",
23 | "url": "git+https://github.com/slevithan/regex-colorizer.git"
24 | },
25 | "keywords": [
26 | "regex",
27 | "regexp",
28 | "syntax-highlighting"
29 | ],
30 | "devDependencies": {
31 | "esbuild": "^0.25.4",
32 | "rimraf": "^6.0.1"
33 | },
34 | "packageManager": "pnpm@10.10.0"
35 | }
36 |
--------------------------------------------------------------------------------
/pnpm-lock.yaml:
--------------------------------------------------------------------------------
1 | lockfileVersion: '9.0'
2 |
3 | settings:
4 | autoInstallPeers: true
5 | excludeLinksFromLockfile: false
6 |
7 | importers:
8 |
9 | .:
10 | devDependencies:
11 | esbuild:
12 | specifier: ^0.25.4
13 | version: 0.25.4
14 | rimraf:
15 | specifier: ^6.0.1
16 | version: 6.0.1
17 |
18 | packages:
19 |
20 | '@esbuild/aix-ppc64@0.25.4':
21 | resolution: {integrity: sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==}
22 | engines: {node: '>=18'}
23 | cpu: [ppc64]
24 | os: [aix]
25 |
26 | '@esbuild/android-arm64@0.25.4':
27 | resolution: {integrity: sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==}
28 | engines: {node: '>=18'}
29 | cpu: [arm64]
30 | os: [android]
31 |
32 | '@esbuild/android-arm@0.25.4':
33 | resolution: {integrity: sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==}
34 | engines: {node: '>=18'}
35 | cpu: [arm]
36 | os: [android]
37 |
38 | '@esbuild/android-x64@0.25.4':
39 | resolution: {integrity: sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==}
40 | engines: {node: '>=18'}
41 | cpu: [x64]
42 | os: [android]
43 |
44 | '@esbuild/darwin-arm64@0.25.4':
45 | resolution: {integrity: sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==}
46 | engines: {node: '>=18'}
47 | cpu: [arm64]
48 | os: [darwin]
49 |
50 | '@esbuild/darwin-x64@0.25.4':
51 | resolution: {integrity: sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==}
52 | engines: {node: '>=18'}
53 | cpu: [x64]
54 | os: [darwin]
55 |
56 | '@esbuild/freebsd-arm64@0.25.4':
57 | resolution: {integrity: sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==}
58 | engines: {node: '>=18'}
59 | cpu: [arm64]
60 | os: [freebsd]
61 |
62 | '@esbuild/freebsd-x64@0.25.4':
63 | resolution: {integrity: sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==}
64 | engines: {node: '>=18'}
65 | cpu: [x64]
66 | os: [freebsd]
67 |
68 | '@esbuild/linux-arm64@0.25.4':
69 | resolution: {integrity: sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==}
70 | engines: {node: '>=18'}
71 | cpu: [arm64]
72 | os: [linux]
73 |
74 | '@esbuild/linux-arm@0.25.4':
75 | resolution: {integrity: sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==}
76 | engines: {node: '>=18'}
77 | cpu: [arm]
78 | os: [linux]
79 |
80 | '@esbuild/linux-ia32@0.25.4':
81 | resolution: {integrity: sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==}
82 | engines: {node: '>=18'}
83 | cpu: [ia32]
84 | os: [linux]
85 |
86 | '@esbuild/linux-loong64@0.25.4':
87 | resolution: {integrity: sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==}
88 | engines: {node: '>=18'}
89 | cpu: [loong64]
90 | os: [linux]
91 |
92 | '@esbuild/linux-mips64el@0.25.4':
93 | resolution: {integrity: sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==}
94 | engines: {node: '>=18'}
95 | cpu: [mips64el]
96 | os: [linux]
97 |
98 | '@esbuild/linux-ppc64@0.25.4':
99 | resolution: {integrity: sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==}
100 | engines: {node: '>=18'}
101 | cpu: [ppc64]
102 | os: [linux]
103 |
104 | '@esbuild/linux-riscv64@0.25.4':
105 | resolution: {integrity: sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==}
106 | engines: {node: '>=18'}
107 | cpu: [riscv64]
108 | os: [linux]
109 |
110 | '@esbuild/linux-s390x@0.25.4':
111 | resolution: {integrity: sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==}
112 | engines: {node: '>=18'}
113 | cpu: [s390x]
114 | os: [linux]
115 |
116 | '@esbuild/linux-x64@0.25.4':
117 | resolution: {integrity: sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==}
118 | engines: {node: '>=18'}
119 | cpu: [x64]
120 | os: [linux]
121 |
122 | '@esbuild/netbsd-arm64@0.25.4':
123 | resolution: {integrity: sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==}
124 | engines: {node: '>=18'}
125 | cpu: [arm64]
126 | os: [netbsd]
127 |
128 | '@esbuild/netbsd-x64@0.25.4':
129 | resolution: {integrity: sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==}
130 | engines: {node: '>=18'}
131 | cpu: [x64]
132 | os: [netbsd]
133 |
134 | '@esbuild/openbsd-arm64@0.25.4':
135 | resolution: {integrity: sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==}
136 | engines: {node: '>=18'}
137 | cpu: [arm64]
138 | os: [openbsd]
139 |
140 | '@esbuild/openbsd-x64@0.25.4':
141 | resolution: {integrity: sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==}
142 | engines: {node: '>=18'}
143 | cpu: [x64]
144 | os: [openbsd]
145 |
146 | '@esbuild/sunos-x64@0.25.4':
147 | resolution: {integrity: sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==}
148 | engines: {node: '>=18'}
149 | cpu: [x64]
150 | os: [sunos]
151 |
152 | '@esbuild/win32-arm64@0.25.4':
153 | resolution: {integrity: sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==}
154 | engines: {node: '>=18'}
155 | cpu: [arm64]
156 | os: [win32]
157 |
158 | '@esbuild/win32-ia32@0.25.4':
159 | resolution: {integrity: sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==}
160 | engines: {node: '>=18'}
161 | cpu: [ia32]
162 | os: [win32]
163 |
164 | '@esbuild/win32-x64@0.25.4':
165 | resolution: {integrity: sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==}
166 | engines: {node: '>=18'}
167 | cpu: [x64]
168 | os: [win32]
169 |
170 | '@isaacs/cliui@8.0.2':
171 | resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==}
172 | engines: {node: '>=12'}
173 |
174 | ansi-regex@5.0.1:
175 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
176 | engines: {node: '>=8'}
177 |
178 | ansi-regex@6.1.0:
179 | resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==}
180 | engines: {node: '>=12'}
181 |
182 | ansi-styles@4.3.0:
183 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
184 | engines: {node: '>=8'}
185 |
186 | ansi-styles@6.2.1:
187 | resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==}
188 | engines: {node: '>=12'}
189 |
190 | balanced-match@1.0.2:
191 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
192 |
193 | brace-expansion@2.0.1:
194 | resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==}
195 |
196 | color-convert@2.0.1:
197 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
198 | engines: {node: '>=7.0.0'}
199 |
200 | color-name@1.1.4:
201 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
202 |
203 | cross-spawn@7.0.6:
204 | resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
205 | engines: {node: '>= 8'}
206 |
207 | eastasianwidth@0.2.0:
208 | resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
209 |
210 | emoji-regex@8.0.0:
211 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
212 |
213 | emoji-regex@9.2.2:
214 | resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
215 |
216 | esbuild@0.25.4:
217 | resolution: {integrity: sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==}
218 | engines: {node: '>=18'}
219 | hasBin: true
220 |
221 | foreground-child@3.3.1:
222 | resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==}
223 | engines: {node: '>=14'}
224 |
225 | glob@11.0.2:
226 | resolution: {integrity: sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ==}
227 | engines: {node: 20 || >=22}
228 | hasBin: true
229 |
230 | is-fullwidth-code-point@3.0.0:
231 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==}
232 | engines: {node: '>=8'}
233 |
234 | isexe@2.0.0:
235 | resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
236 |
237 | jackspeak@4.1.0:
238 | resolution: {integrity: sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw==}
239 | engines: {node: 20 || >=22}
240 |
241 | lru-cache@11.1.0:
242 | resolution: {integrity: sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==}
243 | engines: {node: 20 || >=22}
244 |
245 | minimatch@10.0.1:
246 | resolution: {integrity: sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==}
247 | engines: {node: 20 || >=22}
248 |
249 | minipass@7.1.2:
250 | resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
251 | engines: {node: '>=16 || 14 >=14.17'}
252 |
253 | package-json-from-dist@1.0.1:
254 | resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
255 |
256 | path-key@3.1.1:
257 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
258 | engines: {node: '>=8'}
259 |
260 | path-scurry@2.0.0:
261 | resolution: {integrity: sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==}
262 | engines: {node: 20 || >=22}
263 |
264 | rimraf@6.0.1:
265 | resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==}
266 | engines: {node: 20 || >=22}
267 | hasBin: true
268 |
269 | shebang-command@2.0.0:
270 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
271 | engines: {node: '>=8'}
272 |
273 | shebang-regex@3.0.0:
274 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
275 | engines: {node: '>=8'}
276 |
277 | signal-exit@4.1.0:
278 | resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
279 | engines: {node: '>=14'}
280 |
281 | string-width@4.2.3:
282 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
283 | engines: {node: '>=8'}
284 |
285 | string-width@5.1.2:
286 | resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==}
287 | engines: {node: '>=12'}
288 |
289 | strip-ansi@6.0.1:
290 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==}
291 | engines: {node: '>=8'}
292 |
293 | strip-ansi@7.1.0:
294 | resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==}
295 | engines: {node: '>=12'}
296 |
297 | which@2.0.2:
298 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
299 | engines: {node: '>= 8'}
300 | hasBin: true
301 |
302 | wrap-ansi@7.0.0:
303 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
304 | engines: {node: '>=10'}
305 |
306 | wrap-ansi@8.1.0:
307 | resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
308 | engines: {node: '>=12'}
309 |
310 | snapshots:
311 |
312 | '@esbuild/aix-ppc64@0.25.4':
313 | optional: true
314 |
315 | '@esbuild/android-arm64@0.25.4':
316 | optional: true
317 |
318 | '@esbuild/android-arm@0.25.4':
319 | optional: true
320 |
321 | '@esbuild/android-x64@0.25.4':
322 | optional: true
323 |
324 | '@esbuild/darwin-arm64@0.25.4':
325 | optional: true
326 |
327 | '@esbuild/darwin-x64@0.25.4':
328 | optional: true
329 |
330 | '@esbuild/freebsd-arm64@0.25.4':
331 | optional: true
332 |
333 | '@esbuild/freebsd-x64@0.25.4':
334 | optional: true
335 |
336 | '@esbuild/linux-arm64@0.25.4':
337 | optional: true
338 |
339 | '@esbuild/linux-arm@0.25.4':
340 | optional: true
341 |
342 | '@esbuild/linux-ia32@0.25.4':
343 | optional: true
344 |
345 | '@esbuild/linux-loong64@0.25.4':
346 | optional: true
347 |
348 | '@esbuild/linux-mips64el@0.25.4':
349 | optional: true
350 |
351 | '@esbuild/linux-ppc64@0.25.4':
352 | optional: true
353 |
354 | '@esbuild/linux-riscv64@0.25.4':
355 | optional: true
356 |
357 | '@esbuild/linux-s390x@0.25.4':
358 | optional: true
359 |
360 | '@esbuild/linux-x64@0.25.4':
361 | optional: true
362 |
363 | '@esbuild/netbsd-arm64@0.25.4':
364 | optional: true
365 |
366 | '@esbuild/netbsd-x64@0.25.4':
367 | optional: true
368 |
369 | '@esbuild/openbsd-arm64@0.25.4':
370 | optional: true
371 |
372 | '@esbuild/openbsd-x64@0.25.4':
373 | optional: true
374 |
375 | '@esbuild/sunos-x64@0.25.4':
376 | optional: true
377 |
378 | '@esbuild/win32-arm64@0.25.4':
379 | optional: true
380 |
381 | '@esbuild/win32-ia32@0.25.4':
382 | optional: true
383 |
384 | '@esbuild/win32-x64@0.25.4':
385 | optional: true
386 |
387 | '@isaacs/cliui@8.0.2':
388 | dependencies:
389 | string-width: 5.1.2
390 | string-width-cjs: string-width@4.2.3
391 | strip-ansi: 7.1.0
392 | strip-ansi-cjs: strip-ansi@6.0.1
393 | wrap-ansi: 8.1.0
394 | wrap-ansi-cjs: wrap-ansi@7.0.0
395 |
396 | ansi-regex@5.0.1: {}
397 |
398 | ansi-regex@6.1.0: {}
399 |
400 | ansi-styles@4.3.0:
401 | dependencies:
402 | color-convert: 2.0.1
403 |
404 | ansi-styles@6.2.1: {}
405 |
406 | balanced-match@1.0.2: {}
407 |
408 | brace-expansion@2.0.1:
409 | dependencies:
410 | balanced-match: 1.0.2
411 |
412 | color-convert@2.0.1:
413 | dependencies:
414 | color-name: 1.1.4
415 |
416 | color-name@1.1.4: {}
417 |
418 | cross-spawn@7.0.6:
419 | dependencies:
420 | path-key: 3.1.1
421 | shebang-command: 2.0.0
422 | which: 2.0.2
423 |
424 | eastasianwidth@0.2.0: {}
425 |
426 | emoji-regex@8.0.0: {}
427 |
428 | emoji-regex@9.2.2: {}
429 |
430 | esbuild@0.25.4:
431 | optionalDependencies:
432 | '@esbuild/aix-ppc64': 0.25.4
433 | '@esbuild/android-arm': 0.25.4
434 | '@esbuild/android-arm64': 0.25.4
435 | '@esbuild/android-x64': 0.25.4
436 | '@esbuild/darwin-arm64': 0.25.4
437 | '@esbuild/darwin-x64': 0.25.4
438 | '@esbuild/freebsd-arm64': 0.25.4
439 | '@esbuild/freebsd-x64': 0.25.4
440 | '@esbuild/linux-arm': 0.25.4
441 | '@esbuild/linux-arm64': 0.25.4
442 | '@esbuild/linux-ia32': 0.25.4
443 | '@esbuild/linux-loong64': 0.25.4
444 | '@esbuild/linux-mips64el': 0.25.4
445 | '@esbuild/linux-ppc64': 0.25.4
446 | '@esbuild/linux-riscv64': 0.25.4
447 | '@esbuild/linux-s390x': 0.25.4
448 | '@esbuild/linux-x64': 0.25.4
449 | '@esbuild/netbsd-arm64': 0.25.4
450 | '@esbuild/netbsd-x64': 0.25.4
451 | '@esbuild/openbsd-arm64': 0.25.4
452 | '@esbuild/openbsd-x64': 0.25.4
453 | '@esbuild/sunos-x64': 0.25.4
454 | '@esbuild/win32-arm64': 0.25.4
455 | '@esbuild/win32-ia32': 0.25.4
456 | '@esbuild/win32-x64': 0.25.4
457 |
458 | foreground-child@3.3.1:
459 | dependencies:
460 | cross-spawn: 7.0.6
461 | signal-exit: 4.1.0
462 |
463 | glob@11.0.2:
464 | dependencies:
465 | foreground-child: 3.3.1
466 | jackspeak: 4.1.0
467 | minimatch: 10.0.1
468 | minipass: 7.1.2
469 | package-json-from-dist: 1.0.1
470 | path-scurry: 2.0.0
471 |
472 | is-fullwidth-code-point@3.0.0: {}
473 |
474 | isexe@2.0.0: {}
475 |
476 | jackspeak@4.1.0:
477 | dependencies:
478 | '@isaacs/cliui': 8.0.2
479 |
480 | lru-cache@11.1.0: {}
481 |
482 | minimatch@10.0.1:
483 | dependencies:
484 | brace-expansion: 2.0.1
485 |
486 | minipass@7.1.2: {}
487 |
488 | package-json-from-dist@1.0.1: {}
489 |
490 | path-key@3.1.1: {}
491 |
492 | path-scurry@2.0.0:
493 | dependencies:
494 | lru-cache: 11.1.0
495 | minipass: 7.1.2
496 |
497 | rimraf@6.0.1:
498 | dependencies:
499 | glob: 11.0.2
500 | package-json-from-dist: 1.0.1
501 |
502 | shebang-command@2.0.0:
503 | dependencies:
504 | shebang-regex: 3.0.0
505 |
506 | shebang-regex@3.0.0: {}
507 |
508 | signal-exit@4.1.0: {}
509 |
510 | string-width@4.2.3:
511 | dependencies:
512 | emoji-regex: 8.0.0
513 | is-fullwidth-code-point: 3.0.0
514 | strip-ansi: 6.0.1
515 |
516 | string-width@5.1.2:
517 | dependencies:
518 | eastasianwidth: 0.2.0
519 | emoji-regex: 9.2.2
520 | strip-ansi: 7.1.0
521 |
522 | strip-ansi@6.0.1:
523 | dependencies:
524 | ansi-regex: 5.0.1
525 |
526 | strip-ansi@7.1.0:
527 | dependencies:
528 | ansi-regex: 6.1.0
529 |
530 | which@2.0.2:
531 | dependencies:
532 | isexe: 2.0.0
533 |
534 | wrap-ansi@7.0.0:
535 | dependencies:
536 | ansi-styles: 4.3.0
537 | string-width: 4.2.3
538 | strip-ansi: 6.0.1
539 |
540 | wrap-ansi@8.1.0:
541 | dependencies:
542 | ansi-styles: 6.2.1
543 | string-width: 5.1.2
544 | strip-ansi: 7.1.0
545 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - .
3 |
4 | onlyBuiltDependencies:
5 | - esbuild
6 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | /*!
2 | Regex Colorizer
3 | (c) 2007-2025 Steven Levithan
4 | MIT License
5 | */
6 |
7 | const unicodePropX = '[pP] { (? (?: [A-Za-z_]+ = )? [A-Za-z_]+ ) }';
8 | const cuxTokenX = String.raw`c [A-Za-z] | u (?: [\dA-Fa-f]{4} | { [\dA-Fa-f]+ } ) | x [\dA-Fa-f]{2}`;
9 | const octalRange = '[0-3][0-7]{0,2}|[4-7][0-7]?';
10 |
11 | const regexToken = new RegExp(String.raw`
12 | \[\^?(?:[^\\\]]+|\\.?)*]?
13 | | \\(?:0(?:${octalRange})?|[1-9]\d*|${cuxTokenX}|k(?:<(?\w+)>)?|${unicodePropX}|.?)
14 | | \((?:\?(?:<(?:[=!]|(?[A-Za-z_]\w*)>)|[:=!]))?
15 | | (?:[?*+]|\{\d+(?:,\d*)?\})\??
16 | | [^.?*+^$[\]{}()|\\]+
17 | | .
18 | `.replace(/\s+/g, ''), 'gs');
19 | const charClassToken = new RegExp(String.raw`
20 | [^\\-]+
21 | | \\ (?: ${octalRange} | ${cuxTokenX} | ${unicodePropX} | .? )
22 | | -
23 | `.replace(/\s+/g, ''), 'gs');
24 | const charClassParts = /^(?\[\^?)(?(?:[^\\\]]+|\\.?)*)(?]?)$/s;
25 | const quantifier = /^(?:[?*+]|\{\d+(?:,\d*)?\})\??$/;
26 |
27 | const type = {
28 | NONE: 0,
29 | RANGE_HYPHEN: 1,
30 | SHORT_CLASS: 2,
31 | ALTERNATOR: 3,
32 | };
33 |
34 | const error = {
35 | DUPLICATE_CAPTURE_NAME: 'Duplicate capture name',
36 | EMPTY_TOP_ALTERNATIVE: 'Empty alternative effectively truncates the regex here',
37 | INCOMPLETE_TOKEN: 'Token is incomplete',
38 | INDEX_OVERFLOW: 'Character index cannot use value over 10FFFF',
39 | INTERVAL_OVERFLOW: 'Interval quantifier cannot use value over 65,535',
40 | INTERVAL_REVERSED: 'Interval quantifier range is reversed',
41 | INVALID_BACKREF: 'Backreference to missing or invalid group',
42 | INVALID_ESCAPE: 'Deprecated escaped literal or octal',
43 | INVALID_RANGE: 'Reversed or invalid range',
44 | REQUIRES_ESCAPE: 'Must be escaped unless part of a valid token',
45 | UNBALANCED_LEFT_PAREN: 'Unclosed grouping',
46 | UNBALANCED_RIGHT_PAREN: 'No matching opening parenthesis',
47 | UNCLOSED_CLASS: 'Unclosed character class',
48 | UNQUANTIFIABLE: 'Preceding token is not quantifiable',
49 | };
50 |
51 | const styleId = `rc-${(+new Date).toString(36).slice(-5)}`;
52 |
53 | // HTML generation functions for regex syntax parts
54 | const to = {
55 | // Depth is 0-5
56 | alternator: (s, depth) => `${s}`,
57 | backref: s => `${s}`,
58 | charClass: s => `${s}`,
59 | charClassBoundary: s => `${s}`,
60 | error: (s, msg) => `${s}`,
61 | escapedLiteral: s => `${s}`,
62 | // Depth is 1-5
63 | group: (s, depth) => `${s}`,
64 | metasequence: s => `${s}`,
65 | // Depth is 0-5
66 | quantifier: (s, depth) => `${s}`,
67 | range: s => `${s}`,
68 | };
69 | to.group.openingTagLength = ''.length;
70 |
71 | /**
72 | * Converts special characters to HTML entities.
73 | * @param {string} str String with characters to expand.
74 | * @returns {string} String with characters expanded.
75 | */
76 | function expandEntities(str) {
77 | return str.replace(/&/g, '&').replace(/ 1 && token.charAt(0) === '\\') {
98 | const t = token.slice(1);
99 | // Control character
100 | if (/^c[A-Za-z]$/.test(t)) {
101 | return 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.indexOf(t.charAt(1).toUpperCase()) + 1;
102 | }
103 | // Two or four digit hexadecimal character code
104 | if (/^(?:x[\dA-Fa-f]{2}|u[\dA-Fa-f]{4})$/.test(t)) {
105 | return parseInt(t.slice(1), 16);
106 | }
107 | // '\u{...}'
108 | if (/^u{[\dA-Fa-f]+}$/.test(t)) {
109 | return parseInt(t.slice(2, -1), 16);
110 | }
111 | // One to three digit octal character code up to 377 (0xFF)
112 | if (new RegExp(`^(?:${octalRange})$`).test(t)) {
113 | return parseInt(t, 8);
114 | }
115 | // Shorthand class or incomplete token
116 | if (t.length === 1 && 'cuxDdSsWw'.includes(t)) {
117 | return NaN;
118 | }
119 | // Metacharacter representing a single character index, or escaped literal character
120 | if (t.length === 1) {
121 | switch (t) {
122 | case 'b': return 8; // Backspace
123 | case 'f': return 12; // Form feed
124 | case 'n': return 10; // Line feed
125 | case 'r': return 13; // Carriage return
126 | case 't': return 9; // Horizontal tab
127 | case 'v': return 11; // Vertical tab
128 | default : return t.charCodeAt(0); // Escaped literal character
129 | }
130 | }
131 | }
132 | // Unescaped literal token(s)
133 | if (token !== '\\') {
134 | return token.charCodeAt(0);
135 | }
136 | return NaN;
137 | }
138 |
139 | /**
140 | * Returns HTML for displaying the given character class with syntax highlighting.
141 | * Character classes have their own syntax rules which are different (sometimes subtly) from
142 | * surrounding regex syntax. Hence, they're treated as a single token and parsed separately.
143 | * @param {string} value Character class pattern to be colorized.
144 | * @param {Object} flagsObj Whether each flag is enabled.
145 | * @returns {string}
146 | */
147 | function parseCharClass(value, flagsObj) {
148 | let output = '';
149 | let lastToken = {
150 | rangeable: false,
151 | type: type.NONE,
152 | };
153 | const {opening, content, closing} = charClassParts.exec(value).groups;
154 |
155 | const matches = content.matchAll(charClassToken);
156 | for (const match of matches) {
157 | const m = match[0];
158 | // Escape
159 | if (m.charAt(0) === '\\') {
160 | // Inside character classes, browsers differ on how they handle the following:
161 | // - Any representation of character index zero (\0, \00, \000, \x00, \u0000)
162 | // - '\c', when not followed by A-Z or a-z
163 | // - '\x', when not followed by two hex characters
164 | // - '\u', when not followed by four hex characters
165 | // However, although representations of character index zero within character classes don't
166 | // work on their own in old Firefox, they don't throw an error, they work when used with
167 | // ranges, and it's highly unlikely that the user will actually have such a character in
168 | // their test data, so such tokens are highlighted normally. The remaining metasequences
169 | // are flagged as errors
170 | // '\c', '\u', '\x'
171 | if (/^\\[cux]$/.test(m)) {
172 | output += to.error(m, error.INCOMPLETE_TOKEN);
173 | lastToken = {
174 | rangeable: lastToken.type !== type.RANGE_HYPHEN,
175 | };
176 | // '\p' or '\P' in Unicode mode
177 | } else if (flagsObj.unicode && /^\\p$/i.test(m)) {
178 | output += to.error(m, error.INCOMPLETE_TOKEN);
179 | lastToken = {
180 | rangeable: lastToken.type !== type.RANGE_HYPHEN,
181 | type: type.SHORT_CLASS,
182 | };
183 | // Shorthand class (matches more than one character index)
184 | } else if (/^\\[dsw]$/i.test(m)) {
185 | output += to.metasequence(m);
186 | // Traditional regex behavior is that a shorthand class should be unrangeable. Hence,
187 | // [-\dz], [\d-z], and [z-\d] should all be equivalent. However, some old browsers handle
188 | // this inconsistently. Ex: Firefox 2 throws an invalid range error for [z-\d] and [\d--]
189 | lastToken = {
190 | rangeable: lastToken.type !== type.RANGE_HYPHEN,
191 | type: type.SHORT_CLASS,
192 | };
193 | // Unescaped '\' at the end of the regex
194 | } else if (m === '\\') {
195 | output += to.error(m, error.INCOMPLETE_TOKEN);
196 | // Don't need to set lastToken since this is the end of the line
197 | // '\p{...}' or '\P{...}': Unicode property in Unicode mode, or escaped literal '\p' and following chars
198 | } else if (match.groups.property) {
199 | if (flagsObj.unicode) {
200 | output += to.metasequence(m);
201 | lastToken = {
202 | rangeable: lastToken.type !== type.RANGE_HYPHEN,
203 | type: type.SHORT_CLASS,
204 | };
205 | } else {
206 | output += to.metasequence(m.slice(0, 2)) + m.slice(2);
207 | lastToken = {
208 | rangeable: true,
209 | charCode: getTokenCharCode('}'),
210 | };
211 | }
212 | // Unicode mode: escaped literal character or octal with leading zero becomes error
213 | } else if (flagsObj.unicode && /^\\(?:[^\^$?*+.|(){}[\]\\/\-0bcdDfnpPrsStuvwWx]|0\d)/.test(m)) {
214 | output += to.error(expandEntities(m), error.INVALID_ESCAPE);
215 | lastToken = {
216 | rangeable: lastToken.type !== type.RANGE_HYPHEN,
217 | };
218 | // '\u{...}'
219 | } else if (m.startsWith('\\u{')) {
220 | if (flagsObj.unicode) {
221 | const charCode = getTokenCharCode(m);
222 | output += charCode <= 0x10FFFF ?
223 | to.metasequence(m) :
224 | to.error(m, error.INDEX_OVERFLOW);
225 | lastToken = {
226 | rangeable: lastToken.type !== type.RANGE_HYPHEN,
227 | charCode,
228 | };
229 | } else {
230 | output += to.error('\\u', error.INCOMPLETE_TOKEN) + m.slice(2);
231 | lastToken = {
232 | rangeable: true,
233 | charCode: getTokenCharCode('}'),
234 | };
235 | }
236 | // Metasequence representing a single character index
237 | } else {
238 | output += to.metasequence(expandEntities(m));
239 | lastToken = {
240 | rangeable: lastToken.type !== type.RANGE_HYPHEN,
241 | charCode: getTokenCharCode(m),
242 | };
243 | }
244 | // Hyphen (might indicate a range)
245 | } else if (m === '-') {
246 | if (lastToken.rangeable) {
247 | // Copy the regex to not mess with its lastIndex
248 | const tokenCopy = new RegExp(charClassToken);
249 | tokenCopy.lastIndex = match.index + match[0].length;
250 | const nextToken = tokenCopy.exec(content);
251 |
252 | if (nextToken) {
253 | const nextTokenCharCode = getTokenCharCode(nextToken[0]);
254 | // Hypen for a reverse range (ex: z-a) or shorthand class (ex: \d-x or x-\S)
255 | if (
256 | (!isNaN(nextTokenCharCode) && lastToken.charCode > nextTokenCharCode) ||
257 | lastToken.type === type.SHORT_CLASS ||
258 | /^\\[dsw]$/i.test(nextToken[0]) ||
259 | (flagsObj.unicode && nextToken.groups.property)
260 | ) {
261 | output += to.error('-', error.INVALID_RANGE);
262 | // Hyphen creating a valid range
263 | } else {
264 | output += to.range('-');
265 | }
266 | lastToken = {
267 | rangeable: false,
268 | type: type.RANGE_HYPHEN
269 | };
270 | } else {
271 | // Hyphen at the end of a properly closed character class (literal character)
272 | if (closing) {
273 | // Since this is a literal, it's technically rangeable, but that doesn't matter
274 | output += '-';
275 | // Hyphen at the end of an unclosed character class (i.e., the end of the regex)
276 | } else {
277 | output += to.range('-');
278 | }
279 | }
280 | // Hyphen at the beginning of a character class or after a non-rangeable token
281 | } else {
282 | output += '-';
283 | lastToken = {
284 | rangeable: lastToken.type !== type.RANGE_HYPHEN,
285 | };
286 | }
287 | // Literal character sequence
288 | // Sequences of unescaped, literal tokens are matched in one step
289 | } else {
290 | output += expandEntities(m);
291 | lastToken = {
292 | rangeable: (m.length > 1 || lastToken.type !== type.RANGE_HYPHEN),
293 | charCode: m.charCodeAt(m.length - 1),
294 | };
295 | }
296 | }
297 |
298 | if (closing) {
299 | output = to.charClassBoundary(opening) + output + to.charClassBoundary(closing);
300 | } else {
301 | output = to.error(opening, error.UNCLOSED_CLASS) + output;
302 | }
303 | return output;
304 | }
305 |
306 | /**
307 | * Returns details about the overall pattern.
308 | * @param {string} pattern Regex pattern to be searched.
309 | * @returns {Object}
310 | */
311 | function getPatternDetails(pattern) {
312 | const captureNames = new Set();
313 | let totalCaptures = 0;
314 | const matches = pattern.matchAll(regexToken);
315 | for (const match of matches) {
316 | const captureName = match.groups.captureName;
317 | if (captureName) {
318 | captureNames.add(captureName);
319 | }
320 | if (/^\((?=\?<(?![=!])|$)/.test(match[0])) {
321 | totalCaptures++;
322 | }
323 | }
324 | return {
325 | captureNames,
326 | totalCaptures,
327 | };
328 | }
329 |
330 | /**
331 | * Returns an object indicating whether each flag property is enabled. Throws if duplicate,
332 | * unknown, or unsupported flags are provided.
333 | * @param {string} flags Regex flags.
334 | * @returns {Object}
335 | */
336 | function getFlagsObj(flags) {
337 | const flagNames = {
338 | d: 'hasIndices',
339 | g: 'global',
340 | i: 'ignoreCase',
341 | m: 'multiline',
342 | s: 'dotAll',
343 | u: 'unicode',
344 | v: 'unicodeSets',
345 | y: 'sticky',
346 | };
347 | const flagsObj = Object.fromEntries(
348 | Object.values(flagNames).map(value => [value, false])
349 | );
350 | const flagsSet = new Set();
351 | for (const char of flags) {
352 | if (flagsSet.has(char)) {
353 | throw new Error(`Duplicate flag: ${char}`);
354 | }
355 | if (!Object.hasOwn(flagNames, char)) {
356 | throw new Error(`Unknown flag: ${char}`);
357 | }
358 | flagsSet.add(char);
359 | flagsObj[flagNames[char]] = true;
360 | }
361 | if (flagsObj.unicodeSets) {
362 | throw new Error ('Flag v is not yet supported');
363 | }
364 | return flagsObj;
365 | }
366 |
367 | /**
368 | * Returns HTML for displaying the given regex with syntax highlighting.
369 | * @param {string} pattern Regex pattern to be colorized.
370 | * @param {Object} [options]
371 | * @param {string} [options.flags] Any combination of valid flags.
372 | * @returns {string} HTML with highlighting.
373 | */
374 | function colorizePattern (pattern, {
375 | flags = '',
376 | } = {}) {
377 | // Validate flags and use for regex syntax changes
378 | const flagsObj = getFlagsObj(flags);
379 | const {
380 | // Having any named captures changes the meaning of '\k'
381 | captureNames,
382 | // Used to determine whether escaped numbers should be treated as backrefs
383 | totalCaptures,
384 | } = getPatternDetails(pattern);
385 | const usedCaptureNames = new Set();
386 | const openGroups = [];
387 | let capturingGroupCount = 0;
388 | let groupStyleDepth = 0;
389 | let lastToken = {
390 | quantifiable: false,
391 | type: type.NONE,
392 | };
393 | let output = '';
394 |
395 | const matches = pattern.matchAll(regexToken);
396 | for (const match of matches) {
397 | const m = match[0];
398 | const char0 = m.charAt(0);
399 | const char1 = m.charAt(1);
400 | // Character class
401 | if (char0 === '[') {
402 | output += to.charClass(parseCharClass(m, flagsObj));
403 | lastToken = {
404 | quantifiable: true,
405 | };
406 | // Group opening
407 | } else if (char0 === '(') {
408 | const captureName = match.groups.captureName;
409 | groupStyleDepth = groupStyleDepth === 5 ? 1 : groupStyleDepth + 1;
410 | // '(?' with a duplicate name
411 | if (usedCaptureNames.has(captureName)) {
412 | openGroups.push({
413 | valid: false,
414 | opening: expandEntities(m),
415 | });
416 | output += to.error(expandEntities(m), error.DUPLICATE_CAPTURE_NAME);
417 | // Valid group
418 | } else {
419 | if (m === '(' || captureName) {
420 | capturingGroupCount++;
421 | if (captureName) {
422 | usedCaptureNames.add(captureName);
423 | }
424 | }
425 | // Record the group opening's position and value so we can mark it later as invalid if it
426 | // turns out to be unclosed in the remainder of the regex
427 | openGroups.push({
428 | valid: true,
429 | opening: expandEntities(m),
430 | index: output.length + to.group.openingTagLength,
431 | });
432 | output += to.group(expandEntities(m), groupStyleDepth);
433 | }
434 | lastToken = {
435 | quantifiable: false,
436 | };
437 | // Group closing
438 | } else if (m === ')') {
439 | // If this is an invalid group closing
440 | if (!openGroups.length || !openGroups.at(-1).valid) {
441 | output += to.error(m, error.UNBALANCED_RIGHT_PAREN);
442 | lastToken = {
443 | quantifiable: false,
444 | };
445 | } else {
446 | output += to.group(m, groupStyleDepth);
447 | // Although it's possible to quantify lookarounds, this adds no value, doesn't work as
448 | // you'd expect in JavaScript, and is an error with flag u or v (and in some other regex
449 | // flavors such as PCRE), so flag them as unquantifiable
450 | lastToken = {
451 | quantifiable: !/^\(\?[=!]/.test(collapseEntities(openGroups.at(-1).opening)),
452 | groupStyleDepth,
453 | };
454 | }
455 | groupStyleDepth = groupStyleDepth === 1 ? 5 : groupStyleDepth - 1;
456 | // Drop the last opening paren from depth tracking
457 | openGroups.pop();
458 | // Escape or backreference
459 | } else if (char0 === '\\') {
460 | // Backreference, octal character code without a leading zero, or a literal '\8' or '\9'
461 | if (/^[1-9]/.test(char1)) {
462 | // What does '\10' mean (outside a character class)?
463 | // Non-Unicode mode:
464 | // - Backref 10, if 10 or more capturing groups anywhere in the pattern
465 | // - Octal character index 10, if less than 10 capturing groups anywhere in the pattern
466 | // (since 10 is in octal range 0-377)
467 | // Ex: `\1000\1(a)\10\1\1000` matches '\u{40}0a\u{8}a\u{40}0'
468 | // Ex: `\3\377(a)\3\377` matches '\u{3}\u{FF}a\u{3}\u{FF}'
469 | // Ex: `\3\377()()(a)\3\377` matches '\u{FF}aa\u{FF}'
470 | // In fact, octals are not included in ES3+, but browsers support them for backcompat.
471 | // With flag u or v (Unicode mode):
472 | // - Escaped digits must be a backref or \0 (character index 0) and can't be immediately
473 | // followed by digits
474 | // - An escaped number is a valid backreference if there are as many capturing groups
475 | // anywhere in the pattern
476 | // Numbered backreference
477 | if (+m.slice(1) <= totalCaptures) {
478 | output += to.backref(m);
479 | lastToken = {
480 | quantifiable: true,
481 | };
482 | } else {
483 | // Unicode mode: error
484 | if (flagsObj.unicode) {
485 | output += to.error(m, error.INVALID_BACKREF);
486 | lastToken = {
487 | quantifiable: false,
488 | };
489 | // Octal followed by literal, or escaped literal followed by literal
490 | } else {
491 | const {escapedNum, escapedLiteral, literal} =
492 | new RegExp(String.raw`^\\(?${octalRange}|(?[89]))(?\d*)`).exec(m).groups;
493 | if (escapedLiteral) {
494 | // For \8 and \9 (escaped non-octal digits) when as many capturing groups aren't in
495 | // the pattern, they match '8' and '9' (when not using flag u or v).
496 | // Ex: `\8(a)\8` matches '8a8'
497 | // Ex: `\8()()()()()()()(a)\8` matches 'aa'
498 | // Ex: `\80()()()()()()()(a)\80` matches '80a80'
499 | output += `${to.escapedLiteral(`\\${escapedLiteral}`)}${literal}`;
500 | } else {
501 | output += `${to.metasequence(`\\${escapedNum}`)}${literal}`;
502 | }
503 | lastToken = {
504 | quantifiable: true,
505 | };
506 | }
507 | }
508 | // Named backreference, \k token with error, or a literal '\k' or '\k'
509 | } else if (char1 === 'k') {
510 | // What does '\k' or '\k' mean?
511 | // - If a named capture appears anywhere (before or after), treat as backreference
512 | // - Otherwise, it's a literal '\k' plus any following chars
513 | // - In backreference mode, error if name doesn't appear in a capture (before or after)
514 | // With flag u or v, the rules change to always use backreference mode
515 | // Backreference mode for \k
516 | if (captureNames.size) {
517 | // Valid
518 | if (captureNames.has(match.groups.backrefName)) {
519 | output += to.backref(expandEntities(m));
520 | lastToken = {
521 | quantifiable: true,
522 | };
523 | // References a missing or invalid (ex: \k<1>) named group
524 | } else if (m.endsWith('>')) {
525 | output += to.error(expandEntities(m), error.INVALID_BACKREF);
526 | lastToken = {
527 | quantifiable: false,
528 | };
529 | // '\k'
530 | } else {
531 | output += to.error(m, error.INCOMPLETE_TOKEN);
532 | lastToken = {
533 | quantifiable: false,
534 | };
535 | }
536 | // Unicode mode with no named captures: \k is an error
537 | } else if (flagsObj.unicode) {
538 | output += to.error(expandEntities(m), error.INVALID_BACKREF);
539 | lastToken = {
540 | quantifiable: false,
541 | };
542 | // Literal mode for \k
543 | } else {
544 | output += to.escapedLiteral('\\k') + expandEntities(m.slice(2));
545 | lastToken = {
546 | quantifiable: true,
547 | };
548 | }
549 | // '\p{...}' or '\P{...}': Unicode property in Unicode mode, or escaped literal '\p' and following chars
550 | } else if (match.groups.property) {
551 | if (flagsObj.unicode) {
552 | output += to.metasequence(m);
553 | } else {
554 | output += to.escapedLiteral(`\\${char1}`) + m.slice(2);
555 | }
556 | lastToken = {
557 | quantifiable: true,
558 | };
559 | // Incomplete Unicode property in Unicode mode
560 | // This condition isn't required here but allows for a more specific error
561 | } else if (flagsObj.unicode && 'pP'.includes(char1)) {
562 | output += to.error(m, error.INCOMPLETE_TOKEN);
563 | lastToken = {
564 | quantifiable: false,
565 | };
566 | // Metasequence (shorthand class, word boundary, control character, octal with a leading zero, etc.)
567 | } else if (/^[0bBcdDfnrsStuvwWx]/.test(char1)) {
568 | // Browsers differ on how they handle:
569 | // - '\c', when not followed by A-Z or a-z
570 | // - '\x', when not followed by two hex characters
571 | // - '\u', when not followed by four hex characters
572 | // Hence, such metasequences are flagged as errors
573 | if (/^\\[cux]$/.test(m)) {
574 | output += to.error(m, error.INCOMPLETE_TOKEN);
575 | lastToken = {
576 | quantifiable: false,
577 | };
578 | // '\u{...}'
579 | } else if (m.startsWith('\\u{')) {
580 | if (flagsObj.unicode) {
581 | const charCode = getTokenCharCode(m);
582 | output += charCode <= 0x10FFFF ?
583 | to.metasequence(m) :
584 | to.error(m, error.INDEX_OVERFLOW);
585 | lastToken = {
586 | quantifiable: true,
587 | };
588 | // Non-Unicode mode and '\u{...}' includes only decimal digits, so treat as an escaped
589 | // literal 'u' followed by a quantifier
590 | } else if (/^\\u{\d+}$/.test(m)) {
591 | // If there's a following `?` it will be handled as the next token which technically
592 | // isn't correct, but everything is still highlighted correctly apart from the gap in
593 | // tokens that might be visible depending on styling
594 | output += to.error('\\u', error.INCOMPLETE_TOKEN) + to.error(m.slice(2), error.UNQUANTIFIABLE);
595 | lastToken = {
596 | quantifiable: false,
597 | };
598 | // Non-Unicode mode and '\u{...}' includes hex digits A-F/a-f
599 | } else {
600 | output += to.error('\\u', error.INCOMPLETE_TOKEN) + m.slice(2);
601 | lastToken = {
602 | quantifiable: true,
603 | };
604 | }
605 | // Unquantifiable metasequence
606 | } else if ('bB'.includes(char1)) {
607 | output += to.metasequence(m);
608 | lastToken = {
609 | quantifiable: false,
610 | };
611 | // Unicode mode: octal with a leading zero
612 | } else if (flagsObj.unicode && /^\\0\d/.test(m)) {
613 | output += to.error(m, error.INVALID_ESCAPE);
614 | lastToken = {
615 | quantifiable: false,
616 | };
617 | // Quantifiable metasequence
618 | } else {
619 | output += to.metasequence(m);
620 | lastToken = {
621 | quantifiable: true,
622 | };
623 | }
624 | // Unescaped '\' at the end of the regex
625 | } else if (m === '\\') {
626 | output += to.error(m, error.INCOMPLETE_TOKEN);
627 | // Don't need to set lastToken since this is the end of the line
628 | // Escaped literal character in Unicode mode
629 | } else if (flagsObj.unicode && /^[^\^$?*+.|(){}[\]\\/]$/.test(char1)) {
630 | output += to.error(expandEntities(m), error.INVALID_ESCAPE);
631 | lastToken = {
632 | quantifiable: false,
633 | };
634 | // Escaped special or literal character
635 | } else {
636 | output += to.escapedLiteral(expandEntities(m));
637 | lastToken = {
638 | quantifiable: true,
639 | };
640 | }
641 | // Quantifier
642 | } else if (quantifier.test(m)) {
643 | if (lastToken.quantifiable) {
644 | const interval = /^\{(\d+)(?:,(\d*))?/.exec(m);
645 | // Interval quantifier out of range for old Firefox
646 | if (interval && (+interval[1] > 65535 || (interval[2] && +interval[2] > 65535))) {
647 | output += to.error(m, error.INTERVAL_OVERFLOW);
648 | // Interval quantifier in reverse numeric order
649 | } else if (interval && interval[2] && (+interval[1] > +interval[2])) {
650 | output += to.error(m, error.INTERVAL_REVERSED);
651 | } else {
652 | // Quantifiers for groups are shown in the style of the (preceeding) group's depth
653 | output += to.quantifier(m, lastToken.groupStyleDepth ?? 0);
654 | }
655 | } else {
656 | output += to.error(m, error.UNQUANTIFIABLE);
657 | }
658 | lastToken = {
659 | quantifiable: false,
660 | };
661 | // Alternation
662 | } else if (m === '|') {
663 | // If there is a vertical bar at the very start of the regex, flag it as an error since it
664 | // effectively truncates the regex at that point. If two top-level vertical bars are next
665 | // to each other, flag it as an error for the same reason
666 | if (lastToken.type === type.NONE || (lastToken.type === type.ALTERNATOR && !openGroups.length)) {
667 | output += to.error(m, error.EMPTY_TOP_ALTERNATIVE);
668 | } else {
669 | // Alternators within groups are shown in the style of the containing group's depth
670 | output += to.alternator(m, openGroups.length && groupStyleDepth);
671 | }
672 | lastToken = {
673 | quantifiable: false,
674 | type: type.ALTERNATOR,
675 | };
676 | // ^ or $ anchor
677 | } else if (m === '^' || m === '$') {
678 | output += to.metasequence(m);
679 | lastToken = {
680 | quantifiable: false,
681 | };
682 | // Dot (.)
683 | } else if (m === '.') {
684 | output += to.metasequence(m);
685 | lastToken = {
686 | quantifiable: true,
687 | };
688 | // Unicode mode: Unescaped '{', '}', ']'
689 | } else if (flagsObj.unicode && '{}]'.includes(m)) {
690 | output += to.error(m, error.REQUIRES_ESCAPE);
691 | lastToken = {
692 | quantifiable: false,
693 | };
694 | // Literal character sequence
695 | // Sequences of unescaped, literal tokens are matched in one step, except the following which
696 | // are matched on their own:
697 | // - '{' when not part of a quantifier (non-Unicode mode: allow `{{1}`; Unicode mode: error)
698 | // - '}' and ']' (Unicode mode: error)
699 | } else {
700 | output += expandEntities(m);
701 | lastToken = {
702 | quantifiable: true,
703 | };
704 | }
705 | }
706 |
707 | // Mark the opening character sequence for each unclosed grouping as invalid
708 | let numCharsAdded = 0;
709 | for (const openGroup of openGroups) {
710 | // Skip groups that are already marked as errors
711 | if (!openGroup.valid) {
712 | continue;
713 | }
714 | const errorIndex = openGroup.index + numCharsAdded;
715 | output = (
716 | output.slice(0, errorIndex) +
717 | to.error(openGroup.opening, error.UNBALANCED_LEFT_PAREN) +
718 | output.slice(errorIndex + openGroup.opening.length)
719 | );
720 | numCharsAdded += to.error('', error.UNBALANCED_LEFT_PAREN).length;
721 | }
722 |
723 | return output;
724 | }
725 |
726 | /**
727 | * Applies highlighting to all regex elements on the page, replacing their content with HTML.
728 | * @param {Object} [options]
729 | * @param {string} [options.selector='.regex'] `querySelectorAll` value: elements to highlight.
730 | * @param {string} [options.flags] Any combination of valid flags. Overridden by `data-flags`
731 | * attribute.
732 | */
733 | function colorizeAll({
734 | selector = '.regex',
735 | flags = '',
736 | } = {}) {
737 | const els = document.querySelectorAll(selector);
738 | els.forEach(el => {
739 | el.classList.add(styleId);
740 | el.innerHTML = colorizePattern(el.textContent, {
741 | flags: el.dataset.flags || flags,
742 | });
743 | });
744 | }
745 |
746 | /**
747 | * Adds the default theme styles to the page. Don't run this if you provide your own stylesheet.
748 | */
749 | function loadStyles() {
750 | if (document.getElementById(styleId)) {
751 | return;
752 | }
753 | const ss = document.createElement('style');
754 | ss.id = styleId;
755 | // See `themes/default.css` for details
756 | ss.textContent = `
757 | .${styleId} {color: #000; font-family: Consolas, "Source Code Pro", Monospace; white-space: pre-wrap; word-break: break-all; overflow-wrap: anywhere;}
758 | .${styleId} b {font-weight: normal;}
759 | .${styleId} i {font-style: normal;}
760 | .${styleId} u {text-decoration: none;}
761 | .${styleId} * {border-radius: 0.25em;}
762 | .${styleId} span {background: #eee;}
763 | .${styleId} b {background: #80c0ff; color: #092e7f;}
764 | .${styleId} b.bref {background: #86e9ff; color: #0d47c4;}
765 | .${styleId} b.err {background: #e30000; color: #fff; font-style: normal;}
766 | .${styleId} i {background: #e3e3e3; font-style: italic;}
767 | .${styleId} i span {background: #c3c3c3; font-style: normal;}
768 | .${styleId} i b {background: #c3c3c3; color: #222;}
769 | .${styleId} i u {background: #d3d3d3;}
770 | .${styleId} b.g1 {background: #b4fa50; color: #074d0b;}
771 | .${styleId} b.g2 {background: #8cd400; color: #053c08;}
772 | .${styleId} b.g3 {background: #26b809; color: #fff;}
773 | .${styleId} b.g4 {background: #30ea60; color: #125824;}
774 | .${styleId} b.g5 {background: #0c8d15; color: #fff;}
775 | `;
776 | document.querySelector('head').appendChild(ss);
777 | }
778 |
779 | export {
780 | colorizeAll,
781 | colorizePattern,
782 | loadStyles,
783 | };
784 |
--------------------------------------------------------------------------------
/themes/default.css:
--------------------------------------------------------------------------------
1 | /* Regex Colorizer: Default theme */
2 |
3 | .regex {color: #000; font-family: Consolas, "Source Code Pro", Monospace; white-space: pre-wrap; word-break: break-all; overflow-wrap: anywhere;}
4 | .regex b {font-weight: normal;}
5 | .regex i {font-style: normal;}
6 | .regex u {text-decoration: none;}
7 | .regex * {border-radius: 0.25em;}
8 |
9 | /* escaped literal */
10 | .regex span {background: #eee;}
11 | /* metasequence */
12 | .regex b {background: #80c0ff; color: #092e7f;}
13 | /* backreference */
14 | .regex b.bref {background: #86e9ff; color: #0d47c4;}
15 | /* error */
16 | .regex b.err {background: #e30000; color: #fff; font-style: normal;}
17 |
18 | /* char class */
19 | .regex i {background: #e3e3e3; font-style: italic;}
20 | /* char class: boundaries */
21 | .regex i span {background: #c3c3c3; font-style: normal;}
22 | /* char class: metasequence */
23 | .regex i b {background: #c3c3c3; color: #222;}
24 | /* char class: range-hyphen */
25 | .regex i u {background: #d3d3d3;}
26 |
27 | /* group: depth */
28 | .regex b.g1 {background: #b4fa50; color: #074d0b;}
29 | .regex b.g2 {background: #8cd400; color: #053c08;}
30 | .regex b.g3 {background: #26b809; color: #fff;}
31 | .regex b.g4 {background: #30ea60; color: #125824;}
32 | .regex b.g5 {background: #0c8d15; color: #fff;}
33 |
--------------------------------------------------------------------------------
/themes/nobg.css:
--------------------------------------------------------------------------------
1 | /* Regex Colorizer: No BG theme */
2 |
3 | .regex {color: #000; font-family: Consolas, "Source Code Pro", Monospace; white-space: pre-wrap; word-break: break-all; overflow-wrap: anywhere;}
4 | .regex b {font-weight: normal;}
5 | .regex i {font-style: normal;}
6 | .regex u {text-decoration: none;}
7 |
8 | /* escaped literal */
9 | .regex span {color: #999;}
10 | /* metasequence */
11 | .regex b {color: #3766ff;}
12 | /* backreference */
13 | .regex b.bref {color: #666666; text-decoration: underline dotted;}
14 | /* error */
15 | .regex b.err {color: #d50000; font-weight: bold; font-style: normal;}
16 |
17 | /* char class */
18 | .regex i {color: #1db000; font-style: italic;}
19 | /* char class: boundaries */
20 | .regex i span {color: #007a09; font-style: normal;}
21 | /* char class: metasequence */
22 | .regex i b {color: #007a09;}
23 | /* char class: range-hyphen */
24 | .regex i u {color: #007a09; text-decoration: underline dotted;}
25 |
26 | /* group: depth */
27 | .regex b.g1 {color: #ec9b00;}
28 | .regex b.g2 {color: #e03ba9;}
29 | .regex b.g3 {color: #e7600c;}
30 | .regex b.g4 {color: #00909d;}
31 | .regex b.g5 {color: #9411b2;}
32 |
--------------------------------------------------------------------------------
/themes/regexbuddy.css:
--------------------------------------------------------------------------------
1 | /* Regex Colorizer: RegexBuddy theme */
2 |
3 | .regex {color: #000; font-family: Consolas, "Source Code Pro", Monospace; white-space: pre-wrap; word-break: break-all; overflow-wrap: anywhere;}
4 | .regex b {font-weight: normal;}
5 | .regex i {font-style: normal;}
6 | .regex u {text-decoration: none;}
7 |
8 | /* metasequence */
9 | .regex b {background: #80c0ff; color: #000080;}
10 | /* error */
11 | .regex b.err {background: #ff0000; color: #fff;}
12 |
13 | /* char class */
14 | .regex i {background: #ffc080; color: #603000;}
15 | /* char class: metasequence */
16 | .regex i b {background: #e0a060; color: #302000;}
17 |
18 | /* group: depth */
19 | .regex b.g1 {background: #00c000; color: #fff;}
20 | .regex b.g2 {background: #c0c000; color: #000;}
21 | .regex b.g3 {background: #008000; color: #fff;}
22 | .regex b.g4 {background: #808000; color: #fff;}
23 | .regex b.g5 {background: #0f0; color: #000;}
24 |
--------------------------------------------------------------------------------
/themes/regexpal.css:
--------------------------------------------------------------------------------
1 | /* Regex Colorizer: RegexPal theme */
2 |
3 | .regex {color: #000; font-family: Consolas, "Source Code Pro", Monospace; white-space: pre-wrap; word-break: break-all; overflow-wrap: anywhere;}
4 | .regex b {font-weight: normal;}
5 | .regex i {font-style: normal;}
6 | .regex u {text-decoration: none;}
7 |
8 | /* metasequence */
9 | .regex b {background: #aad1f7;}
10 | /* error */
11 | .regex b.err {background: #ff4300;}
12 |
13 | /* char class */
14 | .regex i {background: #f9ca69;}
15 | /* char class: metasequence */
16 | .regex i b {background: #f7a700;}
17 | /* char class: range-hyphen */
18 | .regex i u {background: #efba4a;}
19 |
20 | /* group: depth */
21 | .regex b.g1 {background: #d2f854;}
22 | .regex b.g2 {background: #9ec70c;}
23 | .regex b.g3 {background: #ecc9f7;}
24 | .regex b.g4 {background: #54b70b;}
25 | .regex b.g5 {background: #b688cf;}
26 |
--------------------------------------------------------------------------------