Skip to content

Commit

Permalink
Merge pull request #623 from pjkaufman/master
Browse files Browse the repository at this point in the history
Add Option for Retaining Created Value as Much as Possible
  • Loading branch information
pjkaufman authored Feb 7, 2023
2 parents 33ec607 + 395f51a commit 6129737
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 72 deletions.
20 changes: 20 additions & 0 deletions __tests__/yaml-timestamp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,26 @@ ruleTest({
format: '',
},
},
{
testName: 'When the date format changes and `forceRetentionOfCreatedValue = true`, date created value is based on the one in the YAML frontmatter.',
before: dedent`
---
created: Wednesday, January 1st 2020, 12:00:00 am
---
`,
after: dedent`
---
created: 2020, 12:00:00 am
---
`,
options: {
dateModified: false,
dateCreatedKey: 'created',
forceRetentionOfCreatedValue: true,
format: 'YYYY, h:mm:ss a',
locale: 'en',
},
},
{
testName: 'Respects modified key when nothing has changed',
before: dedent`
Expand Down
10 changes: 8 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"micromark-extension-gfm-task-list-item": "^1.0.3",
"micromark-extension-math": "^2.0.2",
"micromark-util-combine-extensions": "^1.0.0",
"moment-parseformat": "^4.0.0",
"quick-lru": "^6.1.1",
"ts-dedent": "^2.2.0"
}
Expand Down
9 changes: 9 additions & 0 deletions src/lang/locale/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,12 @@ export default {
'custom-replace-regex-to-replace-placeholder-text': 'regex to replace',
'custom-replace-delete-tooltip': 'Delete',

// mdast.ts
'missing-footnote-error-message': `Footnote '{FOOTNOTE}' has no corresponding footnote reference before the footnote contents and cannot be processed. Please make sure that all footnotes have a corresponding reference before the content of the footnote.`,

// yaml.ts
'invalid-delimiter-error-message': 'delimiter is only allowed to be a single character',

// rules
// auto-correct-common-misspellings.ts
'auto-correct-common-misspellings-name': 'Auto-correct Common Misspellings',
Expand Down Expand Up @@ -406,6 +412,9 @@ export default {
'yaml-timestamp-date-modified-key-description': 'Which YAML key to use for modification date',
'yaml-timestamp-format-name': 'Format',
'yaml-timestamp-format-description': 'Moment date format to use (see [Moment format options](https://momentjscom.readthedocs.io/en/latest/moment/04-displaying/01-format/))',
'yaml-timestamp-force-retention-of-create-value-name': 'Force Retention of `Date Created Key`\'s Value',
'yaml-timestamp-force-retention-of-create-value-description': 'Forces the reuse of the value of `Date Created Key` in the YAML frontmatter if it exists instead of using the file metadata which helps preserve the created at timestamp when syncing a file between devices or after a file has lost its original creation date.',
'invalid-date-format-error': `The format of the created date '{DATE}' could not be parsed or determined so the created date was left alone in '{FILE_NAME}'`,
// yaml-title-alias.ts
'yaml-title-alias-name': 'YAML Title Alias',
'yaml-title-alias-description': 'Inserts the title of the file into the YAML frontmatter\'s aliases section. Gets the title from the first H1 or filename.',
Expand Down
4 changes: 2 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,7 @@ export default class LinterPlugin extends Plugin {
try {
newText = this.rulesRunner.lintText(createRunLinterRulesOptions(oldText, file, this.momentLocale, this.settings));
} catch (error) {
this.handleLintError(file, error, getTextInLanguage('lint-all-files-error-message') + ' \'{FILE_PATH}\'', false);
this.handleLintError(file, error, getTextInLanguage('lint-file-error-message') + ' \'{FILE_PATH}\'', false);
return;
}

Expand Down Expand Up @@ -387,7 +387,7 @@ export default class LinterPlugin extends Plugin {
try {
this.rulesRunner.runCustomCommands(this.settings.lintCommands, this.app.commands);
} catch (error) {
this.handleLintError(file, error, getTextInLanguage('lint-all-files-error-message') + ' \'{FILE_PATH}\'', false);
this.handleLintError(file, error, getTextInLanguage('lint-file-error-message') + ' \'{FILE_PATH}\'', false);
}
}

Expand Down
196 changes: 131 additions & 65 deletions src/rules/yaml-timestamp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@ import {formatYAML, initYAML} from '../utils/yaml';
import {moment} from 'obsidian';
import {escapeDollarSigns} from '../utils/regex';
import {insert} from '../utils/strings';
import parseFormat from 'moment-parseformat';
import {getTextInLanguage} from '../lang/helpers';

class YamlTimestampOptions implements Options {
@RuleBuilder.noSettingControl()
alreadyModified?: boolean;

dateCreatedKey?: string = 'date created';
dateCreated?: boolean = true;
forceRetentionOfCreatedValue?: boolean = false;

@RuleBuilder.noSettingControl()
fileCreatedTime?: string;
Expand All @@ -28,6 +31,9 @@ class YamlTimestampOptions implements Options {

@RuleBuilder.noSettingControl()
currentTime?: moment.Moment;

@RuleBuilder.noSettingControl()
fileName?: string;
}

@RuleBuilder.register
Expand All @@ -49,85 +55,139 @@ export default class YamlTimestamp extends RuleBuilder<YamlTimestampOptions> {
textModified = textModified || newText !== text;

return formatYAML(newText, (text) => {
const created_match_str = `\n${options.dateCreatedKey}: [^\n]+\n`;
const created_key_match_str = `\n${options.dateCreatedKey}:[ \t]*\n`;
const created_key_match = new RegExp(created_key_match_str);
const created_match = new RegExp(created_match_str);

if (options.dateCreated) {
const created_date = moment(options.fileCreatedTime);
created_date.locale(options.locale);
let newTextModified = false;
[text, newTextModified] = this.handleDateCreatedValue(text, options);

const formatted_date = created_date.format(options.format);
const created_date_line = `\n${options.dateCreatedKey}: ${formatted_date}`;
textModified = textModified || newTextModified;
}

const keyWithValueFound = created_match.test(text);
if (!keyWithValueFound && created_key_match.test(text)) {
text = text.replace(
created_key_match,
escapeDollarSigns(created_date_line) + '\n',
);
if (options.dateModified) {
text = this.handleDateModifiedValue(text, textModified, options);
}

textModified = true;
} else if (!keyWithValueFound) {
const yaml_end = text.indexOf('\n---');
text = insert(
text,
yaml_end,
`\n${options.dateCreatedKey}: ${formatted_date}`,
);
return text;
});
}
handleDateCreatedValue(text: string, options: YamlTimestampOptions): [string, boolean] {
let textModified = false;
const created_match_str = `\n${options.dateCreatedKey}: [^\n]+\n`;
const created_key_match_str = `\n${options.dateCreatedKey}:[ \t]*\n`;
const created_key_match = new RegExp(created_key_match_str);
const created_match = new RegExp(created_match_str);

const created_date = moment(options.fileCreatedTime);
created_date.locale(options.locale);

const formatted_date = created_date.format(options.format);
const created_date_line = `\n${options.dateCreatedKey}: ${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 createdDateTime = moment(text.match(created_match)[0].replace(options.dateCreatedKey + ':', '').trim(), options.format, options.locale, true);
if (createdDateTime == undefined || !createdDateTime.isValid()) {
text = text.replace(
created_match,
escapeDollarSigns(created_date_line) + '\n',
);
textModified = true;
} else if (!keyWithValueFound) {
const yaml_end = text.indexOf('\n---');
text = insert(
text,
yaml_end,
`\n${options.dateCreatedKey}: ${formatted_date}`,
);

textModified = true;
}
textModified = true;
} else if (keyWithValueFound) {
const createdDateString = this.getYAMLTimestampString(text, created_match, options.dateCreatedKey);
const createdDateTime = moment(createdDateString, options.format, options.locale, true);
if (createdDateTime == undefined || (!createdDateTime.isValid() && !options.forceRetentionOfCreatedValue)) {
text = text.replace(
created_match,
escapeDollarSigns(created_date_line) + '\n',
);

textModified = true;
} else if (options.forceRetentionOfCreatedValue) {
const yamlCreatedDateTime = this.parseValueToCurrentFormatIfPossible(createdDateString, options.format, options.locale);
if (yamlCreatedDateTime == null) {
throw new Error(getTextInLanguage('invalid-date-format-error').replace('{DATE}', createdDateString).replace('{FILE_NAME}', options.fileName));
}

const created_date_yaml_line = `\n${options.dateCreatedKey}: ${yamlCreatedDateTime.format(options.format)}`;
text = text.replace(
created_match,
escapeDollarSigns(created_date_yaml_line) + '\n',
);

textModified = true;
}
}

if (options.dateModified) {
const modified_match_str = `\n${options.dateModifiedKey}: [^\n]+\n`;
const modified_key_match_str = `\n${options.dateModifiedKey}:[ \t]*\n`;
const modified_key_match = new RegExp(modified_key_match_str);
const modified_match = new RegExp(modified_match_str);
return [text, textModified];
}
handleDateModifiedValue(text: string, textModified: boolean, options: YamlTimestampOptions): string {
const modified_match_str = `\n${options.dateModifiedKey}: [^\n]+\n`;
const modified_key_match_str = `\n${options.dateModifiedKey}:[ \t]*\n`;
const modified_key_match = new RegExp(modified_key_match_str);
const modified_match = new RegExp(modified_match_str);

const modified_date = moment(options.fileModifiedTime);
modified_date.locale(options.locale);
// using the current time helps prevent issues where the previous modified time was greater
// than 5 seconds prior to the time the linter will finish with the file (i.e. helps prevent accidental infinite loops on updating the date modified value)
const formatted_modified_date = options.currentTime.format(options.format);
const modified_date_line = `\n${options.dateModifiedKey}: ${formatted_modified_date}`;
const modified_date = moment(options.fileModifiedTime);
modified_date.locale(options.locale);
// using the current time helps prevent issues where the previous modified time was greater
// than 5 seconds prior to the time the linter will finish with the file (i.e. helps prevent accidental infinite loops on updating the date modified value)
const formatted_modified_date = options.currentTime.format(options.format);
const modified_date_line = `\n${options.dateModifiedKey}: ${formatted_modified_date}`;

const keyWithValueFound = modified_match.test(text);
if (keyWithValueFound) {
const modifiedDateTime = moment(text.match(modified_match)[0].replace(options.dateModifiedKey + ':', '').trim(), options.format, options.locale, true);
if (textModified || modifiedDateTime == undefined || !modifiedDateTime.isValid() ||
const keyWithValueFound = modified_match.test(text);
if (keyWithValueFound) {
const modifiedDateTime = moment(text.match(modified_match)[0].replace(options.dateModifiedKey + ':', '').trim(), options.format, options.locale, true);
if (textModified || modifiedDateTime == undefined || !modifiedDateTime.isValid() ||
this.getTimeDifferenceInSeconds(modifiedDateTime, modified_date, options) > 5
) {
text = text.replace(
modified_match,
escapeDollarSigns(modified_date_line) + '\n',
);
}
} else if (modified_key_match.test(text)) {
text = text.replace(
modified_key_match,
escapeDollarSigns(modified_date_line) + '\n',
);
} else if (!keyWithValueFound) {
const yaml_end = text.indexOf('\n---');
text = insert(text, yaml_end, modified_date_line);
}
) {
text = text.replace(
modified_match,
escapeDollarSigns(modified_date_line) + '\n',
);
}
} else if (modified_key_match.test(text)) {
text = text.replace(
modified_key_match,
escapeDollarSigns(modified_date_line) + '\n',
);
} else if (!keyWithValueFound) {
const yaml_end = text.indexOf('\n---');
text = insert(text, yaml_end, modified_date_line);
}

return text;
});
return text;
}
parseValueToCurrentFormatIfPossible(timestamp: string, format: string, locale: string): moment.Moment | null {
if (timestamp == undefined) {
return null;
}

const desiredFormatDate = moment(timestamp, format, locale, true);
if (desiredFormatDate != undefined && desiredFormatDate.isValid()) {
return desiredFormatDate;
}

// @ts-ignore
const actualFormat = parseFormat(timestamp);
if (actualFormat != undefined) {
const date = moment(timestamp, actualFormat);
date.locale(locale);

return moment(date.format(format), format, locale, true);
}

return null;
}
getYAMLTimestampString(text: string, matchRegex: RegExp, key: string): string {
const match = text.match(matchRegex)[0];

return match.replace(key + ':', '').trim();
}
getTimeDifferenceInSeconds(modifiedDateTimeMetadata: moment.Moment, yamlModifiedDateTime: moment.Moment, options: YamlTimestampOptions): number {
// the metadata value may not be in the correct format, so we need to convert it to the correct format
Expand Down Expand Up @@ -233,6 +293,12 @@ export default class YamlTimestamp extends RuleBuilder<YamlTimestampOptions> {
descriptionTextKey: 'yaml-timestamp-date-created-key-description',
optionsKey: 'dateCreatedKey',
}),
new BooleanOptionBuilder({
OptionsClass: YamlTimestampOptions,
nameTextKey: 'yaml-timestamp-force-retention-of-create-value-name',
descriptionTextKey: 'yaml-timestamp-force-retention-of-create-value-description',
optionsKey: 'forceRetentionOfCreatedValue',
}),
new BooleanOptionBuilder({
OptionsClass: YamlTimestampOptions,
nameTextKey: 'yaml-timestamp-date-modified-name',
Expand Down
3 changes: 3 additions & 0 deletions src/typings/moment-parseformat.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
declare module 'moment-parseformat' {
function parseFormat(format: string, options?: any): string;
}
4 changes: 2 additions & 2 deletions src/utils/mdast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {fromMarkdown} from 'mdast-util-from-markdown';
import {gfmFootnoteFromMarkdown} from 'mdast-util-gfm-footnote';
import {gfmTaskListItemFromMarkdown} from 'mdast-util-gfm-task-list-item';
import QuickLRU from 'quick-lru';
import {getTextInLanguage} from '../lang/helpers';

const LRU = new QuickLRU({maxSize: 200});

Expand Down Expand Up @@ -112,14 +113,13 @@ export function moveFootnotesToEnd(text: string) {
} while (alreadyUsedReferencePositions.has(footnoteReferenceLocation) && footnoteReferenceLocation !== -1 );

if (footnoteReferenceLocation === -1) {
throw new Error(`Footnote '${footnote}' has no corresponding footnote reference before the footnote contents and cannot be processed. Please make sure that all footnotes have a corresponding reference before the content of the footnote.`);
throw new Error(getTextInLanguage('missing-footnote-error-message').replace('{FOOTNOTE}', footnote));
}

alreadyUsedReferencePositions.add(footnoteReferenceLocation);
return footnoteReferenceLocation;
};


for (const position of positions) {
const footnote = text.substring(position.start.offset, position.end.offset);
footnotes.push(footnote);
Expand Down
3 changes: 2 additions & 1 deletion src/utils/yaml.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {load} from 'js-yaml';
import {getTextInLanguage} from '../lang/helpers';
import {escapeDollarSigns, yamlRegex} from './regex';


Expand Down Expand Up @@ -273,7 +274,7 @@ export function convertYAMLStringToArray(value: string, delimiter: string = ',')
}

if (delimiter.length > 1) {
throw new Error('delimiter is only allowed to be a single character');
throw new Error(getTextInLanguage('invalid-delimiter-error-message'));
}

const arrayItems: string[] = [];
Expand Down

0 comments on commit 6129737

Please sign in to comment.