diff --git a/docs/settings/task-defaults.md b/docs/settings/task-defaults.md index e5f9b8b5..4b10dc81 100644 --- a/docs/settings/task-defaults.md +++ b/docs/settings/task-defaults.md @@ -134,6 +134,13 @@ When using the **custom** filename format, you can create templates using variab - `{{status}}` - Task status (e.g., "todo", "in-progress", "done") - `{{dueDate}}` - Task due date (YYYY-MM-DD format) - `{{scheduledDate}}` - Task scheduled date (YYYY-MM-DD format) +- `{{context}}` - First context from the task's contexts array +- `{{contexts}}` - All contexts joined by `/` +- `{{tags}}` - Task tags (comma-separated) +- `{{hashtags}}` - Task tags as space-separated hashtags (e.g., "#work #urgent") +- `{{timeEstimate}}` - Time estimate in minutes +- `{{details}}` - Task details/description (truncated to 50 characters) +- `{{parentNote}}` - Parent note name where task was created - `{{priorityShort}}` - First letter of priority in uppercase (e.g., "H") - `{{statusShort}}` - First letter of status in uppercase (e.g., "T") - `{{titleLower}}` - Task title in lowercase @@ -266,6 +273,25 @@ For detailed reminder documentation, see [Task Reminders](../features/task-manag TaskNotes supports **Templates** for both the YAML frontmatter and the body of your task notes. You can use templates to pre-fill common values, add boilerplate text, and create a consistent structure for your tasks. Templates can also include variables, such as `{{title}}`, `{{date}}`, and `{{parentNote}}`, which will be automatically replaced with the appropriate values when a new task is created. +### Unified Template Variables + +Body templates now support the same variables as filename templates. All variables listed in the [Filename Template Variables](#filename-template-variables) section above are available in body templates, including: + +- All date/time variables (`{{year}}`, `{{month}}`, `{{timestamp}}`, etc.) +- All title variations (`{{titleKebab}}`, `{{titleSnake}}`, etc.) +- Task property variations (`{{priorityShort}}`, `{{statusShort}}`) +- Unique identifiers (`{{zettel}}`, `{{nano}}`) +- Advanced variables (`{{unix}}`, `{{unixMs}}`, etc.) + +### Body-Specific Variables + +- `{{contexts}}` - Task contexts (comma-separated) +- `{{tags}}` - Task tags (comma-separated) +- `{{hashtags}}` - Task tags as space-separated hashtags +- `{{timeEstimate}}` - Time estimate in minutes +- `{{details}}` - User-provided details/description +- `{{parentNote}}` - Parent note name/path (properly quoted for YAML) + The `{{parentNote}}` variable is particularly useful for project organization. It inserts the parent note as a properly formatted markdown link. ### Basic Usage diff --git a/src/utils/filenameGenerator.ts b/src/utils/filenameGenerator.ts index dc165b75..ac3d27aa 100644 --- a/src/utils/filenameGenerator.ts +++ b/src/utils/filenameGenerator.ts @@ -9,6 +9,12 @@ export interface FilenameContext { date?: Date; dueDate?: string; // YYYY-MM-DD format scheduledDate?: string; // YYYY-MM-DD format + // Additional body template variables (optional for backwards compatibility) + contexts?: string[]; + tags?: string[]; + timeEstimate?: number; + details?: string; + parentNote?: string; } export interface ICSFilenameContext extends FilenameContext { @@ -214,6 +220,10 @@ function generateCustomFilename( ? context.status : "open"; + // Process array values for contexts and tags + const contexts = Array.isArray(context.contexts) ? context.contexts : []; + const tags = Array.isArray(context.tags) ? context.tags : []; + const variables: Record = { title: sanitizedTitle, date: format(date, "yyyy-MM-dd"), @@ -230,6 +240,14 @@ function generateCustomFilename( second: format(date, "ss"), dueDate: context.dueDate || "", scheduledDate: context.scheduledDate || "", + // Body template variables (contexts, tags, etc.) + context: contexts[0] ? sanitizeForFilename(contexts[0]) : "", + contexts: contexts.map((c) => sanitizeForFilename(c)).join("/"), + tags: tags.map((t) => sanitizeForFilename(t)).join(", "), + hashtags: tags.map((t) => `#${sanitizeForFilename(t)}`).join(" "), + timeEstimate: context.timeEstimate?.toString() || "", + details: context.details ? sanitizeForFilename(context.details.substring(0, 50)) : "", + parentNote: context.parentNote ? sanitizeForFilename(context.parentNote) : "", // New date format variations shortDate: format(date, "yyMMdd"), shortYear: format(date, "yy"), diff --git a/src/utils/templateProcessor.ts b/src/utils/templateProcessor.ts index 0cf13037..0b2ace83 100644 --- a/src/utils/templateProcessor.ts +++ b/src/utils/templateProcessor.ts @@ -196,6 +196,74 @@ function processTemplateVariablesForYaml( // {{time}} - Current time (basic format only) result = result.replace(/\{\{time\}\}/g, format(now, "HH:mm")); + // Extended date/time variables (consistent with filename templates) + result = result.replace(/\{\{year\}\}/g, format(now, "yyyy")); + result = result.replace(/\{\{month\}\}/g, format(now, "MM")); + result = result.replace(/\{\{day\}\}/g, format(now, "dd")); + result = result.replace(/\{\{hour\}\}/g, format(now, "HH")); + result = result.replace(/\{\{minute\}\}/g, format(now, "mm")); + result = result.replace(/\{\{second\}\}/g, format(now, "ss")); + result = result.replace(/\{\{timestamp\}\}/g, format(now, "yyyy-MM-dd-HHmmss")); + result = result.replace(/\{\{dateTime\}\}/g, format(now, "yyyy-MM-dd-HHmm")); + result = result.replace(/\{\{shortDate\}\}/g, format(now, "yyMMdd")); + result = result.replace(/\{\{shortYear\}\}/g, format(now, "yy")); + result = result.replace(/\{\{monthName\}\}/g, format(now, "MMMM")); + result = result.replace(/\{\{monthNameShort\}\}/g, format(now, "MMM")); + result = result.replace(/\{\{dayName\}\}/g, format(now, "EEEE")); + result = result.replace(/\{\{dayNameShort\}\}/g, format(now, "EEE")); + result = result.replace(/\{\{week\}\}/g, format(now, "ww")); + result = result.replace(/\{\{quarter\}\}/g, format(now, "q")); + result = result.replace(/\{\{time12\}\}/g, format(now, "hh:mm a")); + result = result.replace(/\{\{time24\}\}/g, format(now, "HH:mm")); + result = result.replace(/\{\{hourPadded\}\}/g, format(now, "HH")); + result = result.replace(/\{\{hour12\}\}/g, format(now, "hh")); + result = result.replace(/\{\{ampm\}\}/g, format(now, "a")); + result = result.replace(/\{\{unix\}\}/g, Math.floor(now.getTime() / 1000).toString()); + result = result.replace(/\{\{unixMs\}\}/g, now.getTime().toString()); + result = result.replace(/\{\{milliseconds\}\}/g, format(now, "SSS")); + result = result.replace(/\{\{ms\}\}/g, format(now, "SSS")); + result = result.replace(/\{\{timezone\}\}/g, format(now, "xxx")); + result = result.replace(/\{\{timezoneShort\}\}/g, format(now, "xx")); + result = result.replace(/\{\{utcOffset\}\}/g, format(now, "xxx")); + result = result.replace(/\{\{utcOffsetShort\}\}/g, format(now, "xx")); + result = result.replace(/\{\{utcZ\}\}/g, "Z"); + + // Date-based identifiers (consistent with filename templates) + const datePart = format(now, "yyMMdd"); + const midnight = new Date(now); + midnight.setHours(0, 0, 0, 0); + const secondsSinceMidnight = Math.floor((now.getTime() - midnight.getTime()) / 1000); + const zettelId = `${datePart}${secondsSinceMidnight.toString(36)}`; + result = result.replace(/\{\{zettel\}\}/g, zettelId); + result = result.replace(/\{\{nano\}\}/g, Date.now().toString() + Math.random().toString(36).substring(2, 7)); + + // Priority and status variations + const priority = taskData.priority || ""; + const status = taskData.status || ""; + result = result.replace(/\{\{priorityShort\}\}/g, priority.substring(0, 1).toUpperCase()); + result = result.replace(/\{\{statusShort\}\}/g, status.substring(0, 1).toUpperCase()); + + // Title variations + const titleForVariations = taskData.title || ""; + result = result.replace(/\{\{titleLower\}\}/g, titleForVariations.toLowerCase()); + result = result.replace(/\{\{titleUpper\}\}/g, titleForVariations.toUpperCase()); + result = result.replace(/\{\{titleSnake\}\}/g, titleForVariations.toLowerCase().replace(/\s+/g, "_")); + result = result.replace(/\{\{titleKebab\}\}/g, titleForVariations.toLowerCase().replace(/\s+/g, "-")); + result = result.replace( + /\{\{titleCamel\}\}/g, + titleForVariations + .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => + index === 0 ? word.toLowerCase() : word.toUpperCase() + ) + .replace(/\s+/g, "") + ); + result = result.replace( + /\{\{titlePascal\}\}/g, + titleForVariations + .replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => word.toUpperCase()) + .replace(/\s+/g, "") + ); + // ICS Event template variables (only available if taskData is ICSTemplateData) if ("icsEventTitle" in taskData) { const icsData = taskData as ICSTemplateData; @@ -334,6 +402,74 @@ function processTemplateVariables( // {{time}} - Current time (basic format only) result = result.replace(/\{\{time\}\}/g, format(now, "HH:mm")); + // Extended date/time variables (consistent with filename templates) + result = result.replace(/\{\{year\}\}/g, format(now, "yyyy")); + result = result.replace(/\{\{month\}\}/g, format(now, "MM")); + result = result.replace(/\{\{day\}\}/g, format(now, "dd")); + result = result.replace(/\{\{hour\}\}/g, format(now, "HH")); + result = result.replace(/\{\{minute\}\}/g, format(now, "mm")); + result = result.replace(/\{\{second\}\}/g, format(now, "ss")); + result = result.replace(/\{\{timestamp\}\}/g, format(now, "yyyy-MM-dd-HHmmss")); + result = result.replace(/\{\{dateTime\}\}/g, format(now, "yyyy-MM-dd-HHmm")); + result = result.replace(/\{\{shortDate\}\}/g, format(now, "yyMMdd")); + result = result.replace(/\{\{shortYear\}\}/g, format(now, "yy")); + result = result.replace(/\{\{monthName\}\}/g, format(now, "MMMM")); + result = result.replace(/\{\{monthNameShort\}\}/g, format(now, "MMM")); + result = result.replace(/\{\{dayName\}\}/g, format(now, "EEEE")); + result = result.replace(/\{\{dayNameShort\}\}/g, format(now, "EEE")); + result = result.replace(/\{\{week\}\}/g, format(now, "ww")); + result = result.replace(/\{\{quarter\}\}/g, format(now, "q")); + result = result.replace(/\{\{time12\}\}/g, format(now, "hh:mm a")); + result = result.replace(/\{\{time24\}\}/g, format(now, "HH:mm")); + result = result.replace(/\{\{hourPadded\}\}/g, format(now, "HH")); + result = result.replace(/\{\{hour12\}\}/g, format(now, "hh")); + result = result.replace(/\{\{ampm\}\}/g, format(now, "a")); + result = result.replace(/\{\{unix\}\}/g, Math.floor(now.getTime() / 1000).toString()); + result = result.replace(/\{\{unixMs\}\}/g, now.getTime().toString()); + result = result.replace(/\{\{milliseconds\}\}/g, format(now, "SSS")); + result = result.replace(/\{\{ms\}\}/g, format(now, "SSS")); + result = result.replace(/\{\{timezone\}\}/g, format(now, "xxx")); + result = result.replace(/\{\{timezoneShort\}\}/g, format(now, "xx")); + result = result.replace(/\{\{utcOffset\}\}/g, format(now, "xxx")); + result = result.replace(/\{\{utcOffsetShort\}\}/g, format(now, "xx")); + result = result.replace(/\{\{utcZ\}\}/g, "Z"); + + // Date-based identifiers (consistent with filename templates) + const datePartBody = format(now, "yyMMdd"); + const midnightBody = new Date(now); + midnightBody.setHours(0, 0, 0, 0); + const secondsSinceMidnightBody = Math.floor((now.getTime() - midnightBody.getTime()) / 1000); + const zettelIdBody = `${datePartBody}${secondsSinceMidnightBody.toString(36)}`; + result = result.replace(/\{\{zettel\}\}/g, zettelIdBody); + result = result.replace(/\{\{nano\}\}/g, Date.now().toString() + Math.random().toString(36).substring(2, 7)); + + // Priority and status variations + const priority = taskData.priority || ""; + const status = taskData.status || ""; + result = result.replace(/\{\{priorityShort\}\}/g, priority.substring(0, 1).toUpperCase()); + result = result.replace(/\{\{statusShort\}\}/g, status.substring(0, 1).toUpperCase()); + + // Title variations + const titleForVariations = taskData.title || ""; + result = result.replace(/\{\{titleLower\}\}/g, titleForVariations.toLowerCase()); + result = result.replace(/\{\{titleUpper\}\}/g, titleForVariations.toUpperCase()); + result = result.replace(/\{\{titleSnake\}\}/g, titleForVariations.toLowerCase().replace(/\s+/g, "_")); + result = result.replace(/\{\{titleKebab\}\}/g, titleForVariations.toLowerCase().replace(/\s+/g, "-")); + result = result.replace( + /\{\{titleCamel\}\}/g, + titleForVariations + .replace(/(?:^\w|[A-Z]|\b\w)/g, (word, index) => + index === 0 ? word.toLowerCase() : word.toUpperCase() + ) + .replace(/\s+/g, "") + ); + result = result.replace( + /\{\{titlePascal\}\}/g, + titleForVariations + .replace(/(?:^\w|[A-Z]|\b\w)/g, (word) => word.toUpperCase()) + .replace(/\s+/g, "") + ); + // ICS Event template variables (only available if taskData is ICSTemplateData) if ("icsEventTitle" in taskData) { const icsData = taskData as ICSTemplateData;