feat: Unify template variables across filename and body templates

Both template systems now share the same set of variables:
- Filename templates gain: contexts, tags, hashtags, timeEstimate,
  details, parentNote
- Body templates gain: zettel, nano, and all extended date/time
  variables from filename templates

Extended FilenameContext interface to support additional task
properties while maintaining backwards compatibility.

Updated documentation to reflect the unified variable system.
This commit is contained in:
callumalpass 2026-01-06 20:38:16 +11:00
parent 2b145a45da
commit c3d58659ec
3 changed files with 180 additions and 0 deletions

View file

@ -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

View file

@ -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<string, string> = {
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"),

View file

@ -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;