Repository: platers/obsidian-linter
Pull request #49 (2021-09-17 05:43:38)

Disable rules in YAML
Implements #46 

YAML setting to disable rules for a file.


Patch:
diff --git a/README.md b/README.md
index dd5f893..b093679 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,18 @@ Add a rule by adding its alias in the settings. The listed rules are applied in
 Some rules have configurable options, which are set with the format `option=value`.
 For example, `yaml-timestamp format="YYYY-MM-DD"`
 
+### Disable rules
+
+```markdown
+---
+disabled rules: [ capitalize-headings ]
+---
+```
+
+Add `disabled rules: [ ... ]` to the YAML frontmatter of a file to disable those rules when linting that file. 
+
+Add `disabled rules: [ all ]` to the YAML frontmatter of a file to disable all rules.
+
 ## Development Instructions
 
 Pull requests are welcome, especially for new rules.
diff --git a/docs/readme_template.md b/docs/readme_template.md
index 28731bd..11bcc73 100644
--- a/docs/readme_template.md
+++ b/docs/readme_template.md
@@ -20,6 +20,18 @@ Add a rule by adding its alias in the settings. The listed rules are applied in
 Some rules have configurable options, which are set with the format `option=value`.
 For example, `yaml-timestamp format="YYYY-MM-DD"`
 
+### Disable rules
+
+```markdown
+---
+disabled rules: [ capitalize-headings ]
+---
+```
+
+Add `disabled rules: [ ... ]` to the YAML frontmatter of a file to disable those rules when linting that file. 
+
+Add `disabled rules: [ all ]` to the YAML frontmatter of a file to disable all rules.
+
 ## Development Instructions
 
 Pull requests are welcome, especially for new rules.
diff --git a/package-lock.json b/package-lock.json
index a99d735..e1a1eb6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,9 @@
       "version": "0.12.0",
       "license": "MIT",
       "dependencies": {
+        "@types/js-yaml": "^4.0.3",
         "diff": "^4.0.2",
+        "js-yaml": "^3.14.1",
         "moment": "^2.29.1",
         "ts-dedent": "^2.2.0"
       },
@@ -2405,6 +2407,11 @@
         "node": ">= 10"
       }
     },
+    "node_modules/@types/js-yaml": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.3.tgz",
+      "integrity": "sha512-5t9BhoORasuF5uCPr+d5/hdB++zRFUTMIZOzbNkr+jZh3yQht4HYbRDyj9fY8n2TZT30iW9huzav73x4NikqWg=="
+    },
     "node_modules/@types/json-schema": {
       "version": "7.0.9",
       "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
@@ -2823,7 +2830,6 @@
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
       "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
-      "dev": true,
       "dependencies": {
         "sprintf-js": "~1.0.2"
       }
@@ -3808,7 +3814,6 @@
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
       "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
-      "dev": true,
       "bin": {
         "esparse": "bin/esparse.js",
         "esvalidate": "bin/esvalidate.js"
@@ -5207,7 +5212,6 @@
       "version": "3.14.1",
       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
       "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
-      "dev": true,
       "dependencies": {
         "argparse": "^1.0.7",
         "esprima": "^4.0.0"
@@ -6214,8 +6218,7 @@
     "node_modules/sprintf-js": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
-      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
-      "dev": true
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
     },
     "node_modules/stack-utils": {
       "version": "2.0.3",
@@ -8639,6 +8642,11 @@
         }
       }
     },
+    "@types/js-yaml": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.3.tgz",
+      "integrity": "sha512-5t9BhoORasuF5uCPr+d5/hdB++zRFUTMIZOzbNkr+jZh3yQht4HYbRDyj9fY8n2TZT30iW9huzav73x4NikqWg=="
+    },
     "@types/json-schema": {
       "version": "7.0.9",
       "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
@@ -8925,7 +8933,6 @@
       "version": "1.0.10",
       "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
       "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
-      "dev": true,
       "requires": {
         "sprintf-js": "~1.0.2"
       }
@@ -9678,8 +9685,7 @@
     "esprima": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
-      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
-      "dev": true
+      "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="
     },
     "esquery": {
       "version": "1.4.0",
@@ -10755,7 +10761,6 @@
       "version": "3.14.1",
       "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
       "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
-      "dev": true,
       "requires": {
         "argparse": "^1.0.7",
         "esprima": "^4.0.0"
@@ -11504,8 +11509,7 @@
     "sprintf-js": {
       "version": "1.0.3",
       "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
-      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=",
-      "dev": true
+      "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw="
     },
     "stack-utils": {
       "version": "2.0.3",
diff --git a/package.json b/package.json
index 08b4c79..7972774 100644
--- a/package.json
+++ b/package.json
@@ -37,7 +37,9 @@
     "typescript": "^4.2.4"
   },
   "dependencies": {
+    "@types/js-yaml": "^4.0.3",
     "diff": "^4.0.2",
+    "js-yaml": "^3.14.1",
     "moment": "^2.29.1",
     "ts-dedent": "^2.2.0"
   }
diff --git a/src/main.ts b/src/main.ts
index f2dd40e..185d9cd 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,7 +1,7 @@
 import {App, Editor, MarkdownView, Notice, Plugin, PluginSettingTab, Setting, TFile} from 'obsidian';
 import dedent from 'ts-dedent';
 import {rulesDict} from './rules';
-import {parseOptions} from './utils';
+import {getDisabledRules, parseOptions} from './utils';
 import Diff from 'diff';
 import moment from 'moment';
 
@@ -79,8 +79,10 @@ 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('// ')) {
           continue;
         }
@@ -88,6 +90,10 @@ export default class LinterPlugin extends Plugin {
         // Split the line into the rule name and the rule options
         const ruleName = line.split(/\s+/)[0];
 
+        if (disabledRules.includes(ruleName)) {
+          continue;
+        }
+
         if (ruleName in rulesDict) {
           const options: { [id: string]: string; } =
             Object.assign({
diff --git a/src/utils.ts b/src/utils.ts
index 46225bc..3541944 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -1,3 +1,6 @@
+import {rules} from './rules';
+import {load} from 'js-yaml';
+
 // Useful regexes
 
 export const headerRegex = /^(\s*)(#+)(\s*)(.*)$/;
@@ -31,6 +34,39 @@ export function parseOptions(line: string) {
   return options;
 }
 
+/**
+ * Returns a list of ignored rules in the YAML frontmatter of the text.
+ * @param {string} text The text to parse
+ * @return {string[]} The list of ignored rules
+ */
+export function getDisabledRules(text: string): string[] {
+  const yaml = text.match(yamlRegex);
+  if (!yaml) {
+    return [];
+  }
+
+  const yaml_text = yaml[1];
+  const parsed_yaml = load(yaml_text) as {};
+  if (!parsed_yaml.hasOwnProperty('disabled rules')) {
+    return [];
+  }
+
+  let disabled_rules = (parsed_yaml as { 'disabled rules': string[] | string; })['disabled rules'];
+  if (!disabled_rules) {
+    return [];
+  }
+
+  if (typeof disabled_rules === 'string') {
+    disabled_rules = [disabled_rules];
+  }
+
+  if (disabled_rules.includes('all')) {
+    return rules.map((rule) => rule.alias());
+  }
+
+  return disabled_rules;
+}
+
 /**
  * Substitutes YAML and codeblocks in a text with a placeholder.
  * Then applies the given function to the text.


Test patch:
diff --git a/src/test.ts b/src/test.ts
index 573f02a..5144aac 100644
--- a/src/test.ts
+++ b/src/test.ts
@@ -1,6 +1,6 @@
 import dedent from 'ts-dedent';
 import {rules, Rule, rulesDict, Example} from './rules';
-import {parseOptions} from './utils';
+import {getDisabledRules, parseOptions} from './utils';
 
 describe('Examples pass', () => {
   for (const rule of rules) {
@@ -191,3 +191,48 @@ describe('Option parsing', () => {
     });
   });
 });
+
+describe('Ignored rules parsing', () => {
+  it('No YAML', () => {
+    const text = dedent`
+      Text
+      `;
+    expect(getDisabledRules(text)).toEqual([]);
+  });
+  it('No ignored rules', () => {
+    const text = dedent`
+      ---
+      ---
+      Text
+      `;
+    expect(getDisabledRules(text)).toEqual([]);
+  });
+  it('Ignore some rules', () => {
+    const text = dedent`
+      ---
+      random-rule: true
+      disabled rules: [ yaml-timestamp, capitalize-headings ]
+      ---
+      Text
+      `;
+    expect(getDisabledRules(text)).toEqual(['yaml-timestamp', 'capitalize-headings']);
+  });
+  it('Ignored no rules', () => {
+    const text = dedent`
+      ---
+      disabled rules:
+      ---
+      Text
+      `;
+    expect(getDisabledRules(text)).toEqual([]);
+  });
+  it('Ignored all rules', () => {
+    const text = dedent`
+      ---
+      disabled rules: all
+      ---
+      Text
+      `;
+    expect(getDisabledRules(text)).toEqual(rules.map((r) => r.alias()));
+  });
+});