@@ -2,7 +2,7 @@ import escapeAtom from "./escapeAtom.ts";
22
33const OCCURRENCES_REGEX = / \{ \d + , ? \d * \} / y;
44const NOT_NUMBERS_REGEX = / \D / g;
5- const ALTERNATION_TO_END_OF_INPUT = "|$(?![\\s\\S]))" ;
5+ const DISJUNCTION_TO_END_OF_INPUT = "|$(?![\\s\\S]))" ;
66const MAYBE_HAS_BACKREFERENCE = / \\ [ 1 - 9 ] | \\ k < / ;
77
88interface NumericBackreference {
@@ -26,12 +26,46 @@ const isBackreference = (part: Part): part is Backreference =>
2626const isNumericBackreference = ( part : Part ) : part is NumericBackreference =>
2727 isBackreference ( part ) && typeof part . ref === "number" ;
2828
29- function walk ( regex : RegExp ) : { parts : Part [ ] ; groupCount : number } {
29+ export type RegexFeature =
30+ | "patternCharacter"
31+ | "startAnchor"
32+ | "endAnchor"
33+ | "wordBoundary"
34+ | "nonWordBoundary"
35+ | "lookahead"
36+ | "negativeLookahead"
37+ | "lookbehind"
38+ | "negativeLookbehind"
39+ | "backreference"
40+ | "namedBackreference"
41+ | "namedGroup"
42+ | "capturingGroup"
43+ | "nonCapturingGroup"
44+ | "modifierGroup"
45+ | "modifierGroupWithRemoval"
46+ | "characterClass"
47+ | "nestedCharacterClass"
48+ | "classIntersection"
49+ | "classSubtraction"
50+ | "disjunction"
51+ | "quantifier"
52+ | "unicodePropertyEscape"
53+ | "characterClassEscape"
54+ | "controlEscape"
55+ | "controlLetterEscape"
56+ | "hexEscapeSequence"
57+ | "unicodeEscapeSequence"
58+ | "otherEscape" ;
59+
60+ function walk (
61+ regex : RegExp
62+ ) : { parts : Part [ ] ; groupCount : number ; features : Set < RegexFeature > } {
3063 const source = regex . source ;
3164 const isUnicode = regex . unicode || regex . unicodeSets ;
3265
3366 let i = 0 ;
3467 let groupCount = 0 ;
68+ const features = new Set < RegexFeature > ( ) ;
3569
3670 function extractSlice ( length : number ) : string {
3771 return source . slice ( i , ( i += length ) ) ;
@@ -41,7 +75,7 @@ function walk(regex: RegExp): { parts: Part[]; groupCount: number } {
4175 const result : Part [ ] = [ ] ;
4276
4377 function appendOptional ( length : number ) {
44- result . push ( "(?:" + extractSlice ( length ) + ALTERNATION_TO_END_OF_INPUT ) ;
78+ result . push ( "(?:" + extractSlice ( length ) + DISJUNCTION_TO_END_OF_INPUT ) ;
4579 }
4680
4781 function appendRaw ( length : number ) {
@@ -60,6 +94,7 @@ function walk(regex: RegExp): { parts: Part[]; groupCount: number } {
6094 case "\\" :
6195 switch ( source [ i + 1 ] ) {
6296 case "c" :
97+ features . add ( "controlLetterEscape" ) ;
6398 appendOptional ( 3 ) ;
6499 break ;
65100 case "k" : {
@@ -68,6 +103,7 @@ function walk(regex: RegExp): { parts: Part[]; groupCount: number } {
68103 if ( referenceEnd === - 1 ) {
69104 appendOptional ( 2 ) ;
70105 } else {
106+ features . add ( "namedBackreference" ) ;
71107 const start = i ;
72108 const ref = source . slice ( i + 3 , referenceEnd ) ;
73109 i = referenceEnd + 1 ;
@@ -76,6 +112,7 @@ function walk(regex: RegExp): { parts: Part[]; groupCount: number } {
76112 break ;
77113 }
78114 case "u" :
115+ features . add ( "unicodeEscapeSequence" ) ;
79116 if ( isUnicode && source [ i + 2 ] === "{" ) {
80117 appendOptional ( source . indexOf ( "}" , i ) - i + 1 ) ;
81118 } else {
@@ -85,14 +122,32 @@ function walk(regex: RegExp): { parts: Part[]; groupCount: number } {
85122 case "p" :
86123 case "P" :
87124 if ( isUnicode ) {
125+ features . add ( "unicodePropertyEscape" ) ;
88126 appendOptional ( source . indexOf ( "}" , i ) - i + 1 ) ;
89127 } else {
90128 appendOptional ( 2 ) ;
91129 }
92130 break ;
93131 case "x" :
132+ features . add ( "hexEscapeSequence" ) ;
94133 appendOptional ( 4 ) ;
95134 break ;
135+ case "b" :
136+ features . add ( "wordBoundary" ) ;
137+ appendOptional ( 2 ) ;
138+ break ;
139+ case "B" :
140+ features . add ( "nonWordBoundary" ) ;
141+ appendOptional ( 2 ) ;
142+ break ;
143+ case "f" :
144+ case "n" :
145+ case "r" :
146+ case "t" :
147+ case "v" :
148+ features . add ( "controlEscape" ) ;
149+ appendOptional ( 2 ) ;
150+ break ;
96151 case "1" :
97152 case "2" :
98153 case "3" :
@@ -102,6 +157,7 @@ function walk(regex: RegExp): { parts: Part[]; groupCount: number } {
102157 case "7" :
103158 case "8" :
104159 case "9" : {
160+ features . add ( "backreference" ) ;
105161 NOT_NUMBERS_REGEX . lastIndex = i + 1 ;
106162 const nextNonDigit = NOT_NUMBERS_REGEX . exec ( source ) ;
107163 const start = i ;
@@ -111,43 +167,82 @@ function walk(regex: RegExp): { parts: Part[]; groupCount: number } {
111167 result . push ( { ref, start, end } ) ;
112168 break ;
113169 }
170+ case "d" :
171+ case "D" :
172+ case "w" :
173+ case "W" :
174+ case "s" :
175+ case "S" :
176+ features . add ( "characterClassEscape" ) ;
177+ appendOptional ( 2 ) ;
178+ break ;
114179 default :
180+ features . add ( "otherEscape" ) ;
115181 appendOptional ( 2 ) ;
116182 break ;
117183 }
118184 break ;
119185 case "[" : {
186+ features . add ( "characterClass" ) ;
120187 let depth = 1 ,
121- j = i + 1 ;
188+ j = i + 1 ,
189+ previousSetOperatorCharacter : string | undefined ;
122190 while ( depth ) {
123- switch ( source [ j ] ) {
191+ const character = source [ j ] ;
192+ switch ( character ) {
124193 case "\\" :
125194 j += 2 ;
195+ previousSetOperatorCharacter = undefined ;
126196 continue ;
127197 case "[" :
128- if ( regex . unicodeSets ) depth ++ ;
198+ if ( regex . unicodeSets ) {
199+ features . add ( "nestedCharacterClass" ) ;
200+ depth ++ ;
201+ }
129202 break ;
130203 case "]" :
131204 depth -- ;
132205 break ;
206+ case "&" :
207+ if ( regex . unicodeSets && previousSetOperatorCharacter === "&" ) {
208+ features . add ( "classIntersection" ) ;
209+ }
210+ break ;
211+ case "-" :
212+ if ( regex . unicodeSets && previousSetOperatorCharacter === "-" ) {
213+ features . add ( "classSubtraction" ) ;
214+ }
215+ break ;
133216 }
217+ previousSetOperatorCharacter = character ;
134218 j ++ ;
135219 }
136220 appendOptional ( j - i ) ;
137221 break ;
138222 }
139- case "|" :
140223 case "^" :
224+ features . add ( "startAnchor" ) ;
225+ appendRaw ( 1 ) ;
226+ break ;
227+ case "$" :
228+ features . add ( "endAnchor" ) ;
229+ appendRaw ( 1 ) ;
230+ break ;
231+ case "|" :
232+ features . add ( "disjunction" ) ;
233+ appendRaw ( 1 ) ;
234+ break ;
141235 case "*" :
142236 case "+" :
143237 case "?" :
144- case "$" :
238+ features . add ( "quantifier" ) ;
145239 appendRaw ( 1 ) ;
146240 break ;
147241 case "{" : {
148242 OCCURRENCES_REGEX . lastIndex = i ;
149243 const regExpExecArray = OCCURRENCES_REGEX . exec ( source ) ;
150244 if ( regExpExecArray ) {
245+ features . add ( "quantifier" ) ;
151246 appendRaw ( regExpExecArray [ 0 ] . length ) ;
152247 } else {
153248 appendOptional ( 1 ) ;
@@ -158,11 +253,13 @@ function walk(regex: RegExp): { parts: Part[]; groupCount: number } {
158253 if ( source [ i + 1 ] == "?" ) {
159254 switch ( source [ i + 2 ] ) {
160255 case ":" :
256+ features . add ( "nonCapturingGroup" ) ;
161257 result . push ( "(?:" ) ;
162258 i += 3 ;
163- result . push ( ...process ( ) , ALTERNATION_TO_END_OF_INPUT ) ;
259+ result . push ( ...process ( ) , DISJUNCTION_TO_END_OF_INPUT ) ;
164260 break ;
165261 case "=" :
262+ features . add ( "lookahead" ) ;
166263 result . push ( "(?=" ) ;
167264 i += 3 ;
168265 result . push ( ...process ( ) , ")" ) ;
@@ -173,38 +270,53 @@ function walk(regex: RegExp): { parts: Part[]; groupCount: number } {
173270 case "m" : {
174271 const flagsStart = i + 2 ,
175272 colonIndex = source . indexOf ( ":" , flagsStart ) ;
176- result . push ( "(?" + source . slice ( flagsStart , colonIndex ) + ":" ) ;
273+ const modifiers = source . slice ( flagsStart , colonIndex ) ;
274+ features . add (
275+ modifiers . includes ( "-" )
276+ ? "modifierGroupWithRemoval"
277+ : "modifierGroup"
278+ ) ;
279+ result . push ( "(?" + modifiers + ":" ) ;
177280 i = colonIndex + 1 ;
178281 result . push ( ...process ( ) , ")" ) ;
179282 break ;
180283 }
181284 case "!" :
285+ features . add ( "negativeLookahead" ) ;
182286 appendRawGroup ( 3 ) ;
183287 break ;
184288 case "<" :
185289 switch ( source [ i + 3 ] ) {
186290 case "=" :
291+ features . add ( "lookbehind" ) ;
292+ appendRawGroup ( 4 ) ;
293+ break ;
187294 case "!" :
295+ features . add ( "negativeLookbehind" ) ;
188296 appendRawGroup ( 4 ) ;
189297 break ;
190298 default :
299+ features . add ( "namedGroup" ) ;
300+ features . add ( "capturingGroup" ) ;
191301 ++ groupCount ;
192302 appendRaw ( source . indexOf ( ">" , i ) - i + 1 ) ;
193- result . push ( ...process ( ) , ALTERNATION_TO_END_OF_INPUT ) ;
303+ result . push ( ...process ( ) , DISJUNCTION_TO_END_OF_INPUT ) ;
194304 break ;
195305 }
196306 break ;
197307 }
198308 } else {
309+ features . add ( "capturingGroup" ) ;
199310 ++ groupCount ;
200311 appendRaw ( 1 ) ;
201- result . push ( ...process ( ) , ALTERNATION_TO_END_OF_INPUT ) ;
312+ result . push ( ...process ( ) , DISJUNCTION_TO_END_OF_INPUT ) ;
202313 }
203314 break ;
204315 case ")" :
205316 ++ i ;
206317 return result ;
207318 default :
319+ features . add ( "patternCharacter" ) ;
208320 appendOptional (
209321 isUnicode && ( source . codePointAt ( i ) ?? 0 ) > 0xffff ? 2 : 1
210322 ) ;
@@ -214,7 +326,7 @@ function walk(regex: RegExp): { parts: Part[]; groupCount: number } {
214326 return result ;
215327 }
216328
217- return { parts : process ( ) , groupCount } ;
329+ return { parts : process ( ) , groupCount, features } ;
218330}
219331
220332function render (
@@ -239,7 +351,7 @@ function reclassifyOctalEscapes(
239351) : Part [ ] {
240352 return parts . map ( ( part ) =>
241353 isNumericBackreference ( part ) && part . ref > groupCount
242- ? "(?:" + source . slice ( part . start , part . end ) + ALTERNATION_TO_END_OF_INPUT
354+ ? "(?:" + source . slice ( part . start , part . end ) + DISJUNCTION_TO_END_OF_INPUT
243355 : part
244356 ) ;
245357}
@@ -259,7 +371,7 @@ function spliceOriginalSource(
259371
260372function expandCaptured ( value : string , isUnicode : boolean ) : string {
261373 return ( isUnicode ? Array . from ( value ) : value . split ( "" ) )
262- . map ( ( atom ) => "(?:" + escapeAtom ( atom ) + ALTERNATION_TO_END_OF_INPUT )
374+ . map ( ( atom ) => "(?:" + escapeAtom ( atom ) + DISJUNCTION_TO_END_OF_INPUT )
263375 . join ( "" ) ;
264376}
265377
@@ -269,17 +381,19 @@ export interface DynamicPath {
269381 expand : ( capture : RegExpExecArray ) => string ;
270382}
271383
272- export type CompiledPartial =
384+ export type CompiledPartial = (
273385 | { kind : "static" ; regex : RegExp }
274- | { kind : "dynamic" ; dynamic : DynamicPath } ;
386+ | { kind : "dynamic" ; dynamic : DynamicPath }
387+ ) & { features : Set < RegexFeature > } ;
275388
276389export const compilePartial = ( regex : RegExp ) : CompiledPartial => {
277- const { parts, groupCount } = walk ( regex ) ;
390+ const { parts, groupCount, features } = walk ( regex ) ;
278391
279392 if ( ! MAYBE_HAS_BACKREFERENCE . test ( regex . source ) ) {
280393 return {
281394 kind : "static" ,
282- regex : new RegExp ( render ( parts , backrefToken ) , regex . flags )
395+ regex : new RegExp ( render ( parts , backrefToken ) , regex . flags ) ,
396+ features
283397 } ;
284398 }
285399
@@ -291,12 +405,14 @@ export const compilePartial = (regex: RegExp): CompiledPartial => {
291405 if ( backreferences . length === 0 ) {
292406 return {
293407 kind : "static" ,
294- regex : new RegExp ( render ( sanitizedParts , backrefToken ) , regex . flags )
408+ regex : new RegExp ( render ( sanitizedParts , backrefToken ) , regex . flags ) ,
409+ features
295410 } ;
296411 }
297412
298413 return {
299414 kind : "dynamic" ,
415+ features,
300416 dynamic : {
301417 originalCaptureScan : new RegExp (
302418 spliceOriginalSource ( regex . source , backreferences ) ,
@@ -312,7 +428,7 @@ export const compilePartial = (regex: RegExp): CompiledPartial => {
312428 ? capture [ backref . ref ]
313429 : capture . groups ?. [ backref . ref ] ;
314430 return captured === undefined
315- ? "(?:" + backrefToken ( backref ) + ALTERNATION_TO_END_OF_INPUT
431+ ? "(?:" + backrefToken ( backref ) + DISJUNCTION_TO_END_OF_INPUT
316432 : expandCaptured ( captured , isUnicode ) ;
317433 } )
318434 }
0 commit comments