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.
Regular expression pattern modifiers attributes reached stage 4 in October 2024 and will probably be part of ECMAScript 2025.
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”