Repository: platers/obsidian-linter
Pull request #54 (2021-09-19 01:43:14)

New settings UI
Replace the text box with normal obsidian settings. #51 

Organize all rules into sections #43 
Currently YAML, Heading, Footnote, Spacing.

Rule order is in the order of sections, then order of the rules in the rules array. #42 

Display a notice on lint showing how many characters changed.

Big rewrite of how options work. This update is not backwards compatible. Users current settings will be lost.


Patch:
diff --git a/README.md b/README.md
index b093679..d69e2f1 100644
--- a/README.md
+++ b/README.md
@@ -53,16 +53,16 @@ Run `npm run lint` to lint the code.
 
 Documentation for all rules can be found in the [rules docs](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md).
 
+- [format-tags-in-yaml](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#format-tags-in-yaml)
+- [yaml-timestamp](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#yaml-timestamp)
+- [header-increment](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#header-increment)
+- [file-name-heading](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#file-name-heading)
+- [capitalize-headings](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#capitalize-headings)
+- [move-footnotes-to-the-bottom](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#move-footnotes-to-the-bottom)
+- [re-index-footnotes](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#re-index-footnotes)
 - [trailing-spaces](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#trailing-spaces)
 - [heading-blank-lines](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#heading-blank-lines)
 - [paragraph-blank-lines](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#paragraph-blank-lines)
 - [space-after-list-markers](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#space-after-list-markers)
-- [yaml-timestamp](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#yaml-timestamp)
 - [compact-yaml](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#compact-yaml)
-- [header-increment](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#header-increment)
 - [consecutive-blank-lines](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#consecutive-blank-lines)
-- [capitalize-headings](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#capitalize-headings)
-- [file-name-heading](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#file-name-heading)
-- [format-tags-in-yaml](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#format-tags-in-yaml)
-- [move-footnotes-to-the-bottom](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#move-footnotes-to-the-bottom)
-- [re-index-footnotes](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#re-index-footnotes)
diff --git a/docs/rules.md b/docs/rules.md
index fffb397..07311f9 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -2,509 +2,520 @@
 
 # Rules
 
-## Trailing spaces
 
-Alias: `trailing-spaces`
+## YAML
+### Format Tags in YAML
 
-Removes extra spaces after every line.
+Alias: `format-tags-in-yaml`
 
+Remove Hashtags from tags in the YAML frontmatter, as they make the tags there invalid.
 
 
-Example: Removes trailing spaces and tabs
+
+Example: Format Tags in YAML frontmatter
 
 Before:
 
 ```markdown
-# H1   
-line with trailing spaces and tabs            
+---
+tags: #one #two #three
+---
 ```
 
 After:
 
 ```markdown
-# H1
-line with trailing spaces and tabs
+---
+tags: one, two, three
+---
 ```
 
-## Heading blank lines
+### YAML Timestamp
 
-Alias: `heading-blank-lines`
+Alias: `yaml-timestamp`
 
-All headings have a blank line both before and after (except where the heading is at the beginning or end of the document).
+Keep track of the date the file was last edited in the YAML front matter. Gets dates from file metadata.
 
 Options:
-- bottom: Insert a blank line after headings, default=`true`
+- Date Created: Insert the file creation date
+  - Default: `true`
+- Date Modified: Insert the date the file was last modified
+  - Default: `true`
+- Format: Date format
+  - Default: `dddd, MMMM Do YYYY, h:mm:ss a`
 
-Example: Headings should be surrounded by blank lines
+Example: Adds a header with the date.
 
 Before:
 
 ```markdown
 # H1
-## H2
-
-
-# H1
-line
-## H2
-
 ```
 
 After:
 
 ```markdown
+---
+date created: Wednesday, January 1st 2020, 12:00:00 am
+date modified: Thursday, January 2nd 2020, 12:00:00 am
+---
 # H1
-
-## H2
-
-# H1
-
-line
-
-## H2
 ```
-Example: With `bottom=false`
+Example: dateCreated option is false
 
 Before:
 
 ```markdown
 # H1
-line
-## H2
-# H1
-line
 ```
 
 After:
 
 ```markdown
+---
+date modified: Wednesday, January 1st 2020, 12:00:00 am
+---
 # H1
-line
-
-## H2
-
-# H1
-line
 ```
 
-## Paragraph blank lines
+## Heading
+### Header Increment
 
-Alias: `paragraph-blank-lines`
+Alias: `header-increment`
 
-All paragraphs should have exactly one blank line both before and after.
+Heading levels should only increment by one level at a time
 
 
 
-Example: Paragraphs should be surrounded by blank lines
+Example: 
 
 Before:
 
 ```markdown
 # H1
-Newlines are inserted.
-A paragraph is a line that starts with a letter.
+### H3
+### H3
+#### H4
+###### H6
+
+We skipped a 2nd level heading
 ```
 
 After:
 
 ```markdown
 # H1
+## H3
+## H3
+### H4
+#### H6
 
-Newlines are inserted.
-
-A paragraph is a line that starts with a letter.
+We skipped a 2nd level heading
 ```
 
-## Space after list markers
+### File Name Heading
 
-Alias: `space-after-list-markers`
+Alias: `file-name-heading`
 
-There should be a single space after list markers and checkboxes.
+Inserts the file name as a H1 heading if no H1 heading exists.
 
 
 
-Example: 
+Example: Inserts an H1 heading
 
 Before:
 
 ```markdown
-1.      Item 1
-2.  Item 2
-
--   [ ] Item 1
-- [x]    Item 2
+This is a line of text
 ```
 
 After:
 
 ```markdown
-1. Item 1
-2. Item 2
+# File Name
+This is a line of text
+```
+Example: Inserts heading after YAML front matter
 
-- [ ] Item 1
-- [x] Item 2
+Before:
+
+```markdown
+---
+title: My Title
+---
+This is a line of text
 ```
 
-## YAML Timestamp
+After:
 
-Alias: `yaml-timestamp`
+```markdown
+---
+title: My Title
+---
+# File Name
+This is a line of text
+```
 
-Keep track of the date the file was last edited in the YAML front matter. Gets dates from file metadata.
+### Capitalize Headings
+
+Alias: `capitalize-headings`
+
+Headings should be formatted with capitalization
 
 Options:
-- format: [date format](https://momentjs.com/docs/#/displaying/format/), default=`"dddd, MMMM Do YYYY, h:mm:ss a"`
-- dateCreated: Insert the current date if date created is not present, default=`true`
-- dateUpdated: Update the current date, default=`true`
+- Title Case: Format headings with title case capitalization
+  - Default: `true`
+- All Caps: Format headings with all capitals
+  - Default: `false`
 
-Example: Adds a header with the date.
+Example: The first letter of a heading should be capitalized
 
 Before:
 
 ```markdown
-# H1
+# this is a heading 1
+## this is a heading 2
 ```
 
 After:
 
 ```markdown
----
-date created: Wednesday, January 1st 2020, 12:00:00 am
-date updated: Thursday, January 2nd 2020, 12:00:00 am
----
-# H1
+# This is a heading 1
+## This is a heading 2
 ```
-Example: dateCreated option is false
+Example: With `Title Case=true`
 
 Before:
 
 ```markdown
-# H1
+# this is a heading 1
+## THIS IS A HEADING 2
+### a heading 3
 ```
 
 After:
 
 ```markdown
----
-date updated: Wednesday, January 1st 2020, 12:00:00 am
----
-# H1
+# This is a Heading 1
+## This is a Heading 2
+### A Heading 3
 ```
-
-## Compact YAML
-
-Alias: `compact-yaml`
-
-Removes leading and trailing blank lines in the YAML front matter.
-
-
-
-Example: 
+Example: With `All Caps=true`
 
 Before:
 
 ```markdown
----
-
-date: today
-
----
+# this is a heading 1
+## this is a heading 2
 ```
 
 After:
 
 ```markdown
----
-date: today
----
+# THIS IS A HEADING 1
+## THIS IS A HEADING 2
 ```
 
-## Header Increment
+## Footnote
+### Move Footnotes to the bottom
 
-Alias: `header-increment`
+Alias: `move-footnotes-to-the-bottom`
 
-Heading levels should only increment by one level at a time
+Move all footnotes to the bottom of the document.
 
 
 
-Example: 
+Example: Moving footnotes to the bottom
 
 Before:
 
 ```markdown
-# H1
-### H3
-### H3
-#### H4
-###### H6
+Lorem ipsum, consectetur adipiscing elit. [^1] Donec dictum turpis quis ipsum pellentesque.
 
-We skipped a 2nd level heading
+[^1]: first footnote
+
+Quisque lorem est, fringilla sed enim at, sollicitudin lacinia nisi.[^2]
+[^2]: second footnote
+
+Maecenas malesuada dignissim purus ac volutpat.
 ```
 
 After:
 
 ```markdown
-# H1
-## H3
-## H3
-### H4
-#### H6
+Lorem ipsum, consectetur adipiscing elit. [^1] Donec dictum turpis quis ipsum pellentesque.
 
-We skipped a 2nd level heading
+Quisque lorem est, fringilla sed enim at, sollicitudin lacinia nisi.[^2]
+Maecenas malesuada dignissim purus ac volutpat.
+
+[^1]: first footnote
+[^2]: second footnote
 ```
 
-## Consecutive blank lines
+### Re-Index Footnotes
 
-Alias: `consecutive-blank-lines`
+Alias: `re-index-footnotes`
 
-There should be at most one consecutive blank line.
+Re-indexes footnote keys and footnote, based on the order of occurence (NOTE: This rule deliberately does *not* preserve the relation between key and footnote, to be able to re-index duplicate keys.)
 
 
 
-Example: 
+Example: Re-indexing footnotes after having deleted previous footnotes
 
 Before:
 
 ```markdown
-Some text
-
+Lorem ipsum at aliquet felis.[^3] Donec dictum turpis quis pellentesque,[^5] et iaculis tortor condimentum.
 
-Some more text
+[^3]: first footnote
+[^5]: second footnote
 ```
 
 After:
 
 ```markdown
-Some text
+Lorem ipsum at aliquet felis.[^1] Donec dictum turpis quis pellentesque,[^2] et iaculis tortor condimentum.
 
-Some more text
+[^1]: first footnote
+[^2]: second footnote
 ```
-
-## Capitalize Headings
-
-Alias: `capitalize-headings`
-
-Headings should be formatted with capitalization
-
-Options:
-- titleCase: Format headings with title case capitalization, default=`false`
-- allCaps: Format headings with all capitals, default= `false`
-
-Example: The first letter of a heading should be capitalized
+Example: Re-indexing footnotes after inserting a footnote between
 
 Before:
 
 ```markdown
-# this is a heading 1
-## this is a heading 2
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.[^1] Aenean at aliquet felis. Donec dictum turpis quis ipsum pellentesque, et iaculis tortor condimentum.[^1a] Vestibulum nec blandit felis, vulputate finibus purus.[^2] Praesent quis iaculis diam.
+
+[^1]: first footnote
+[^1a]: third footnote, inserted later
+[^2]: second footnotes
 ```
 
 After:
 
 ```markdown
-# This is a heading 1
-## This is a heading 2
+Lorem ipsum dolor sit amet, consectetur adipiscing elit.[^1] Aenean at aliquet felis. Donec dictum turpis quis ipsum pellentesque, et iaculis tortor condimentum.[^2] Vestibulum nec blandit felis, vulputate finibus purus.[^3] Praesent quis iaculis diam.
+
+[^1]: first footnote
+[^2]: third footnote, inserted later
+[^3]: second footnotes
 ```
-Example: With `titleCase=true`
+Example: Re-indexing duplicate footnote keys
 
 Before:
 
 ```markdown
-# this is a heading 1
-## THIS IS A HEADING 2
-### a heading 3
+Lorem ipsum at aliquet felis.[^1] Donec dictum turpis quis pellentesque,[^1] et iaculis tortor condimentum.
+
+[^1]: first footnote
+[^1]: second footnote
 ```
 
 After:
 
 ```markdown
-# This is a Heading 1
-## This is a Heading 2
-### A Heading 3
+Lorem ipsum at aliquet felis.[^1] Donec dictum turpis quis pellentesque,[^2] et iaculis tortor condimentum.
+
+[^1]: first footnote
+[^2]: second footnote
 ```
-Example: With `allCaps=true`
+
+## Spacing
+### Trailing spaces
+
+Alias: `trailing-spaces`
+
+Removes extra spaces after every line.
+
+
+
+Example: Removes trailing spaces and tabs
 
 Before:
 
 ```markdown
-# this is a heading 1
-## this is a heading 2
+# H1   
+line with trailing spaces and tabs            
 ```
 
 After:
 
 ```markdown
-# THIS IS A HEADING 1
-## THIS IS A HEADING 2
+# H1
+line with trailing spaces and tabs
 ```
 
-## File Name Heading
+### Heading blank lines
 
-Alias: `file-name-heading`
-
-Inserts the file name as a H1 heading if no H1 heading exists.
+Alias: `heading-blank-lines`
 
+All headings have a blank line both before and after (except where the heading is at the beginning or end of the document).
 
+Options:
+- Bottom: Insert a blank line after headings
+  - Default: `true`
 
-Example: Inserts an H1 heading
+Example: Headings should be surrounded by blank lines
 
 Before:
 
 ```markdown
-This is a line of text
+# H1
+## H2
+
+
+# H1
+line
+## H2
+
 ```
 
 After:
 
 ```markdown
-# File Name
-This is a line of text
+# H1
+
+## H2
+
+# H1
+
+line
+
+## H2
 ```
-Example: Inserts heading after YAML front matter
+Example: With `Bottom=false`
 
 Before:
 
 ```markdown
----
-title: My Title
----
-This is a line of text
+# H1
+line
+## H2
+# H1
+line
 ```
 
 After:
 
 ```markdown
----
-title: My Title
----
-# File Name
-This is a line of text
+# H1
+line
+
+## H2
+
+# H1
+line
 ```
 
-## Format Tags in YAML
+### Paragraph blank lines
 
-Alias: `format-tags-in-yaml`
+Alias: `paragraph-blank-lines`
 
-Remove Hashtags from tags in the YAML frontmatter, as they make the tags there invalid.
+All paragraphs should have exactly one blank line both before and after.
 
 
 
-Example: Format Tags in YAML frontmatter
+Example: Paragraphs should be surrounded by blank lines
 
 Before:
 
 ```markdown
----
-tags: #one #two #three
----
+# H1
+Newlines are inserted.
+A paragraph is a line that starts with a letter.
 ```
 
 After:
 
 ```markdown
----
-tags: one, two, three
----
+# H1
+
+Newlines are inserted.
+
+A paragraph is a line that starts with a letter.
 ```
 
-## Move Footnotes to the bottom
+### Space after list markers
 
-Alias: `move-footnotes-to-the-bottom`
+Alias: `space-after-list-markers`
 
-Move all footnotes to the bottom of the document.
+There should be a single space after list markers and checkboxes.
 
 
 
-Example: Moving footnotes to the bottom
+Example: 
 
 Before:
 
 ```markdown
-Lorem ipsum, consectetur adipiscing elit. [^1] Donec dictum turpis quis ipsum pellentesque.
-
-[^1]: first footnote
-
-Quisque lorem est, fringilla sed enim at, sollicitudin lacinia nisi.[^2]
-[^2]: second footnote
+1.      Item 1
+2.  Item 2
 
-Maecenas malesuada dignissim purus ac volutpat.
+-   [ ] Item 1
+- [x]    Item 2
 ```
 
 After:
 
 ```markdown
-Lorem ipsum, consectetur adipiscing elit. [^1] Donec dictum turpis quis ipsum pellentesque.
-
-Quisque lorem est, fringilla sed enim at, sollicitudin lacinia nisi.[^2]
-Maecenas malesuada dignissim purus ac volutpat.
+1. Item 1
+2. Item 2
 
-[^1]: first footnote
-[^2]: second footnote
+- [ ] Item 1
+- [x] Item 2
 ```
 
-## Re-Index Footnotes
+### Compact YAML
 
-Alias: `re-index-footnotes`
+Alias: `compact-yaml`
 
-Re-indexes footnote keys and footnote, based on the order of occurence (NOTE: This rule deliberately does *not* preserve the relation between key and footnote, to be able to re-index duplicate keys.)
+Removes leading and trailing blank lines in the YAML front matter.
 
 
 
-Example: Re-indexing footnotes after having deleted previous footnotes
+Example: 
 
 Before:
 
 ```markdown
-Lorem ipsum at aliquet felis.[^3] Donec dictum turpis quis pellentesque,[^5] et iaculis tortor condimentum.
+---
 
-[^3]: first footnote
-[^5]: second footnote
+date: today
+
+---
 ```
 
 After:
 
 ```markdown
-Lorem ipsum at aliquet felis.[^1] Donec dictum turpis quis pellentesque,[^2] et iaculis tortor condimentum.
-
-[^1]: first footnote
-[^2]: second footnote
+---
+date: today
+---
 ```
-Example: Re-indexing footnotes after inserting a footnote between
 
-Before:
+### Consecutive blank lines
 
-```markdown
-Lorem ipsum dolor sit amet, consectetur adipiscing elit.[^1] Aenean at aliquet felis. Donec dictum turpis quis ipsum pellentesque, et iaculis tortor condimentum.[^1a] Vestibulum nec blandit felis, vulputate finibus purus.[^2] Praesent quis iaculis diam.
+Alias: `consecutive-blank-lines`
 
-[^1]: first footnote
-[^1a]: third footnote, inserted later
-[^2]: second footnotes
-```
+There should be at most one consecutive blank line.
 
-After:
 
-```markdown
-Lorem ipsum dolor sit amet, consectetur adipiscing elit.[^1] Aenean at aliquet felis. Donec dictum turpis quis ipsum pellentesque, et iaculis tortor condimentum.[^2] Vestibulum nec blandit felis, vulputate finibus purus.[^3] Praesent quis iaculis diam.
 
-[^1]: first footnote
-[^2]: third footnote, inserted later
-[^3]: second footnotes
-```
-Example: Re-indexing duplicate footnote keys
+Example: 
 
 Before:
 
 ```markdown
-Lorem ipsum at aliquet felis.[^1] Donec dictum turpis quis pellentesque,[^1] et iaculis tortor condimentum.
+Some text
 
-[^1]: first footnote
-[^1]: second footnote
+
+Some more text
 ```
 
 After:
 
 ```markdown
-Lorem ipsum at aliquet felis.[^1] Donec dictum turpis quis pellentesque,[^2] et iaculis tortor condimentum.
+Some text
 
-[^1]: first footnote
-[^2]: second footnote
+Some more text
 ```
diff --git a/src/docs.ts b/src/docs.ts
index 8b46905..acaaf5a 100644
--- a/src/docs.ts
+++ b/src/docs.ts
@@ -8,8 +8,7 @@ const autogen_warning = '<!--- This file was automatically generated. See docs.t
 
 const readme_template = readFileSync('./docs/readme_template.md', 'utf8');
 
-const url = 'https://github.com/platers/obsidian-linter/blob/master/docs/rules.md';
-const rules_list = rules.map((rule) => `- [${rule.alias()}](${url}#${rule.alias()})`).join('\n');
+const rules_list = rules.map((rule) => `- [${rule.alias()}](${rule.getURL()})`).join('\n');
 
 const readme = dedent`
         ${autogen_warning}
@@ -27,7 +26,9 @@ console.log('README.md updated');
 
 const rules_template = readFileSync('./docs/rules_template.md', 'utf8');
 
-const rules_docs = rules.map((rule) => {
+let rules_docs = '';
+let prevSection = '';
+for (const rule of rules) {
   const examples = rule.examples.map((test) => dedent`
     Example: ${test.description}
 
@@ -44,7 +45,11 @@ const rules_docs = rules.map((rule) => {
     \`\`\`
   `).join('\n');
 
-  const options_list = rule.options.map((option) => `- ${option}`).join('\n');
+  const options_list = rule.options.slice(1).map((option) => {
+    return dedent`
+                  - ${option.name}: ${option.description}
+                    - Default: \`${option.defaultValue}\``;
+  }).join('\n');
   let options = '';
   if (options_list.length > 0) {
     options = dedent`
@@ -53,8 +58,17 @@ const rules_docs = rules.map((rule) => {
     `;
   }
 
-  return dedent`
-  ## ${rule.name}
+  if (rule.type !== prevSection) {
+    rules_docs += dedent`
+
+      ## ${rule.type}
+    `;
+    prevSection = rule.type;
+  }
+
+  rules_docs += dedent`
+
+  ### ${rule.name}
 
   Alias: \`${rule.alias()}\`
 
@@ -65,7 +79,7 @@ const rules_docs = rules.map((rule) => {
   ${examples}
 
   `;
-}).join('\n');
+}
 
 const rules_documentation = dedent`
   ${autogen_warning}
diff --git a/src/main.ts b/src/main.ts
index 185d9cd..3e45f30 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,28 +1,16 @@
 import {App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, TFile} from 'obsidian';
-import dedent from 'ts-dedent';
-import {rulesDict} from './rules';
-import {getDisabledRules, parseOptions} from './utils';
+import {LinterSettings, Options, rules} from './rules';
+import {getDisabledRules} from './utils';
 import Diff from 'diff';
 import moment from 'moment';
-
-interface LinterSettings {
-    enabledRules: string;
-    lintOnSave: boolean;
-}
-
-const DEFAULT_SETTINGS: LinterSettings = {
-  enabledRules: dedent`
-        trailing-spaces
-        heading-blank-lines
-        space-after-list-markers
-        `,
-  lintOnSave: false,
-};
+import {BooleanOption, MomentFormatOption} from './option';
+import dedent from 'ts-dedent';
 
 export default class LinterPlugin extends Plugin {
     settings: LinterSettings;
 
     async onload() {
+      console.log('Loading Linter plugin');
       await this.loadSettings();
 
       this.addCommand({
@@ -69,7 +57,22 @@ export default class LinterPlugin extends Plugin {
     }
 
     async loadSettings() {
-      this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
+      this.settings = {
+        ruleConfigs: {},
+        lintOnSave: false,
+      };
+      const storedSettings = await this.loadData();
+
+      for (const rule of rules) {
+        this.settings.ruleConfigs[rule.name] = rule.getDefaultOptions();
+        if (storedSettings.ruleConfigs[rule.name]) {
+          Object.assign(this.settings.ruleConfigs[rule.name], storedSettings.ruleConfigs[rule.name]);
+        }
+      }
+
+      if (storedSettings.lintOnSave) {
+        this.settings.lintOnSave = storedSettings.lintOnSave;
+      }
     }
 
     async saveSettings() {
@@ -78,32 +81,22 @@ export default class LinterPlugin extends Plugin {
 
     lintText(oldText: string, file: TFile) {
       let newText = oldText;
-      const enabledRules = this.settings.enabledRules.split('\n');
       const disabledRules = getDisabledRules(oldText);
 
-      for (const line of enabledRules) {
-        // Skip empty or commented lines
-        if (line.match(/^\s*$/) || line.startsWith('// ')) {
+      for (const rule of rules) {
+        if (disabledRules.includes(rule.name)) {
           continue;
         }
 
-        // Split the line into the rule name and the rule options
-        const ruleName = line.split(/\s+/)[0];
+        const options: Options =
+          Object.assign({
+            'metadata: file created time': moment(file.stat.ctime).format(),
+            'metadata: file modified time': moment(file.stat.mtime).format(),
+            'metadata: file name': file.basename,
+          }, rule.getOptions(this.settings));
 
-        if (disabledRules.includes(ruleName)) {
-          continue;
-        }
-
-        if (ruleName in rulesDict) {
-          const options: { [id: string]: string; } =
-            Object.assign({
-              'metadata: file created time': moment(file.stat.ctime).format(),
-              'metadata: file modified time': moment(file.stat.mtime).format(),
-              'metadata: file name': file.basename,
-            }, parseOptions(line));
-          newText = rulesDict[ruleName].apply(newText, options);
-        } else {
-          new Notice(`Rule ${ruleName} not recognized`);
+        if (options['Enabled']) {
+          newText = rule.apply(newText, options);
         }
       }
 
@@ -145,6 +138,13 @@ export default class LinterPlugin extends Plugin {
           curText += change.value;
         }
       });
+
+      const charsAdded = changes.map((change) => change.added ? change.value.length : 0).reduce((a, b) => a + b, 0);
+      const charsRemoved = changes.map((change) => change.removed ? change.value.length : 0).reduce((a, b) => a + b, 0);
+      new Notice(dedent`
+        ${charsAdded} characters added
+        ${charsRemoved} characters removed
+      `);
     }
 }
 
@@ -154,6 +154,36 @@ class SettingTab extends PluginSettingTab {
     constructor(app: App, plugin: LinterPlugin) {
       super(app, plugin);
       this.plugin = plugin;
+
+      // Inject display functions. Necessary because the Settings object cannot be used in tests.
+      BooleanOption.prototype.display = function(containerEl: HTMLElement, settings: LinterSettings, plugin: LinterPlugin): void {
+        new Setting(containerEl)
+            .setName(this.name)
+            .setDesc(this.description)
+            .addToggle((toggle) => {
+              toggle.setValue(settings.ruleConfigs[this.ruleName][this.name]);
+              toggle.onChange((value) => {
+                this.setOption(value, settings);
+                plugin.settings = settings;
+                plugin.saveData(plugin.settings);
+              });
+            });
+      };
+
+      MomentFormatOption.prototype.display = function(containerEl: HTMLElement, settings: LinterSettings, plugin: LinterPlugin): void {
+        new Setting(containerEl)
+            .setName(this.name)
+            .setDesc(this.description)
+            .addMomentFormat((format) => {
+              format.setValue(settings.ruleConfigs[this.ruleName][this.name]);
+              format.setPlaceholder('dddd, MMMM Do YYYY, h:mm:ss a');
+              format.onChange((value) => {
+                this.setOption(value, settings);
+                plugin.settings = settings;
+                plugin.saveData(plugin.settings);
+              });
+            });
+      };
     }
 
     display(): void {
@@ -161,21 +191,7 @@ class SettingTab extends PluginSettingTab {
 
       containerEl.empty();
 
-      containerEl.createEl('h2', {text: 'Settings for Linter'});
-
-      new Setting(containerEl)
-          .setName('Rules to apply')
-          .setDesc('List the rules to apply to the markdown file')
-          .addTextArea((text) => {
-            text
-                .setValue(this.plugin.settings.enabledRules)
-                .onChange(async (value) => {
-                  this.plugin.settings.enabledRules = value;
-                  await this.plugin.saveSettings();
-                });
-            text.inputEl.rows = 20;
-            text.inputEl.cols = 40;
-          });
+      containerEl.createEl('h2', {text: 'General Settings'});
 
       new Setting(containerEl)
           .setName('Lint on save')
@@ -188,5 +204,22 @@ class SettingTab extends PluginSettingTab {
                   await this.plugin.saveSettings();
                 });
           });
+
+      let prevSection = '';
+
+      for (const rule of rules) {
+        if (rule.type !== prevSection) {
+          containerEl.createEl('h2', {text: rule.type});
+          prevSection = rule.type;
+        }
+
+        containerEl.createEl('h3', {text: rule.name});
+        containerEl.createEl('a', {text: rule.alias(), href: rule.getURL()});
+        containerEl.createEl('p', {text: rule.description});
+
+        for (const option of rule.options) {
+          option.display(containerEl, this.plugin.settings, this.plugin);
+        }
+      }
     }
 }
diff --git a/src/option.ts b/src/option.ts
new file mode 100644
index 0000000..a25e81d
--- /dev/null
+++ b/src/option.ts
@@ -0,0 +1,45 @@
+import LinterPlugin from './main';
+import {LinterSettings} from './rules';
+
+/** Class representing an option of a rule */
+
+export class Option {
+  public name: string;
+  public description: string;
+  public ruleName: string;
+  public defaultValue: any;
+
+  /**
+   * Create an option
+   * @param {string} name - The name of the option
+   * @param {string} description - The description of the option
+   * @param {any} defaultValue - The default value of the option
+   * @param {string?} ruleName - The name of the rule this option belongs to
+   */
+  constructor(name: string, description: string, defaultValue: any, ruleName?: string | null) {
+    this.name = name;
+    this.description = description;
+    this.defaultValue = defaultValue;
+
+    if (ruleName) {
+      this.ruleName = ruleName;
+    }
+  }
+
+  public display(containerEl: HTMLElement, settings: LinterSettings, plugin: LinterPlugin): void {
+    throw new Error('Not implemented');
+  }
+
+  protected setOption(value: any, settings: LinterSettings): void {
+    settings.ruleConfigs[this.ruleName][this.name] = value;
+  }
+}
+
+export class BooleanOption extends Option {
+  public defaultValue: boolean;
+}
+
+
+export class MomentFormatOption extends Option {
+  public defaultValue: boolean;
+}
diff --git a/src/rules.ts b/src/rules.ts
index 27fb900..339f61f 100644
--- a/src/rules.ts
+++ b/src/rules.ts
@@ -1,14 +1,34 @@
 import dedent from 'ts-dedent';
 import moment from 'moment';
 import {headerRegex, ignoreCodeBlocksAndYAML, initYAML, insert} from './utils';
+import {Option, BooleanOption, MomentFormatOption} from './option';
 
-type ApplyFunction = (text: string, options?: { [id: string]: string }) => string;
+export type Options = { [optionName: string]: any };
+type ApplyFunction = (text: string, options?: Options) => string;
+
+export interface LinterSettings {
+  ruleConfigs: {
+    [ruleName: string]: Options;
+  };
+  lintOnSave: boolean;
+}
+/* eslint-disable no-unused-vars */
+enum RuleType {
+  YAML = 'YAML',
+  HEADING = 'Heading',
+  FOOTNOTE = 'Footnote',
+  SPACING = 'Spacing',
+}
+/* eslint-enable no-unused-vars */
+
+const RuleTypeOrder = Object.values(RuleType);
 
 /** Class representing a rule */
 export class Rule {
     public name: string;
     public description: string;
-    public options: Array<string>;
+    public type: RuleType;
+    public options: Array<Option>;
     public apply: ApplyFunction;
 
     public examples: Array<Example>;
@@ -17,32 +37,59 @@ export class Rule {
      * Create a rule
      * @param {string} name - The name of the rule
      * @param {string} description - The description of the rule
+     * @param {RuleType} type - The type of the rule
      * @param {ApplyFunction} apply - The function to apply the rule
      * @param {Array<Example>} examples - The examples to be displayed in the documentation
-     * @param {Array<string>} [options=[]] - The options of the rule to be displayed in the documentation
+     * @param {Array<Option>} [options=[]] - The options of the rule to be displayed in the documentation
      */
     constructor(
         name: string,
         description: string,
+        type: RuleType,
         apply: ApplyFunction,
         examples: Array<Example>,
-        options: Array<string> = []) {
+        options: Array<Option> = []) {
       this.name = name;
       this.description = description;
+      this.type = type;
       this.apply = apply;
       this.examples = examples;
+
+      options.unshift(new BooleanOption('Enabled', '', false));
+      for (const option of options) {
+        option.ruleName = name;
+      }
       this.options = options;
     }
 
     public alias(): string {
       return this.name.replace(/ /g, '-').toLowerCase();
     }
+
+    public getDefaultOptions() {
+      const options: { [optionName: string]: any } = {};
+
+      for (const option of this.options) {
+        options[option.name] = option.defaultValue;
+      }
+
+      return options;
+    }
+
+    public getOptions(settings: LinterSettings) {
+      return settings.ruleConfigs[this.name];
+    }
+
+    public getURL(): string {
+      const url = 'https://github.com/platers/obsidian-linter/blob/master/docs/rules.md';
+      return url + '#' + this.alias();
+    }
 }
 
 /** Class representing an example of a rule */
 export class Example {
     public description: string;
-    public options: { [id: string]: string };
+    public options: Options;
 
     public before: string;
     public after: string;
@@ -54,7 +101,7 @@ export class Example {
      * @param {string} after - The text after the rule is applied
      * @param {object} options - The options of the example
      */
-    constructor(description: string, before: string, after: string, options: { [id: string]: string } = {}) {
+    constructor(description: string, before: string, after: string, options: Options = {}) {
       this.description = description;
       this.options = options;
       this.before = before;
@@ -67,6 +114,7 @@ export const rules: Rule[] = [
   new Rule(
       'Trailing spaces',
       'Removes extra spaces after every line.',
+      RuleType.SPACING,
       (text: string) => {
         return text.replace(/[ \t]+$/gm, '');
       },
@@ -85,11 +133,10 @@ export const rules: Rule[] = [
   new Rule(
       'Heading blank lines',
       'All headings have a blank line both before and after (except where the heading is at the beginning or end of the document).',
+      RuleType.SPACING,
       (text: string, options = {}) => {
-        options = Object.assign({'bottom': 'true'}, options);
-
         return ignoreCodeBlocksAndYAML(text, (text) => {
-          if (options['bottom'] === 'false') {
+          if (options['Bottom'] === false) {
             text = text.replace(/(^#+\s.*)\n+/gm, '$1\n'); // trim blank lines after headings
             text = text.replace(/\n+(#+\s.*)/g, '\n\n$1'); // trim blank lines before headings
           } else {
@@ -128,7 +175,7 @@ export const rules: Rule[] = [
         `,
         ),
         new Example(
-            'With `bottom=false`',
+            'With `Bottom=false`',
             dedent`
         # H1
         line
@@ -145,16 +192,17 @@ export const rules: Rule[] = [
         # H1
         line
         `,
-            {bottom: 'false'},
+            {Bottom: false},
         ),
       ],
       [
-        'bottom: Insert a blank line after headings, default=`true`',
+        new BooleanOption('Bottom', 'Insert a blank line after headings', true),
       ],
   ),
   new Rule(
       'Paragraph blank lines',
       'All paragraphs should have exactly one blank line both before and after.',
+      RuleType.SPACING,
       (text: string) => {
         return ignoreCodeBlocksAndYAML(text, (text) => {
           text = text.replace(/\n+([a-zA-Z].*)/g, '\n\n$1'); // trim blank lines before
@@ -184,6 +232,7 @@ export const rules: Rule[] = [
   new Rule(
       `Space after list markers`,
       'There should be a single space after list markers and checkboxes.',
+      RuleType.SPACING,
       (text: string) => {
         // Space after marker
         text = text.replace(/^(\s*\d+\.|[-+*])[^\S\r\n]+/gm, '$1 ');
@@ -210,29 +259,103 @@ export const rules: Rule[] = [
         ),
       ],
   ),
+  new Rule(
+      'Compact YAML',
+      'Removes leading and trailing blank lines in the YAML front matter.',
+      RuleType.SPACING,
+      (text: string) => {
+        text = text.replace(/^---\n+/, '---\n');
+        return text.replace(/\n+---/, '\n---');
+      },
+      [
+        new Example(
+            '',
+            dedent`
+        ---
+
+        date: today
+
+        ---
+        `,
+            dedent`
+        ---
+        date: today
+        ---
+        `,
+        ),
+      ],
+  ),
+  new Rule(
+      'Consecutive blank lines',
+      'There should be at most one consecutive blank line.',
+      RuleType.SPACING,
+      (text: string) => {
+        return text.replace(/\n{2,}/g, '\n\n');
+      },
+      [
+        new Example(
+            '',
+            dedent`
+        Some text
+
+
+        Some more text
+        `,
+            dedent`
+        Some text
+
+        Some more text
+        `,
+        ),
+      ],
+  ),
+  new Rule(
+      'Format Tags in YAML',
+      'Remove Hashtags from tags in the YAML frontmatter, as they make the tags there invalid.',
+      RuleType.YAML,
+      (text: string) => {
+        return text.replace(/^tags: ((?:#\w+(?: |$))+)$/im, function(tagsYAML) {
+          return tagsYAML.replaceAll('#', '').replaceAll(' ', ', ').replaceAll(',,', ',').replace('tags:,', 'tags:');
+        });
+      },
+      [
+        new Example(
+            'Format Tags in YAML frontmatter',
+            dedent`
+         ---
+         tags: #one #two #three
+         ---
+        `,
+            dedent`
+         ---
+         tags: one, two, three
+         ---
+        `,
+        ),
+      ],
+  ),
+
+  // YAML rules
+
   new Rule(
       'YAML Timestamp',
       'Keep track of the date the file was last edited in the YAML front matter. Gets dates from file metadata.',
+      RuleType.YAML,
       (text: string, options = {}) => {
-        options = Object.assign({
-          'format': 'dddd, MMMM Do YYYY, h:mm:ss a',
-          'dateCreated': 'true',
-          'dateUpdated': 'true',
-        }, options);
-
         text = initYAML(text);
 
-        if (options['dateCreated'] === 'true') {
+        if (options['Date Created'] === true) {
           text = text.replace(/\ndate created:.*\n/, '\n');
           const yaml_end = text.indexOf('\n---');
-          const formatted_date = moment(options['metadata: file created time']).format(options['format']);
+          const formatted_date = moment(options['metadata: file created time']).format(options['Format']);
           text = insert(text, yaml_end, `\ndate created: ${formatted_date}`);
         }
-        if (options['dateUpdated'] === 'true') {
-          text = text.replace(/\ndate updated:.*\n/, '\n');
+        if (options['Date Modified'] === true) {
+          text = text.replace(/\ndate modified:.*\n/, '\n');
+          text = text.replace(/\ndate updated:.*\n/, '\n'); // for backwards compatibility
           const yaml_end = text.indexOf('\n---');
-          const formatted_date = moment(options['metadata: file modified time']).format(options['format']);
-          text = insert(text, yaml_end, `\ndate updated: ${formatted_date}`);
+          const formatted_date = moment(options['metadata: file modified time']).format(options['Format']);
+          text = insert(text, yaml_end, `\ndate modified: ${formatted_date}`);
         }
         return text;
       },
@@ -245,7 +368,7 @@ export const rules: Rule[] = [
             dedent`
         ---
         date created: Wednesday, January 1st 2020, 12:00:00 am
-        date updated: Thursday, January 2nd 2020, 12:00:00 am
+        date modified: Thursday, January 2nd 2020, 12:00:00 am
         ---
         # H1
         `,
@@ -261,51 +384,30 @@ export const rules: Rule[] = [
         `,
             dedent`
         ---
-        date updated: Wednesday, January 1st 2020, 12:00:00 am
+        date modified: Wednesday, January 1st 2020, 12:00:00 am
         ---
         # H1
         `,
             {
-              'dateCreated': 'false',
+              'Date Created': false,
               'metadata: file created time': '2020-01-01T00:00:00-00:00',
               'metadata: file modified time': '2020-01-01T00:00:00-00:00',
             },
         ),
       ],
       [
-        'format: [date format](https://momentjs.com/docs/#/displaying/format/), default=`"dddd, MMMM Do YYYY, h:mm:ss a"`',
-        'dateCreated: Insert the current date if date created is not present, default=`true`',
-        'dateUpdated: Update the current date, default=`true`',
+        new BooleanOption('Date Created', 'Insert the file creation date', true),
+        new BooleanOption('Date Modified', 'Insert the date the file was last modified', true),
+        new MomentFormatOption('Format', 'Date format', 'dddd, MMMM Do YYYY, h:mm:ss a'),
       ],
   ),
-  new Rule(
-      'Compact YAML',
-      'Removes leading and trailing blank lines in the YAML front matter.',
-      (text: string) => {
-        text = text.replace(/^---\n+/, '---\n');
-        return text.replace(/\n+---/, '\n---');
-      },
-      [
-        new Example(
-            '',
-            dedent`
-        ---
 
-        date: today
+  // Heading rules
 
-        ---
-        `,
-            dedent`
-        ---
-        date: today
-        ---
-        `,
-        ),
-      ],
-  ),
   new Rule(
       'Header Increment',
       'Heading levels should only increment by one level at a time',
+      RuleType.HEADING,
       (text: string) => {
         const lines = text.split('\n');
         let lastLevel = 0; // level of last header processed
@@ -352,44 +454,65 @@ export const rules: Rule[] = [
       ],
   ),
   new Rule(
-      'Consecutive blank lines',
-      'There should be at most one consecutive blank line.',
-      (text: string) => {
-        return text.replace(/\n{2,}/g, '\n\n');
+      'File Name Heading',
+      'Inserts the file name as a H1 heading if no H1 heading exists.',
+      RuleType.HEADING,
+      (text: string, options = {}) => {
+        // check if there is a H1 heading
+        const hasH1 = text.match(/^#\s.*/m);
+        if (hasH1) {
+          return text;
+        }
+
+        const fileName = options['metadata: file name'];
+        // insert H1 heading after front matter
+        let yaml_end = text.indexOf('\n---');
+        yaml_end = yaml_end == -1 || !text.startsWith('---\n') ? 0 : yaml_end + 5;
+        return insert(text, yaml_end, `# ${fileName}\n`);
       },
       [
         new Example(
-            '',
+            'Inserts an H1 heading',
             dedent`
-        Some text
-
-
-        Some more text
-        `,
+              This is a line of text
+            `,
             dedent`
-        Some text
-
-        Some more text
-        `,
+              # File Name
+              This is a line of text
+            `,
+            {'metadata: file name': 'File Name'},
+        ),
+        new Example(
+            'Inserts heading after YAML front matter',
+            dedent`
+              ---
+              title: My Title
+              ---
+              This is a line of text
+            `,
+            dedent`
+              ---
+              title: My Title
+              ---
+              # File Name
+              This is a line of text
+            `,
+            {'metadata: file name': 'File Name'},
         ),
       ],
   ),
   new Rule(
       'Capitalize Headings',
       'Headings should be formatted with capitalization',
+      RuleType.HEADING,
       (text: string, options = {}) => {
-        options = Object.assign({
-          'titleCase': 'false',
-          'allCaps': 'false',
-        }, options);
-
         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;
           }
-          if (options['titleCase'] == 'true') {
+          if (options['Title Case'] == true) {
             const headerWords = lines[i].match(/\S+/g);
             const ignoreNames = ['macOS', 'iOS', 'iPhone', 'iPad', 'JavaScript', 'TypeScript', 'AppleScript'];
             const ignoreAbbreviations = ['CSS', 'HTML', 'YAML', 'PDF', 'USA', 'EU', 'NATO', 'ASCII'];
@@ -409,7 +532,7 @@ export const rules: Rule[] = [
             }
 
             lines[i] = lines[i].replace(headerRegex, `${headerWords.join(' ')}`);
-          } else if (options['allCaps'] == 'true') {
+          } else if (options['All Caps'] == true) {
             lines[i] = lines[i].toUpperCase(); // convert full heading to uppercase
           } else {
             lines[i] = lines[i].replace(/^#*\s([a-z])/, (string) => string.toUpperCase()); // capitalize first letter of heading
@@ -430,7 +553,7 @@ export const rules: Rule[] = [
         `,
         ),
         new Example(
-            'With `titleCase=true`',
+            'With `Title Case=true`',
             dedent`
         # this is a heading 1
         ## THIS IS A HEADING 2
@@ -441,10 +564,10 @@ export const rules: Rule[] = [
         ## This is a Heading 2
         ### A Heading 3
         `,
-            {titleCase: 'true'},
+            {'Title Case': true},
         ),
         new Example(
-            'With `allCaps=true`',
+            'With `All Caps=true`',
             dedent`
         # this is a heading 1
         ## this is a heading 2
@@ -453,88 +576,21 @@ export const rules: Rule[] = [
         # THIS IS A HEADING 1
         ## THIS IS A HEADING 2
         `,
-            {allCaps: 'true'},
+            {'All Caps': true, 'Title Case': false},
         ),
       ],
       [
-        'titleCase: Format headings with title case capitalization, default=`false`',
-        'allCaps: Format headings with all capitals, default= `false`',
+        new BooleanOption('Title Case', 'Format headings with title case capitalization', true),
+        new BooleanOption('All Caps', 'Format headings with all capitals', false),
       ],
   ),
-  new Rule(
-      'File Name Heading',
-      'Inserts the file name as a H1 heading if no H1 heading exists.',
-      (text: string, options = {}) => {
-        // check if there is a H1 heading
-        const hasH1 = text.match(/^#\s.*/m);
-        if (hasH1) {
-          return text;
-        }
 
-        const fileName = options['metadata: file name'];
-        // insert H1 heading after front matter
-        let yaml_end = text.indexOf('\n---');
-        yaml_end = yaml_end == -1 || !text.startsWith('---\n') ? 0 : yaml_end + 5;
-        return insert(text, yaml_end, `# ${fileName}\n`);
-      },
-      [
-        new Example(
-            'Inserts an H1 heading',
-            dedent`
-              This is a line of text
-            `,
-            dedent`
-              # File Name
-              This is a line of text
-            `,
-            {'metadata: file name': 'File Name'},
-        ),
-        new Example(
-            'Inserts heading after YAML front matter',
-            dedent`
-              ---
-              title: My Title
-              ---
-              This is a line of text
-            `,
-            dedent`
-              ---
-              title: My Title
-              ---
-              # File Name
-              This is a line of text
-            `,
-            {'metadata: file name': 'File Name'},
-        ),
-      ],
-  ),
-  new Rule(
-      'Format Tags in YAML',
-      'Remove Hashtags from tags in the YAML frontmatter, as they make the tags there invalid.',
-      (text: string) => {
-        return text.replace(/^tags: ((?:#\w+(?: |$))+)$/im, function(tagsYAML) {
-          return tagsYAML.replaceAll('#', '').replaceAll(' ', ', ').replaceAll(',,', ',').replace('tags:,', 'tags:');
-        });
-      },
-      [
-        new Example(
-            'Format Tags in YAML frontmatter',
-            dedent`
-         ---
-         tags: #one #two #three
-         ---
-        `,
-            dedent`
-         ---
-         tags: one, two, three
-         ---
-        `,
-        ),
-      ],
-  ),
+  // Footnote rules
+
   new Rule(
       'Move Footnotes to the bottom',
       'Move all footnotes to the bottom of the document.',
+      RuleType.FOOTNOTE,
       (text: string) => {
         const footnotes = text.match(/^\[\^\w+\]: .*$/gm); // collect footnotes
         if (footnotes != null) {
@@ -579,6 +635,7 @@ export const rules: Rule[] = [
   new Rule(
       'Re-Index Footnotes',
       'Re-indexes footnote keys and footnote, based on the order of occurence (NOTE: This rule deliberately does *not* preserve the relation between key and footnote, to be able to re-index duplicate keys.)',
+      RuleType.FOOTNOTE,
       (text: string) => {
         // re-index footnote-text
         let ft_index = 0;
@@ -646,6 +703,6 @@ export const rules: Rule[] = [
         ),
       ],
   ),
-];
+].sort((a, b) => RuleTypeOrder.indexOf(a.type) - RuleTypeOrder.indexOf(b.type));
 
 export const rulesDict = rules.reduce((dict, rule) => (dict[rule.alias()] = rule, dict), {} as Record<string, Rule>);
diff --git a/src/utils.ts b/src/utils.ts
index 3541944..05bba01 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -14,26 +14,6 @@ export const codeBlockRegex = new RegExp(`${backtickBlockRegexTemplate}|${tildeB
 
 // Helper functions
 
-// Export parseOptions here so it can be tested
-export function parseOptions(line: string) {
-  // Match arguments with format: optionName=value or optionName="value" or optionName='value'
-  const args = line.matchAll(/\s+(\S+)=("[^"]*"|'[^']*'|\S+)/g);
-  const options: { [id: string]: string; } = {};
-
-  for (const arg of args) {
-    let [, option_name, option_value] = arg;
-
-    if (option_value.startsWith('\'') && option_value.endsWith('\'')) {
-      option_value = option_value.slice(1, -1);
-    } else if (option_value.startsWith('"') && option_value.endsWith('"')) {
-      option_value = option_value.slice(1, -1);
-    }
-
-    options[option_name] = option_value;
-  }
-  return options;
-}
-
 /**
  * Returns a list of ignored rules in the YAML frontmatter of the text.
  * @param {string} text The text to parse


Test patch:
diff --git a/src/test.ts b/src/test.ts
index 5144aac..b4c867e 100644
--- a/src/test.ts
+++ b/src/test.ts
@@ -1,13 +1,14 @@
 import dedent from 'ts-dedent';
 import {rules, Rule, rulesDict, Example} from './rules';
-import {getDisabledRules, parseOptions} from './utils';
+import {getDisabledRules} from './utils';
 
 describe('Examples pass', () => {
   for (const rule of rules) {
     describe(rule.name, () => {
       test.each(rule.examples)('$description', (testObject: Example) => {
         if (testObject.options && Object.keys(testObject.options).length > 0) {
-          expect(rule.apply(testObject.before, testObject.options)).toBe(testObject.after);
+          const options = Object.assign(rule.getDefaultOptions(), testObject.options);
+          expect(rule.apply(testObject.before, options)).toBe(testObject.after);
         } else {
           expect(rule.apply(testObject.before)).toBe(testObject.after);
         }
@@ -128,7 +129,7 @@ describe('Rules tests', () => {
         ## I Can't Do This
         ## Comma, Comma, Comma
         `;
-      expect(rulesDict['capitalize-headings'].apply(before, {titleCase: 'true'})).toBe(after);
+      expect(rulesDict['capitalize-headings'].apply(before, {'Title Case': true})).toBe(after);
     });
   });
   describe('File Name Heading', () => {
@@ -153,45 +154,6 @@ describe('Rules tests', () => {
   });
 });
 
-describe('Option parsing', () => {
-  it('Basic case', () => {
-    const line = 'yaml-timestamp';
-    expect(parseOptions(line)).toEqual({});
-  });
-  it('No quotes', () => {
-    const line = 'yaml-timestamp format=YYYY-MM';
-    expect(parseOptions(line)).toEqual({
-      'format': 'YYYY-MM',
-    });
-  });
-  it('Single quotes', () => {
-    const line = 'yaml-timestamp format=\'YYYY-MM\'';
-    expect(parseOptions(line)).toEqual({
-      'format': 'YYYY-MM',
-    });
-  });
-  it('Double quotes', () => {
-    const line = 'yaml-timestamp format="YYYY-MM"';
-    expect(parseOptions(line)).toEqual({
-      'format': 'YYYY-MM',
-    });
-  });
-  it('Many options', () => {
-    const line = `yaml-timestamp format="YYYY - MM" time=3 arg='123' `;
-    expect(parseOptions(line)).toEqual({
-      'format': 'YYYY - MM',
-      'time': '3',
-      'arg': '123',
-    });
-  });
-  it('Nested quotes', () => {
-    const line = `yaml-timestamp format="YYYY '-' MM"`;
-    expect(parseOptions(line)).toEqual({
-      'format': 'YYYY \'-\' MM',
-    });
-  });
-});
-
 describe('Ignored rules parsing', () => {
   it('No YAML', () => {
     const text = dedent`