\\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(/^\?(?::|[!=])/)
236 | const s = match && match[0]
237 | if (s === '?:') {
238 | token.l = 3
239 | token.type = 'noncapgroup'
240 | } else if (s) {
241 | token.behind = s[1] === '<'
242 | token.negative = s[1 + token.behind] === '!'
243 | token.clss = 'lookaround'
244 | token.type = (token.negative ? 'neg' : 'pos') + 'look' + (token.behind ? 'behind' : 'ahead')
245 | token.l = s.length + 1
246 | if (token.behind) {
247 | token.err = 'lookbehind'
248 | } // not supported in JS
249 | } else {
250 | token.type = 'group'
251 | token.capture = true
252 | }
253 | return token
254 | }
255 |
256 | private static parseEsc = (str, token, charset, capgroups, closeIndex) => {
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 |