Repository: platers/obsidian-linter (ts)

Issue:
Bug: Word "I" not Capitalized when First Letter Capitalization Used 
## Describe the Bug

"Headings should be formatted with capitalization" lowercases words that should always be capitalized when "First letter" is selected.

In practice, this may only mean the word "I".

## How to Reproduce

Steps to reproduce the behavior:

1. Write a heading such as `### What did I learn?`
2. Lint
3. The heading is changed to `### What did i learn?`

## Expected Behavior

No change, i.e. 

``` 
### What did I learn?
```

## Device

- [ ] Desktop
- [x] Mobile

## Additional Context

I realize I can add "I" to the list of ignored words, but I shouldn't have to.

Pull request description:
Fix Capitalization not Respecting Ignore Words for First Letter
Fixes #518 

Changes Made:
- Made sure that the logic for capitalizing headers only has to touch header lines and not each line in the file
- Made sure to reuse the logic for capitalization for first letter and title case
- Added a UT to hopefully not break this moving forward

Patch:
diff --git a/src/rules/capitalize-headings.ts b/src/rules/capitalize-headings.ts
index 216f20f..2523add 100644
--- a/src/rules/capitalize-headings.ts
+++ b/src/rules/capitalize-headings.ts
@@ -2,7 +2,7 @@ import {Options, RuleType} from '../rules';
 import RuleBuilder, {BooleanOptionBuilder, DropdownOptionBuilder, ExampleBuilder, OptionBuilderBase, TextAreaOptionBuilder} from './rule-builder';
 import dedent from 'ts-dedent';
 import {ignoreListOfTypes, IgnoreTypes} from '../utils/ignore-types';
-import {escapeDollarSigns, headerRegex, wordSplitterRegex} from '../utils/regex';
+import {allHeadersRegex, escapeDollarSigns, wordSplitterRegex} from '../utils/regex';
 
 type Style = 'Title Case' | 'ALL CAPS' | 'First letter';
 
@@ -16,6 +16,7 @@ class CapitalizeHeadingsOptions implements Options {
     'JavaScript',
     'TypeScript',
     'AppleScript',
+    'I',
   ];
   lowercaseWords?: string[] = [
     'via',
@@ -67,63 +68,41 @@ export default class CapitalizeHeadings extends RuleBuilder<CapitalizeHeadingsOp
   }
   apply(text: string, options: CapitalizeHeadingsOptions): string {
     return ignoreListOfTypes([IgnoreTypes.code, IgnoreTypes.yaml, IgnoreTypes.link, IgnoreTypes.wikiLink, IgnoreTypes.tag], text, (text) => {
-      const lines = text.split('\n');
-      for (let i = 0; i < lines.length; i++) {
-        const match = lines[i].match(headerRegex); // match only headings
-        if (!match) {
-          continue;
+      return text.replace(allHeadersRegex, (headerText: string) => {
+        if (options.style === 'ALL CAPS') {
+          return headerText.toUpperCase(); // convert full heading to uppercase
         }
-        switch (options.style) {
-          case 'Title Case': {
-            const headerWords = lines[i].match(/\S+/g);
-            // split by comma or whitespace
-            const keepCasing = options.ignoreWords;
-            const ignoreShortWords = options.lowercaseWords;
-            let firstWord = true;
-            for (let j = 1; j < headerWords.length; j++) {
-              const isWord = headerWords[j].match(/^[\p{L}'-]+[.?!,:;]?$/u);
-              if (!isWord) {
-                continue;
-              }
 
-              const ignoreCasedWord =
-                options.ignoreCasedWords &&
-                headerWords[j] !== headerWords[j].toLowerCase();
-              const keepWordCasing =
-                ignoreCasedWord || keepCasing.includes(headerWords[j]);
-              if (!keepWordCasing) {
-                headerWords[j] = headerWords[j].toLowerCase();
-                const ignoreWord = ignoreShortWords.includes(headerWords[j]);
-                if (!ignoreWord || firstWord === true) {
-                  // ignore words that are not capitalized in titles except if they are the first word
-                  headerWords[j] =
-                    headerWords[j][0].toUpperCase() + headerWords[j].slice(1);
-                }
-              }
+        const capitalizeJustFirstLetter = options.style === 'First letter';
+        // split by whitespace
+        const headerWords = headerText.match(/\S+/g);
+        const keepCasing = options.ignoreWords;
+        const ignoreShortWords = options.lowercaseWords;
+        let firstWord = true;
+        for (let j = 1; j < headerWords.length; j++) {
+          // based on https://stackoverflow.com/a/62032796 "/\p{L}/u" accounts for all unicode letters across languages
+          const isWord = headerWords[j].match(/^[\p{L}'-]{1,}[.?!,:;]?$/u);
+          if (!isWord) {
+            continue;
+          }
 
-              firstWord = false;
+          const ignoreCasedWord = (options.ignoreCasedWords && !capitalizeJustFirstLetter) && headerWords[j] !== headerWords[j].toLowerCase();
+          const keepWordCasing = ignoreCasedWord || keepCasing.includes(headerWords[j]);
+          if (!keepWordCasing) {
+            headerWords[j] = headerWords[j].toLowerCase();
+            const ignoreWord = ignoreShortWords.includes(headerWords[j]);
+            if ((!ignoreWord && !capitalizeJustFirstLetter) || firstWord === true) {
+              headerWords[j] = headerWords[j][0].toUpperCase() + headerWords[j].slice(1);
             }
-
-            lines[i] = lines[i].replace(
-                headerRegex,
-                escapeDollarSigns(`${headerWords.join(' ')}`),
-            );
-            break;
           }
-          case 'ALL CAPS':
-            lines[i] = lines[i].toUpperCase(); // convert full heading to uppercase
-            break;
-          case 'First letter':
-            // based on https://stackoverflow.com/a/62032796 "/\p{L}/u" accounts for all unicode letters across languages
-            lines[i] = lines[i]
-                .toLowerCase()
-                .replace(/\s+([\p{L}])/u, (lowerCaseMatch: string) => {
-                  return lowerCaseMatch.toUpperCase();
-                }); // capitalize first letter of heading
-            break;
+
+          firstWord = false;
         }
-      }
-      return lines.join('\n');
+
+        headerText = escapeDollarSigns(`${headerWords.join(' ')}`);
+
+        return headerText;
+      });
     });
   }
   get exampleBuilders(): ExampleBuilder<CapitalizeHeadingsOptions>[] {


Test patch:
diff --git a/__tests__/capitalize-headings.test.ts b/__tests__/capitalize-headings.test.ts
index 34c3832..62d7f95 100644
--- a/__tests__/capitalize-headings.test.ts
+++ b/__tests__/capitalize-headings.test.ts
@@ -125,5 +125,17 @@ ruleTest({
         style: 'First letter',
       },
     },
+    { // accounts for https://github.com/platers/obsidian-linter/issues/518
+      testName: `Make sure that 'I' is capitalized by default when First Letter is the style of the header `,
+      before: dedent`
+        ### It Is I
+      `,
+      after: dedent`
+        ### It is I
+      `,
+      options: {
+        style: 'First letter',
+      },
+    },
   ],
 });