Repository: platers/obsidian-linter (ts)

Issue:
Bug: no-bare-urls puts closing angle bracket before link's trailing parentheses
- [X] I have verified that I am on the latest version of the Linter

## Describe the Bug

When using the `no-bare-urls` rule on a link definition like `https://en.wikipedia.org/wiki/The_Score_(album)`, the closing angle bracket will get inserted before the ending parentheses instead of getting inserted at the end.

## How to Reproduce

Steps to reproduce the behavior:

Put the following into an obsidian document:

```
[The Score]: https://en.wikipedia.org/wiki/The_Score_(album)
```

Then run the linter. The new format in the document will be:

```
[The Score]: <https://en.wikipedia.org/wiki/The_Score_(album>)
```

## Expected Behavior

Upon running the linter, it is expected that the closing angle bracket is put at the end:

``` markdown
[The Score]: <https://en.wikipedia.org/wiki/The_Score_(album)>
```

## Screenshots

If applicable, add screenshots to help explain your problem.

## Device

- [X] Desktop
- [ ] Mobile

## Additional Context

Add any other context about the problem here.


Pull request description:
Fix `No-Bare-URLs` Not Accounting for 
Fixes #908 

Changes Made:
- If a link has an opening paren and no closing paren, then if the link ends in a paren it will be considered part of the URL
- If a link ends in a closing paren, but does not have an opening paren, it will be considered to not be part of the URL
- Allowed URL matches to end in `!` and `?`, but did not do so for `.`
- Added related UTs

Patch:
diff --git a/src/rules/no-bare-urls.ts b/src/rules/no-bare-urls.ts
index 25e7676..6431ee9 100644
--- a/src/rules/no-bare-urls.ts
+++ b/src/rules/no-bare-urls.ts
@@ -2,7 +2,7 @@ import {Options, RuleType} from '../rules';
 import RuleBuilder, {BooleanOptionBuilder, ExampleBuilder, OptionBuilderBase} from './rule-builder';
 import dedent from 'ts-dedent';
 import {IgnoreTypes} from '../utils/ignore-types';
-import {replaceTextBetweenStartAndEndWithNewValue} from '../utils/strings';
+import {countInstances, replaceTextBetweenStartAndEndWithNewValue} from '../utils/strings';
 import {simpleURIRegex, urlRegex} from '../utils/regex';
 
 class NoBareUrlsOptions implements Options {
@@ -60,12 +60,25 @@ export default class NoBareUrls extends RuleBuilder<NoBareUrlsOptions> {
       }
 
       const previousChar = urlStart === 0 ? undefined : text.charAt(urlStart - 1);
-      const nextChar = urlEnd >= text.length ? undefined : text.charAt(urlEnd);
+      let nextChar = urlEnd >= text.length ? undefined : text.charAt(urlEnd);
+      // check for an unmatched opening paren
+      const openingParentheses = countInstances(urlMatch, '(');
+      if (openingParentheses != 0 && openingParentheses != countInstances(urlMatch, ')') && nextChar == ')') {
+        urlMatch += nextChar;
+        urlEnd++;
+        nextChar = urlEnd >= text.length ? undefined : text.charAt(urlEnd);
+      } else if (openingParentheses == 0 && urlMatch.endsWith(')')) {
+        nextChar = ')';
+        urlEnd--;
+        urlMatch = urlMatch.substring(0, urlMatch.length -1);
+      }
+
       if (this.skipMatch(previousChar, nextChar, urlMatch, isURISearch)) {
         startSearch = urlStart + urlMatch.length;
         continue;
       }
 
+
       if (previousChar != undefined && previousChar === '<' && nextChar != undefined && nextChar === '>') {
         let startOfOpeningChevrons = urlStart - 1;
         while (startOfOpeningChevrons > 0 && text.charAt(startOfOpeningChevrons-1) === '<') {
diff --git a/src/utils/regex.ts b/src/utils/regex.ts
index e96b45c..8b0f902 100644
--- a/src/utils/regex.ts
+++ b/src/utils/regex.ts
@@ -24,7 +24,7 @@ export const tableRow = /[^\n]*?\|[^\n]*?(\n|$)/m;
 // based on https://gist.github.com/skeller88/5eb73dc0090d4ff1249a
 export const simpleURIRegex = /(([a-z\-0-9]+:)\/{2})([^\s/?#]*[^\s")'.?!/]|[/])?(([/?#][^\s")']*[^\s")'.?!])|[/])?/gi;
 // generated from https://github.com/spamscanner/url-regex-safe using strict: true, returnString: true, and re2: false as options
-export const urlRegex = /(?:(?:(?:[a-z]+:)?\/\/)|www\.)(?:localhost|(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?|(?:(?:[a-z\u00a1-\uffff0-9][-_]*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:(?:[/?#][^\s")']*[^\s")'.?!])|[/])?/gi;
+export const urlRegex = /(?:(?:(?:[a-z]+:)?\/\/)|www\.)(?:localhost|(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?:(?:[a-fA-F\d]{1,4}:){7}(?:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){6}(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|:[a-fA-F\d]{1,4}|:)|(?:[a-fA-F\d]{1,4}:){5}(?::(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,2}|:)|(?:[a-fA-F\d]{1,4}:){4}(?:(?::[a-fA-F\d]{1,4}){0,1}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,3}|:)|(?:[a-fA-F\d]{1,4}:){3}(?:(?::[a-fA-F\d]{1,4}){0,2}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,4}|:)|(?:[a-fA-F\d]{1,4}:){2}(?:(?::[a-fA-F\d]{1,4}){0,3}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,5}|:)|(?:[a-fA-F\d]{1,4}:){1}(?:(?::[a-fA-F\d]{1,4}){0,4}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,6}|:)|(?::(?:(?::[a-fA-F\d]{1,4}){0,5}:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}|(?::[a-fA-F\d]{1,4}){1,7}|:)))(?:%[0-9a-zA-Z]{1,})?|(?:(?:[a-z\u00a1-\uffff0-9][-_]*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:(?:[/?#][^\s")']*[^\s")'.])|[/])?/gi;
 export const anchorTagRegex = /<a[\s]+([^>]+)>((?:.(?!<\/a>))*.)<\/a>/g;
 export const wordRegex = /[\p{L}\p{N}\p{Pc}\p{M}\-'’`]+/gu;
 // regex from https://stackoverflow.com/a/26128757/8353749


Test patch:
diff --git a/__tests__/no-bare-urls.test.ts b/__tests__/no-bare-urls.test.ts
index 9ad7f98..7ec9436 100644
--- a/__tests__/no-bare-urls.test.ts
+++ b/__tests__/no-bare-urls.test.ts
@@ -97,6 +97,33 @@ ruleTest({
         noBareURIs: false,
       },
     },
+    {// accounts for https://github.com/platers/obsidian-linter/issues/908
+      testName: 'Make sure that a link ending in ) gets fully put in the angle brackets if it has an opening paren',
+      before: dedent`
+        [The Score]: https://en.wikipedia.org/wiki/The_Score_(album)
+      `,
+      after: dedent`
+        [The Score]: <https://en.wikipedia.org/wiki/The_Score_(album)>
+      `,
+    },
+    {// relates for https://github.com/platers/obsidian-linter/issues/908
+      testName: 'Make sure that a link ending in ), but without an opening paren does not get angle brackets added around it',
+      before: dedent`
+        [The Score]: https://en.wikipedia.org/wiki/The_Score_album)
+      `,
+      after: dedent`
+        [The Score]: <https://en.wikipedia.org/wiki/The_Score_album>)
+      `,
+    },
+    {// accounts for https://github.com/platers/obsidian-linter/issues/908
+      testName: 'Make sure that a link ending in ! gets fully put in the angle brackets',
+      before: dedent`
+        [This Is Fats Domino!]: https://en.wikipedia.org/wiki/This_Is_Fats_Domino!
+      `,
+      after: dedent`
+        [This Is Fats Domino!]: <https://en.wikipedia.org/wiki/This_Is_Fats_Domino!>
+      `,
+    },
   ],
 });