Repository: platers/obsidian-linter (ts)

Issue:
FR: Add Support for Custom Auto-Correct Misspellings
## Is Your Feature Request Related to a Problem? Please Describe.

I would like to add in custom auto-correct misspellings for things that might be specific to me (i.e. misspelling PowerShell as Powershell) but might be too frivolous to request adding to the main auto-correct map located here [Auto-correct map.ts](https://github.com/platers/obsidian-linter/tree/master/src/utils/auto-correct-misspellings.ts)

## Describe the Solution You'd Like

A new input box similar to "Ignore Words" in the Content tab which allows me to enter in custom autocorrect words. 
I'd be happy with any format the devs choose, but I think using an array format like below makes most sense.
```
['Powershell'],['PowerShell'], 
['diahorrea', 'diarrhea']
```

**Please include an example where applicable:**

*Current example*
``` markdown
You'll need: [A supported version of Windows](https://docs.chocolatey.org/en-us/chocolatey-components-dependencies-and-support-lifecycle#supported-windows-versions)
Powershell 2.0 or higher
	Powershell 3.0 
```

*Corrected example*
``` markdown
You'll need: [A supported version of Windows](https://docs.chocolatey.org/en-us/chocolatey-components-dependencies-and-support-lifecycle#supported-windows-versions)
PowerShell 2.0 or higher
	PowerShell 3.0 
```

## Describe Alternatives You've Considered

I tried using the plugin Completr, but found it to be excessive for my needs.

## Additional Context

I have included a mockup of the UI element for your consideration.
<img width="643" alt="image" src="https://github.com/platers/obsidian-linter/assets/143239044/994af400-af01-4849-8d12-75b77f6718b3">



Pull request description:
Add Custom Auto-Correct Options
Fixes #866 

There is a desire to have the ability to specify a list of files to get custom replacement rules from.

This will be a basic implementation without a lot of bells and whistles. It can be improved upon in the future as well.

## Expected Format of Custom Replacements

You can have as many tables in a file as you would like. So long as they only have 2 columns. The expectation would be that you have something like the following as the input table:
``` markdown
| Header 1 | Header 2 |
| ------------ | --------------- |
| th | the |
| hw | how |
```

This would get interpreted as
`th` gets converted to `the` or `The` and `hw` gets converted to `how` or `How` (first letter in the replace word being capitalized where found results in a capital first letter in the word that will replace it).

## Limitations

With this being an early implementation to get something out there that is usable for users, there are some limitations to how things work:
- The list of custom replacements is only loaded when the plugin initially loads or when the file is added to the list of files that include custom replacements
  - This means that making a change to a file that is already in the list of files to get custom replacement information from will not work without reloading the Linter
- There is no way to specify that a word is to always be capitalized
  - This is due to how the auto-correct rule was designed, but this may be feasible to handle down the road

## Things Left to Do

- [x] Move new logs to `en.ts`/localization
- [x] Add documentation for use so users can more easily determine how this feature is to be used
- [x] Cleanup ghost code and other logic
- [x] Add UTs for the parser
- [x] Add UTs for handling custom replacements for the rule
- [x] Add modal to show words in a file


Patch:
diff --git a/__mocks__/obsidian.ts b/__mocks__/obsidian.ts
index e6bb2ed..50bc4fb 100644
--- a/__mocks__/obsidian.ts
+++ b/__mocks__/obsidian.ts
@@ -2,3 +2,6 @@
 import moment from 'moment';
 
 export {moment as moment};
+
+// Needed to make sure that auto-correct tests work due to the auto-correct option using a modal in some scenarios
+export class Modal {}
diff --git a/docs/additional-info/rules/auto-correct-common-misspellings.md b/docs/additional-info/rules/auto-correct-common-misspellings.md
new file mode 100644
index 0000000..1161e3a
--- /dev/null
+++ b/docs/additional-info/rules/auto-correct-common-misspellings.md
@@ -0,0 +1,32 @@
+#### How to Use Custom Misspellings
+
+There is a default list of common misspellings that is used as the base for how this rule works.
+However, there may be instances where the user may want to add their own list of misspellings to handle.
+In those scenarios, they can add files to the list of files that have custom misspellings in them.
+
+##### Format
+
+A file that has custom misspellings in them can have any content in them. But the only content that will
+be parsed as custom misspellings should be found in a two column table. For example the following table
+will result in `th` being replaced with `the` and `tht` being replaced with `that`:
+
+``` markdown
+The following is a table with custom misspellings:
+| Replace | With |
+| ------- | ---- |
+| th | the |
+| tht | that |
+```
+
+!!! Note
+    The first two lines of the table are skipped (the header and separator) and all rows after that
+    must start and end with a pipe (`|`). If any do not start or end with a pipe or they have more
+    than 2 columns, then they will be skipped.
+
+##### Current Limitations
+
+
+- The list of custom replacements is only loaded when the plugin initially loads or when the file is added to the list of files that include custom misspellings
+    * This means that making a change to a file that is already in the list of custom misspelling files will not work unless the Linter is reloaded or the file is removed and re-added to the list of custom misspelling files
+- There is no way to specify that a word is to always be capitalized
+    - This is due to how the auto-correct rule was designed as it sets the first letter of the replacement word to the case of the first letter of the word being replaced
diff --git a/docs/docs/settings/content-rules.md b/docs/docs/settings/content-rules.md
index b551962..fd9a1bb 100644
--- a/docs/docs/settings/content-rules.md
+++ b/docs/docs/settings/content-rules.md
@@ -15,7 +15,43 @@ Uses a dictionary of common misspellings to automatically convert them to their
 | Name | Description | List Items | Default Value |
 | ---- | ----------- | ---------- | ------------- |
 | `Ignore Words` | A comma separated list of lowercased words to ignore when auto-correcting | N/A |  |
+| `Extra Auto-Correct Source Files` | These are files that have a markdown table in them that have the initial word and the word to correct it to (these are case insensitive corrections). **Note: the tables used should have the starting and ending `|` indicators present for each line.** | N/A |  |
 
+### Additional Info
+
+
+#### How to Use Custom Misspellings
+
+There is a default list of common misspellings that is used as the base for how this rule works.
+However, there may be instances where the user may want to add their own list of misspellings to handle.
+In those scenarios, they can add files to the list of files that have custom misspellings in them.
+
+##### Format
+
+A file that has custom misspellings in them can have any content in them. But the only content that will
+be parsed as custom misspellings should be found in a two column table. For example the following table
+will result in `th` being replaced with `the` and `tht` being replaced with `that`:
+
+``` markdown
+The following is a table with custom misspellings:
+| Replace | With |
+| ------- | ---- |
+| th | the |
+| tht | that |
+```
+
+!!! Note
+    The first two lines of the table are skipped (the header and separator) and all rows after that
+    must start and end with a pipe (`|`). If any do not start or end with a pipe or they have more
+    than 2 columns, then they will be skipped.
+
+##### Current Limitations
+
+
+- The list of custom replacements is only loaded when the plugin initially loads or when the file is added to the list of files that include custom misspellings
+    * This means that making a change to a file that is already in the list of custom misspelling files will not work unless the Linter is reloaded or the file is removed and re-added to the list of custom misspelling files
+- There is no way to specify that a word is to always be capitalized
+    - This is due to how the auto-correct rule was designed as it sets the first letter of the replacement word to the case of the first letter of the word being replaced
 
 
 ### Examples
diff --git a/esbuild.config.mjs b/esbuild.config.mjs
index 87228e5..958d373 100644
--- a/esbuild.config.mjs
+++ b/esbuild.config.mjs
@@ -28,6 +28,16 @@ const mockedPlugins = [replace({
     'import {Setting} from \'obsidian\';': '',
     // remove the use of obsidian in settings helper to allow for docs.js to run
     'import {Component, MarkdownRenderer} from \'obsidian\';': '',
+    // remove the use of obsidian in the auto-correct files picker to allow for docs.js to run
+    'import {Setting, Component, App, TFile, normalizePath, ExtraButtonComponent} from \'obsidian\';': '',
+    // remove the use of obsidian in add custom row to allow for docs.js to run
+    'import {Component, Setting} from \'obsidian\';': '',
+    // remove the use of obsidian in suggest to allow for docs.js to run
+    'import {App, ISuggestOwner, Scope} from \'obsidian\';': '',
+    // remove the use of obsidian in md file suggester to allow for docs.js to run
+    'import {App, TFile} from \'obsidian\';': '',
+    // remove the use of obsidian in parse results modal to allow for docs.js to run
+    'import {Modal, App} from \'obsidian\';': 'class Modal {}',
   },
   delimiters: ['', ''],
 })];
diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts
index eec6421..c6af694 100644
--- a/src/lang/locale/en.ts
+++ b/src/lang/locale/en.ts
@@ -104,9 +104,18 @@ export default {
   'warning-text': 'Warning',
   'file-backup-text': 'Make sure you have backed up your files.',
   'custom-command-warning': 'Linting multiple files with custom commands enabled is a slow process that requires the ability to open panes in the side panel. It is noticeably slower than running without custom commands enabled. Please proceed with caution.',
+  'cancel-button-text': 'Cancel',
 
   'copy-aria-label': 'Copy',
 
+  // parse-results-modal.ts
+  'parse-results-heading-text': 'Custom Parse Values',
+  'file-parse-description-text': 'The following is the list of custom replacements found in {FILE}.',
+  'no-parsed-values-found-text': 'There were no custom replacements found in {FILE}. Please make sure that all tables with custom replacements in {FILE} only have two columns and all rows start and end with a pipe (i.e. |).',
+  'find-header-text': 'Word to Find',
+  'replace-header-text': 'Replacement Word',
+  'close-button-text': 'Close',
+
   'tabs': {
     'names': {
       // tab.ts
@@ -231,6 +240,13 @@ export default {
       'move-down-tooltip': 'Move down',
       'delete-tooltip': 'Delete',
     },
+    'custom-auto-correct': {
+      'delete-tooltip': 'Delete',
+      'show-parsed-contents-tooltip': 'View parsed replacements',
+      'custom-row-parse-warning': '"{ROW}" is not a valid row with custom replacements. It must have only 2 columns.',
+      'file-search-placeholder-text': 'File name',
+      'add-input-button-text': 'Add another custom file',
+    },
   },
 
   // rules
@@ -243,6 +259,10 @@ export default {
         'name': 'Ignore Words',
         'description': 'A comma separated list of lowercased words to ignore when auto-correcting',
       },
+      'extra-auto-correct-files': {
+        'name': 'Extra Auto-Correct Source Files',
+        'description': 'These are files that have a markdown table in them that have the initial word and the word to correct it to (these are case insensitive corrections). **Note: the tables used should have the starting and ending `|` indicators present for each line.**',
+      },
     },
     // add-blank-line-after-yaml.ts
     'add-blank-line-after-yaml': {
diff --git a/src/main.ts b/src/main.ts
index 677560b..5310eb7 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,8 +1,8 @@
-import {App, Editor, EventRef, MarkdownView, Menu, Notice, Plugin, TAbstractFile, TFile, TFolder, addIcon, htmlToMarkdown, EditorSelection, EditorChange, MarkdownViewModeType, getFrontMatterInfo, FileView} from 'obsidian';
+import {App, Editor, EventRef, MarkdownView, Menu, Notice, Plugin, TAbstractFile, TFile, TFolder, addIcon, htmlToMarkdown, EditorSelection, EditorChange, MarkdownViewModeType, getFrontMatterInfo, FileView, normalizePath} from 'obsidian';
 import {Options, RuleType, ruleTypeToRules, rules} from './rules';
 import DiffMatchPatch from 'diff-match-patch';
 import dedent from 'ts-dedent';
-import {stripCr} from './utils/strings';
+import {parseCustomReplacements, stripCr} from './utils/strings';
 import {logInfo, logError, logDebug, setLogLevel, logWarn, setCollectLogs, clearLogs, convertNumberToLogLevel} from './utils/logger';
 import {moment} from 'obsidian';
 import './rules-registry';
@@ -17,6 +17,7 @@ import {RuleAliasSuggest} from './cm6/rule-alias-suggester';
 import {DEFAULT_SETTINGS, LinterSettings} from './settings-data';
 import AsyncLock from 'async-lock';
 import {warn} from 'loglevel';
+import {CustomAutoCorrectContent} from './ui/linter-components/auto-correct-files-picker-option';
 
 // https://github.com/liamcain/obsidian-calendar-ui/blob/03ceecbf6d88ef260dadf223ee5e483d98d24ffc/src/localization.ts#L20-L43
 const langToMomentLocale = {
@@ -138,6 +139,16 @@ export default class LinterPlugin extends Plugin {
       }
     }
 
+    // load the custom replacements since they are not stored in the data.json when the settings data is saved
+    for (const replacementFileInfo of this.settings.ruleConfigs['auto-correct-common-misspellings']['extra-auto-correct-files'] as CustomAutoCorrectContent[]) {
+      if (replacementFileInfo.filePath != '') {
+        const file = this.getFileFromPath(replacementFileInfo.filePath);
+        if (file) {
+          replacementFileInfo.customReplacements = parseCustomReplacements(stripCr(await this.app.vault.cachedRead(file)));
+        }
+      }
+    }
+
     this.updatePasteOverrideStatus();
     this.updateHasCustomCommandStatus();
   }
@@ -869,4 +880,13 @@ export default class LinterPlugin extends Plugin {
     const lines = doc.split('\n');
     return {line: lines.length - 1, ch: lines[lines.length - 1].length};
   }
+
+  private getFileFromPath(filePath: string): TFile {
+    const file = this.app.vault.getAbstractFileByPath(normalizePath(filePath));
+    if (file instanceof TFile) {
+      return file;
+    }
+
+    return null;
+  }
 }
diff --git a/src/option.ts b/src/option.ts
index c5e01d3..9065a31 100644
--- a/src/option.ts
+++ b/src/option.ts
@@ -3,6 +3,7 @@ import {getTextInLanguage, LanguageStringKey} from './lang/helpers';
 import LinterPlugin from './main';
 import {parseTextToHTMLWithoutOuterParagraph} from './ui/helpers';
 import {LinterSettings} from './settings-data';
+import {AutoCorrectFilesPickerOption} from './ui/linter-components/auto-correct-files-picker-option';
 
 export type SearchOptionInfo = {name: string, description: string, options?: DropdownRecord[]}
 
@@ -171,3 +172,18 @@ export class DropdownOption extends Option {
     this.parseNameAndDescriptionAndRemoveSettingBorder(setting, plugin);
   }
 }
+
+
+export class MdFilePickerOption extends Option {
+  constructor(configKey: string, nameKey: LanguageStringKey, descriptionKey: LanguageStringKey, ruleAlias?: string | null) {
+    super(configKey, nameKey, descriptionKey, [], ruleAlias);
+  }
+
+  public display(containerEl: HTMLElement, settings: LinterSettings, plugin: LinterPlugin): void {
+    settings.ruleConfigs[this.ruleAlias][this.configKey] = settings.ruleConfigs[this.ruleAlias][this.configKey] ?? [];
+
+    new AutoCorrectFilesPickerOption(containerEl, plugin.settingsTab.component, settings.ruleConfigs[this.ruleAlias][this.configKey], plugin.app, () => {
+      plugin.saveSettings();
+    }, this.nameKey, this.descriptionKey);
+  }
+}
diff --git a/src/rules/auto-correct-common-misspellings.ts b/src/rules/auto-correct-common-misspellings.ts
index 6602e1d..bd2203d 100644
--- a/src/rules/auto-correct-common-misspellings.ts
+++ b/src/rules/auto-correct-common-misspellings.ts
@@ -1,12 +1,14 @@
 import {IgnoreTypes} from '../utils/ignore-types';
 import {Options, RuleType} from '../rules';
-import RuleBuilder, {ExampleBuilder, OptionBuilderBase, TextAreaOptionBuilder} from './rule-builder';
+import RuleBuilder, {ExampleBuilder, MdFilePickerOptionBuilder, OptionBuilderBase, TextAreaOptionBuilder} from './rule-builder';
 import dedent from 'ts-dedent';
 import {misspellingToCorrection} from '../utils/auto-correct-misspellings';
 import {wordRegex, wordSplitterRegex} from '../utils/regex';
+import {CustomAutoCorrectContent} from '../ui/linter-components/auto-correct-files-picker-option';
 
 class AutoCorrectCommonMisspellingsOptions implements Options {
   ignoreWords?: string[] = [];
+  extraAutoCorrectFiles?: CustomAutoCorrectContent[] = [];
 }
 
 @RuleBuilder.register
@@ -27,16 +29,30 @@ export default class AutoCorrectCommonMisspellings extends RuleBuilder<AutoCorre
   }
   replaceWordWithCorrectCasing(word: string, options: AutoCorrectCommonMisspellingsOptions): string {
     const lowercasedWord = word.toLowerCase();
-    if (!misspellingToCorrection.has(lowercasedWord) || options.ignoreWords.includes(lowercasedWord)) {
+    if (options.ignoreWords.includes(lowercasedWord)) {
       return word;
     }
 
-    let correctedWord = misspellingToCorrection.get(lowercasedWord);
-    if (word.charAt(0) == word.charAt(0).toUpperCase()) {
-      correctedWord = correctedWord.charAt(0).toUpperCase() + correctedWord.substring(1);
+    if (misspellingToCorrection.has(lowercasedWord)) {
+      return this.determineCorrectedWord(word, misspellingToCorrection.get(lowercasedWord));
     }
 
-    return correctedWord;
+    if (options.extraAutoCorrectFiles) {
+      for (let i = 0; i < options.extraAutoCorrectFiles.length; i++) {
+        if (options.extraAutoCorrectFiles[i].customReplacements?.has(lowercasedWord)) {
+          return this.determineCorrectedWord(word, options.extraAutoCorrectFiles[i].customReplacements?.get(lowercasedWord));
+        }
+      }
+    }
+
+    return word;
+  }
+  determineCorrectedWord(originalWord: string, replacement: string): string {
+    if (originalWord.charAt(0) == originalWord.charAt(0).toUpperCase()) {
+      replacement = replacement.charAt(0).toUpperCase() + replacement.substring(1);
+    }
+
+    return replacement;
   }
   get exampleBuilders(): ExampleBuilder<AutoCorrectCommonMisspellingsOptions>[] {
     return [
@@ -115,6 +131,13 @@ export default class AutoCorrectCommonMisspellings extends RuleBuilder<AutoCorre
         splitter: wordSplitterRegex,
         separator: ', ',
       }),
+      new MdFilePickerOptionBuilder({
+        OptionsClass: AutoCorrectCommonMisspellingsOptions,
+        nameKey: 'rules.auto-correct-common-misspellings.extra-auto-correct-files.name',
+        descriptionKey: 'rules.auto-correct-common-misspellings.extra-auto-correct-files.description',
+        // @ts-expect-error since it looks like there is an issue with the types here
+        optionsKey: 'extraAutoCorrectFiles',
+      }),
     ];
   }
 }
diff --git a/src/rules/rule-builder.ts b/src/rules/rule-builder.ts
index 2609132..8269574 100644
--- a/src/rules/rule-builder.ts
+++ b/src/rules/rule-builder.ts
@@ -1,5 +1,5 @@
 import {Example, Options, Rule, RuleType, registerRule, wrapLintError} from '../rules';
-import {BooleanOption, DropdownOption, DropdownRecord, MomentFormatOption, Option, TextAreaOption, TextOption} from '../option';
+import {BooleanOption, DropdownOption, DropdownRecord, MdFilePickerOption, MomentFormatOption, Option, TextAreaOption, TextOption} from '../option';
 import {logDebug, timingBegin, timingEnd} from '../utils/logger';
 import {getTextInLanguage, LanguageStringKey} from '../lang/helpers';
 import {IgnoreType, IgnoreTypes} from '../utils/ignore-types';
@@ -294,3 +294,9 @@ export class MomentFormatOptionBuilder<TOptions extends Options> extends OptionB
     return new MomentFormatOption(this.configKey, this.nameKey, this.descriptionKey, this.defaultValue);
   }
 }
+
+export class MdFilePickerOptionBuilder<TOptions extends Options> extends OptionBuilder<TOptions, string[]> {
+  protected buildOption(): Option {
+    return new MdFilePickerOption(this.configKey, this.nameKey, this.descriptionKey);
+  }
+}
diff --git a/src/ui/linter-components/auto-correct-files-picker-option.ts b/src/ui/linter-components/auto-correct-files-picker-option.ts
new file mode 100644
index 0000000..9bea7cb
--- /dev/null
+++ b/src/ui/linter-components/auto-correct-files-picker-option.ts
@@ -0,0 +1,113 @@
+import {Setting, Component, App, TFile, normalizePath, ExtraButtonComponent} from 'obsidian';
+import {LanguageStringKey, getTextInLanguage} from '../../lang/helpers';
+import {AddCustomRow} from '../components/add-custom-row';
+import MdFileSuggester from '../suggesters/md-file-suggester';
+import {parseCustomReplacements, stripCr} from '../../utils/strings';
+import {ParseResultsModal} from '../modals/parse-results-modal';
+
+export type CustomAutoCorrectContent = {filePath: string, customReplacements: Map<string, string>};
+
+export class AutoCorrectFilesPickerOption extends AddCustomRow {
+  private selectedFiles: string[] = [];
+
+  constructor(containerEl: HTMLElement, parentComponent: Component, public filesPicked: CustomAutoCorrectContent[], private app: App, saveSettings: () => void, name: LanguageStringKey, description: LanguageStringKey) {
+    super(
+        containerEl,
+        parentComponent,
+        getTextInLanguage(name),
+        getTextInLanguage(description),
+        null,
+        getTextInLanguage('options.custom-auto-correct.add-input-button-text'),
+        saveSettings,
+        ()=>{
+          this.selectedFiles = [];
+          for (const filePicked of this.filesPicked) {
+            this.selectedFiles.push(filePicked.filePath);
+          }
+
+          const newPickedFile: CustomAutoCorrectContent = {filePath: '', customReplacements: null};
+          this.filesPicked.push(newPickedFile);
+          this.selectedFiles.push('');
+          this.saveSettings();
+          this.addPickedFile(newPickedFile, this.filesPicked.length - 1, true);
+        });
+    this.display();
+    this.inputElDiv.addClass('linter-folder-ignore-container');
+  }
+
+  protected showInputEls(): void {
+    this.filesPicked.forEach((pickedFile, index) => {
+      this.addPickedFile(pickedFile, index);
+    });
+  }
+
+  private addPickedFile(pickedFile: CustomAutoCorrectContent, index: number, focusOnCommand: boolean = false) {
+    let showCustomParseContentButton: ExtraButtonComponent;
+    const setting = new Setting(this.inputElDiv)
+        .addSearch((cb) => {
+          new MdFileSuggester(this.app, cb.inputEl, this.selectedFiles);
+          cb.setPlaceholder(getTextInLanguage('options.custom-auto-correct.file-search-placeholder-text'))
+              .setValue(pickedFile.filePath)
+              .onChange(async (newPickedFile) => {
+                const customReplacementFile = newPickedFile;
+
+                if (customReplacementFile === '' || customReplacementFile === cb.inputEl.getAttribute('fileName')) {
+                  const file = this.getFileFromPath(customReplacementFile);
+                  pickedFile.filePath = customReplacementFile;
+                  if (file) {
+                    pickedFile.customReplacements = parseCustomReplacements(stripCr(await this.app.vault.read(file)));
+                    showCustomParseContentButton.disabled = false;
+                    showCustomParseContentButton.extraSettingsEl.addClass('clickable-icon');
+                  } else {
+                    showCustomParseContentButton.disabled = true;
+                    pickedFile.customReplacements = null;
+                    showCustomParseContentButton.extraSettingsEl.removeClass('clickable-icon');
+                  }
+
+                  this.filesPicked[index] = pickedFile;
+                  this.saveSettings();
+                }
+              });
+
+          cb.inputEl.setAttr('tabIndex', index);
+          cb.inputEl.addClass('linter-folder-ignore');
+
+          if (focusOnCommand) {
+            cb.inputEl.focus();
+          }
+        })
+        .addExtraButton((cb) => {
+          showCustomParseContentButton = cb;
+          cb.setIcon('info')
+              .setTooltip(getTextInLanguage('options.custom-auto-correct.show-parsed-contents-tooltip'))
+              .onClick(() => {
+                new ParseResultsModal(this.app, pickedFile).open();
+              });
+
+          if (pickedFile.filePath === '') {
+            cb.disabled = true;
+            cb.extraSettingsEl.removeClass('clickable-icon');
+          }
+        })
+        .addExtraButton((cb) => {
+          cb.setIcon('cross')
+              .setTooltip(getTextInLanguage('options.custom-auto-correct.delete-tooltip'))
+              .onClick(() => {
+                this.filesPicked.splice(index, 1);
+                this.saveSettings();
+                this.resetInputEls();
+              });
+        });
+
+    setting.settingEl.addClass('linter-no-border');
+  }
+
+  private getFileFromPath(filePath: string): TFile {
+    const file = this.app.vault.getAbstractFileByPath(normalizePath(filePath));
+    if (file instanceof TFile) {
+      return file;
+    }
+
+    return null;
+  }
+}
diff --git a/src/ui/modals/lint-confirmation-modal.ts b/src/ui/modals/lint-confirmation-modal.ts
index 42ce24b..8cdfa24 100644
--- a/src/ui/modals/lint-confirmation-modal.ts
+++ b/src/ui/modals/lint-confirmation-modal.ts
@@ -18,7 +18,7 @@ export class LintConfirmationModal extends Modal {
         {text: startModalMessageText + ' ' + getTextInLanguage('file-backup-text')}).id = 'confirm-dialog';
 
     this.contentEl.createDiv('modal-button-container', (buttonsEl) => {
-      buttonsEl.createEl('button', {text: 'Cancel'}).addEventListener('click', () => this.close());
+      buttonsEl.createEl('button', {text: getTextInLanguage('cancel-button-text')}).addEventListener('click', () => this.close());
 
       const btnSubmit = buttonsEl.createEl('button', {
         attr: {type: 'submit'},
diff --git a/src/ui/modals/parse-results-modal.ts b/src/ui/modals/parse-results-modal.ts
new file mode 100644
index 0000000..ac9ccd3
--- /dev/null
+++ b/src/ui/modals/parse-results-modal.ts
@@ -0,0 +1,36 @@
+import {Modal, App} from 'obsidian';
+import {getTextInLanguage} from '../../lang/helpers';
+import {CustomAutoCorrectContent} from '../linter-components/auto-correct-files-picker-option';
+
+// https://github.com/nothingislost/obsidian-workspaces-plus/blob/bbba928ec64b30b8dec7fe8fc9e5d2d96543f1f3/src/modal.ts#L68
+export class ParseResultsModal extends Modal {
+  constructor(app: App, customAutoCorrectionInfo: CustomAutoCorrectContent) {
+    super(app);
+    this.modalEl.addClass('confirm-modal');
+
+    this.contentEl.createEl('h3', {text: getTextInLanguage('parse-results-heading-text')}).style.textAlign = 'center';
+
+    if (customAutoCorrectionInfo?.customReplacements.size > 0) {
+      this.contentEl.createEl('p', {text: getTextInLanguage('file-parse-description-text').replace('{FILE}', customAutoCorrectionInfo.filePath)}).id = 'confirm-dialog';
+
+      const tableEl = this.contentEl.createDiv().createEl('table', {cls: 'markdown-rendered'});
+      const tableHeaderEl = tableEl.createEl('tr');
+      tableHeaderEl.createEl('th', {text: getTextInLanguage('find-header-text')});
+      tableHeaderEl.createEl('th', {text: getTextInLanguage('replace-header-text')});
+
+      let tableRow: HTMLElement;
+      for (const replacementInfo of customAutoCorrectionInfo.customReplacements) {
+        tableRow = tableEl.createEl('tr');
+        tableRow.createEl('td', {text: replacementInfo[0]});
+        tableRow.createEl('td', {text: replacementInfo[1]});
+      }
+    } else {
+      this.contentEl.createEl('p', {text: getTextInLanguage('no-parsed-values-found-text').replaceAll('{FILE}', customAutoCorrectionInfo.filePath)}).style.fontWeight = 'bold';
+    }
+
+    this.contentEl.createDiv('modal-button-container', (buttonsEl) => {
+      buttonsEl.createEl('button', {text: getTextInLanguage('close-button-text')}).addEventListener('click', () => this.close());
+    });
+  }
+}
+
diff --git a/src/ui/suggesters/folder-suggester.ts b/src/ui/suggesters/folder-suggester.ts
index 47585cd..6fb6486 100644
--- a/src/ui/suggesters/folder-suggester.ts
+++ b/src/ui/suggesters/folder-suggester.ts
@@ -17,7 +17,7 @@ export default class FolderSuggester extends TextInputSuggest<string> {
     }
 
     const nonSelectedFolders = all_folders.filter((el: string) => {
-      return !this.valuesToExclude.includes(el) || el === this.inputEl.getAttribute('folderExists');
+      return !this.valuesToExclude.includes(el) || el === this.inputEl.getAttribute('folderName');
     });
 
     const folders: string[] = [];
diff --git a/src/ui/suggesters/md-file-suggester.ts b/src/ui/suggesters/md-file-suggester.ts
new file mode 100644
index 0000000..26da7a3
--- /dev/null
+++ b/src/ui/suggesters/md-file-suggester.ts
@@ -0,0 +1,44 @@
+import {TextInputSuggest} from './suggest';
+import {App, TFile} from 'obsidian';
+
+export default class MdFileSuggester extends TextInputSuggest<string> {
+  constructor(
+    public app: App,
+    public inputEl: HTMLInputElement,
+    public valuesToExclude: string[] = [],
+  ) {
+    super(app, inputEl);
+  }
+
+  getSuggestions(input_str: string): string[] {
+    const all_md_files = this.app.vault.getAllLoadedFiles().filter((f) => f instanceof TFile && f.path.endsWith('.md')).map((f) => f.path);
+    if (!all_md_files) {
+      return [];
+    }
+
+    const nonSelectedMdFiles = all_md_files.filter((el: string) => {
+      return !this.valuesToExclude.includes(el) || el === this.inputEl.getAttribute('fileName');
+    });
+
+    const files: string[] = [];
+    const lower_input_str = input_str.toLowerCase();
+    nonSelectedMdFiles.forEach((folderPath: string) => {
+      if (folderPath.toLowerCase().contains(lower_input_str)) {
+        files.push(folderPath);
+      }
+    });
+
+    return files;
+  }
+
+  renderSuggestion(filePath: string, el: HTMLElement): void {
+    el.setText(filePath);
+  }
+
+  selectSuggestion(filePath: string): void {
+    this.inputEl.setAttribute('fileName', filePath);
+    this.inputEl.value = filePath;
+    this.inputEl.trigger('input');
+    this.close();
+  }
+}
diff --git a/src/utils/strings.ts b/src/utils/strings.ts
index e6cc13b..ff050ce 100644
--- a/src/utils/strings.ts
+++ b/src/utils/strings.ts
@@ -1,4 +1,7 @@
 import {calloutRegex, codeBlockBlockquoteRegex} from './regex';
+import {getTextInLanguage} from '../lang/helpers';
+import {logWarn} from './logger';
+import {getAllTablesInText} from './mdast';
 /**
  * Inserts a string at the given position in a string.
  * @param {string} str - The string to insert into
@@ -505,3 +508,30 @@ function getIndexOfEndOfLastNonEmptyLine(text: string, currentEndOfBlockquote: n
 
   return currentEndOfBlockquote;
 }
+
+export function parseCustomReplacements(text: string): Map<string, string> {
+  const tableInfo = getAllTablesInText(text);
+  const customReplacements = new Map<string, string>();
+
+  let tableContent = '';
+  let tableRows = [] as string[];
+  let rowParts = [] as string[];
+  for (const table of tableInfo) {
+    tableContent = text.substring(table.startIndex, table.endIndex);
+    tableRows = tableContent.split('\n');
+    tableRows.splice(0, 2); // skip header and divider rows
+
+    for (const row of tableRows) {
+      rowParts = row.split('|');
+
+      if (rowParts.length !== 4) {
+        logWarn(getTextInLanguage('options.custom-auto-correct.custom-row-parse-warning').replace('{ROW}', row));
+        continue;
+      }
+
+      customReplacements.set(rowParts[1].trim().toLowerCase(), rowParts[2].trim());
+    }
+  }
+
+  return customReplacements;
+}


Test patch:
diff --git a/__tests__/auto-correct-common-misspellings.test.ts b/__tests__/auto-correct-common-misspellings.test.ts
index 469ad84..5e55b3a 100644
--- a/__tests__/auto-correct-common-misspellings.test.ts
+++ b/__tests__/auto-correct-common-misspellings.test.ts
@@ -36,5 +36,20 @@ ruleTest({
         être
       `,
     },
+    {
+      testName: 'Custom replacements should work on file content',
+      before: dedent`
+        The cartt is over theree.
+      `,
+      after: dedent`
+        The cart is over there.
+      `,
+      options: {
+        extraAutoCorrectFiles: [{
+          filePath: 'file_path',
+          customReplacements: new Map<string, string>([['cartt', 'cart'], ['theree', 'there']]),
+        }],
+      },
+    },
   ],
 });
diff --git a/__tests__/parse-custom-replacements.test.ts b/__tests__/parse-custom-replacements.test.ts
new file mode 100644
index 0000000..1bf1e8c
--- /dev/null
+++ b/__tests__/parse-custom-replacements.test.ts
@@ -0,0 +1,77 @@
+import {parseCustomReplacements} from '../src/utils/strings';
+import dedent from 'ts-dedent';
+
+type parseCustomReplacementsTestCase = {
+  name: string,
+  text: string,
+  expectedCustomReplacements: Map<string, string>
+};
+
+const getTablesInTextTestCases: parseCustomReplacementsTestCase[] = [
+  {
+    name: 'parsing a file with a single table with two columns gets the correct amount of parsed out entries',
+    text: dedent`
+      Here is some text
+      | column1 | column2 |
+      | ------- | ------- |
+      | replaceme   | withme   |
+      |     replace | with |
+    `,
+    expectedCustomReplacements: new Map<string, string>([['replaceme', 'withme'], ['replace', 'with']]),
+  },
+  {
+    name: 'parsing a file with a single table with three columns gets no entries parsed out',
+    text: dedent`
+      Here is some text
+      | column1 | column2 | column3 |
+      | ------- | ------- | ---- |
+      | replaceme   | withme   | me |
+      |     replace | with | me2 |
+    `,
+    expectedCustomReplacements: new Map<string, string>(),
+  },
+  {
+    name: 'parsing a file with two tables with two columns gets the correct amount of parsed out entries',
+    text: dedent`
+      Here is some text
+      | column1 | column2 |
+      | ------- | ------- |
+      | replaceme   | withme   |
+      |     replace | with |
+      The following table also has some replacements
+      | header1 | header2 |
+      | ------- | ------- |
+      | var   | variablE   |
+      |     hack | hacker |
+    `,
+    expectedCustomReplacements: new Map<string, string>([['replaceme', 'withme'], ['replace', 'with'], ['var', 'variablE'], ['hack', 'hacker']]),
+  },
+  {
+    name: 'parsing a file with no tables gets no parsed out entries',
+    text: dedent`
+      Here is some text without any tables in it.
+    `,
+    expectedCustomReplacements: new Map<string, string>(),
+  },
+  {
+    name: 'parsing a file with a single table with two columns gets no entries parsed out when each line is missing a pipe (i.e. |)',
+    text: dedent`
+      Here is some text
+      | column1 | column2 |
+      | ------- | ------- |
+      | replaceme   | withme   
+           replace | with |
+    `,
+    expectedCustomReplacements: new Map<string, string>(),
+  },
+];
+
+describe('Get All Tables in Text', () => {
+  for (const testCase of getTablesInTextTestCases) {
+    it(testCase.name, () => {
+      const customReplacements = parseCustomReplacements(testCase.text);
+
+      expect(customReplacements).toEqual(testCase.expectedCustomReplacements);
+    });
+  }
+});