├── .env ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public ├── CNAME ├── favicon.ico ├── index.html └── manifest.json ├── src ├── App.css ├── App.test.tsx ├── App.tsx ├── CodeFormatter.css ├── CodeFormatter.tsx ├── Docs.ts ├── ExpressionHighlighter.ts ├── RegExLexer.ts ├── RegexInput.css ├── RegexInput.tsx ├── Results │ ├── Exec.tsx │ ├── Match.tsx │ ├── Replace.tsx │ ├── Search.tsx │ ├── Test.tsx │ ├── index.css │ └── index.tsx ├── ShareButtons.tsx ├── StringInput.css ├── StringInput.tsx ├── index.css ├── index.tsx └── registerServiceWorker.ts ├── tsconfig.json ├── tsconfig.prod.json ├── tsconfig.test.json └── tslint.json /.env: -------------------------------------------------------------------------------- 1 | # This env file has certain properties that must be overridden because they are unique to each individual, use untracked .env.local file 2 | 3 | # needs override 4 | REACT_APP_GA_ID='' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env.local 59 | 60 | # See https://help.github.com/ignore-files/ for more about ignoring files. 61 | 62 | # dependencies 63 | /node_modules 64 | 65 | # testing 66 | /coverage 67 | 68 | # production 69 | /build 70 | 71 | # misc 72 | .DS_Store 73 | .env.local 74 | .env.development.local 75 | .env.test.local 76 | .env.production.local 77 | 78 | npm-debug.log* 79 | yarn-debug.log* 80 | yarn-error.log* 81 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 chipto 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 | # Regexly 2 | 3 | #### *TL;DR* 4 | > A WYSIWYG Regex Playground for JavaScript Developers — [*JavaScript Daily*](https://twitter.com/JavaScriptDaily/status/919892692657680384) 5 | 6 | ___ 7 | 8 | > Anything that makes writing regexes for my JavaScript stuff easier is 😎 — [*Kent C. Dodds*](https://twitter.com/kentcdodds/status/907256355978756096) 9 | 10 | ___ 11 | 12 | > Pretty sweet little open source React app to help you learn Regexes — [*WesBos*](https://twitter.com/wesbos/status/907338038187106304) 13 | 14 | ___ 15 | 16 | > If you write #JavaScript and Regular Expressions, then Regexly is great! — [*Elijah Manor*](https://twitter.com/elijahmanor/status/943256724403781633) 17 | 18 | # License 19 | 20 | Licensed under MIT license. You are free to do whatever you want with source code in this repository. 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "regexly", 3 | "description": "WYSIWYG Regex playground for those who JavaScript", 4 | "version": "0.2.0", 5 | "private": true, 6 | "homepage": "https://regexly.js.org", 7 | "dependencies": { 8 | "@types/codemirror": "^0.0.46", 9 | "@types/jest": "^20.0.2", 10 | "@types/node": "^8.0.4", 11 | "@types/react": "^16.8.2", 12 | "@types/react-dom": "^16.8.0", 13 | "@types/react-ga": "^2.1.1", 14 | "classnames": "^2.2.5", 15 | "codemirror": "^5.28.0", 16 | "react": "^16.8.1", 17 | "react-dom": "^16.8.1", 18 | "react-ga": "^2.2.0", 19 | "react-share": "git+https://github.com/neeksandhu/react-share.git" 20 | }, 21 | "scripts": { 22 | "predeploy": "npm run build", 23 | "deploy": "gh-pages -d build", 24 | "start": "npm run update_version react-scripts-ts start", 25 | "build": "npm run update_version react-scripts-ts build", 26 | "test": "react-scripts-ts test --env=jsdom", 27 | "eject": "react-scripts-ts eject", 28 | "update_version": "cross-env REACT_APP_VERSION=$npm_package_version" 29 | }, 30 | "devDependencies": { 31 | "cross-env": "^5.0.5", 32 | "gh-pages": "^1.0.0", 33 | "react-scripts-ts": "^2.6.0", 34 | "tslint": "^5.12.1", 35 | "typescript": "^3.3.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/CNAME: -------------------------------------------------------------------------------- 1 | regexly.js.org -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zikaari/regexly/25e9f84f3373fce35741d176416f034a07b6e1c5/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 13 | 14 | 15 | 24 | Regexly | Chipto 25 | 26 | 27 | 28 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Regexly", 3 | "name": "WYSIWYG Regex Editor For JS", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | } 10 | ], 11 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | ::-webkit-scrollbar { 2 | width: 8px; 3 | height: 8px; 4 | background: transparent; 5 | } 6 | ::-webkit-scrollbar-corner { 7 | background: transparent; 8 | } 9 | ::-webkit-scrollbar-track { 10 | background: transparent; 11 | } 12 | 13 | ::-webkit-scrollbar-thumb { 14 | background: #636363; 15 | } 16 | 17 | @media (max-width:750px) { 18 | .code.var-type { 19 | display: none; 20 | } 21 | } 22 | .wrapper { 23 | height: 100%; 24 | display: flex; 25 | flex-direction: column; 26 | /*background: black;*/ 27 | } 28 | 29 | .wrapper header { 30 | padding: 10px; 31 | } 32 | 33 | .wrapper main { 34 | overflow: auto; 35 | width: 100%; 36 | flex-grow: 1; 37 | } 38 | 39 | .wrapper main > div { 40 | padding: 30px; 41 | max-width: 900px; 42 | margin: auto; 43 | width: 100%; 44 | display: flex; 45 | flex-direction: column; 46 | } 47 | 48 | .wrapper footer { 49 | background: #333940; 50 | color: #a5a5a5; 51 | padding: 6px 14px; 52 | font-size: 14px; 53 | font-family: sans-serif; 54 | display: flex; 55 | align-items: center; 56 | } 57 | 58 | .wrapper footer a { 59 | color: inherit; 60 | margin-left: auto; 61 | } 62 | 63 | .wrapper .buttons button { 64 | border: 0; 65 | font-size: 15px; 66 | background: transparent; 67 | color: darkgrey; 68 | font-family: 'Segoe UI'; 69 | cursor: pointer; 70 | text-decoration: underline; 71 | outline: none; 72 | } 73 | 74 | .input-fields label { 75 | display: flex; 76 | margin-bottom: 24px; 77 | } 78 | 79 | .input-fields label>textarea, 80 | input { 81 | background: #1e2227; 82 | color: #e1e1e1; 83 | outline-color: #00826a; 84 | border: none; 85 | font-family: monospace; 86 | } -------------------------------------------------------------------------------- /src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import * as ReactDOM from 'react-dom' 3 | import App from './App' 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div') 7 | ReactDOM.render(, div) 8 | }) 9 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react' 2 | import './App.css' 3 | 4 | import { tokenizeDeclaration } from './CodeFormatter' 5 | import RegexInput from './RegexInput' 6 | import Results from './Results' 7 | import StringInput from './StringInput' 8 | 9 | import { version } from '../package.json' 10 | 11 | interface IAppState { 12 | regex: RegExp 13 | str: string 14 | } 15 | class App extends React.Component<{}, IAppState> { 16 | constructor(props) { 17 | super(props) 18 | const { regex = /(https?)/g, str = 'https://github.com/' } = this.parseURI() 19 | setTimeout(() => { 20 | this.clearPermanlink() 21 | }, 1000) 22 | this.state = { 23 | regex, 24 | str, 25 | } 26 | } 27 | render() { 28 | const { regex, str } = this.state 29 | return ( 30 |
31 |
32 |
33 |
34 | 38 | 42 |
43 |
44 | 45 |
46 |
47 | 51 |
52 | ) 53 | } 54 | 55 | handleRegexInputOnChange = (text: string) => { 56 | this.setState({ 57 | regex: this.constructRegExp(text), 58 | }) 59 | } 60 | 61 | constructRegExp(rawStr: string): RegExp | null { 62 | let regex = null 63 | if (typeof rawStr === 'string') { 64 | const regexFirstSlashIndex = rawStr.indexOf('/') 65 | const regexLastSlashIndex = rawStr.lastIndexOf('/') 66 | let flags = '' 67 | if ((rawStr.length - regexLastSlashIndex) > 6 || regexFirstSlashIndex !== 0) { 68 | return null 69 | } 70 | flags = rawStr.substr(regexLastSlashIndex + 1) 71 | if (/[^gmiuy]/.test(flags)) { 72 | return null 73 | } 74 | const regexstr = rawStr.substr(regexFirstSlashIndex + 1, regexLastSlashIndex - 1) 75 | try { 76 | regex = RegExp(regexstr, flags) 77 | } catch (error) { 78 | 79 | } 80 | if (regex !== null && regex.toString() !== rawStr) { 81 | return null 82 | } 83 | } 84 | return regex 85 | } 86 | 87 | handleStringInputOnChange = (value: string) => { 88 | if (typeof value === 'string') { 89 | this.setState({ 90 | str: value, 91 | }) 92 | } 93 | } 94 | 95 | private parseURI = () => { 96 | const match = window.location.search.match(/(\w+)=[^&]+/g) 97 | let regex 98 | let str 99 | if (Array.isArray(match)) { 100 | match.forEach(pair => { 101 | const [key, value] = pair.split('=') 102 | if (key === 'regexp') { 103 | const rawStr = decodeURI(value) 104 | regex = this.constructRegExp(rawStr) 105 | } 106 | if (key === 'string') { 107 | str = decodeURI(value) 108 | } 109 | }) 110 | } 111 | return { regex, str } 112 | } 113 | private clearPermanlink() { 114 | history.replaceState({}, '', '/') 115 | } 116 | private generatePermalink = () => { 117 | const { regex, str } = this.state 118 | if (!(regex instanceof RegExp) || typeof str !== 'string') { 119 | return 120 | } 121 | history.replaceState({}, '', `/?regexp=${encodeURI(this.state.regex.toString())}&string=${encodeURI(this.state.str)}`) 122 | } 123 | } 124 | 125 | export default App 126 | -------------------------------------------------------------------------------- /src/CodeFormatter.css: -------------------------------------------------------------------------------- 1 | .code { 2 | font-family: monospace; 3 | font-size: 1.4em; 4 | color: #FFF; 5 | } 6 | 7 | .code.protofn-call .code { 8 | font-size: 1em; 9 | } 10 | 11 | .code.obj { 12 | color: #8f8fd7; 13 | } 14 | 15 | .code.protofn {} 16 | 17 | .code.protofn-opening-bracket {} 18 | 19 | .code.protofn-arg { 20 | color: #ff6666; 21 | } 22 | 23 | .code.protofn-closing-bracket {} 24 | 25 | .code.var-declartion .code { 26 | margin: 0 4px; 27 | } 28 | 29 | .code.var-type { 30 | color: #00b8ff; 31 | } 32 | 33 | .code.var-identifier { 34 | color: #00c89a; 35 | } 36 | 37 | .code.assignment-operator {} 38 | 39 | ul.array.value-type:before { 40 | content: '['; 41 | color: #9e9e9e; 42 | /* font-style: italic; */ 43 | font-size: 13px; 44 | } 45 | 46 | ul.array.value-type:after { 47 | content: ']'; 48 | color: #9e9e9e; 49 | font-size: 13px; 50 | /* font-style: italic; */ 51 | } 52 | 53 | li.code.key-value { 54 | font-size: 1em; 55 | padding-left: 24px; 56 | } 57 | 58 | li.code.key-value span:first-child { 59 | color: #ff009e; 60 | } 61 | 62 | li.code.key-value span[data-value-type=nill] { 63 | color: #a2a2a2; 64 | } 65 | 66 | li.code.key-value span[data-value-type=number] { 67 | color: #00c9ff; 68 | } 69 | 70 | li.code.key-value span[data-value-type=string] { 71 | color: orange; 72 | } 73 | 74 | li.code.key-value span[data-value-type=string]:before { 75 | content: '"'; 76 | /*color: #ff5656;*/ 77 | } 78 | 79 | li.code.key-value span[data-value-type=string]:after { 80 | content: '"'; 81 | /*color: #ff5656;*/ 82 | } 83 | 84 | ul.array.value-type { 85 | list-style: none; 86 | display: inline; 87 | padding: 0; 88 | } 89 | 90 | .value-type.boolean { 91 | color: #31baff; 92 | } 93 | 94 | .value-type.string { 95 | color: orange; 96 | } 97 | 98 | .value-type.nill { 99 | color: #a2a2a2; 100 | } 101 | 102 | .value-type.string:before { 103 | content: '"'; 104 | } 105 | 106 | .value-type.string:after { 107 | content: '"'; 108 | } 109 | 110 | .value-type.number { 111 | color: #00c9ff; 112 | } 113 | 114 | li.code.key-value span:last-child { 115 | margin-left: 4px; 116 | } -------------------------------------------------------------------------------- /src/CodeFormatter.tsx: -------------------------------------------------------------------------------- 1 | import * as classNames from 'classnames' 2 | import * as React from 'react' 3 | import './CodeFormatter.css' 4 | 5 | export const tokenizeFnCall = (code: string, jsx?: JSX.Element) => { 6 | const match = code.match(/(\w+)(\.\w+)(\()(\w+)(\))/) 7 | if (match) { 8 | const [_, obj, fn, openingBracket, arg, closingBracket] = match 9 | return ( 10 | 11 | {obj} 12 | {fn} 13 | {openingBracket} 14 | {arg} 15 | {closingBracket} 16 | 17 | ) 18 | } 19 | return null 20 | } 21 | 22 | export const tokenizeDeclaration = (code: string) => { 23 | const match = code.match(/(\w+)\s+(\w+)\s(=)/) 24 | if (match) { 25 | const [_, type, identifier, assignmentOperator] = match 26 | return ( 27 | 28 | {type} 29 | {identifier} 30 | {assignmentOperator} 31 | 32 | ) 33 | } 34 | return null 35 | } 36 | 37 | export const objFormat = (obj: any) => { 38 | const isNodeArray = (Array.isArray(obj)) 39 | const nodes = [] 40 | // tslint:disable-next-line:forin 41 | for (const key in obj) { 42 | const value = obj[key] 43 | nodes.push( 44 |
  • 45 | {key} 46 | : 47 | { 48 | (value === null) ? 'null' : 49 | (typeof value === 'undefined') ? 'undefined' : 50 | (typeof value === 'object') ? objFormat(value) : value} 51 | 52 |
  • , 53 | ) 54 | } 55 | return ( 56 | 59 | ) 60 | } 61 | -------------------------------------------------------------------------------- /src/Docs.ts: -------------------------------------------------------------------------------- 1 | export const Docs = [ 2 | { 3 | id: 'dot', 4 | desc: 'Matches any character except line breaks.', 5 | ext: ' Equivalent to [^\\n\\r].', 6 | example: [ 7 | '.', 8 | 'glib jocks vex dwarves!', 9 | ], 10 | token: '.', 11 | }, 12 | { 13 | id: 'word', 14 | desc: 'Matches any word character (alphanumeric & underscore).', 15 | ext: ' Only matches low-ascii characters (no accented or non-roman characters). Equivalent to [A-Za-z0-9_]', 16 | example: [ 17 | '\\w', 18 | 'bonjour, mon frère', 19 | ], 20 | token: '\\w', 21 | }, 22 | { 23 | id: 'notword', 24 | label: 'not word', 25 | desc: 'Matches any character that is not a word character (alphanumeric & underscore).', 26 | ext: ' Equivalent to [^A-Za-z0-9_]', 27 | example: [ 28 | '\\W', 29 | 'bonjour, mon frère', 30 | ], 31 | token: '\\W', 32 | }, 33 | { 34 | id: 'digit', 35 | desc: 'Matches any digit character (0-9).', 36 | ext: ' Equivalent to [0-9].', 37 | example: [ 38 | '\\d', 39 | '+1-(444)-555-1234', 40 | ], 41 | token: '\\d', 42 | }, 43 | { 44 | id: 'notdigit', 45 | label: 'not digit', 46 | desc: 'Matches any character that is not a digit character (0-9).', 47 | ext: ' Equivalent to [^0-9].', 48 | example: [ 49 | '\\D', 50 | '+1-(444)-555-1234', 51 | ], 52 | token: '\\D', 53 | }, 54 | { 55 | id: 'whitespace', 56 | desc: 'Matches any whitespace character (spaces, tabs, line breaks).', 57 | example: [ 58 | '\\s', 59 | 'glib jocks vex dwarves!', 60 | ], 61 | token: '\\s', 62 | }, 63 | { 64 | id: 'notwhitespace', 65 | label: 'not whitespace', 66 | desc: 'Matches any character that is not a whitespace character (spaces, tabs, line breaks).', 67 | example: [ 68 | '\\S', 69 | 'glib jocks vex dwarves!', 70 | ], 71 | token: '\\S', 72 | }, 73 | { 74 | id: 'set', 75 | label: 'character set', 76 | desc: 'Match any character in the set.', 77 | example: [ 78 | '[aeiou]', 79 | 'glib jocks vex dwarves!', 80 | ], 81 | token: '[ABC]', 82 | }, 83 | { 84 | id: 'setnot', 85 | label: 'negated set', 86 | desc: 'Match any character that is not in the set.', 87 | example: [ 88 | '[^aeiou]', 89 | 'glib jocks vex dwarves!', 90 | ], 91 | token: '[^ABC]', 92 | }, 93 | { 94 | id: 'range', 95 | tip: 'Matches a character in the range {{getChar(prev)}} to {{getChar(next)}} (char code {{prev.code}} to {{next.code}}).', 96 | example: [ 97 | '[g-s]', 98 | 'abcdefghijklmnopqrstuvwxyz', 99 | ], 100 | desc: 'Matches a character having a character code between the two specified characters inclusive.', 101 | token: '[A-Z]', 102 | }, 103 | { 104 | id: 'bof', 105 | label: 'beginning', 106 | desc: 'Matches the beginning of the string, or the beginning of a line if the multiline flag (m) is enabled.', 107 | ext: ' This matches a position, not a character.', 108 | example: [ 109 | '^\\w+', 110 | 'she sells seashells', 111 | ], 112 | token: '^', 113 | }, 114 | { 115 | id: 'eof', 116 | label: 'end', 117 | desc: 'Matches the end of the string, or the end of a line if the multiline flag (m) is enabled.', 118 | ext: ' This matches a position, not a character.', 119 | example: [ 120 | '\\w+$', 121 | 'she sells seashells', 122 | ], 123 | token: '$', 124 | }, 125 | { 126 | id: 'wordboundary', 127 | label: 'word boundary', 128 | desc: 'Matches a word boundary position such as whitespace, punctuation, or the start/end of the string.', 129 | ext: ' This matches a position, not a character.', 130 | example: [ 131 | 's\\b', 132 | 'she sells seashells', 133 | ], 134 | token: '\\b', 135 | }, 136 | { 137 | id: 'notwordboundary', 138 | label: 'not word boundary', 139 | desc: 'Matches any position that is not a word boundary.', 140 | ext: ' This matches a position, not a character.', 141 | example: [ 142 | 's\\B', 143 | 'she sells seashells', 144 | ], 145 | token: '\\B', 146 | }, 147 | { 148 | id: 'escoctal', 149 | label: 'octal escape', 150 | desc: 'Octal escaped character in the form \\000.', 151 | ext: ' Value must be less than 255 (\\377).', 152 | example: [ 153 | '\\251', 154 | 'RegExr is ©2014', 155 | ], 156 | token: '\\000', 157 | }, 158 | { 159 | id: 'eschexadecimal', 160 | label: 'hexadecimal escape', 161 | desc: 'Hexadecimal escaped character in the form \\xFF.', 162 | example: [ 163 | '\\xA9', 164 | 'RegExr is ©2014', 165 | ], 166 | token: '\\xFF', 167 | }, 168 | { 169 | id: 'escunicode', 170 | label: 'unicode escape', 171 | desc: 'Unicode escaped character in the form \\uFFFF.', 172 | example: [ 173 | '\\u00A9', 174 | 'RegExr is ©2014', 175 | ], 176 | token: '\\uFFFF', 177 | }, 178 | { 179 | id: 'esccontrolchar', 180 | label: 'control character escape', 181 | desc: 'Escaped control character in the form \\cZ.', 182 | ext: ' This can range from \\cA (NULL, char code 0) to \\cZ (EM, char code 25).

    Example:

    \\cI matches TAB (char code 9).', 183 | token: '\\cI', 184 | }, 185 | { 186 | id: 'group', 187 | label: 'capturing group', 188 | desc: 'Groups multiple tokens together and creates a capture group for extracting a substring or using a backreference.', 189 | example: [ 190 | '(ha)+', 191 | 'hahaha haa hah!', 192 | ], 193 | token: '(ABC)', 194 | }, 195 | { 196 | id: 'backref', 197 | label: 'backreference', 198 | tip: 'Matches the results of capture group #{{group.num}}.', 199 | desc: 'Matches the results of a previous capture group. For example \\1 matches the results of the first capture group & \\3 matches the third.', 200 | example: [ 201 | '(\\w)a\\1', 202 | 'hah dad bad dab gag gab', 203 | ], 204 | token: '\\1', 205 | }, 206 | { 207 | id: 'noncapgroup', 208 | label: 'non-capturing group', 209 | desc: 'Groups multiple tokens together without creating a capture group.', 210 | example: [ 211 | '(?:ha)+', 212 | 'hahaha haa hah!', 213 | ], 214 | token: '(?:ABC)', 215 | }, 216 | { 217 | id: 'poslookahead', 218 | label: 'positive lookahead', 219 | desc: 'Matches a group after the main expression without including it in the result.', 220 | example: [ 221 | '\\d(?=px)', 222 | '1pt 2px 3em 4px', 223 | ], 224 | token: '(?=ABC)', 225 | }, 226 | { 227 | id: 'neglookahead', 228 | label: 'negative lookahead', 229 | desc: 'Specifies a group that can not match after the main expression (if it matches, the result is discarded).', 230 | example: [ 231 | '\\d(?!px)', 232 | '1pt 2px 3em 4px', 233 | ], 234 | token: '(?!ABC)', 235 | }, 236 | { 237 | id: 'poslookbehind', 238 | label: 'positive lookbehind*', 239 | desc: '*Not supported in JavaScript. Matches a group before the main expression without including it in the result.', 240 | token: '(?<=ABC)', 241 | }, 242 | { 243 | id: 'neglookbehind', 244 | label: 'negative lookbehind*', 245 | desc: '*Not supported in JavaScript. Specifies a group that can not match before the main expression (if it matches, the result is discarded).', 246 | token: '(?<!ABC)', 247 | }, 248 | { 249 | id: 'plus', 250 | desc: 'Matches 1 or more of the preceding token.', 251 | example: [ 252 | 'b\\w+', 253 | 'b be bee beer beers', 254 | ], 255 | token: '+', 256 | }, 257 | { 258 | id: 'star', 259 | desc: 'Matches 0 or more of the preceding token.', 260 | example: [ 261 | 'b\\w*', 262 | 'b be bee beer beers', 263 | ], 264 | token: '*', 265 | }, 266 | { 267 | id: 'quant', 268 | label: 'quantifier', 269 | desc: 'Matches the specified quantity of the previous token. {1,3} will match 1 to 3. {3} will match exactly 3. {3,} will match 3 or more. ', 270 | example: [ 271 | 'b\\w{2,3}', 272 | 'b be bee beer beers', 273 | ], 274 | token: '{1,3}', 275 | }, 276 | { 277 | id: 'opt', 278 | label: 'optional', 279 | desc: 'Matches 0 or 1 of the preceding token, effectively making it optional.', 280 | example: [ 281 | 'colou?r', 282 | 'color colour', 283 | ], 284 | token: '?', 285 | }, 286 | { 287 | id: 'lazy', 288 | desc: 'Makes the preceding quantifier lazy, causing it to match as few characters as possible.', 289 | ext: ' By default, quantifiers are greedy, and will match as many characters as possible.', 290 | example: [ 291 | 'b\\w+?', 292 | 'b be bee beer beers', 293 | ], 294 | token: '?', 295 | }, 296 | { 297 | id: 'alt', 298 | label: 'alternation', 299 | desc: 'Acts like a boolean OR. Matches the expression before or after the |.', 300 | ext: '

    It can operate within a group, or on a whole expression. The patterns will be tested in order.

    ', 301 | example: [ 302 | 'b(a|e|i)d', 303 | 'bad bud bod bed bid', 304 | ], 305 | token: '|', 306 | }, 307 | { 308 | id: 'subst_match', 309 | label: 'match', 310 | desc: 'Inserts the matched text.', 311 | token: '$$&', 312 | }, 313 | { 314 | id: 'subst_num', 315 | label: 'capture group', 316 | tip: 'Inserts the results of capture group #{{group.num}}.', 317 | desc: 'Inserts the results of the specified capture group (ex. $3 will insert the third capture group).', 318 | token: '$1', 319 | }, 320 | { 321 | id: 'subst_pre', 322 | label: 'before match', 323 | desc: 'Inserts the portion of the source string that precedes the match.', 324 | token: '$$`', 325 | }, 326 | { 327 | id: 'subst_post', 328 | label: 'after match', 329 | desc: 'Inserts the portion of the source string that follows the match.', 330 | token: '$$\'', 331 | }, 332 | { 333 | id: 'subst_$', 334 | label: 'escaped $', 335 | desc: 'Inserts a dollar sign character ($).', 336 | token: '$$$$', 337 | }, 338 | { 339 | id: 'flag_i', 340 | label: 'ignore case', 341 | desc: 'Makes the whole expression case-insensitive.', 342 | ext: ' For example, /aBc/i would match AbC.', 343 | token: 'i', 344 | }, 345 | { 346 | id: 'flag_g', 347 | label: 'global search', 348 | tip: 'Retain the index of the last match, allowing iterative searches.', 349 | desc: 'Retain the index of the last match, allowing subsequent searches to start from the end of the previous match.

    Without the global flag, subsequent searches will return the same match.


    RegExr only searches for a single match when the global flag is disabled to avoid infinite match errors.', 350 | token: 'g', 351 | }, 352 | { 353 | id: 'flag_m', 354 | label: 'multiline', 355 | tip: 'Beginning/end anchors (^/$) will match the start/end of a line.', 356 | desc: 'When the multiline flag is enabled, beginning and end anchors (^ and $) will match the start and end of a line, instead of the start and end of the whole string.

    Note that patterns such as /^[\\s\\S]+$/m may return matches that span multiple lines because the anchors will match the start/end of any line.

    ', 357 | token: 'm', 358 | }, 359 | ] 360 | -------------------------------------------------------------------------------- /src/ExpressionHighlighter.ts: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014 gskinner.com, inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | class ExpressionHighlighter { 26 | 27 | static readonly GROUP_CLASS_BY_TYPE = { 28 | set: 'exp-group-set', 29 | setnot: 'exp-group-set', 30 | group: 'exp-group-%depth%', 31 | lookaround: 'exp-group-%depth%', 32 | } 33 | private prefix: string 34 | private selectedMarks: any[] 35 | private activeMarks: any[] 36 | private offset: number 37 | private cm: CodeMirror.Editor 38 | 39 | constructor(cm: CodeMirror.Editor, offset?: number) { 40 | this.cm = cm 41 | this.offset = offset || 0 42 | this.activeMarks = [] 43 | this.selectedMarks = [] 44 | this.prefix = 'exp-' 45 | } 46 | 47 | public draw = function(token) { 48 | const cm = this.cm 49 | const pre = this.prefix 50 | 51 | this.clear() 52 | cm.operation(() => { 53 | 54 | const groupClasses = ExpressionHighlighter.GROUP_CLASS_BY_TYPE 55 | const doc = cm.getDoc() 56 | const marks = this.activeMarks 57 | let endToken 58 | 59 | while (token) { 60 | if (token.clear) { 61 | token = token.next 62 | continue 63 | } 64 | token = this._calcTokenPos(doc, token) 65 | 66 | let className = pre + (token.clss || token.type) 67 | if (token.err) { 68 | className += ' ' + pre + 'error' 69 | } 70 | 71 | if (className) { 72 | marks.push(doc.markText(token.startPos, token.endPos, { className })) 73 | } 74 | 75 | if (token.close) { 76 | endToken = this._calcTokenPos(doc, token.close) 77 | className = groupClasses[token.clss || token.type] 78 | if (className) { 79 | className = className.replace('%depth%', token.depth) 80 | marks.push(doc.markText(token.startPos, endToken.endPos, { className })) 81 | } 82 | } 83 | token = token.next 84 | } 85 | }) 86 | 87 | } 88 | 89 | private clear = function() { 90 | this.cm.operation(() => { 91 | const marks = this.activeMarks 92 | for (let i = 0, l = marks.length; i < l; i++) { 93 | marks[i].clear() 94 | } 95 | marks.length = 0 96 | }) 97 | } 98 | 99 | private selectToken = function(token) { 100 | if (token === this.selectedToken) { 101 | return 102 | } 103 | if (token && token.set && token.set.indexOf(this.selectedToken) !== -1) { 104 | return 105 | } 106 | while (this.selectedMarks.length) { 107 | this.selectedMarks.pop().clear() 108 | } 109 | this.selectedToken = token 110 | if (!token) { 111 | return 112 | } 113 | 114 | if (token.open) { 115 | this._drawSelect(token.open) 116 | } 117 | else { 118 | this._drawSelect(token) 119 | } 120 | if (token.related) { 121 | for (let i = 0; i < token.related.length; i++) { 122 | this._drawSelect(token.related[i], 'exp-related') 123 | } 124 | } 125 | } 126 | 127 | private _drawSelect = function(token, style) { 128 | let endToken = token.close || token 129 | if (token.set) { 130 | endToken = token.set[token.set.length - 1] 131 | token = token.set[0] 132 | } 133 | style = style || 'exp-selected' 134 | const doc = this.cm.getDoc() 135 | this._calcTokenPos(doc, endToken) 136 | this._calcTokenPos(doc, token) 137 | this.selectedMarks.push(doc.markText(token.startPos, endToken.endPos, { 138 | className: style, 139 | startStyle: style + '-left', 140 | endStyle: style + '-right', 141 | })) 142 | } 143 | 144 | private _calcTokenPos = function(doc, token) { 145 | if (token.startPos || token == null) { 146 | return token 147 | } 148 | token.startPos = doc.posFromIndex(token.i + this.offset) 149 | token.endPos = doc.posFromIndex(token.end + this.offset) 150 | return token 151 | } 152 | } 153 | 154 | export { ExpressionHighlighter } 155 | -------------------------------------------------------------------------------- /src/RegExLexer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2014 gskinner.com, inc. 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | 25 | class RegExLexer { 26 | // \ ^ $ . | ? * + ( ) [ { 27 | static readonly CHAR_TYPES = { 28 | '.': 'dot', 29 | '|': 'alt', 30 | '$': 'eof', 31 | '^': 'bof', 32 | '?': 'opt', // also: "lazy" 33 | '+': 'plus', 34 | '*': 'star', 35 | } 36 | 37 | static readonly ESC_CHARS_SPECIAL = { 38 | w: 'word', 39 | W: 'notword', 40 | d: 'digit', 41 | D: 'notdigit', 42 | s: 'whitespace', 43 | S: 'notwhitespace', 44 | b: 'wordboundary', 45 | B: 'notwordboundary', 46 | // u-uni, x-hex, c-ctrl, oct handled in parseEsc 47 | } 48 | 49 | static readonly UNQUANTIFIABLE = { 50 | quant: true, 51 | plus: true, 52 | star: true, 53 | opt: true, 54 | eof: true, 55 | bof: true, 56 | group: true, // group open 57 | lookaround: true, // lookaround open 58 | wordboundary: true, 59 | notwordboundary: true, 60 | lazy: true, 61 | alt: true, 62 | open: true, 63 | } 64 | 65 | static readonly ESC_CHAR_CODES = { 66 | 0: 0, // null 67 | t: 9, // tab 68 | n: 10, // lf 69 | v: 11, // vertical tab 70 | f: 12, // form feed 71 | r: 13, // cr 72 | } 73 | 74 | public static parse = (str): IToken => { 75 | if (str === RegExLexer.string) { 76 | return RegExLexer.token 77 | } 78 | 79 | RegExLexer.token = null 80 | RegExLexer.string = str 81 | RegExLexer.errors = [] 82 | const capgroups = RegExLexer.captureGroups = [] 83 | const groups = [] 84 | let i = 0 85 | const l = str.length 86 | let o 87 | let c 88 | let token 89 | let prev = null 90 | let charset = null 91 | const unquantifiable = RegExLexer.UNQUANTIFIABLE 92 | const charTypes = RegExLexer.CHAR_TYPES 93 | const closeIndex = str.lastIndexOf('/') 94 | 95 | while (i < l) { 96 | c = str[i] 97 | 98 | token = { i, l: 1, prev } 99 | 100 | if (i === 0 || i >= closeIndex) { 101 | RegExLexer.parseFlag(str, token) 102 | } else if (c === '(' && !charset) { 103 | RegExLexer.parseGroup(str, token) 104 | token.depth = groups.length 105 | groups.push(token) 106 | if (token.capture) { 107 | capgroups.push(token) 108 | token.num = capgroups.length 109 | } 110 | } else if (c === ')' && !charset) { 111 | token.type = 'groupclose' 112 | if (groups.length) { 113 | o = token.open = groups.pop() 114 | o.close = token 115 | } else { 116 | token.err = 'groupclose' 117 | } 118 | } else if (c === '[' && !charset) { 119 | token.type = token.clss = 'set' 120 | charset = token 121 | if (str[i + 1] === '^') { 122 | token.l++ 123 | token.type += 'not' 124 | } 125 | } else if (c === ']' && charset) { 126 | token.type = 'setclose' 127 | token.open = charset 128 | charset.close = token 129 | charset = null 130 | } else if ((c === '+' || c === '*') && !charset) { 131 | token.type = charTypes[c] 132 | token.clss = 'quant' 133 | token.min = (c === '+' ? 1 : 0) 134 | token.max = -1 135 | } else if (c === '{' && !charset && str.substr(i).search(/^{\d+,?\d*}/) !== -1) { 136 | RegExLexer.parseQuant(str, token) 137 | } else if (c === '\\') { 138 | RegExLexer.parseEsc(str, token, charset, capgroups, closeIndex) 139 | } else if (c === '?' && !charset) { 140 | if (!prev || prev.clss !== 'quant') { 141 | token.type = charTypes[c] 142 | token.clss = 'quant' 143 | token.min = 0 144 | token.max = 1 145 | } else { 146 | token.type = 'lazy' 147 | token.related = [prev] 148 | } 149 | } else if (c === '-' && charset && prev.code != null && prev.prev && prev.prev.type !== 'range') { 150 | // this may be the start of a range, but we'll need to validate after the next token. 151 | token.type = 'range' 152 | } else { 153 | RegExLexer.parseChar(str, token, charset) 154 | } 155 | 156 | if (prev) { 157 | prev.next = token 158 | } 159 | 160 | // post processing: 161 | if (token.clss === 'quant') { 162 | if (!prev || unquantifiable[prev.type]) { 163 | token.err = 'quanttarg' 164 | } 165 | else { 166 | token.related = [prev.open || prev] 167 | } 168 | } 169 | if (prev && prev.type === 'range' && prev.l === 1) { 170 | token = RegExLexer.validateRange(str, prev) 171 | } 172 | if (token.open && !token.clss) { 173 | token.clss = token.open.clss 174 | } 175 | 176 | if (!RegExLexer.token) { 177 | RegExLexer.token = token 178 | } 179 | i = token.end = token.i + token.l 180 | if (token.err) { 181 | RegExLexer.errors.push(token.err) 182 | } 183 | prev = token 184 | } 185 | 186 | while (groups.length) { 187 | RegExLexer.errors.push(groups.pop().err = 'groupopen') 188 | } 189 | if (charset) { 190 | RegExLexer.errors.push(charset.err = 'setopen') 191 | } 192 | 193 | return RegExLexer.token 194 | } 195 | 196 | private static string = null 197 | private static token = null 198 | private static errors = null 199 | private static captureGroups = null 200 | 201 | private static parseFlag = (str, token) => { 202 | // note that this doesn't deal with misformed patterns or incorrect flags. 203 | const i = token.i 204 | const c = str[i] 205 | if (str[i] === '/') { 206 | token.type = (i === 0) ? 'open' : 'close' 207 | if (i !== 0) { 208 | token.related = [RegExLexer.token] 209 | RegExLexer.token.related = [token] 210 | } 211 | } else { 212 | token.type = 'flag_' + c 213 | } 214 | token.clear = true 215 | } 216 | 217 | private static parseChar = (str, token, charset?) => { 218 | const c = str[token.i] 219 | token.type = (!charset && RegExLexer.CHAR_TYPES[c]) || 'char' 220 | if (!charset && c === '/') { 221 | token.err = 'fwdslash' 222 | } 223 | if (token.type === 'char') { 224 | token.code = c.charCodeAt(0) 225 | } else if (token.type === 'bof' || token.type === 'eof') { 226 | token.clss = 'anchor' 227 | } else if (token.type === 'dot') { 228 | token.clss = 'charclass' 229 | } 230 | return token 231 | } 232 | 233 | private static parseGroup = (str, token) => { 234 | token.clss = 'group' 235 | const match = str.substr(token.i + 1).match(/^\?(?::| { 257 | // jsMode tries to read escape chars as a JS string which is less permissive than JS RegExp, and doesn't support \c or backreferences, used for subst 258 | 259 | // Note: \8 & \9 are treated differently: IE & Chrome match "8", Safari & FF match "\8", we support the former case since Chrome & IE are dominant 260 | // Note: Chrome does weird things with \x & \u depending on a number of factors, we ignore RegExLexer. 261 | const i = token.i 262 | const jsMode = token.js 263 | let match 264 | let o 265 | let sub = str.substr(i + 1) 266 | const c = sub[0] 267 | if (i + 1 === (closeIndex || str.length)) { 268 | token.err = 'esccharopen' 269 | return 270 | } 271 | 272 | // tslint:disable-next-line:no-conditional-assignment 273 | if (!jsMode && !charset && (match = sub.match(/^\d\d?/)) && (o = capgroups[parseInt(match[0], 10) - 1])) { 274 | // back reference - only if there is a matching capture group 275 | token.type = 'backref' 276 | token.related = [o] 277 | token.group = o 278 | token.l += match[0].length 279 | return token 280 | } 281 | 282 | // tslint:disable-next-line:no-conditional-assignment 283 | if (match = sub.match(/^u[\da-fA-F]{4}/)) { 284 | // unicode: \uFFFF 285 | sub = match[0].substr(1) 286 | token.type = 'escunicode' 287 | token.l += 5 288 | token.code = parseInt(sub, 16) 289 | // tslint:disable-next-line:no-conditional-assignment 290 | } else if (match = sub.match(/^x[\da-fA-F]{2}/)) { 291 | // hex ascii: \xFF 292 | // \x{} not supported in JS regexp 293 | sub = match[0].substr(1) 294 | token.type = 'eschexadecimal' 295 | token.l += 3 296 | token.code = parseInt(sub, 16) 297 | // tslint:disable-next-line:no-conditional-assignment 298 | } else if (!jsMode && (match = sub.match(/^c[a-zA-Z]/))) { 299 | // control char: \cA \cz 300 | // not supported in JS strings 301 | sub = match[0].substr(1) 302 | token.type = 'esccontrolchar' 303 | token.l += 2 304 | const code = sub.toUpperCase().charCodeAt(0) - 64 // A=65 305 | if (code > 0) { 306 | token.code = code 307 | } 308 | // tslint:disable-next-line:no-conditional-assignment 309 | } else if (match = sub.match(/^[0-7]{1,3}/)) { 310 | // octal ascii 311 | sub = match[0] 312 | if (parseInt(sub, 8) > 255) { 313 | sub = sub.substr(0, 2) 314 | } 315 | token.type = 'escoctal' 316 | token.l += sub.length 317 | token.code = parseInt(sub, 8) 318 | } else if (!jsMode && c === 'c') { 319 | // control char without a code - strangely, this is decomposed into literals equivalent to "\\c" 320 | return RegExLexer.parseChar(str, token, charset) // this builds the "/" token 321 | } else { 322 | // single char 323 | token.l++ 324 | if (jsMode && (c === 'x' || c === 'u')) { 325 | token.err = 'esccharbad' 326 | } 327 | if (!jsMode) { 328 | token.type = RegExLexer.ESC_CHARS_SPECIAL[c] 329 | } 330 | 331 | if (token.type) { 332 | token.clss = (c.toLowerCase() === 'b') ? 'anchor' : 'charclass' 333 | return token 334 | } 335 | token.type = 'escchar' 336 | token.code = RegExLexer.ESC_CHAR_CODES[c] 337 | if (token.code == null) { 338 | token.code = c.charCodeAt(0) 339 | } 340 | } 341 | token.clss = 'esc' 342 | return token 343 | } 344 | 345 | private static parseQuant = (str, token) => { 346 | token.type = token.clss = 'quant' 347 | const i = token.i 348 | const end = str.indexOf('}', i + 1) 349 | token.l += end - i 350 | const arr = str.substring(i + 1, end).split(',') 351 | token.min = parseInt(arr[0], 10) 352 | token.max = (arr[1] == null) ? token.min : (arr[1] === '') ? -1 : parseInt(arr[1], 10) 353 | if (token.max !== -1 && token.min > token.max) { 354 | token.err = 'quantrev' 355 | } 356 | return token 357 | } 358 | 359 | private static validateRange = (str, token) => { 360 | const prev = token.prev 361 | const next = token.next 362 | if (prev.code == null || next.code == null) { 363 | // not a range, rewrite as a char: 364 | RegExLexer.parseChar(str, token) 365 | } else { 366 | token.clss = 'set' 367 | if (prev.code > next.code) { 368 | token.err = 'rangerev' 369 | } 370 | // preserve as separate tokens, but treat as one in the UI: 371 | next.proxy = prev.proxy = token 372 | token.set = [prev, token, next] 373 | } 374 | return next 375 | } 376 | } 377 | 378 | interface IToken { 379 | i: number 380 | l: number 381 | end: number 382 | next?: IToken 383 | prev: IToken 384 | type: TokenType 385 | } 386 | 387 | type TokenType = 'open' | 'dot' | 'word' | 'notword' | 'digit' | 'notdigit' | 'whitespace' | 'notwhitespace' | 'set' | 'setnot' | 'range' | 'bof' | 'eof' | 'wordboundary' | 'notwordboundary' | 'escoctal' | 'eschexadecimal' | 'escunicode' | 'esccontrolchar' | 'group' | 'backref' | 'noncapgroup' | 'poslookahead' | 'neglookahead' | 'poslookbehind' | 'neglookbehind' | 'plus' | 'star' | 'quant' | 'opt' | 'lazy' | 'alt' | 'subst_match' | 'subst_num' | 'subst_pre' | 'subst_post' | 'subst_$' | 'flag_i' | 'flag_g' | 'flag_m' 388 | 389 | export { RegExLexer } 390 | -------------------------------------------------------------------------------- /src/RegexInput.css: -------------------------------------------------------------------------------- 1 | .CodeMirror { 2 | font-size: 18px; 3 | height: auto; 4 | width: 100%; 5 | background: #1e2227; 6 | color: #dbdbdb; 7 | } 8 | 9 | .CodeMirror-cursor { 10 | border-left: 1px solid white; 11 | } 12 | 13 | .CodeMirror-focused { 14 | outline: 1px solid #00826a; 15 | } 16 | 17 | .CodeMirror-selected { 18 | background: #484848 !important; 19 | } 20 | 21 | .exp-char { 22 | color: #dbdbdb; 23 | } 24 | 25 | .exp-decorator { 26 | color: #b9babf; 27 | font-weight: 700 28 | } 29 | 30 | .exp-related { 31 | border-bottom: solid 1px rgba(0,0,0,.22); 32 | border-top: solid 1px rgba(0,0,0,.22); 33 | margin-bottom: -1px; 34 | margin-top: -1px 35 | } 36 | 37 | .exp-related-left { 38 | border-left: solid 1px rgba(0,0,0,.22); 39 | margin-left: -1px 40 | } 41 | 42 | .exp-related-right { 43 | border-right: solid 1px rgba(0,0,0,.22); 44 | margin-right: -1px 45 | } 46 | 47 | .exp-selected { 48 | border-top: solid 2px rgba(0,0,0,.33); 49 | border-bottom: solid 2px rgba(0,0,0,.33); 50 | margin-bottom: -2px; 51 | margin-top: -2px 52 | } 53 | 54 | .exp-selected-left { 55 | border-left: solid 2px rgba(0,0,0,.33); 56 | margin-left: -2px 57 | } 58 | 59 | .exp-selected-right { 60 | border-right: solid 2px rgba(0,0,0,.33); 61 | margin-right: -2px 62 | } 63 | 64 | .exp-error { 65 | border-bottom: solid 2px red 66 | } 67 | 68 | .exp-esc { 69 | color: #C0C 70 | } 71 | 72 | .exp-alt,.exp-lazy,.exp-quant { 73 | color: #35F 74 | } 75 | 76 | .exp-anchor { 77 | color: #930 78 | } 79 | 80 | .exp-backref,.exp-group,.exp-lookaround { 81 | color: #090 82 | } 83 | 84 | .exp-charclass,.exp-set,.exp-subst { 85 | color: #D70 86 | } 87 | 88 | .exp-group-0 { 89 | background: rgba(0,238,0,.11) 90 | } 91 | 92 | .exp-group-1 { 93 | background: rgba(0,238,0,.22) 94 | } 95 | 96 | .exp-group-2 { 97 | background: rgba(0,238,0,.33) 98 | } 99 | 100 | .exp-group-3 { 101 | background: rgba(0,238,0,.44) 102 | } 103 | 104 | .exp-group-set { 105 | background: rgba(255,246,0,.3) 106 | } -------------------------------------------------------------------------------- /src/RegexInput.tsx: -------------------------------------------------------------------------------- 1 | import * as CodeMirror from 'codemirror' 2 | import * as React from 'react' 3 | import { ExpressionHighlighter } from './ExpressionHighlighter' 4 | import { RegExLexer } from './RegExLexer' 5 | 6 | import 'codemirror/addon/edit/closebrackets' 7 | 8 | import 'codemirror/lib/codemirror.css' 9 | import './RegexInput.css' 10 | 11 | interface IRegexInputProps { 12 | onChange: (text: string) => void 13 | defaultValue: string 14 | } 15 | 16 | class RegexInput extends React.PureComponent { 17 | private cmEditor: CodeMirror.Editor 18 | private highligher: ExpressionHighlighter 19 | constructor(props) { 20 | super(props) 21 | } 22 | public render(): JSX.Element { 23 | return ( 24 |