From f1eba5ed53af2426e47ffa5c5ccb5a57c9c5f838 Mon Sep 17 00:00:00 2001 From: hsayed21 Date: Thu, 18 Dec 2025 20:28:24 +0200 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20date=20format=20p?= =?UTF-8?q?laceholders=20for=20note=20creation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- exampleVault/Buttons/Button Example.md | 14 ++++++++ .../actions/CreateNoteButtonActionConfig.ts | 32 ++++++++++++++++--- .../TemplaterCreateNoteButtonActionConfig.ts | 32 ++++++++++++++++--- 3 files changed, 70 insertions(+), 8 deletions(-) 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..4c864d2f 100644 --- a/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts +++ b/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts @@ -8,12 +8,33 @@ import type { 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 moment from 'moment'; export class CreateNoteButtonActionConfig extends AbstractButtonActionConfig { constructor(mb: MetaBind) { super(ButtonActionType.CREATE_NOTE, mb); } + private processDateFormatPlaceholders(value: string | undefined): string | undefined { + if (!value) { + return value; + } + + // Match {...} patterns and replace with formatted dates + // Uses a regex to find all {format} patterns + const placeholderRegex = /\{([^}]+)\}/g; + + return value.replace(placeholderRegex, (match, format) => { + try { + // Format the current date/time using the captured format string + return moment().format(format); + } catch (error) { + // If formatting fails, return the original placeholder + return match; + } + }); + } + async run( _config: ButtonConfig | undefined, action: CreateNoteButtonAction, @@ -21,8 +42,11 @@ export class CreateNoteButtonActionConfig extends AbstractButtonActionConfig { - if (action.openIfAlreadyExists) { - const filePath = ensureFileExtension(joinPath(action.folderPath ?? '', action.fileName), 'md'); + const processedFileName = this.processDateFormatPlaceholders(action.fileName); + const processedFolderPath = this.processDateFormatPlaceholders(action.folderPath); + + if (action.openIfAlreadyExists && processedFileName) { + 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 +55,8 @@ export class CreateNoteButtonActionConfig extends AbstractButtonActionConfig { constructor(mb: MetaBind) { super(ButtonActionType.TEMPLATER_CREATE_NOTE, mb); } + private processDateFormatPlaceholders(value: string | undefined): string | undefined { + if (!value) { + return value; + } + + // Match {...} patterns and replace with formatted dates + // Uses a regex to find all {format} patterns + const placeholderRegex = /\{([^}]+)\}/g; + + return value.replace(placeholderRegex, (match, format) => { + try { + // Format the current date/time using the captured format string + return moment().format(format); + } catch (error) { + // If formatting fails, return the original placeholder + return match; + } + }); + } + async run( _config: ButtonConfig | undefined, action: TemplaterCreateNoteButtonAction, @@ -21,8 +42,11 @@ export class TemplaterCreateNoteButtonActionConfig extends AbstractButtonActionC _context: ButtonContext, click: ButtonClickContext, ): Promise { - if (action.openIfAlreadyExists && action.fileName) { - const filePath = ensureFileExtension(joinPath(action.folderPath ?? '', action.fileName), 'md'); + const processedFileName = this.processDateFormatPlaceholders(action.fileName); + const processedFolderPath = this.processDateFormatPlaceholders(action.folderPath); + + if (action.openIfAlreadyExists && processedFileName) { + 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 +56,8 @@ export class TemplaterCreateNoteButtonActionConfig extends AbstractButtonActionC await this.mb.internal.createNoteWithTemplater( action.templateFile, - action.folderPath, - action.fileName, + processedFolderPath, + processedFileName, action.openNote, click.openInNewTab(), ); From 659b666d625ea69911d436d6ec06c1e1ecd05ded Mon Sep 17 00:00:00 2001 From: hsayed21 Date: Fri, 19 Dec 2025 18:58:03 +0200 Subject: [PATCH 2/3] new changes after reviewing --- .../actions/CreateNoteButtonActionConfig.ts | 31 +++---------------- .../TemplaterCreateNoteButtonActionConfig.ts | 31 +++---------------- packages/core/src/utils/Utils.ts | 18 +++++++++++ 3 files changed, 28 insertions(+), 52 deletions(-) diff --git a/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts b/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts index 4c864d2f..73f99e3b 100644 --- a/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts +++ b/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts @@ -7,34 +7,13 @@ 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 moment from 'moment'; +import { ensureFileExtension, joinPath, processDateFormatPlaceholders } from 'packages/core/src/utils/Utils'; export class CreateNoteButtonActionConfig extends AbstractButtonActionConfig { constructor(mb: MetaBind) { super(ButtonActionType.CREATE_NOTE, mb); } - private processDateFormatPlaceholders(value: string | undefined): string | undefined { - if (!value) { - return value; - } - - // Match {...} patterns and replace with formatted dates - // Uses a regex to find all {format} patterns - const placeholderRegex = /\{([^}]+)\}/g; - - return value.replace(placeholderRegex, (match, format) => { - try { - // Format the current date/time using the captured format string - return moment().format(format); - } catch (error) { - // If formatting fails, return the original placeholder - return match; - } - }); - } - async run( _config: ButtonConfig | undefined, action: CreateNoteButtonAction, @@ -42,11 +21,11 @@ export class CreateNoteButtonActionConfig extends AbstractButtonActionConfig { - const processedFileName = this.processDateFormatPlaceholders(action.fileName); - const processedFolderPath = this.processDateFormatPlaceholders(action.folderPath); + const processedFileName = processDateFormatPlaceholders(action.fileName) ?? action.fileName; + const processedFolderPath = processDateFormatPlaceholders(action.folderPath); - if (action.openIfAlreadyExists && processedFileName) { - const filePath = ensureFileExtension(joinPath(processedFolderPath ?? '', processedFileName), 'md'); + 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); diff --git a/packages/core/src/fields/button/actions/TemplaterCreateNoteButtonActionConfig.ts b/packages/core/src/fields/button/actions/TemplaterCreateNoteButtonActionConfig.ts index 65206615..9ab6d3e5 100644 --- a/packages/core/src/fields/button/actions/TemplaterCreateNoteButtonActionConfig.ts +++ b/packages/core/src/fields/button/actions/TemplaterCreateNoteButtonActionConfig.ts @@ -7,34 +7,13 @@ 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 moment from 'moment'; +import { ensureFileExtension, joinPath, processDateFormatPlaceholders } from 'packages/core/src/utils/Utils'; export class TemplaterCreateNoteButtonActionConfig extends AbstractButtonActionConfig { constructor(mb: MetaBind) { super(ButtonActionType.TEMPLATER_CREATE_NOTE, mb); } - private processDateFormatPlaceholders(value: string | undefined): string | undefined { - if (!value) { - return value; - } - - // Match {...} patterns and replace with formatted dates - // Uses a regex to find all {format} patterns - const placeholderRegex = /\{([^}]+)\}/g; - - return value.replace(placeholderRegex, (match, format) => { - try { - // Format the current date/time using the captured format string - return moment().format(format); - } catch (error) { - // If formatting fails, return the original placeholder - return match; - } - }); - } - async run( _config: ButtonConfig | undefined, action: TemplaterCreateNoteButtonAction, @@ -42,11 +21,11 @@ export class TemplaterCreateNoteButtonActionConfig extends AbstractButtonActionC _context: ButtonContext, click: ButtonClickContext, ): Promise { - const processedFileName = this.processDateFormatPlaceholders(action.fileName); - const processedFolderPath = this.processDateFormatPlaceholders(action.folderPath); + const processedFileName = processDateFormatPlaceholders(action.fileName); + const processedFolderPath = processDateFormatPlaceholders(action.folderPath); - if (action.openIfAlreadyExists && processedFileName) { - const filePath = ensureFileExtension(joinPath(processedFolderPath ?? '', processedFileName), 'md'); + 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); diff --git a/packages/core/src/utils/Utils.ts b/packages/core/src/utils/Utils.ts index 5ba9f6ca..c24c5fd8 100644 --- a/packages/core/src/utils/Utils.ts +++ b/packages/core/src/utils/Utils.ts @@ -1,3 +1,5 @@ +import moment from 'moment'; + /** * Clamp * @@ -371,6 +373,22 @@ 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) { + return value; + } + + const placeholderRegex = /\{([^}]+)\}/g; + + return value.replace(placeholderRegex, (_match, format: string) => { + return moment().format(format); + }); +} + export function toArray(value: T | T[] | undefined): T[] { if (value === undefined) { return []; From 67500997ca60e3423aee02eae8284a286a6cd4e3 Mon Sep 17 00:00:00 2001 From: hsayed21 Date: Fri, 19 Dec 2025 19:22:46 +0200 Subject: [PATCH 3/3] improve date format processing and add tests for validation --- .../actions/CreateNoteButtonActionConfig.ts | 2 +- packages/core/src/utils/Utils.ts | 18 ++- tests/utils/Utils.test.ts | 105 ++++++++++++++++++ 3 files changed, 120 insertions(+), 5 deletions(-) diff --git a/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts b/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts index 73f99e3b..d6060aa7 100644 --- a/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts +++ b/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts @@ -21,7 +21,7 @@ export class CreateNoteButtonActionConfig extends AbstractButtonActionConfig { - const processedFileName = processDateFormatPlaceholders(action.fileName) ?? action.fileName; + const processedFileName = processDateFormatPlaceholders(action.fileName); const processedFolderPath = processDateFormatPlaceholders(action.folderPath); if (action.openIfAlreadyExists && action.fileName) { diff --git a/packages/core/src/utils/Utils.ts b/packages/core/src/utils/Utils.ts index c24c5fd8..8c679f42 100644 --- a/packages/core/src/utils/Utils.ts +++ b/packages/core/src/utils/Utils.ts @@ -1,4 +1,4 @@ -import moment from 'moment'; +import Moment from 'moment/moment'; /** * Clamp @@ -378,14 +378,24 @@ export function ensureFileExtension(filePath: string, extension: string): string * Replaces patterns like {YYYY-MM-DD} with formatted dates using moment.js. */ export function processDateFormatPlaceholders(value: string | undefined): string | undefined { - if (!value) { + if (value === undefined || value === '') { return value; } const placeholderRegex = /\{([^}]+)\}/g; - return value.replace(placeholderRegex, (_match, format: string) => { - return moment().format(format); + 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); }); } 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)$/); + }); + }); });