Traditionally, we could only apply regular expression flags such as i (for ignoring case) to all of a regular expression. The ECMAScript feature “Regular Expression Pattern Modifiers” (by Ron Buckton) enables us to apply them to only part of a regular expression. In this blog post we examine how they work and what their use cases are.
This proposal reached stage 4 on 2024-10-08.
How a regular expression works is influenced by so-called flags – e.g., the flag i ignores case during matching:
> /yes/i.test('yes')
true
> /yes/i.test('YES')
true
Pattern modifiers let us apply flags to parts of a regular expression (vs. all of the regular expression) – for example, in the following regular expression, the flag i is only applied to “HELLO”:
> /^x(?i:HELLO)x$/.test('xHELLOx')
true
> /^x(?i:HELLO)x$/.test('xhellox')
true
> /^x(?i:HELLO)x$/.test('XhelloX')
false
In a way, pattern modifiers are inline flags.
This is what the syntax looks like:
(?ims-ims:pattern)
(?ims:pattern)
(?-ims:pattern)
Notes:
?) is activated.-) is deactivated.(?:pattern)Let’s change the previous example: Now all of the regular expression is case-insensitive – except for “HELLO”:
> /^x(?-i:HELLO)x$/i.test('xHELLOx')
true
> /^x(?-i:HELLO)x$/i.test('XHELLOX')
true
> /^x(?-i:HELLO)x$/i.test('XhelloX')
false
The following flags can be used in pattern modifiers:
| Literal flag | Property name | ES | Description |
|---|---|---|---|
| i | ignoreCase | ES3 | Match case-insensitively |
| m | multiline | ES3 | ^ and $ match per line |
| s | dotAll | ES2018 | Dot matches line terminators |
For more information, see the section on flags in “Exploring JavaScript”.
The remaining flags are not supported because they would either make regular expression semantics too complicated (e.g. flag v) or because they only make sense if applied to the whole regular expression (e.g. flag g).
It’s sometimes useful if you can change flags for part of a regular expression. For example, Ron Buckton explains that changing flag m helps with matching a Markdown frontmatter block at the start of a file (I slightly edited his version):
const re = /(?-m:^)---\r?\n((?:^(?!---$).*\r?\n)*)^---$/m;
assert.equal(re.test('---a'), false);
assert.equal(re.test('---\n---'), true);
assert.equal(
re.exec('---\n---')[1],
''
);
assert.equal(
re.exec('---\na: b\n---')[1],
'a: b\n'
);
How does this regular expression work?
m is on and the anchor ^ matches at the beginning of a line and the anchor $ matches at the end of a line.^ is different: It must match at the beginning of a string. That’s why we use a pattern modifier there and switch flag m off.This is the regular expression, annotated with insignificant whitespace and explanatory comments:
(?-m:^)---\r?\n # first line of string
( # capturing group for the frontmatter
(?: # pattern for one line (non-capturing group)
^(?!---$) # line must not start with "---" + EOL (lookahead)
.*\r?\n
)*
)
^---$ # closing delimiter of frontmatter
In some situations, flags being outside the actual regular expressions is inconvenient. Then pattern modifiers help. Examples include:
regex('i')`world`
regex`(?i:world)`
In complex applications, it helps if you can compose large regular expressions out of smaller regular expressions. The aforementioned Regex+ library supports that. If a smaller regular expression needs different flags (e.g. because it wants to ignore case) then it can – thanks to pattern modifiers.
RegExp)” in “Exploring JavaScript”