Repository: platers/obsidian-linter (ts)

Issue:
Timestamp improvement
I have linting set to auto-lint on save.

As a habit, I always lint files, even if I did not modify. The issue is if there was no linting issue (the file was not changed), the modified date is updated regardless, even if nothing was actually modified.

It gets worse when I lint all files, because all files will have their modified timestamps changed. This is an issue esp when using version control, because this will modify all files all the time.

NB: I don't think anyone would want to change the timestamps of an unchanged file, so maybe it would be better to just add this improvement without a settings checkbox. Should be a minor version change.

Pull request description:
Timestamp YAML Rule Now Fills in Empty Keys and Only Updates Modified Key with a File Change
Fixes #221 

There was an issue where if you were to lint a file that had no other changes besides the what would be added by date modified, it would update the file with date modified value. This should be fixed via this PR. 
I was also made aware that there was an issue where the keys for the timestamp YAML rule would not be filled in if they were present and blank (this became a new issue for updated date when it was only to be changed on an update to the file). This should be fixed now as well. We may want to consider cleaning up the logic here since it is getting a little clunky, but this should do for now.

Changes Made:
- Added tests to account for different update scenarios as well as blank values to help mitigate any issues with this in the future
- Updated the way we run rules to wait to run timestamp YAML until the very end so we can check if the file has changed more easily

@platers , could you elaborate on why we have logic for backwards compatibility around replacing the date modified and date created values? It would help a good bit if I could remove them since they seem to just be an extra replace something changed a while back (about 9 months ago): https://github.com/platers/obsidian-linter/blame/a423daddd57f4a0b40c99160a50a0b0028797997/src/rules.ts#L432

Patch:
diff --git a/src/main.ts b/src/main.ts
index e9992ea..ec52091 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -149,15 +149,16 @@ export default class LinterPlugin extends Plugin {
 
       // remove hashtags from tags before parsing yaml
       const tag_rule = rules.find((rule) => rule.name === 'Format Tags in YAML');
-      const options = tag_rule.getOptions(this.settings);
-      if (options[tag_rule.enabledOptionName()]) {
-        newText = tag_rule.apply(newText, options);
+      const tag_options = tag_rule.getOptions(this.settings);
+      if (tag_options[tag_rule.enabledOptionName()]) {
+        newText = tag_rule.apply(newText, tag_options);
       }
 
       const disabledRules = getDisabledRules(newText);
 
       for (const rule of rules) {
-        if (disabledRules.includes(rule.alias())) {
+        // if you are yaml timestamp or a disabled rule, skip running them
+        if (disabledRules.includes(rule.alias()) || rule.alias() === 'yaml-timestamp') {
           continue;
         }
 
@@ -174,6 +175,19 @@ export default class LinterPlugin extends Plugin {
         }
       }
 
+      // run yaml timestamp at the end to help determine if something has changed
+      const yaml_timestamp_rule = rules.find((rule) => rule.alias() === 'yaml-timestamp');
+      const yaml_timestamp_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,
+        'Already Modified': oldText != newText,
+      }, yaml_timestamp_rule.getOptions(this.settings));
+      if (yaml_timestamp_options[yaml_timestamp_rule.enabledOptionName()]) {
+        newText = yaml_timestamp_rule.apply(newText, yaml_timestamp_options);
+      }
+
       return newText;
     }
 
diff --git a/src/rules.ts b/src/rules.ts
index a7add76..b0be9c2 100644
--- a/src/rules.ts
+++ b/src/rules.ts
@@ -1296,26 +1296,46 @@ export const rules: Rule[] = [
       '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 = {}) => {
-        text = initYAML(text);
-
-        return formatYAML(text, (text) => {
-          const created_match_str = `\n${options['Date Created Key']}.*\n`;
+        let textModified = options['Already Modified'] === true;
+        const newText = initYAML(text);
+        textModified = textModified || newText !== text;
+
+        return formatYAML(newText, (text) => {
+          const created_match_str = `\n${options['Date Created Key']}: [^\n]+\n`;
+          const created_key_match_str = `\n${options['Date Created Key']}:[ \t]*\n`;
+          const created_key_match = new RegExp(created_key_match_str);
           const created_match = new RegExp(created_match_str);
 
-          if (options['Date Created'] === true && !created_match.test(text)) {
-            const yaml_end = text.indexOf('\n---');
+          if (options['Date Created'] === true) {
             const formatted_date = moment(
                 options['metadata: file created time'],
             ).format(options['Format']);
-            text = insert(
-                text,
-                yaml_end,
-                `\n${options['Date Created Key']}: ${formatted_date}`,
-            );
+            const created_date_line = `\n${options['Date Created Key']}: ${formatted_date}`;
+
+            const keyWithValueFound = created_match.test(text);
+            if (!keyWithValueFound && created_key_match.test(text)) {
+              text = text.replace(
+                  created_key_match,
+                  escapeDollarSigns(created_date_line) + '\n',
+              );
+
+              textModified = true;
+            } else if (!keyWithValueFound) {
+              const yaml_end = text.indexOf('\n---');
+              text = insert(
+                  text,
+                  yaml_end,
+                  `\n${options['Date Created Key']}: ${formatted_date}`,
+              );
+
+              textModified = true;
+            }
           }
 
           if (options['Date Modified'] === true) {
-            const modified_match_str = `\n${options['Date Modified Key']}.*\n`;
+            const modified_match_str = `\n${options['Date Modified Key']}: [^\n]+\n`;
+            const modified_key_match_str = `\n${options['Date Modified Key']}:[ \t]*\n`;
+            const modified_key_match = new RegExp(modified_key_match_str);
             const modified_match = new RegExp(modified_match_str);
 
             const formatted_date = moment(
@@ -1323,20 +1343,23 @@ export const rules: Rule[] = [
             ).format(options['Format']);
             const modified_date_line = `\n${options['Date Modified Key']}: ${formatted_date}`;
 
-            if (modified_match.test(text)) {
+            const keyWithValueFound = modified_match.test(text);
+            if (keyWithValueFound && textModified) {
               text = text.replace(
                   modified_match,
                   escapeDollarSigns(modified_date_line) + '\n',
               );
+            } else if (modified_key_match.test(text)) {
               text = text.replace(
-                  /\ndate updated:.*\n/,
+                  modified_key_match,
                   escapeDollarSigns(modified_date_line) + '\n',
-              ); // for backwards compatibility
-            } else {
+              );
+            } else if (!keyWithValueFound) {
               const yaml_end = text.indexOf('\n---');
               text = insert(text, yaml_end, modified_date_line);
             }
           }
+
           return text;
         });
       },
@@ -1356,6 +1379,7 @@ export const rules: Rule[] = [
             {
               'metadata: file created time': '2020-01-01T00:00:00-00:00',
               'metadata: file modified time': '2020-01-02T00:00:00-00:00',
+              'Already Modified': false,
             },
         ),
         new Example(
@@ -1373,6 +1397,7 @@ export const rules: Rule[] = [
               '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',
+              'Already Modified': false,
             },
         ),
         new Example(
@@ -1391,6 +1416,7 @@ export const rules: Rule[] = [
               'Date Modified': false,
               'Date Created Key': 'created',
               'metadata: file created time': '2020-01-01T00:00:00-00:00',
+              'Already Modified': false,
             },
         ),
         new Example(
@@ -1409,6 +1435,7 @@ export const rules: Rule[] = [
               'Date Modified': true,
               'Date Modified Key': 'modified',
               'metadata: file modified time': '2020-01-01T00:00:00-00:00',
+              'Already Modified': false,
             },
         ),
       ],


Test patch:
diff --git a/src/test.ts b/src/test.ts
index 62134ba..2929fb5 100644
--- a/src/test.ts
+++ b/src/test.ts
@@ -517,7 +517,7 @@ describe('Move Footnotes to the bottom', () => {
   });
 });
 describe('yaml timestamp', () => {
-  it('Doesnt add date created if already there', () => {
+  it('Doesn\'t add date created if already there', () => {
     const before = dedent`
     ---
     date created: 2019-01-01
@@ -530,7 +530,7 @@ describe('yaml timestamp', () => {
     `;
     expect(rulesDict['yaml-timestamp'].apply(before, {'Date Created': true, 'Date Created Key': 'date created'})).toBe(after);
   });
-  it('Respects created and modified key', () => {
+  it('Respects created key', () => {
     const before = dedent`
     ---
     created: 2019-01-01
@@ -543,6 +543,126 @@ describe('yaml timestamp', () => {
     `;
     expect(rulesDict['yaml-timestamp'].apply(before, {'Date Created': true, 'Date Created Key': 'created'})).toBe(after);
   });
+  it('Respects modified key when nothing has changed', () => {
+    const before = dedent`
+    ---
+    modified: Wednesday, January 1st 2020, 12:00:00 am
+    ---
+    `;
+    const after = dedent`
+    ---
+    modified: Wednesday, January 1st 2020, 12:00:00 am
+    ---
+    `;
+
+    const options = {
+      'Date Created': false,
+      'Date Modified': true,
+      'Date Modified Key': 'modified',
+      'metadata: file modified time': '2020-02-01T00:00:00-00:00',
+      'Already Modified': false,
+    };
+    expect(rulesDict['yaml-timestamp'].apply(before, options)).toBe(after);
+  });
+  it('Updates modified key when something has changed outside of the YAML timestamp rule', () => {
+    const before = dedent`
+    ---
+    modified: Wednesday, January 1st 2020, 12:00:00 am
+    ---
+    `;
+    const after = dedent`
+    ---
+    modified: Saturday, February 1st 2020, 12:00:00 am
+    ---
+    `;
+
+    const options = {
+      'Date Created': false,
+      'Date Modified': true,
+      'Date Modified Key': 'modified',
+      'metadata: file modified time': '2020-02-01T00:00:00-00:00',
+      'Already Modified': true,
+      'Format': 'dddd, MMMM Do YYYY, h:mm:ss a',
+    };
+    expect(rulesDict['yaml-timestamp'].apply(before, options)).toBe(after);
+  });
+  it('Updates modified key when something has changed inside of the YAML timestamp rule', () => {
+    const before = dedent`
+    ---
+    modified: Thursday, January 2nd 2020, 12:00:00 am
+    ---
+    `;
+    const after = dedent`
+    ---
+    modified: Thursday, January 2nd 2020, 12:00:00 am
+    created: Wednesday, January 1st 2020, 12:00:00 am
+    ---
+    `;
+
+    const options = {
+      'Date Created': true,
+      'Date Modified': true,
+      'Date Created Key': 'created',
+      'Date Modified Key': 'modified',
+      'metadata: file created time': '2020-01-01T00:00:00-00:00',
+      'metadata: file modified time': '2020-01-02T00:00:00-00:00',
+      'Already Modified': false,
+      'Format': 'dddd, MMMM Do YYYY, h:mm:ss a',
+    };
+    expect(rulesDict['yaml-timestamp'].apply(before, options)).toBe(after);
+  });
+  it('Updates modified key when present, but lacks a value', () => {
+    const before = dedent`
+    ---
+    tag: tag1
+    modified: 
+    location: "path"
+    ---
+    `;
+    const after = dedent`
+    ---
+    tag: tag1
+    modified: Saturday, February 1st 2020, 12:00:00 am
+    location: "path"
+    ---
+    `;
+
+    const options = {
+      'Date Created': false,
+      'Date Modified': true,
+      'Date Modified Key': 'modified',
+      'metadata: file modified time': '2020-02-01T00:00:00-00:00',
+      'Already Modified': false,
+      'Format': 'dddd, MMMM Do YYYY, h:mm:ss a',
+    };
+    expect(rulesDict['yaml-timestamp'].apply(before, options)).toBe(after);
+  });
+  it('Updates created key when present, but lacks a value', () => {
+    const before = dedent`
+    ---
+    tag: tag1
+    created: 
+    location: "path"
+    ---
+    `;
+    const after = dedent`
+    ---
+    tag: tag1
+    created: Saturday, February 1st 2020, 12:00:00 am
+    location: "path"
+    ---
+    `;
+
+    const options = {
+      'Date Created': true,
+      'Date Modified': false,
+      'Date Created Key': 'created',
+      'metadata: file created time': '2020-02-01T00:00:00-00:00',
+      'Already Modified': false,
+      'Format': 'dddd, MMMM Do YYYY, h:mm:ss a',
+    };
+    expect(rulesDict['yaml-timestamp'].apply(before, options)).toBe(after);
+  });
 });
 describe('Insert yaml attributes', () => {
   it('Inits yaml if it does not exist', () => {