feat(#1437): Unify template syntax to use {{variable}} everywhere

Filename templates now support both {{variable}} (preferred) and
{variable} (legacy) syntax for backwards compatibility. This makes
the syntax consistent with body/frontmatter templates.

Changes:
- filenameGenerator.ts: Process {{var}} first, then {var} as fallback
- titlePropertyCard.ts: Add deprecation warning for single-brace syntax
- en.ts: Add translation for legacy syntax warning
- docs/task-defaults.md: Update examples to use {{variable}} syntax

The single-brace syntax remains functional but users will see a warning
in settings encouraging migration to double-brace syntax.
This commit is contained in:
callumalpass 2026-01-06 20:32:25 +11:00
parent 0fd4594bfc
commit 2b145a45da
4 changed files with 106 additions and 60 deletions

View file

@ -123,87 +123,89 @@ The system prevents file overwrites by checking for existing files and showing e
When using the **custom** filename format, you can create templates using variables that are replaced with actual values when tasks are created.
> **Recommended syntax:** Use double braces `{{variable}}` for consistency with body templates. Single braces `{variable}` are supported for backwards compatibility but are deprecated.
### Available Filename Variables
**Task Properties:**
- `{title}` - Task title (sanitized for filenames)
- `{priority}` - Task priority (e.g., "high", "medium", "low")
- `{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)
- `{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
- `{titleUpper}` - Task title in uppercase
- `{titleSnake}` - Task title in snake_case
- `{titleKebab}` - Task title in kebab-case
- `{titleCamel}` - Task title in camelCase
- `{titlePascal}` - Task title in PascalCase
- `{{title}}` - Task title (sanitized for filenames)
- `{{priority}}` - Task priority (e.g., "high", "medium", "low")
- `{{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)
- `{{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
- `{{titleUpper}}` - Task title in uppercase
- `{{titleSnake}}` - Task title in snake_case
- `{{titleKebab}}` - Task title in kebab-case
- `{{titleCamel}}` - Task title in camelCase
- `{{titlePascal}}` - Task title in PascalCase
**Date and Time:**
- `{date}` - Full date (YYYY-MM-DD format, e.g., "2025-08-15")
- `{time}` - Time as HHMMSS (e.g., "143502")
- `{timestamp}` - Date and time as YYYY-MM-DD-HHMMSS (e.g., "2025-08-15-143502")
- `{dateTime}` - Date and time as YYYY-MM-DD-HHMM (e.g., "2025-08-15-1435")
- `{year}` - Year (e.g., "2025")
- `{month}` - Month with leading zero (e.g., "08")
- `{day}` - Day with leading zero (e.g., "15")
- `{hour}` - Hour with leading zero (e.g., "14")
- `{minute}` - Minute with leading zero (e.g., "35")
- `{second}` - Second with leading zero (e.g., "02")
- `{shortDate}` - Short date as YYMMDD (e.g., "250815")
- `{shortYear}` - Short year as YY (e.g., "25")
- `{monthName}` - Full month name (e.g., "August")
- `{monthNameShort}` - Short month name (e.g., "Aug")
- `{dayName}` - Full day name (e.g., "Thursday")
- `{dayNameShort}` - Short day name (e.g., "Thu")
- `{week}` - Week number (e.g., "33")
- `{quarter}` - Quarter number (e.g., "3")
- `{time12}` - 12-hour time with AM/PM (e.g., "02:35 PM")
- `{time24}` - 24-hour time (e.g., "14:35")
- `{hourPadded}` - Hour with leading zero (e.g., "14")
- `{hour12}` - 12-hour format hour with leading zero (e.g., "02")
- `{ampm}` - AM/PM indicator (e.g., "PM")
- `{{date}}` - Full date (YYYY-MM-DD format, e.g., "2025-08-15")
- `{{time}}` - Time as HHMMSS (e.g., "143502")
- `{{timestamp}}` - Date and time as YYYY-MM-DD-HHMMSS (e.g., "2025-08-15-143502")
- `{{dateTime}}` - Date and time as YYYY-MM-DD-HHMM (e.g., "2025-08-15-1435")
- `{{year}}` - Year (e.g., "2025")
- `{{month}}` - Month with leading zero (e.g., "08")
- `{{day}}` - Day with leading zero (e.g., "15")
- `{{hour}}` - Hour with leading zero (e.g., "14")
- `{{minute}}` - Minute with leading zero (e.g., "35")
- `{{second}}` - Second with leading zero (e.g., "02")
- `{{shortDate}}` - Short date as YYMMDD (e.g., "250815")
- `{{shortYear}}` - Short year as YY (e.g., "25")
- `{{monthName}}` - Full month name (e.g., "August")
- `{{monthNameShort}}` - Short month name (e.g., "Aug")
- `{{dayName}}` - Full day name (e.g., "Thursday")
- `{{dayNameShort}}` - Short day name (e.g., "Thu")
- `{{week}}` - Week number (e.g., "33")
- `{{quarter}}` - Quarter number (e.g., "3")
- `{{time12}}` - 12-hour time with AM/PM (e.g., "02:35 PM")
- `{{time24}}` - 24-hour time (e.g., "14:35")
- `{{hourPadded}}` - Hour with leading zero (e.g., "14")
- `{{hour12}}` - 12-hour format hour with leading zero (e.g., "02")
- `{{ampm}}` - AM/PM indicator (e.g., "PM")
**Advanced:**
- `{unix}` - Unix timestamp in seconds (e.g., "1692106502")
- `{unixMs}` - Unix timestamp in milliseconds (e.g., "1692106502123")
- `{milliseconds}` - Milliseconds (e.g., "123")
- `{ms}` - Milliseconds (e.g., "123")
- `{timezone}` - Timezone offset (e.g., "+10:00")
- `{timezoneShort}` - Short timezone offset (e.g., "+1000")
- `{utcOffset}` - UTC offset (e.g., "+10:00")
- `{utcOffsetShort}` - Short UTC offset (e.g., "+1000")
- `{utcZ}` - UTC Z indicator (always "Z")
- `{zettel}` - Zettelkasten ID (e.g., "250815abc")
- `{nano}` - Nano ID with timestamp and random string
- `{{unix}}` - Unix timestamp in seconds (e.g., "1692106502")
- `{{unixMs}}` - Unix timestamp in milliseconds (e.g., "1692106502123")
- `{{milliseconds}}` - Milliseconds (e.g., "123")
- `{{ms}}` - Milliseconds (e.g., "123")
- `{{timezone}}` - Timezone offset (e.g., "+10:00")
- `{{timezoneShort}}` - Short timezone offset (e.g., "+1000")
- `{{utcOffset}}` - UTC offset (e.g., "+10:00")
- `{{utcOffsetShort}}` - Short UTC offset (e.g., "+1000")
- `{{utcZ}}` - UTC Z indicator (always "Z")
- `{{zettel}}` - Zettelkasten ID (e.g., "250815abc")
- `{{nano}}` - Nano ID with timestamp and random string
### Filename Template Examples
**Date-based with title:**
```
{year}-{month}-{day} {title}
{{year}}-{{month}}-{{day}} {{title}}
```
Result: `2025-08-15 Complete documentation.md`
**Zettelkasten with title:**
```
{zettel} {title}
{{zettel}} {{title}}
```
Result: `250815abc Complete documentation.md`
**Priority and status prefix:**
```
[{priorityShort}] {title}
[{{priorityShort}}] {{title}}
```
Result: `[H] Complete documentation.md`
**Custom timestamp:**
```
{timestamp}-{titleKebab}
{{timestamp}}-{{titleKebab}}
```
Result: `2025-08-15-143502-complete-documentation.md`
@ -211,14 +213,14 @@ Result: `2025-08-15-143502-complete-documentation.md`
For ICS event notes, additional variables are available:
- `{icsEventTitle}` - Event title from ICS calendar
- `{icsEventLocation}` - Event location
- `{icsEventDescription}` - Event description (truncated to 50 characters)
- `{icsEventTitleWithDate}` - Event title with formatted date
- `{{icsEventTitle}}` - Event title from ICS calendar
- `{{icsEventLocation}}` - Event location
- `{{icsEventDescription}}` - Event description (truncated to 50 characters)
- `{{icsEventTitleWithDate}}` - Event title with formatted date
### Important Notes
- **Single Braces**: Filename variables use single braces `{variable}` while folder variables use double braces `{{variable}}`
- **Unified Syntax**: Both filename and body templates now use double braces `{{variable}}`. Single braces `{variable}` are supported for backwards compatibility but deprecated.
- **Sanitization**: All variables are automatically sanitized to be safe for filenames (invalid characters removed)
- **Empty Values**: If a property doesn't have a value, the variable is replaced with an empty string
- **Character Limits**: Filenames are limited to 255 characters on most systems

View file

@ -1011,6 +1011,7 @@ export const en: TranslationTree = {
filenameUpdatesWithTitle: "Filename will automatically update when the task title changes.",
filenameFormat: "Filename format:",
customTemplate: "Custom template:",
legacySyntaxWarning: "Single-brace syntax like {title} is deprecated. Please use double-brace syntax {{title}} instead for consistency with body templates.",
},
tagsCard: {
nativeObsidianTags: "Uses native Obsidian tags",

View file

@ -1,3 +1,4 @@
import { setIcon } from "obsidian";
import TaskNotesPlugin from "../../../main";
import {
createCard,
@ -153,10 +154,42 @@ function renderFilenameSettingsContent(
plugin.settings.customFilenameTemplate
);
templateInput.style.width = "100%";
// Warning container for legacy syntax
const warningContainer = container.createDiv();
const updateWarning = () => {
warningContainer.empty();
// Check for single-brace syntax that isn't part of double-brace
// Match {word} but not {{word}}
const template = templateInput.value;
const hasLegacySyntax = /(?<!\{)\{([a-zA-Z]+)\}(?!\})/.test(template);
if (hasLegacySyntax) {
const warningEl = warningContainer.createDiv({
cls: "setting-item-description mod-warning",
});
warningEl.style.color = "var(--text-warning)";
warningEl.style.marginTop = "8px";
warningEl.style.display = "flex";
warningEl.style.alignItems = "flex-start";
warningEl.style.gap = "6px";
const iconEl = warningEl.createSpan();
setIcon(iconEl, "alert-triangle");
iconEl.style.flexShrink = "0";
const textEl = warningEl.createSpan();
textEl.textContent = translate("settings.taskProperties.titleCard.legacySyntaxWarning");
}
};
templateInput.addEventListener("change", () => {
plugin.settings.customFilenameTemplate = templateInput.value;
save();
updateWarning();
});
templateInput.addEventListener("input", updateWarning);
templateContainer.appendChild(templateInput);
// Help text for template variables
@ -164,5 +197,8 @@ function renderFilenameSettingsContent(
text: translate("settings.appearance.taskFilenames.customTemplate.helpText"),
cls: "setting-item-description",
});
// Initial warning check
updateWarning();
}
}

View file

@ -288,17 +288,24 @@ function generateCustomFilename(
}
// Replace all variables in the template
// Support both {{variable}} (preferred) and {variable} (legacy) syntax
Object.entries(variables).forEach(([key, value]) => {
try {
const regex = new RegExp(`\\{${key}\\}`, "g");
result = result.replace(regex, value);
// First: Replace double-brace syntax {{variable}} (preferred, consistent with body templates)
const doubleRegex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
result = result.replace(doubleRegex, value);
// Second: Replace single-brace syntax {variable} (legacy, backwards compatibility)
const singleRegex = new RegExp(`\\{${key}\\}`, "g");
result = result.replace(singleRegex, value);
} catch (regexError) {
console.warn(`Error replacing template variable ${key}:`, regexError);
}
});
// Clean up any remaining unreplaced variables
result = result.replace(/\{[^}]+\}/g, "");
// Clean up any remaining unreplaced variables (both syntaxes)
result = result.replace(/\{\{[^}]+\}\}/g, ""); // {{variable}}
result = result.replace(/\{[^}]+\}/g, ""); // {variable}
// Ensure we have a valid filename
if (!result.trim()) {