Repository: obsidian-tasks-group/obsidian-tasks
Pull request #2135 (2023-07-16 15:57:16)

fix: Support 'u' flag (Unicode) & detect duplicate flags
<!--- Provide a general summary of your changes in the Title above -->

# Description

- Add support for the `u` regex flag (allow Unicode)
- As a by-product, detect and report errors due to use of duplicate regex flags.

I am not actually sure whether the `u` flag is required to enable searching for Unicode strings in tasks, since my own regex searches have been working. But it should be recognised anyway.

This simplifies `validateAndConstruct` in `RegexMatcher` - more simplification coming soon.

## Motivation and Context

<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->

Having enabled `explain` to display regex flags, I noticed that `u` was not being recognised.

## How has this been tested?

- Adding new tests
- Updating existing tests
- Testing manually in demo vault

One new failing test remains, which is that invalid match flags should be rejected. This will be fixed after #1037 is fixed.

## Screenshots (if appropriate)

## Types of changes

<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: -->

Changes visible to users:

- [x] **Bug fix** (prefix: `fix` - non-breaking change which fixes an issue)

Internal changes:

- [x] **Refactor** (prefix: `refactor` - non-breaking change which only improves the design or structure of existing code, and making no changes to its external behaviour)
- [x] **Tests** (prefix: `test` - additions and improvements to unit tests and the smoke tests)

## Checklist

<!--- Go over all the following points, and put an `x` in all the boxes that apply. -->
<!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! -->

- [x] My code follows the code style of this project and passes `yarn run lint`.
- [ ] My change requires a change to the documentation.
- [ ] I have [updated the documentation](https://publish.obsidian.md/tasks-contributing/Documentation/About+Documentation) accordingly.
- [x] My change has adequate [Unit Test coverage](https://publish.obsidian.md/tasks-contributing/Testing/About+Testing).

## Terms

<!--
By submitting this pull request, you must agree to follow our
[contributing guide](https://publish.obsidian.md/tasks-contributing) and
[Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md).
Put an x in the boxes to confirm you agree.
-->

- [x] My contribution follow this project's [contributing guide](https://publish.obsidian.md/tasks-contributing)
- [x] I agree to follow this project's [Code of Conduct](https://github.com/obsidian-tasks-group/obsidian-tasks/blob/main/CODE_OF_CONDUCT.md)



Patch:
diff --git a/src/Query/Matchers/RegexMatcher.ts b/src/Query/Matchers/RegexMatcher.ts
index 3e77c308..855e39e4 100644
--- a/src/Query/Matchers/RegexMatcher.ts
+++ b/src/Query/Matchers/RegexMatcher.ts
@@ -5,7 +5,7 @@ import { IStringMatcher } from './IStringMatcher';
  * Regular-expression-based implementation of IStringMatcher.
  */
 export class RegexMatcher extends IStringMatcher {
-    private readonly regex: RegExp;
+    public readonly regex: RegExp;
 
     /**
      * Construct a RegexMatcher object.
@@ -28,8 +28,7 @@ export class RegexMatcher extends IStringMatcher {
      */
     public static validateAndConstruct(regexInput: string): RegexMatcher | null {
         // Courtesy of https://stackoverflow.com/questions/17843691/javascript-regex-to-match-a-regex
-        const regexPattern =
-            /\/((?![*+?])(?:[^\r\n[/\\]|\\.|\[(?:[^\r\n\]\\]|\\.)*])+)\/((?:g(?:im?|mi?)?|i(?:gm?|mg?)?|m(?:gi?|ig?)?)?)/;
+        const regexPattern = /\/((?![*+?])(?:[^\r\n[/\\]|\\.|\[(?:[^\r\n\]\\]|\\.)*])+)\/([igmu]*)/;
         const query = regexInput.match(regexPattern);
 
         if (query !== null) {


Test patch:
diff --git a/tests/Query/Filter/TextField.test.explains_regular_expression_searches_bulk_test.approved.txt b/tests/Query/Filter/TextField.test.explains_regular_expression_searches_bulk_test.approved.txt
index 6fa8ccb5..f25c2958 100644
--- a/tests/Query/Filter/TextField.test.explains_regular_expression_searches_bulk_test.approved.txt
+++ b/tests/Query/Filter/TextField.test.explains_regular_expression_searches_bulk_test.approved.txt
@@ -23,7 +23,7 @@ description regex matches /[⏫🔼🔽📅⏳🛫🔁]/ =>
   using regex:            '[⏫🔼🔽📅⏳🛫🔁]' with no flags
 
 description regex matches /[⏫🔼🔽📅⏳🛫🔁]/u =>
-  using regex:            '[⏫🔼🔽📅⏳🛫🔁]' with no flags
+  using regex:            '[⏫🔼🔽📅⏳🛫🔁]' with flag 'u'
 
 description regex matches /^$/ =>
   using regex:            '^$' with no flags
diff --git a/tests/Query/Matchers/RegexMatcher.test.ts b/tests/Query/Matchers/RegexMatcher.test.ts
index 4c1a5778..70346d91 100644
--- a/tests/Query/Matchers/RegexMatcher.test.ts
+++ b/tests/Query/Matchers/RegexMatcher.test.ts
@@ -22,3 +22,46 @@ describe('RegexMatcher', () => {
         expect(matcher).toBeNull();
     });
 });
+
+describe('RegexMatcher flags', () => {
+    it('should allow "i" flag - case-insensitive', () => {
+        const matcher = RegexMatcher.validateAndConstruct('/expression/i');
+        expect(matcher!.regex.flags).toEqual('i');
+    });
+
+    it('should allow "g" flag - global', () => {
+        const matcher = RegexMatcher.validateAndConstruct('/expression/g');
+        expect(matcher!.regex.flags).toEqual('g');
+    });
+
+    it('should allow "m" flag - multiline', () => {
+        const matcher = RegexMatcher.validateAndConstruct('/expression/m');
+        expect(matcher!.regex.flags).toEqual('m');
+    });
+
+    it('should allow "u" flag - unicode', () => {
+        const matcher = RegexMatcher.validateAndConstruct('/expression/u');
+        expect(matcher!.regex.flags).toEqual('u');
+    });
+
+    it('should allow "img" flags', () => {
+        const matcher = RegexMatcher.validateAndConstruct('/expression/img');
+        expect(matcher!.regex.flags).toEqual('gim');
+    });
+
+    it.failing('should reject invalid "x" flag', () => {
+        const t = () => {
+            RegexMatcher.validateAndConstruct('/expression/x');
+        };
+        expect(t).toThrow(SyntaxError);
+        expect(t).toThrowError("Invalid flags supplied to RegExp constructor 'x'");
+    });
+
+    it('should reject duplicate "ii" flag', () => {
+        const t = () => {
+            RegexMatcher.validateAndConstruct('/expression/ii');
+        };
+        expect(t).toThrow(SyntaxError);
+        expect(t).toThrowError("Invalid flags supplied to RegExp constructor 'ii'");
+    });
+});