Repository: platers/obsidian-linter (ts)

Issue:
Bug: paragraph-blank-lines only work on paragraphs started with English character
When I enable paragraph-blank-lines, this rule only works on paragraphs started with English characters.
#### bad case 1: paragraphs started with Chinese characters
Before:
```
测试 foo
测试 bar
```

After:
```
测试 foo
测试 bar
```
#### bad case 2: paragraphs started with number
Before:
```
123 foo
123 bar
```

After:
```
123 foo
123 bar
```
#### bad case 3: paragraphs started with whitespace
Before:
```
 foo
 bar
```

After:
```
 foo
 bar
```
#### good case
Before:
```
foo
测试 bar
```

After:
```
foo

测试 bar
```

Pull request description:
Handle More Complex Paragraphs for Paragraph Blank Lines
Fixes #250 

Added a more complex setup for handling paragraph content when trying to add blank lines before and after it.

Changes Made:
- added several new tests for the scenarios listed in the issue and for a couple more that could have been affected by the use of the paragraph parser
- Updated logic to replace simpler use of regex for a more complex function to handle scenarios from the issue

Patch:
diff --git a/src/rules.ts b/src/rules.ts
index f32fe0f..a781fbd 100644
--- a/src/rules.ts
+++ b/src/rules.ts
@@ -13,6 +13,7 @@ import {
   escapeYamlString,
   makeEmphasisOrBoldConsistent,
   addTwoSpacesAtEndOfLinesFollowedByAnotherLineOfTextContent,
+  makeSureThereIsOnlyOneBlankLineBeforeAndAfterParagraphs,
   removeSpacesInLinkText,
 } from './utils';
 import {
@@ -300,12 +301,7 @@ export const rules: Rule[] = [
       'All paragraphs should have exactly one blank line both before and after.',
       RuleType.SPACING,
       (text: string) => {
-        return ignoreCodeBlocksYAMLTagsAndLinks(text, (text) => {
-          text = text.replace(/\n+([a-zA-Z].*)/g, '\n\n$1'); // trim blank lines before
-          text = text.replace(/(^[a-zA-Z].*)\n+/gm, '$1\n\n'); // trim blank lines after
-          text = text.replace(/^\n+([a-zA-Z].*)/, '$1'); // remove blank lines before first line
-          return text;
-        });
+        return makeSureThereIsOnlyOneBlankLineBeforeAndAfterParagraphs(text);
       },
       [
         new Example(
diff --git a/src/utils.ts b/src/utils.ts
index d6c6625..aebabe8 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -190,6 +190,102 @@ export function addTwoSpacesAtEndOfLinesFollowedByAnotherLineOfTextContent(text:
   return text;
 }
 
+/**
+ * Makes sure that paragraphs have a single new line before and after them.
+ * @param {string} text The text to make sure that paragraphs have only 1 new line before and after them
+ * @return {string} The text with paragraphs with a single new line before and after them.
+ */
+export function makeSureThereIsOnlyOneBlankLineBeforeAndAfterParagraphs(text: string): string {
+  // added the placeholder for YAML in order to prevent the accidental misconstruing of the YAML as a paragraph when an array is in multiline form (i.e. it has a list for the array elements)
+  const yamlMatches = text.match(yamlRegex);
+  if (yamlMatches) {
+    text = text.replace(yamlMatches[0], escapeDollarSigns(yamlPlaceholder));
+  }
+
+  const positions: Position[] = getPositions('paragraph', text);
+  if (positions.length === 0) {
+    if (yamlMatches) {
+      text = text.replace(yamlPlaceholder, escapeDollarSigns(yamlMatches[0]));
+    }
+
+    return text;
+  }
+
+  for (const position of positions) {
+    // get index of previous new line character to get actual paragraph contents rather than just a snippet
+    let startIndex = position.start.offset;
+    if (startIndex > 0) {
+      startIndex--;
+    }
+
+    while (startIndex >= 0 && text.charAt(startIndex) != '\n') {
+      startIndex--;
+    }
+    startIndex++;
+
+    const paragraphLines = text.substring(startIndex, position.end.offset).split('\n');
+
+    // exclude list items and blockquotes
+    const firstLine = paragraphLines[0].trimStart();
+    if (firstLine.startsWith('> ') || firstLine.startsWith('>\t') || firstLine.startsWith('- ') || firstLine.startsWith('-\t') ||
+    firstLine.match(/^[0-9]+\.( |\t)+/)) {
+      continue;
+    }
+
+    const lineCount = paragraphLines.length;
+    const newParagraphLines: string[] = [];
+    let nextLineIsSameParagraph = false;
+    for (let i = 0; i < lineCount; i++) {
+      const paragraphLine = paragraphLines[i];
+
+      if (nextLineIsSameParagraph) {
+        const lastParagraphLineAdded = newParagraphLines.length-1;
+        newParagraphLines[lastParagraphLineAdded] += '\n' + paragraphLine;
+      } else {
+        newParagraphLines.push(paragraphLine);
+      }
+
+      // make sure that lines that end in <br>, <br/>, or two or more spaces are in the same paragraph
+      nextLineIsSameParagraph = paragraphLine.endsWith('<br>') || paragraphLine.endsWith('<br/>') || paragraphLine.endsWith('  ');
+    }
+
+    // remove new lines prior to paragraph
+    while (startIndex > 0 && text.charAt(startIndex-1) == '\n') {
+      startIndex--;
+    }
+
+    // remove new lines after paragraph
+    const textLength = text.length;
+    let endIndex = position.end.offset;
+    if (endIndex < textLength) {
+      endIndex++;
+    }
+
+    while (endIndex < textLength && text.charAt(endIndex) == '\n') {
+      endIndex++;
+    }
+
+    // make sure two new lines are only added between the paragraph and other content
+    let startNewLines = '\n\n';
+    if (startIndex == 0) {
+      startNewLines = '';
+    }
+
+    let endNewLines = '\n\n';
+    if (endIndex == textLength) {
+      endNewLines = '';
+    }
+
+    text = text.substring(0, startIndex) + startNewLines + newParagraphLines.join('\n\n') + endNewLines + text.substring(endIndex);
+  }
+
+  if (yamlMatches) {
+    text = text.replace(yamlPlaceholder, escapeDollarSigns(yamlMatches[0]));
+  }
+
+  return text;
+}
+
 
 /**
  * Removes spaces before and after link text


Test patch:
diff --git a/src/test.ts b/src/test.ts
index e567fb6..7269283 100644
--- a/src/test.ts
+++ b/src/test.ts
@@ -343,6 +343,123 @@ describe('Paragraph blank lines', () => {
     `;
     expect(rulesDict['paragraph-blank-lines'].apply(before)).toBe(after);
   });
+  // accounts for https://github.com/platers/obsidian-linter/issues/250
+  it('Paragraphs that start with numbers are spaced out', () => {
+    const before = dedent`
+    # Hello world
+
+
+    123 foo
+    123 bar
+    `;
+
+    const after = dedent`
+    # Hello world
+
+    123 foo
+
+    123 bar
+    `;
+
+    expect(rulesDict['paragraph-blank-lines'].apply(before)).toBe(after);
+  });
+  // accounts for https://github.com/platers/obsidian-linter/issues/250
+  it('Paragraphs that start with foreign characters are spaced out', () => {
+    const before = dedent`
+    # Hello world
+
+
+    测试 foo
+    测试 bar
+    `;
+
+    const after = dedent`
+    # Hello world
+
+    测试 foo
+
+    测试 bar
+    `;
+
+    expect(rulesDict['paragraph-blank-lines'].apply(before)).toBe(after);
+  });
+  // accounts for https://github.com/platers/obsidian-linter/issues/250
+  it('Paragraphs that start with whitespace characters are spaced out', () => {
+    const before = dedent`
+    # Hello world
+
+     foo
+     bar
+    `;
+
+    const after = dedent`
+    # Hello world
+
+     foo
+
+     bar
+    `;
+
+    expect(rulesDict['paragraph-blank-lines'].apply(before)).toBe(after);
+  });
+  it('Make sure blockquotes are not affected', () => {
+    const before = dedent`
+    # Hello world
+
+    > blockquote
+    > blockquote line 2
+    `;
+
+    const after = dedent`
+    # Hello world
+
+    > blockquote
+    > blockquote line 2
+    `;
+
+    expect(rulesDict['paragraph-blank-lines'].apply(before)).toBe(after);
+  });
+  it('Make sure lists are not affected', () => {
+    const before = dedent`
+    # Hello world
+
+    > blockquote
+    > blockquote line 2
+    `;
+
+    const after = dedent`
+    # Hello world
+
+    > blockquote
+    > blockquote line 2
+    `;
+
+    expect(rulesDict['paragraph-blank-lines'].apply(before)).toBe(after);
+  });
+  it('Make sure lines ending in a line break are not affected', () => {
+    const before = dedent`
+    # Hello world
+
+
+    paragraph line 1 <br>
+    paragraph line 2  
+    paragraph line 3 <br/>
+    paragraph final line
+
+
+    `;
+
+    const after = dedent`
+    # Hello world
+
+    paragraph line 1 <br>
+    paragraph line 2  
+    paragraph line 3 <br/>
+    paragraph final line
+    `;
+
+    expect(rulesDict['paragraph-blank-lines'].apply(before)).toBe(after);
+  });
 });
 describe('Consecutive blank lines', () => {
   it('Handles ignores code blocks', () => {