Repository: platers/obsidian-linter (ts)

Issue:
FR: Allow for Sorting YAML Keys
Hi! 

The timestamp will be automatically inserted at the bottom of the front matter. I want it to be shown at the very beginning of the front matter, so I try to add my creat key, which is "creation date", and my modified key, which is "last modified", in `Text to insert`
```
creation date:
last modified:

title:
categories:
tags:
```

When I press Ctrl+Alt+L, it will only update the "last modified"

![image](https://user-images.githubusercontent.com/48120444/146381739-044f8b66-b3c2-46ce-8c3d-8046f638e2cd.png)


Rule suggestion: Sort YAML attributes
#160 #167

Seems hairy to figure out the logic of attribute placement in every yaml rule, would be nice to have a rule that can sort attributes. This way it can be applied to every document.

Simplest option is to sort alphabetically.
Should also have an option to specify the order of attributes manually in a text box.

Pull request:
Sort Yaml
Fixes #170 
Fixes #151 

Allows for the sorting of YAML frontmatter. It will remove blank lines since it does not know how to sort them.

Changes Made:
- Added a new rule for sorting yaml
- Added UTs for it
- Updated yaml value regex to allow for yaml with blank lines in it
- Updates date modified if the yaml is updated and the date modified key is set to be updated

Patch:
diff --git a/README.md b/README.md
index 5422b9e..12018f7 100644
--- a/README.md
+++ b/README.md
@@ -45,6 +45,7 @@ Documentation for all rules can be found in the [rules docs](https://github.com/
 - [yaml-title](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#yaml-title)
 - [yaml-title-alias](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#yaml-title-alias)
 - [escape-yaml-special-characters](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#escape-yaml-special-characters)
+- [yaml-key-sort](https://github.com/platers/obsidian-linter/blob/master/docs/rules.md#yaml-key-sort)
 
 ### Heading rules
 
diff --git a/docs/rules.md b/docs/rules.md
index f4706ac..e25e407 100644
--- a/docs/rules.md
+++ b/docs/rules.md
@@ -401,6 +401,133 @@ nestedArray2: [["value: with colon in the middle"], "value with ' a single quote
 _Note that escaped commas in a YAML array will be treated as a separator._
 ``````
 
+### YAML Key Sort
+
+Alias: `yaml-key-sort`
+
+Sorts the YAML keys based on the order and priority specified. Note: removes blank lines as well.
+
+Options:
+- YAML Key Priority Sort Order: The order in which to sort keys with one on each line where it sorts in the order found in the list
+	- Default: ``
+- Priority Keys at Start of YAML: YAML Key Priority Sort Order is placed at the start of the YAML frontmatter
+	- Default: `true`
+- YAML Sort Order for Other Keys: The way in which to sort the keys that are not found in the YAML Key Priority Sort Order text area
+	- Default: `None`
+	- `None`: No sorting other than what is in the YAML Key Priority Sort Order text area
+	- `Ascending Alphabetical`: Sorts the keys based on key value from a to z
+	- `Descending Alphabetical`: Sorts the keys based on key value from z to a
+
+Example: Sorts YAML keys in order specified by `YAML Key Priority Sort Order` has a sort order of `date type language`
+
+Before:
+
+``````markdown
+---
+language: Typescript
+type: programming
+tags: computer
+keywords: []
+status: WIP
+date: 02/15/2022
+---
+``````
+
+After:
+
+``````markdown
+---
+date: 02/15/2022
+type: programming
+language: Typescript
+tags: computer
+keywords: []
+status: WIP
+---
+``````
+Example: Sorts YAML keys in order specified by `YAML Key Priority Sort Order` has a sort order of `date type language` with `'YAML Sort Order for Other Keys' = Ascending Alphabetical`
+
+Before:
+
+``````markdown
+---
+language: Typescript
+type: programming
+tags: computer
+keywords: []
+status: WIP
+date: 02/15/2022
+---
+``````
+
+After:
+
+``````markdown
+---
+date: 02/15/2022
+type: programming
+language: Typescript
+keywords: []
+status: WIP
+tags: computer
+---
+``````
+Example: Sorts YAML keys in order specified by `YAML Key Priority Sort Order` has a sort order of `date type language` with `'YAML Sort Order for Other Keys' = Descending Alphabetical`
+
+Before:
+
+``````markdown
+---
+language: Typescript
+type: programming
+tags: computer
+keywords: []
+status: WIP
+date: 02/15/2022
+---
+``````
+
+After:
+
+``````markdown
+---
+date: 02/15/2022
+type: programming
+language: Typescript
+tags: computer
+status: WIP
+keywords: []
+---
+``````
+Example: Sorts YAML keys in order specified by `YAML Key Priority Sort Order` has a sort order of `date type language` with `'YAML Sort Order for Other Keys' = Descending Alphabetical` and `'Priority Keys at Start of YAML' = false`
+
+Before:
+
+``````markdown
+---
+language: Typescript
+type: programming
+tags: computer
+keywords: []
+
+status: WIP
+date: 02/15/2022
+---
+``````
+
+After:
+
+``````markdown
+---
+tags: computer
+status: WIP
+keywords: []
+date: 02/15/2022
+type: programming
+language: Typescript
+---
+``````
+
 ## Heading
 ### Header Increment
 
diff --git a/src/main.ts b/src/main.ts
index d9453dd..b667db2 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -209,7 +209,8 @@ export default class LinterPlugin extends Plugin {
 
       for (const rule of rules) {
         // if you are run prior to or after the regular rules or are a disabled rule, skip running the rule
-        if (disabledRules.includes(rule.alias()) || rule.alias() === 'yaml-timestamp' || rule.alias() === 'format-tags-in-yaml' || rule.alias() === 'escape-yaml-special-characters') {
+        if (disabledRules.includes(rule.alias()) || rule.alias() === 'yaml-timestamp' || rule.alias() === 'format-tags-in-yaml' || rule.alias() === 'escape-yaml-special-characters' ||
+          rule.alias() === 'yaml-key-sort') {
           continue;
         }
 
@@ -238,10 +239,24 @@ export default class LinterPlugin extends Plugin {
         'Already Modified': oldText != newText,
         'moment': this.momentInstance,
       }, yaml_timestamp_rule.getOptions(this.settings));
-      if (yaml_timestamp_options[yaml_timestamp_rule.enabledOptionName()]) {
+      const yaml_timestamp_is_enabled = yaml_timestamp_options[yaml_timestamp_rule.enabledOptionName()];
+      if (yaml_timestamp_is_enabled) {
         newText = yaml_timestamp_rule.apply(newText, yaml_timestamp_options);
       }
 
+      const yaml_key_sort_rule = rules.find((rule) => rule.alias() === 'yaml-key-sort');
+      const yaml_key_sort_options: Options = Object.assign({
+        'metadata: file created time': this.momentInstance(file.stat.ctime).format(),
+        'metadata: file modified time': this.momentInstance(file.stat.mtime).format(),
+        'metadata: file name': file.basename,
+        'Current Time Formatted': this.momentInstance().format(yaml_timestamp_options['Format']),
+        'Yaml Timestamp Date Modified Enabled': yaml_timestamp_is_enabled && yaml_timestamp_options['Date Modified'],
+        'Date Modified Key': yaml_timestamp_options['Date Modified Key'],
+      }, yaml_key_sort_rule.getOptions(this.settings));
+      if (yaml_key_sort_options[yaml_key_sort_rule.enabledOptionName()]) {
+        newText = yaml_key_sort_rule.apply(newText, yaml_key_sort_options);
+      }
+
       return newText;
     }
 
diff --git a/src/rules.ts b/src/rules.ts
index 19cabbc..74b3b50 100644
--- a/src/rules.ts
+++ b/src/rules.ts
@@ -2161,6 +2161,215 @@ export const rules: Rule[] = [
       ],
   ),
 
+  new Rule(
+      'YAML Key Sort',
+      'Sorts the YAML keys based on the order and priority specified. Note: removes blank lines as well.',
+      RuleType.YAML,
+      (text: string, options = {}) => {
+        const yaml = text.match(yamlRegex);
+        if (!yaml) {
+          return text;
+        }
+
+        let yamlText = yaml[1];
+
+        const priorityAtStartOfYaml: boolean = options['Priority Keys at Start of YAML'];
+        const updateDateModifiedIfYamlChanged = function(oldYaml: string, newYaml: string): string {
+          if (oldYaml == newYaml) {
+            return newYaml;
+          }
+
+          return setYamlSection(newYaml, options['Date Modified Key'], ' ' + options['Current Time Formatted']);
+        };
+        const getTextWithNewYamlFrontmatter = function(priorityKeysSorted: string, remainingKeys: string, priorityAtStart: boolean): string {
+          let newYaml = `${remainingKeys}${priorityKeysSorted}`;
+          if (priorityAtStart) {
+            newYaml = `${priorityKeysSorted}${remainingKeys}`;
+          }
+
+          if (options['Yaml Timestamp Date Modified Enabled']) {
+            newYaml = updateDateModifiedIfYamlChanged(yaml[1], newYaml);
+          }
+
+          return text.replace(yaml[1], newYaml);
+        };
+
+        const getYAMLKeysSorted = function(yaml: string, keys: string[]): {remainingYaml: string, sortedYamlKeyValues: string} {
+          let specifiedYamlKeysSorted = '';
+          for (const key of keys) {
+            const value = getYamlSectionValue(yaml, key);
+
+            if (value !== null) {
+              if (value.includes('\n')) {
+                specifiedYamlKeysSorted += `${key}:${value}\n`;
+              } else {
+                specifiedYamlKeysSorted += `${key}: ${value}\n`;
+              }
+
+              yaml = removeYamlSection(yaml, key);
+            }
+          }
+
+          return {
+            remainingYaml: yaml,
+            sortedYamlKeyValues: specifiedYamlKeysSorted,
+          };
+        };
+
+        const yamlKeys: string[] = options['YAML Key Priority Sort Order'].split('\n');
+        const sortKeysResult = getYAMLKeysSorted(yamlText, yamlKeys);
+        const priorityKeysSorted = sortKeysResult.sortedYamlKeyValues;
+        yamlText = sortKeysResult.remainingYaml;
+
+        const sortOrder = options['YAML Sort Order for Other Keys'];
+        const yamlObject = loadYAML(yamlText);
+        if (yamlObject == null) {
+          return getTextWithNewYamlFrontmatter(priorityKeysSorted, yamlText, priorityAtStartOfYaml);
+        }
+
+        const sortAlphabeticallyDesc = function(previousKey: string, currentKey: string): number {
+          previousKey = previousKey.toLowerCase();
+          currentKey = currentKey.toLowerCase();
+
+          return previousKey > currentKey ? -1 : currentKey > previousKey ? 1 : 0;
+        };
+        const sortAlphabeticallyAsc = function(previousKey: string, currentKey: string): number {
+          previousKey = previousKey.toLowerCase();
+          currentKey = currentKey.toLowerCase();
+
+          return previousKey < currentKey ? -1 : currentKey < previousKey ? 1 : 0;
+        };
+
+        let remainingKeys = Object.keys(yamlObject);
+        let sortMethod: (previousKey: string, currentKey: string) => number;
+        if (sortOrder === 'Ascending Alphabetical') {
+          sortMethod = sortAlphabeticallyAsc;
+        } else if (sortOrder === 'Descending Alphabetical') {
+          sortMethod = sortAlphabeticallyDesc;
+        } else {
+          return getTextWithNewYamlFrontmatter(priorityKeysSorted, yamlText, priorityAtStartOfYaml);
+        }
+
+        remainingKeys = remainingKeys.sort(sortMethod);
+        const remainingKeysSortResult = getYAMLKeysSorted(yamlText, remainingKeys);
+
+        return getTextWithNewYamlFrontmatter(priorityKeysSorted, remainingKeysSortResult.sortedYamlKeyValues, priorityAtStartOfYaml);
+      },
+      [
+        new Example(
+            'Sorts YAML keys in order specified by `YAML Key Priority Sort Order` has a sort order of `date type language`',
+            dedent`
+      ---
+      language: Typescript
+      type: programming
+      tags: computer
+      keywords: []
+      status: WIP
+      date: 02/15/2022
+      ---
+      `,
+            dedent`
+      ---
+      date: 02/15/2022
+      type: programming
+      language: Typescript
+      tags: computer
+      keywords: []
+      status: WIP
+      ---
+      `,
+            {'YAML Key Priority Sort Order': 'date\ntype\nlanguage', 'YAML Sort Order for Other Keys': 'None', 'Priority Keys at Start of YAML': true},
+        ),
+        new Example(
+            'Sorts YAML keys in order specified by `YAML Key Priority Sort Order` has a sort order of `date type language` with `\'YAML Sort Order for Other Keys\' = Ascending Alphabetical`',
+            dedent`
+      ---
+      language: Typescript
+      type: programming
+      tags: computer
+      keywords: []
+      status: WIP
+      date: 02/15/2022
+      ---
+      `,
+            dedent`
+      ---
+      date: 02/15/2022
+      type: programming
+      language: Typescript
+      keywords: []
+      status: WIP
+      tags: computer
+      ---
+      `,
+            {'YAML Key Priority Sort Order': 'date\ntype\nlanguage', 'YAML Sort Order for Other Keys': 'Ascending Alphabetical'},
+        ),
+        new Example(
+            'Sorts YAML keys in order specified by `YAML Key Priority Sort Order` has a sort order of `date type language` with `\'YAML Sort Order for Other Keys\' = Descending Alphabetical`',
+            dedent`
+      ---
+      language: Typescript
+      type: programming
+      tags: computer
+      keywords: []
+      status: WIP
+      date: 02/15/2022
+      ---
+      `,
+            dedent`
+      ---
+      date: 02/15/2022
+      type: programming
+      language: Typescript
+      tags: computer
+      status: WIP
+      keywords: []
+      ---
+      `,
+            {'YAML Key Priority Sort Order': 'date\ntype\nlanguage', 'YAML Sort Order for Other Keys': 'Descending Alphabetical', 'Priority Keys at Start of YAML': true},
+        ),
+        new Example(
+            'Sorts YAML keys in order specified by `YAML Key Priority Sort Order` has a sort order of `date type language` with `\'YAML Sort Order for Other Keys\' = Descending Alphabetical` and `\'Priority Keys at Start of YAML\' = false`',
+            dedent`
+  ---
+  language: Typescript
+  type: programming
+  tags: computer
+  keywords: []
+
+  status: WIP
+  date: 02/15/2022
+  ---
+  `,
+            dedent`
+  ---
+  tags: computer
+  status: WIP
+  keywords: []
+  date: 02/15/2022
+  type: programming
+  language: Typescript
+  ---
+  `,
+            {'YAML Key Priority Sort Order': 'date\ntype\nlanguage', 'YAML Sort Order for Other Keys': 'Descending Alphabetical', 'Priority Keys at Start of YAML': false},
+        ),
+      ],
+      [
+        new TextAreaOption('YAML Key Priority Sort Order', 'The order in which to sort keys with one on each line where it sorts in the order found in the list', ''),
+        new BooleanOption('Priority Keys at Start of YAML', 'YAML Key Priority Sort Order is placed at the start of the YAML frontmatter', true),
+        new DropdownOption(
+            'YAML Sort Order for Other Keys',
+            'The way in which to sort the keys that are not found in the YAML Key Priority Sort Order text area',
+            'None',
+            [
+              new DropdownRecord('None', 'No sorting other than what is in the YAML Key Priority Sort Order text area'),
+              new DropdownRecord('Ascending Alphabetical', 'Sorts the keys based on key value from a to z'),
+              new DropdownRecord('Descending Alphabetical', 'Sorts the keys based on key value from z to a'),
+            ],
+        ),
+      ],
+  ),
+
   // Heading rules
 
   new Rule(
diff --git a/src/utils.ts b/src/utils.ts
index 746d4aa..ad1cb9a 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -503,7 +503,7 @@ export function toSingleLineArrayYamlString<T>(arr: T[]): string {
 }
 
 function getYamlSectionRegExp(rawKey: string): RegExp {
-  return new RegExp(`(?<=^|\\n)${rawKey}:[ \\t]*(\\S.*|(?:\\n {2}\\S.*)*)\\n(?=$|\\S)`);
+  return new RegExp(`(?<=^|\\n)${rawKey}:[ \\t]*(\\S.*|(?:\\n {2}\\S.*)*)\\n`);
 }
 
 export function setYamlSection(yaml: string, rawKey: string, rawValue: string): string {


Test patch:
diff --git a/src/test/yaml-key-sort.test.ts b/src/test/yaml-key-sort.test.ts
new file mode 100644
index 0000000..2e40512
--- /dev/null
+++ b/src/test/yaml-key-sort.test.ts
@@ -0,0 +1,40 @@
+import dedent from 'ts-dedent';
+import {rulesDict} from '../rules';
+
+describe('YAML Key Sort', () => {
+  it('When the sort changes the yaml contents and yaml timestamp date modified is active, update date modified value', () => {
+    const before = dedent`
+      ---
+      language: Typescript
+      type: programming
+      tags: computer
+      keywords: []
+      status: WIP
+      date: 01/15/2022
+      modified: Wednesday, January 1st 2020, 12:00:00 am
+      ---
+      `;
+    const after = dedent`
+      ---
+      date: 01/15/2022
+      type: programming
+      language: Typescript
+      tags: computer
+      keywords: []
+      status: WIP
+      modified: Thursday, January 2nd 2020, 12:00:00 am
+      ---
+      `;
+
+    const options = {
+      'YAML Key Priority Sort Order': 'date\ntype\nlanguage',
+      'YAML Sort Order for Other Keys': 'None',
+      'Priority Keys at Start of YAML': true,
+      'Date Modified Key': 'modified',
+      'Current Time Formatted': 'Thursday, January 2nd 2020, 12:00:00 am',
+      'Already Modified': false,
+      'Yaml Timestamp Date Modified Enabled': true,
+    };
+    expect(rulesDict['yaml-key-sort'].apply(before, options)).toBe(after);
+  });
+});