├── .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 |

76 | ``` 77 | 78 | ## 👗 Themes 79 | 80 | Several themes are available as stylesheets, but you don't need to add a stylesheet to your page to use the default theme. Just run `loadStyles()`. 81 | 82 | ## 🏷️ About 83 | 84 | Regex Colorizer was created by [Steven Levithan](https://github.com/slevithan). It started in 2007 as part of [RegexPal](https://stevenlevithan.com/regexpal/), the first web-based regex tester with regex syntax highlighting. It was extracted into a standalone library in 2010. 85 | 86 | If you want to support this project, I'd love your help by contributing improvements, sharing it with others, or [sponsoring](https://github.com/sponsors/slevithan) ongoing development. 87 | 88 | © 2007–present. MIT License. 89 | 90 | 91 | 92 | [npm-version-src]: https://img.shields.io/npm/v/regex-colorizer?color=78C372 93 | [npm-version-href]: https://npmjs.com/package/regex-colorizer 94 | [bundle-src]: https://img.shields.io/bundlejs/size/regex-colorizer?color=78C372&label=minzip 95 | [bundle-href]: https://bundlejs.com/?q=regex-colorizer&treeshake=[*] 96 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Regex Colorizer 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 |

18 | Regex Colorizer 19 | 20 | GitHub 21 | 22 |

23 | 27 |

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.

29 | 30 |

Examples

31 |

SSN, excluding invalid ranges

32 |
^(?!000|666)(?:[0-6]\d{2}|7(?:[0-6]\d|7[012]))-(?!00)\d{2}-(?!0000)\d{4}$
33 |

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 | 59 | 60 | 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.

66 |
Oh h\i+?? x*+ |? a{1,2}b{2,1} ||(?:a|b)* (?<=(?<name>x))* (?>n)
 67 | ((((?:((a))b.c)d|x(y){65536,}))))
 68 | [^1-59-6\b-\cX.a-\w!---] \xFF \x \uFF\uFFFF\z\v\1\\\
69 |

Octals and backreferences

70 |

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 | 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">(?&lt;=\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">(?&lt;=\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: !/^\(\?${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 | --------------------------------------------------------------------------------