diff --git a/exampleVault/Buttons/Button Example.md b/exampleVault/Buttons/Button Example.md index e3e58141..41e72705 100644 --- a/exampleVault/Buttons/Button Example.md +++ b/exampleVault/Buttons/Button Example.md @@ -160,6 +160,20 @@ actions: ``` +```meta-bind-button +label: Daily Note (with date) +hidden: false +id: "" +style: primary +actions: + - type: templaterCreateNote + templateFile: "templates/templater/Templater Template.md" + folderPath: "Daily/{YYYY}/{MM}" + fileName: "{YYYY-MM-DD}" + openIfAlreadyExists: true + +``` + ```meta-bind-button label: Sleep hidden: false diff --git a/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts b/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts index 7610ab60..d6060aa7 100644 --- a/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts +++ b/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts @@ -7,7 +7,7 @@ import type { } from 'packages/core/src/config/ButtonConfig'; import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; -import { ensureFileExtension, joinPath } from 'packages/core/src/utils/Utils'; +import { ensureFileExtension, joinPath, processDateFormatPlaceholders } from 'packages/core/src/utils/Utils'; export class CreateNoteButtonActionConfig extends AbstractButtonActionConfig { constructor(mb: MetaBind) { @@ -21,8 +21,11 @@ export class CreateNoteButtonActionConfig extends AbstractButtonActionConfig { - if (action.openIfAlreadyExists) { - const filePath = ensureFileExtension(joinPath(action.folderPath ?? '', action.fileName), 'md'); + const processedFileName = processDateFormatPlaceholders(action.fileName); + const processedFolderPath = processDateFormatPlaceholders(action.folderPath); + + if (action.openIfAlreadyExists && action.fileName) { + const filePath = ensureFileExtension(joinPath(processedFolderPath ?? '', processedFileName ?? ''), 'md'); // if the file already exists, open it in the same tab if (await this.mb.file.exists(filePath)) { await this.mb.file.open(filePath, '', false); @@ -31,8 +34,8 @@ export class CreateNoteButtonActionConfig extends AbstractButtonActionConfig { constructor(mb: MetaBind) { @@ -21,8 +21,11 @@ export class TemplaterCreateNoteButtonActionConfig extends AbstractButtonActionC _context: ButtonContext, click: ButtonClickContext, ): Promise { + const processedFileName = processDateFormatPlaceholders(action.fileName); + const processedFolderPath = processDateFormatPlaceholders(action.folderPath); + if (action.openIfAlreadyExists && action.fileName) { - const filePath = ensureFileExtension(joinPath(action.folderPath ?? '', action.fileName), 'md'); + const filePath = ensureFileExtension(joinPath(processedFolderPath ?? '', processedFileName ?? ''), 'md'); // if the file already exists, open it in the same tab if (await this.mb.file.exists(filePath)) { await this.mb.file.open(filePath, '', false); @@ -32,8 +35,8 @@ export class TemplaterCreateNoteButtonActionConfig extends AbstractButtonActionC await this.mb.internal.createNoteWithTemplater( action.templateFile, - action.folderPath, - action.fileName, + processedFolderPath, + processedFileName, action.openNote, click.openInNewTab(), ); diff --git a/packages/core/src/utils/Utils.ts b/packages/core/src/utils/Utils.ts index 5ba9f6ca..8c679f42 100644 --- a/packages/core/src/utils/Utils.ts +++ b/packages/core/src/utils/Utils.ts @@ -1,3 +1,5 @@ +import Moment from 'moment/moment'; + /** * Clamp * @@ -371,6 +373,32 @@ export function ensureFileExtension(filePath: string, extension: string): string return filePath + extension; } +/** + * Processes date format placeholders in a string. + * Replaces patterns like {YYYY-MM-DD} with formatted dates using moment.js. + */ +export function processDateFormatPlaceholders(value: string | undefined): string | undefined { + if (value === undefined || value === '') { + return value; + } + + const placeholderRegex = /\{([^}]+)\}/g; + + return value.replace(placeholderRegex, (match, format: string) => { + // Validate that the format string only contains valid moment.js tokens and delimiters + // Moment.js tokens: Y M D d H h m s S a A Q W w X x Z z G g E e o k l + // Common delimiters: : / - space . , [ ] + const validMomentFormat = /^[YMDdHhmsaAQWwXxZzGgEeSsokl:/\-\s.,[\]]+$/.test(format); + + if (!validMomentFormat) { + // Leave unknown/invalid formats unchanged + return match; + } + + return Moment().format(format); + }); +} + export function toArray(value: T | T[] | undefined): T[] { if (value === undefined) { return []; diff --git a/tests/utils/Utils.test.ts b/tests/utils/Utils.test.ts index f0e9f66f..158c7ac8 100644 --- a/tests/utils/Utils.test.ts +++ b/tests/utils/Utils.test.ts @@ -8,6 +8,7 @@ import { joinPath, mod, optClamp, + processDateFormatPlaceholders, remapRange, toArray, toEnumeration, @@ -400,4 +401,108 @@ describe('utils', () => { ).toBe(false); }); }); + + describe('processDateFormatPlaceholders function', () => { + test('should return undefined for undefined input', () => { + expect(processDateFormatPlaceholders(undefined)).toBeUndefined(); + }); + + test('should return empty string for empty string input', () => { + expect(processDateFormatPlaceholders('')).toBe(''); + }); + + test('should return original string if no placeholders are present', () => { + expect(processDateFormatPlaceholders('folder/subfolder')).toBe('folder/subfolder'); + expect(processDateFormatPlaceholders('note-title')).toBe('note-title'); + }); + + test('should leave invalid format placeholders unchanged', () => { + expect(processDateFormatPlaceholders('{INVALID}')).toBe('{INVALID}'); + expect(processDateFormatPlaceholders('{random text}')).toBe('{random text}'); + expect(processDateFormatPlaceholders('{123}')).toBe('{123}'); + expect(processDateFormatPlaceholders('{abc}')).toBe('{abc}'); + }); + + test('should process valid year formats', () => { + const result = processDateFormatPlaceholders('{YYYY}'); + expect(result).toMatch(/^\d{4}$/); // 4-digit year + + const yearShort = processDateFormatPlaceholders('{YY}'); + expect(yearShort).toMatch(/^\d{2}$/); // 2-digit year + }); + + test('should process valid month formats', () => { + const result = processDateFormatPlaceholders('{MM}'); + expect(result).toMatch(/^(0[1-9]|1[0-2])$/); // 01-12 + + const monthName = processDateFormatPlaceholders('{MMMM}'); + expect(monthName).toMatch(/^(January|February|March|April|May|June|July|August|September|October|November|December)$/); + }); + + test('should process valid day formats', () => { + const result = processDateFormatPlaceholders('{DD}'); + expect(result).toMatch(/^(0[1-9]|[12][0-9]|3[01])$/); // 01-31 + + const dayOfWeek = processDateFormatPlaceholders('{dddd}'); + expect(dayOfWeek).toMatch(/^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)$/); + }); + + test('should process combined date format', () => { + const result = processDateFormatPlaceholders('{YYYY-MM-DD}'); + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/); // YYYY-MM-DD format + }); + + test('should process multiple placeholders in one string', () => { + const result = processDateFormatPlaceholders('folder/{YYYY}/{MM}/{YYYY-MM-DD}'); + expect(result).toMatch(/^folder\/\d{4}\/\d{2}\/\d{4}-\d{2}-\d{2}$/); + }); + + test('should mix processed and unprocessed placeholders', () => { + const result = processDateFormatPlaceholders('{YYYY}-{INVALID}-{MM}'); + expect(result).toMatch(/^\d{4}-\{INVALID\}-\d{2}$/); + }); + + test('should handle nested braces correctly', () => { + const result = processDateFormatPlaceholders('{{YYYY}}'); + // The outer braces capture "{YYYY}" which contains braces that aren't valid moment tokens + // So it should be left unchanged + expect(result).toBe('{{YYYY}}'); + }); + + test('should handle strings with text around placeholders', () => { + const result = processDateFormatPlaceholders('Daily note {YYYY-MM-DD}.md'); + expect(result).toMatch(/^Daily note \d{4}-\d{2}-\d{2}\.md$/); + }); + + test('should handle hour, minute, second formats', () => { + const hourResult = processDateFormatPlaceholders('{HH}'); + expect(hourResult).toMatch(/^\d{2}$/); // 00-23 + + const minuteResult = processDateFormatPlaceholders('{mm}'); + expect(minuteResult).toMatch(/^\d{2}$/); // 00-59 + + const secondResult = processDateFormatPlaceholders('{ss}'); + expect(secondResult).toMatch(/^\d{2}$/); // 00-59 + }); + + test('should handle quarter format', () => { + const result = processDateFormatPlaceholders('{Q}'); + expect(result).toMatch(/^[1-4]$/); // 1-4 + }); + + test('should handle week formats', () => { + const weekResult = processDateFormatPlaceholders('{W}'); + expect(weekResult).toMatch(/^\d{1,2}$/); // 1-53 + }); + + test('should handle time with AM/PM', () => { + const result = processDateFormatPlaceholders('{h:mm A}'); + expect(result).toMatch(/^\d{1,2}:\d{2} (AM|PM)$/); + }); + + test('should handle locale-aware formats', () => { + const result = processDateFormatPlaceholders('{YYYY-MM-DD, dddd}'); + expect(result).toMatch(/^\d{4}-\d{2}-\d{2}, (Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)$/); + }); + }); });