Repository: platers/obsidian-linter
Pull request #243 (2022-06-29 00:25:53)

Update Link Parsing
Updated the link parsing to be more robust than before. Before it would often include characters outside the link if the link was followed by a parentheses after the end of the link. To fix this, I let remark handle finding the regular links and just identified wiki links. The downside of this is that images/transclusion of regular makdown links is ignored for the sake of removing spaces in link text. We can add that by including images in the types to pull back from remark.

Changes Made:
- Updated regular markdown link identification to go through `remark-js`
- Added a UT for a scenario I noticed was not working around cleaning up whitespace
- Updated example to show translusion is not affected by removing spaces around links
- Updated documentation to include updated name for a method that was previously updated


Patch:
diff --git a/README.md b/README.md
index ba6f273..56ef805 100644
--- a/README.md
+++ b/README.md
@@ -96,7 +96,7 @@ Pull requests are welcome, especially for new rules.
    1. Insert a new rule in the corresponding rule type(spacing, headings, etc)
    2. Follow the format of the existing rules
    3. Add tests for edge cases in `test.ts`
-   4. You should probably use some helper functions from `utils.ts`, such as `ignoreCodeBlocksYAMLAndLinks`.
+   4. You should probably use some helper functions from `utils.ts`, such as `ignoreCodeBlocksYAMLTagsAndLinks`.
 5. Run `npm run compile` to build, generate documentation, and test the plugin.
 6. Run `npm run lint` to lint the plugin.
 
diff --git a/docs/readme_template.md b/docs/readme_template.md
index 9dcb706..13e16ae 100644
--- a/docs/readme_template.md
+++ b/docs/readme_template.md
@@ -48,7 +48,7 @@ Pull requests are welcome, especially for new rules.
    1. Insert a new rule in the corresponding rule type(spacing, headings, etc)
    2. Follow the format of the existing rules
    3. Add tests for edge cases in `test.ts`
-   4. You should probably use some helper functions from `utils.ts`, such as `ignoreCodeBlocksYAMLAndLinks`.
+   4. You should probably use some helper functions from `utils.ts`, such as `ignoreCodeBlocksYAMLTagsAndLinks`.
 5. Run `npm run compile` to build, generate documentation, and test the plugin.
 6. Run `npm run lint` to lint the plugin.
 
diff --git a/docs/rules.md b/docs/rules.md
index 83baa6d..34a4e12 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -1766,6 +1766,8 @@ Before:
 [here is link text4](link_here)
 [	here is link text5	](link_here)
 [](link_here)
+**Note that image markdown syntax does not get affected even if it is transclusion:**
+![	here is link text6 ](link_here)
 ```
 
 After:
@@ -1777,6 +1779,8 @@ After:
 [here is link text4](link_here)
 [here is link text5](link_here)
 [](link_here)
+**Note that image markdown syntax does not get affected even if it is transclusion:**
+![	here is link text6 ](link_here)
 ```
 Example: Space in wiki link text
 
@@ -1788,6 +1792,7 @@ Before:
 [[link_here| here is link text3]]
 [[link_here|here is link text4]]
 [[link_here|	here is link text5	]]
+![[link_here|	here is link text6	]]
 [[link_here]]
 ```
 
@@ -1799,5 +1804,6 @@ After:
 [[link_here|here is link text3]]
 [[link_here|here is link text4]]
 [[link_here|here is link text5]]
+![[link_here|here is link text6]]
 [[link_here]]
 ```
diff --git a/src/rules.ts b/src/rules.ts
index 92ce0e7..65553dc 100644
--- a/src/rules.ts
+++ b/src/rules.ts
@@ -13,7 +13,7 @@ import {
   escapeYamlString,
   makeEmphasisOrBoldConsistent,
   addTwoSpacesAtEndOfLinesFollowedByAnotherLineOfTextContent,
-  linkRegex,
+  removeSpacesInLinkText,
 } from './utils';
 import {
   Option,
@@ -2341,26 +2341,7 @@ export const rules: Rule[] = [
       'Removes spacing around link text.',
       RuleType.SPACING,
       (text: string) => {
-        const linkMatches = text.match(linkRegex);
-
-        if (!linkMatches) {
-          return text;
-        }
-
-        for (const link of linkMatches) {
-          // regular markdown link
-          if (!link.startsWith('[[')) {
-            const endLinkTextPosition = link.lastIndexOf(']');
-            const newLink = link.substring(0, 1) + link.substring(1, endLinkTextPosition).trim() + link.substring(endLinkTextPosition);
-            text = text.replace(link, newLink);
-          } else if (link.includes('|')) { // wiki link with link text
-            const startLinkTextPosition = link.indexOf('|');
-            const newLink = link.substring(0, startLinkTextPosition+1) + link.substring(startLinkTextPosition+1, link.length - 2).trim() + ']]';
-            text = text.replace(link, newLink);
-          }
-        }
-
-        return text;
+        return removeSpacesInLinkText(text);
       },
       [
         new Example(
@@ -2372,6 +2353,8 @@ export const rules: Rule[] = [
       [here is link text4](link_here)
       [\there is link text5\t](link_here)
       [](link_here)
+      **Note that image markdown syntax does not get affected even if it is transclusion:**
+      ![\there is link text6 ](link_here)
       `,
             dedent`
       [here is link text1](link_here)
@@ -2380,25 +2363,29 @@ export const rules: Rule[] = [
       [here is link text4](link_here)
       [here is link text5](link_here)
       [](link_here)
+      **Note that image markdown syntax does not get affected even if it is transclusion:**
+      ![\there is link text6 ](link_here)
       `,
         ),
         new Example(
             'Space in wiki link text',
             dedent`
-    [[link_here| here is link text1 ]]
-    [[link_here|here is link text2 ]]
-    [[link_here| here is link text3]]
-    [[link_here|here is link text4]]
-    [[link_here|\there is link text5\t]]
-    [[link_here]]
+      [[link_here| here is link text1 ]]
+      [[link_here|here is link text2 ]]
+      [[link_here| here is link text3]]
+      [[link_here|here is link text4]]
+      [[link_here|\there is link text5\t]]
+      ![[link_here|\there is link text6\t]]
+      [[link_here]]
     `,
             dedent`
-    [[link_here|here is link text1]]
-    [[link_here|here is link text2]]
-    [[link_here|here is link text3]]
-    [[link_here|here is link text4]]
-    [[link_here|here is link text5]]
-    [[link_here]]
+      [[link_here|here is link text1]]
+      [[link_here|here is link text2]]
+      [[link_here|here is link text3]]
+      [[link_here|here is link text4]]
+      [[link_here|here is link text5]]
+      ![[link_here|here is link text6]]
+      [[link_here]]
     `,
         ),
       ],
diff --git a/src/utils.ts b/src/utils.ts
index f3c64b1..d6c6625 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -13,7 +13,7 @@ export const backtickBlockRegexTemplate = fencedRegexTemplate.replaceAll('X', '`
 export const tildeBlockRegexTemplate = fencedRegexTemplate.replaceAll('X', '~');
 export const indentedBlockRegex = '^((\t|( {4})).*\n)+';
 export const codeBlockRegex = new RegExp(`${backtickBlockRegexTemplate}|${tildeBlockRegexTemplate}|${indentedBlockRegex}`, 'gm');
-export const linkRegex = /((!?)\[[^[\n\]]*\]\([^[\n\]]*\))|(\[{2}[^[\n\]]*\]{2})/g;
+export const wikiLinkRegex = /(!?)(\[{2}[^[\n\]]*\]{2})/g;
 export const tagRegex = /#[^\s#]{1,}/g;
 
 // Reused placeholders
@@ -44,7 +44,7 @@ function getPositions(type: string, text: string) {
  * Replaces all codeblocks in the given text with a placeholder.
  * @param {string} text The text to replace codeblocks in
  * @param {string} placeholder The placeholder to use
- * @return {string} The text with codeblocks replaced, and the list of codeblocks
+ * @return {string} The text with codeblocks replaced
  * @return {string[]} The codeblocks replaced
  */
 function replaceCodeblocks(text: string, placeholder: string): {text: string, replacedCodeBlocks: string[]} {
@@ -63,6 +63,49 @@ function replaceCodeblocks(text: string, placeholder: string): {text: string, re
   return {text, replacedCodeBlocks};
 }
 
+/**
+ * Replaces all links in the given text with a placeholder.
+ * @param {string} text The text to replace links in
+ * @param {string} regularLinkPlaceholder The placeholder to use for regular markdown links
+ * @param {string} wikiLinkPlaceholder The placeholder to use for wiki links
+ * @return {string} The text with links replaced
+ * @return {string[]} The regular markdown links replaced
+ * @return {string[]} The wiki links replaced
+ */
+function replaceLinks(text: string, regularLinkPlaceholder: string, wikiLinkPlaceholder: string): {text: string, replacedRegularLinks: string[], replacedWikiLinks: string[]} {
+  const positions: Position[] = getPositions('link', text);
+  const replacedRegularLinks: string[] = [];
+
+  for (const position of positions) {
+    if (position == undefined) {
+      continue;
+    }
+
+    const regularLink = text.substring(position.start.offset, position.end.offset);
+    // skip links that are not are not in markdown format
+    if (!regularLink.includes('[')) {
+      continue;
+    }
+
+    replacedRegularLinks.push(regularLink);
+    text = text.substring(0, position.start.offset) + regularLinkPlaceholder + text.substring(position.end.offset);
+  }
+
+  // Reverse the regular links so that they are in the same order as the original text
+  replacedRegularLinks.reverse();
+
+  const replacedWikiLinks: string[] = [];
+  const linkMatches = text.match(wikiLinkRegex);
+  text = text.replaceAll(wikiLinkRegex, wikiLinkPlaceholder);
+  if (linkMatches) {
+    for (const link of linkMatches) {
+      replacedWikiLinks.push(link);
+    }
+  }
+
+  return {text, replacedRegularLinks, replacedWikiLinks};
+}
+
 /**
  * Makes sure that the style of either strong or emphasis is consistent.
  * @param {string} text The text to style either the strong or emphasis in a consistent manner
@@ -147,6 +190,46 @@ export function addTwoSpacesAtEndOfLinesFollowedByAnotherLineOfTextContent(text:
   return text;
 }
 
+
+/**
+ * Removes spaces before and after link text
+ * @param {string} text The text to make that there are no spaces around the link text of
+ * @return {string} The text with spaces around link text removed
+ */
+export function removeSpacesInLinkText(text: string): string {
+  const positions: Position[] = getPositions('link', text);
+
+  for (const position of positions) {
+    if (position == undefined) {
+      continue;
+    }
+
+    const regularLink = text.substring(position.start.offset, position.end.offset);
+    // skip links that are not are not in markdown format
+    if (!regularLink.includes('[')) {
+      continue;
+    }
+
+    const endLinkTextPosition = regularLink.lastIndexOf(']');
+    const newLink = regularLink.substring(0, 1) + regularLink.substring(1, endLinkTextPosition).trim() + regularLink.substring(endLinkTextPosition);
+    text = text.substring(0, position.start.offset) + newLink + text.substring(position.end.offset);
+  }
+
+  const linkMatches = text.match(wikiLinkRegex);
+  if (linkMatches) {
+    for (const link of linkMatches) {
+      // wiki link with link text
+      if (link.includes('|')) {
+        const startLinkTextPosition = link.indexOf('|');
+        const newLink = link.substring(0, startLinkTextPosition+1) + link.substring(startLinkTextPosition+1, link.length - 2).trim() + ']]';
+        text = text.replace(link, newLink);
+      }
+    }
+  }
+
+  return text;
+}
+
 /**
  * Moves footnote declarations to the end of the document.
  * @param {string} text The text to move footnotes in
@@ -203,9 +286,10 @@ export function ignoreCodeBlocksYAMLTagsAndLinks(text: string, func: (text: stri
     text = text.replace(yamlMatches[0], escapeDollarSigns(yamlPlaceholder));
   }
 
-  const linkPlaceHolder = '{PLACEHOLDER_LINK 57849}';
-  const linkMatches = text.match(linkRegex);
-  text = text.replaceAll(linkRegex, linkPlaceHolder);
+  const regularLinkPlaceHolder = '{PLACEHOLDER_REGULAR_LINK}';
+  const wikiLinkPlaceHolder = '{PLACEHOLDER_WIKI_LINK}';
+  const linkResults = replaceLinks(text, regularLinkPlaceHolder, wikiLinkPlaceHolder);
+  text = linkResults.text;
 
   const tagMatches = text.match(tagRegex);
   const tagPlaceholder = '#tag-placeholder';
@@ -219,12 +303,16 @@ export function ignoreCodeBlocksYAMLTagsAndLinks(text: string, func: (text: stri
     }
   }
 
-  if (linkMatches) {
-    for (const link of linkMatches) {
-      // Regex was added to fix capitalization issue  where another rule made the text not match the original place holder's case
-      // see https://github.com/platers/obsidian-linter/issues/201
-      text = text.replace(new RegExp(linkPlaceHolder, 'i'), link);
-    }
+  for (const regularLink of linkResults.replacedRegularLinks) {
+    // Regex was added to fix capitalization issue  where another rule made the text not match the original place holder's case
+    // see https://github.com/platers/obsidian-linter/issues/201
+    text = text.replace(new RegExp(regularLinkPlaceHolder, 'i'), regularLink);
+  }
+
+  for (const wikiLink of linkResults.replacedWikiLinks) {
+    // Regex was added to fix capitalization issue  where another rule made the text not match the original place holder's case
+    // see https://github.com/platers/obsidian-linter/issues/201
+    text = text.replace(new RegExp(wikiLinkPlaceHolder, 'i'), wikiLink);
   }
 
   if (yamlMatches) {


Test patch:
diff --git a/src/test.ts b/src/test.ts
index f33c694..e36278d 100644
--- a/src/test.ts
+++ b/src/test.ts
@@ -1030,6 +1030,17 @@ describe('Remove Multiple Spaces', () => {
     Paragraph contents (something). Something else .
     `;
 
+    expect(rulesDict['remove-multiple-spaces'].apply(before)).toBe(after);
+  });
+  it('Links followed by parentheses do not prevent other removal of multiple spaces in a row', () => {
+    const before = dedent`
+    [Link text](path/fileName.md) (2 spaces in between  text)
+    `;
+
+    const after = dedent`
+    [Link text](path/fileName.md) (2 spaces in between text)
+    `;
+
     expect(rulesDict['remove-multiple-spaces'].apply(before)).toBe(after);
   });
 });