From 6d497055c303f1f50d31e595b238eb74fbe01120 Mon Sep 17 00:00:00 2001 From: brontybuutveld Date: Sun, 15 Feb 2026 19:25:06 +1300 Subject: [PATCH] feat: modifier support --- lib/reconstruct.ts | 10 +++++++- lib/tokenizer.ts | 61 +++++++++++++++++++++++++++++++++++++++++++++ lib/types/tokens.ts | 3 +++ 3 files changed, 73 insertions(+), 1 deletion(-) diff --git a/lib/reconstruct.ts b/lib/reconstruct.ts index 274cd22..93b5d93 100644 --- a/lib/reconstruct.ts +++ b/lib/reconstruct.ts @@ -41,7 +41,15 @@ export const reconstruct = (token: Tokens): string => { token.followedBy ? '?=' : token.notFollowedBy ? '?!' : '?:'; - return `(${prefix}${createAlternate(token)})`; + let preprefix = ''; + if (token.enableStack || token.disableStack) { + preprefix += '?'; + if (token.enableStack) preprefix += reduceStack(token.enableStack); + if (token.dash) preprefix += '-'; + if (token.disableStack) preprefix += reduceStack(token.disableStack); + preprefix += ':'; + } + return `(${preprefix+prefix}${createAlternate(token)})`; } case types.REPETITION: { const { min, max } = token; diff --git a/lib/tokenizer.ts b/lib/tokenizer.ts index 34a522a..3ae62f9 100644 --- a/lib/tokenizer.ts +++ b/lib/tokenizer.ts @@ -14,6 +14,11 @@ const captureGroupFirstChar = /^[a-zA-Z_$]$/i; */ const captureGroupChars = /^[a-zA-Z0-9_$]$/i; +/** + * Valid characters for group modifiers. + */ +const modeModifiers = /^(i|m|s)$/i; + const digit = /\d/; /** @@ -42,6 +47,42 @@ export const tokenizer = (regexpStr: string): Root => { ); }; + const modifierErr = (idx: number) => { + throw new SyntaxError( + `Invalid regular expression: /${ + regexpStr + }/: Unknown group modifier flag, expected 'i', or 'm', or 's', found` + + ` '${str[idx]}' at column ${idx + 1}`, + ); + } + + const tokenizeModifiers = (group: Group, i: number, enable: boolean): number => { + group[enable ? 'enableStack' : 'disableStack'] = []; + + while (i < str.length && modeModifiers.test(str[i])) { + const char: Char = { + type: types.CHAR, + value: str[i].charCodeAt(0), + }; + group[enable ? 'enableStack' : 'disableStack'].push(char); + i++; + } + + if (str[i] !== ':' && ((str[i] !== '-' && enable) || (!enable))) { + modifierErr(i); + } + + return i; + } + + const enableModifers = (group: Group, i: number): number => { + return tokenizeModifiers(group, i, true); + } + + const disableModifers = (group: Group, i: number): number => { + return tokenizeModifiers(group, i, false); + } + // Decode a few escaped characters. let str = util.strToChars(regexpStr); @@ -222,6 +263,26 @@ export const tokenizer = (regexpStr: string): Root => { i++; } else if (c === ':') { group.remember = false; + } else if (modeModifiers.test(c)) { + // the index logic is complicated + i = enableModifers(group, --i); // i is ahead of c so we decrement + if (str[i] === '-') { + group.dash = true; + i = disableModifers(group, ++i) +1; // increment move off of '-', add 1 move index + if (group.disableStack.length === 0) { + group.disableStack = undefined; + } + } else if (str[i] === ':') { // like (?i:) + i++; + } else { + modifierErr(i); + } + } else if (c === '-') { + group.dash = true; + i = disableModifers(group, i) +1; // no increment here because i is ahead of c + if (group.disableStack.length === 0) { + group.disableStack = undefined; + } } else { throw new SyntaxError( `Invalid regular expression: /${ diff --git a/lib/types/tokens.ts b/lib/types/tokens.ts index 9acb7ae..4bf2c41 100644 --- a/lib/types/tokens.ts +++ b/lib/types/tokens.ts @@ -18,6 +18,9 @@ export type Group = Base export type Set = Base