Repository: platers/obsidian-linter (ts)

Issue:
Bug: "Blockquote Style" affects nested non-blockquote ">" symbols
- [x] I have verified that I am on the latest version of the Linter

## Describe the Bug

When "Content -> Blockquote Style" is enabled, it affects the ">" symbols even when they are not part of the formatting.
A space will be added after them if the style is set to "space".
A space will be removed after them if the style is set to "no space".

## How to Reproduce

This is an example which is affected by the bug when the style is set to "space":

``` markdown
> Using a C++ "member of pointer" operator: `pointer->field`

> `>`
```

Will be turned into:

``` markdown
> Using a C++ "member of pointer" operator: `pointer-> field`

> `> `
```

## Logs

<details>
<summary>Spoiler</summary>

```
Running linter
rules before regular rules: 0.10000000149011612 ms
Running Remove Multiple Spaces
remove-multiple-spaces: 1.8999999985098839 ms

> Using a C++ "member of pointer" operator: `pointer->field`

> `>`

Running Compact YAML
compact-yaml: 0.10000000149011612 ms

> Using a C++ "member of pointer" operator: `pointer->field`

> `>`

Running Empty Line Around Code Fences
empty-line-around-code-fences: 0 ms

> Using a C++ "member of pointer" operator: `pointer->field`

> `>`

Running Empty Line Around Math Blocks
empty-line-around-math-blocks: 0.10000000149011612 ms

> Using a C++ "member of pointer" operator: `pointer->field`

> `>`

Running Empty Line Around Tables
empty-line-around-tables: 0.20000000298023224 ms

> Using a C++ "member of pointer" operator: `pointer->field`

> `>`

Running Heading blank lines
heading-blank-lines: 0.20000000298023224 ms

> Using a C++ "member of pointer" operator: `pointer->field`

> `>`

Running Line Break at Document End
line-break-at-document-end: 0 ms

> Using a C++ "member of pointer" operator: `pointer->field`

> `>`

Running Remove link spacing
remove-link-spacing: 0.09999999403953552 ms

> Using a C++ "member of pointer" operator: `pointer->field`

> `>`

Running Space after list markers
space-after-list-markers: 0.10000000149011612 ms

> Using a C++ "member of pointer" operator: `pointer->field`

> `>`

Running Trailing spaces
trailing-spaces: 0.20000000298023224 ms

> Using a C++ "member of pointer" operator: `pointer->field`

> `>`

Running Custom Regex
custom regex rules: 0 ms
Running Blockquote Style
blockquote-style: 0.09999999403953552 ms

> Using a C++ "member of pointer" operator: `pointer-> field`

> `> `

Running YAML Key Sort
yaml-key-sort: 0.10000000149011612 ms

> Using a C++ "member of pointer" operator: `pointer-> field`

> `> `

rules after regular rules: 0.29999999701976776 ms
rules running: 3.7000000029802322 ms
Running Custom Lint Commands
```

</details>

## Expected Behavior

No change to the document.

## Device

- [x] Desktop
- [x] Mobile (assumed)



Pull request description:
`Blockquote Style`: Handle `>` in Blockquotes So They Are Not Affected
Fixes #935 

Changes Made:
- Added UTs for the 2 scenarios provided
- Updated the blockquote style logic to make sure that the `>` values are styled properly are only affected if they are a part of the blockquote indicators and not a part of the content in the the blockquote
- Fixed a typo in a function name
- Exported a function for getting the start of a blockquote that was previously not exported

Patch:
diff --git a/package-lock.json b/package-lock.json
index 56108a4..ca77306 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "obsidian-linter",
-  "version": "1.12.0",
+  "version": "1.20.1",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "obsidian-linter",
-      "version": "1.12.0",
+      "version": "1.20.1",
       "license": "MIT",
       "dependencies": {
         "@popperjs/core": "^2.11.6",
@@ -24,7 +24,8 @@
         "micromark-util-combine-extensions": "^1.0.0",
         "moment-parseformat": "^4.0.0",
         "quick-lru": "^6.1.1",
-        "ts-dedent": "^2.2.0"
+        "ts-dedent": "^2.2.0",
+        "unist-util-visit": "^4.1.2"
       },
       "devDependencies": {
         "@babel/core": "^7.20.2",
@@ -32,6 +33,7 @@
         "@babel/plugin-proposal-private-methods": "^7.18.6",
         "@babel/preset-env": "^7.20.2",
         "@babel/preset-typescript": "^7.18.6",
+        "@jest/types": "^29.5.0",
         "@types/diff": "^5.0.2",
         "@types/diff-match-patch": "^1.0.32",
         "@types/jest": "^29.2.3",
diff --git a/src/rules/blockquote-style.ts b/src/rules/blockquote-style.ts
index e00fd58..ce71a3d 100644
--- a/src/rules/blockquote-style.ts
+++ b/src/rules/blockquote-style.ts
@@ -3,6 +3,7 @@ import {Options, RuleType} from '../rules';
 import RuleBuilder, {DropdownOptionBuilder, ExampleBuilder, OptionBuilderBase} from './rule-builder';
 import dedent from 'ts-dedent';
 import {IgnoreTypes} from '../utils/ignore-types';
+import {getStartOfLineWhitespaceOrBlockquoteLevel, replaceTextBetweenStartAndEndWithNewValue} from '../utils/strings';
 
 type BlockquoteStyleValues = 'no space' | 'space';
 
@@ -26,18 +27,50 @@ export default class BlockquoteStyle extends RuleBuilder<BlockquoteStyleOptions>
   }
   apply(text: string, options: BlockquoteStyleOptions): string {
     if (options.style === 'space') {
-      return updateBlockquotes(text, this.addSpaceToIndicator);
+      return updateBlockquotes(text, (blockquote: string) => {
+        return this.updateBlockquoteLines(blockquote, this.addSpaceToIndicator);
+      });
     }
 
-    return updateBlockquotes(text, this.removeSpaceFromIndicator);
+    return updateBlockquotes(text, (blockquote: string) => {
+      return this.updateBlockquoteLines(blockquote, this.removeSpaceFromIndicator);
+    });
   }
-  removeSpaceFromIndicator(blockquote: string): string {
-    return blockquote.replace(/>( |\t)+/g, '>');
+  removeSpaceFromIndicator(startOfLine: string): string {
+    return startOfLine.replace(/>( |\t)+/g, '>');
   }
-  addSpaceToIndicator(blockquote: string): string {
+  addSpaceToIndicator(startOfLine: string): string {
     // first we add spaces to blockquote indicators that are not followed by a space and then to catch any that were not handled already
     // we make sure to add a space between any 2 indicators that are side by side
-    return blockquote.replace(/>([^ ])/g, '> $1').replace(/>>/g, '> >');
+    return startOfLine.replace(/>([^ \t]|$)/g, '> $1').replace(/>>/g, '> >');
+  }
+  updateBlockquoteLines(blockquote: string, startOfLineModification: (startOfLine: string) => string): string {
+    let currentIndex = 0;
+    let nextNewLine = 0;
+    let startOfLine = '';
+    let updatedStartOfLine = '';
+    let startOfIndex = 0;
+    let newBlockquote = blockquote;
+    let breakOutOfLoop = false;
+    do {
+      nextNewLine = newBlockquote.indexOf('\n', currentIndex);
+      if (nextNewLine === -1) {
+        nextNewLine = newBlockquote.length -1;
+        breakOutOfLoop = true;
+      }
+
+      [startOfLine, startOfIndex] = getStartOfLineWhitespaceOrBlockquoteLevel(newBlockquote, nextNewLine-1);
+      updatedStartOfLine = startOfLineModification(startOfLine);
+
+      // since start of index refers to where the new line character is
+      startOfIndex++;
+
+      newBlockquote = replaceTextBetweenStartAndEndWithNewValue(newBlockquote, startOfIndex, startOfIndex + startOfLine.length, updatedStartOfLine);
+
+      currentIndex = nextNewLine+ 1 + startOfLine.length - updatedStartOfLine.length;
+    } while (!breakOutOfLoop);
+
+    return newBlockquote;
   }
   get exampleBuilders(): ExampleBuilder<BlockquoteStyleOptions>[] {
     return [
diff --git a/src/utils/strings.ts b/src/utils/strings.ts
index b09f36b..0af656b 100644
--- a/src/utils/strings.ts
+++ b/src/utils/strings.ts
@@ -31,7 +31,7 @@ export function stripCr(text: string): string {
   return text.replace(/\r/g, '');
 }
 
-function getStartOfLineWhitespaceOrBlockquoteLevel(text: string, startPosition: number): [string, number] {
+export function getStartOfLineWhitespaceOrBlockquoteLevel(text: string, startPosition: number): [string, number] {
   if (startPosition === 0) {
     return ['', 0];
   }
@@ -60,7 +60,7 @@ function getEmptyLine(priorLine: string = ''): string {
   return '\n' + priorLineStart.trim();
 }
 
-function getEmptyLineForBlockqute(priorLine: string = '', isCallout: boolean = false, blockquoteLevel: number = 1): string {
+function getEmptyLineForBlockquote(priorLine: string = '', isCallout: boolean = false, blockquoteLevel: number = 1): string {
   const potentialEmptyLine = getEmptyLine(priorLine);
   const previousBlockquoteLevel = countInstances(potentialEmptyLine, '>');
   const dealingWithACallout = isCallout || calloutRegex.test(priorLine);
@@ -151,7 +151,7 @@ function makeSureContentHasASingleEmptyLineBeforeItUnlessItStartsAFileForBlockqu
     priorLine = text.substring(indexOfLastNewLine, startOfNewContent);
   }
 
-  const emptyLine = addingEmptyLinesAroundBlockquotes ? getEmptyLineForBlockqute(priorLine, isCallout, nestingLevel) : getEmptyLine(priorLine);
+  const emptyLine = addingEmptyLinesAroundBlockquotes ? getEmptyLineForBlockquote(priorLine, isCallout, nestingLevel) : getEmptyLine(priorLine);
 
   return text.substring(0, startOfNewContent) + emptyLine + text.substring(startOfContent);
 }
@@ -247,7 +247,7 @@ function makeSureContentHasASingleEmptyLineAfterItUnlessItEndsAFileForBlockquote
     nextLine = text.substring(endOfNewContent + 1, indexOfSecondNewLineAfterContent);
   }
 
-  const emptyLine = addingEmptyLinesAroundBlockquotes ? getEmptyLineForBlockqute(nextLine, isCallout, nestingLevel) : getEmptyLine(nextLine);
+  const emptyLine = addingEmptyLinesAroundBlockquotes ? getEmptyLineForBlockquote(nextLine, isCallout, nestingLevel) : getEmptyLine(nextLine);
 
   return text.substring(0, endOfContent) + emptyLine + text.substring(endOfNewContent);
 }


Test patch:
diff --git a/__tests__/blockquote-style.test.ts b/__tests__/blockquote-style.test.ts
new file mode 100644
index 0000000..8596677
--- /dev/null
+++ b/__tests__/blockquote-style.test.ts
@@ -0,0 +1,33 @@
+import BlockquoteStyle from '../src/rules/blockquote-style';
+import dedent from 'ts-dedent';
+import {ruleTest} from './common';
+
+ruleTest({
+  RuleBuilderClass: BlockquoteStyle,
+  testCases: [
+    { // accounts for https://github.com/platers/obsidian-linter/issues/935
+      testName: 'Make sure we properly handle adding spaces to blockquote indicators instead of adding them to values that are not at the start of the line',
+      before: dedent`
+        > Using a C++ "member of pointer" operator: \`pointer->field\`
+        >\`>\`
+      `,
+      after: dedent`
+        > Using a C++ "member of pointer" operator: \`pointer->field\`
+        > \`>\`
+      `,
+      options: {style: 'space'},
+    },
+    { // accounts for https://github.com/platers/obsidian-linter/issues/935
+      testName: 'Make sure we properly handle removing spaces from blockquote indicators instead of removing them from values that are not at the start of the line',
+      before: dedent`
+        > Using a C++ "member of pointer" operator: \`pointer-> field\`
+        > \`> \`
+      `,
+      after: dedent`
+        >Using a C++ "member of pointer" operator: \`pointer-> field\`
+        >\`> \`
+      `,
+      options: {style: 'no space'},
+    },
+  ],
+});