Normalize relative folder template paths

This commit is contained in:
callumalpass 2026-05-17 23:15:53 +10:00
parent ad87647918
commit da246c2f21
6 changed files with 53 additions and 17 deletions

View file

@ -26,6 +26,7 @@ Example:
## Added
- ([#1254](https://github.com/callumalpass/tasknotes/issues/1254)) Added `../` and `./` normalization for folder templates after variables are expanded, so instant task conversion can target sibling folders such as `{{currentNotePath}}/../Tasks`. Thanks to @rdick for suggesting this.
- ([#1267](https://github.com/callumalpass/tasknotes/issues/1267)) Added Daily Notes-style date tokens such as `YYYY`, `MM`, `DD`, `MMMM`, and `dddd` to TaskNotes folder templates. Thanks to @paulsen-it for suggesting this.
- ([#1380](https://github.com/callumalpass/tasknotes/issues/1380)) Added tag actions to task context menus, including add/remove actions for individual tasks and batch-selected tasks. Thanks to @Chuhtra for suggesting this.
- ([#1377](https://github.com/callumalpass/tasknotes/issues/1377)) Added right-click Mini Calendar actions for opening daily notes from day cells and weekly notes from week numbers. Thanks to @hasanyilmaz for suggesting this.

View file

@ -7,7 +7,7 @@ These settings control the foundational aspects of the plugin, such as task iden
## Task Storage
Task storage settings define where new and inline-created task files are created and how archived tasks are relocated. **Default tasks folder** sets the base location and supports folder template variables, including `{{currentNotePath}}` and `{{currentNoteTitle}}` for creating command-palette or ribbon tasks beside the active note. It also supports date variables such as `{{year}}` and Daily Notes-style date tokens such as `YYYY/MM/DD`. **Folder for inline-created tasks** controls the Create Inline Task command and instant conversion output. Leave it empty to use the default tasks folder, set a fixed folder path to centralize inline tasks, or use `{{currentNotePath}}` and `{{currentNoteTitle}}` placeholders for contextual routing. If archive moves are enabled, completed archived tasks are moved automatically to your configured archive folder.
Task storage settings define where new and inline-created task files are created and how archived tasks are relocated. **Default tasks folder** sets the base location and supports folder template variables, including `{{currentNotePath}}` and `{{currentNoteTitle}}` for creating command-palette or ribbon tasks beside the active note. It also supports date variables such as `{{year}}` and Daily Notes-style date tokens such as `YYYY/MM/DD`. **Folder for inline-created tasks** controls the Create Inline Task command and instant conversion output. Leave it empty to use the default tasks folder, set a fixed folder path to centralize inline tasks, or use `{{currentNotePath}}` and `{{currentNoteTitle}}` placeholders for contextual routing. Relative path segments such as `../` are normalized after variables are expanded. If archive moves are enabled, completed archived tasks are moved automatically to your configured archive folder.
## Task Identification

View file

@ -10,7 +10,9 @@ You can enable or disable the **Task Link Overlay** feature, which replaces wiki
You can enable or disable the **Instant Task Conversion** feature, which allows you to convert any line type (checkboxes, bullet points, numbered lists, blockquotes, headers, or plain text) to TaskNotes with a single click. You can also choose whether to apply your default task settings to converted tasks.
The folder where inline-created and converted tasks are created is configured in **Settings → General → Folder for inline-created tasks**. See [General Settings](general.md#task-storage) for details on folder configuration options.
The folder where inline-created and converted tasks are created is configured in **Settings → General → Folder for inline-created tasks**. See [General Settings](general.md#task-storage) for details on folder configuration options, including `{{currentNotePath}}` and `../` sibling-folder paths.
Instant conversion uses the same filename settings as other new tasks. Configure these in **Settings → Task Properties → Title → Filename format**, including custom templates such as `{{date}} {{title}}`.
## Natural Language Processing

View file

@ -9,7 +9,7 @@ This page documents folder management, filename templates, archive settings, and
You can specify a **Default Tasks Folder** where new tasks will be created. You can also configure the **Task Tag** that identifies notes as TaskNotes, and you can specify a list of **Excluded Folders** that will be ignored by the plugin.
Filename generation settings are configured in the **Task Properties** tab within the Title property card. You can choose from title-based, timestamp-based, Zettelkasten-style patterns, or create a custom filename template.
Filename generation settings are configured in the **Task Properties** tab within the Title property card. You can choose from title-based, timestamp-based, Zettelkasten-style patterns, or create a custom filename template. These filename settings also apply to inline task conversion and instant-created task notes.
These settings define folder paths and filename behavior for new tasks, which affects long-term vault structure.
### Folder Template Variables
@ -96,6 +96,7 @@ Advanced variables increase uniqueness and entropy, but may reduce path readabil
- **Title Sanitization**: The `{{title}}` variable automatically removes invalid folder characters (`<>:"/\|?*`) and replaces them with underscores
- **Folder Creation**: Folders are automatically created if they don't exist
- **Inline Tasks**: Template variables also work for the inline-created task folder setting. Leave that setting empty to use the default tasks folder.
- **Relative Paths**: `.` and `..` path segments are normalized after variables are expanded, so `{{currentNotePath}}/../Tasks` can target a sibling folder.
When templates contain nested variables, create a small test set first to validate the resulting paths.
#### Advanced Usage

View file

@ -68,6 +68,35 @@ function replaceDailyNotesDateTokens(template: string, date: Date): string {
);
}
function hasRelativePathSegments(folderPath: string): boolean {
return folderPath
.replace(/\\/g, "/")
.split("/")
.some((segment) => segment === "." || segment === "..");
}
function normalizeRelativeFolderPath(folderPath: string): string {
const slashNormalizedPath = folderPath.replace(/\\/g, "/");
const preserveTrailingSlash = slashNormalizedPath.endsWith("/");
const normalizedSegments: string[] = [];
for (const segment of slashNormalizedPath.split("/")) {
if (!segment || segment === ".") {
continue;
}
if (segment === "..") {
normalizedSegments.pop();
continue;
}
normalizedSegments.push(segment);
}
const normalizedPath = normalizedSegments.join("/");
return preserveTrailingSlash && normalizedPath ? `${normalizedPath}/` : normalizedPath;
}
/**
* Process a folder path template by replacing template variables with actual values
*
@ -136,6 +165,7 @@ export function processFolderTemplate(
const { date = new Date(), taskData, icsData, extractProjectBasename } = options;
let processedPath = folderTemplate;
const shouldNormalizeRelativeSegments = hasRelativePathSegments(folderTemplate);
processedPath = replaceDailyNotesDateTokens(processedPath, date);
@ -342,5 +372,7 @@ export function processFolderTemplate(
const nanoId = Date.now().toString() + Math.random().toString(36).substring(2, 7);
processedPath = processedPath.replace(/\{\{nano\}\}/g, nanoId);
return processedPath;
return shouldNormalizeRelativeSegments
? normalizeRelativeFolderPath(processedPath)
: processedPath;
}

View file

@ -13,7 +13,7 @@
* {{currentNotePath}}/../Tasks/{{currentNoteTitle}} to navigate to sibling folders
* - Currently, ../ is not normalized in folder paths, causing tasks to be created in wrong locations
*
* @see https://github.com/obsidian-tasknotes/tasknotes/issues/1254
* @see https://github.com/callumalpass/tasknotes/issues/1254
*/
import { processFolderTemplate, TaskTemplateData } from '../../../src/utils/folderTemplateProcessor';
@ -32,7 +32,7 @@ describe('Issue #1254: Instant Note Creation - Change File Name + Support ../ Pa
* a clear way to configure this for instant task conversion specifically.
*/
it.skip('reproduces issue #1254: should support date variables in custom filename template', () => {
it('reproduces issue #1254: should support date variables in custom filename template', () => {
// User's desired template: {{date}} {{title}}
// Expected filename: "2025-01-15 My Task"
const context: FilenameContext = {
@ -55,7 +55,7 @@ describe('Issue #1254: Instant Note Creation - Change File Name + Support ../ Pa
expect(filename).toBe('2025-01-15 My Task');
});
it.skip('reproduces issue #1254: should support year/month/day variables in filename', () => {
it('reproduces issue #1254: should support year/month/day variables in filename', () => {
// User's alternative template: {{year}}-{{month}}-{{day}} {{title}}
const context: FilenameContext = {
title: 'Meeting Notes',
@ -76,7 +76,7 @@ describe('Issue #1254: Instant Note Creation - Change File Name + Support ../ Pa
expect(filename).toBe('2025-01-15 Meeting Notes');
});
it.skip('reproduces issue #1254: should support timestamp in filename for unique sorting', () => {
it('reproduces issue #1254: should support timestamp in filename for unique sorting', () => {
// For users who want time-based uniqueness: {{date}}-{{time}} {{title}}
const context: FilenameContext = {
title: 'Quick Task',
@ -117,7 +117,7 @@ describe('Issue #1254: Instant Note Creation - Change File Name + Support ../ Pa
* Currently, ../ is not normalized, causing incorrect folder paths.
*/
it.skip('reproduces issue #1254: should normalize ../ in folder path templates', () => {
it('reproduces issue #1254: should normalize ../ in folder path templates', () => {
// Simulate the scenario where {{currentNotePath}} = "Project/Meetings"
// and the template is "{{currentNotePath}}/../Tasks/{{currentNoteTitle}}"
// After variable substitution: "Project/Meetings/../Tasks/MeetingA"
@ -133,7 +133,7 @@ describe('Issue #1254: Instant Note Creation - Change File Name + Support ../ Pa
expect(result).toBe('Project/Tasks/MeetingA');
});
it.skip('reproduces issue #1254: should handle multiple ../ segments', () => {
it('reproduces issue #1254: should handle multiple ../ segments', () => {
// User navigating up multiple levels
// Project/SubA/SubB/../../../Tasks
// Should normalize to: Tasks
@ -144,7 +144,7 @@ describe('Issue #1254: Instant Note Creation - Change File Name + Support ../ Pa
expect(result).toBe('Tasks');
});
it.skip('reproduces issue #1254: should handle ../ at the beginning of path', () => {
it('reproduces issue #1254: should handle ../ at the beginning of path', () => {
// Edge case: ../Tasks (relative to vault root)
// Should normalize appropriately (or error gracefully)
@ -156,7 +156,7 @@ describe('Issue #1254: Instant Note Creation - Change File Name + Support ../ Pa
expect(result).toBe('Tasks');
});
it.skip('reproduces issue #1254: should handle mixed template variables and ../ ', () => {
it('reproduces issue #1254: should handle mixed template variables and ../ ', () => {
// Real-world scenario from the issue
const folderPath = '{{currentNotePath}}/../Tasks/{{currentNoteTitle}}';
@ -175,7 +175,7 @@ describe('Issue #1254: Instant Note Creation - Change File Name + Support ../ Pa
expect(result).toBe('Project/Tasks/MeetingA');
});
it.skip('reproduces issue #1254: should normalize ../ with task variables', () => {
it('reproduces issue #1254: should normalize ../ with task variables', () => {
// Combining ../ with task-specific variables
const taskData: TaskTemplateData = {
title: 'Review PR',
@ -195,7 +195,7 @@ describe('Issue #1254: Instant Note Creation - Change File Name + Support ../ Pa
expect(result).toBe('Projects/web-app/Tasks/high');
});
it.skip('reproduces issue #1254: should preserve trailing slash after normalization', () => {
it('reproduces issue #1254: should preserve trailing slash after normalization', () => {
// Some users might want a trailing slash
const folderPath = 'Project/Meetings/../Tasks/';
const result = processFolderTemplate(folderPath, { date: testDate });
@ -203,7 +203,7 @@ describe('Issue #1254: Instant Note Creation - Change File Name + Support ../ Pa
expect(result).toBe('Project/Tasks/');
});
it.skip('reproduces issue #1254: should handle consecutive ../ segments', () => {
it('reproduces issue #1254: should handle consecutive ../ segments', () => {
// Edge case: Project/A/B/C/../../Tasks -> Project/A/Tasks
const folderPath = 'Project/A/B/C/../../Tasks';
const result = processFolderTemplate(folderPath, { date: testDate });
@ -211,7 +211,7 @@ describe('Issue #1254: Instant Note Creation - Change File Name + Support ../ Pa
expect(result).toBe('Project/A/Tasks');
});
it.skip('reproduces issue #1254: should handle ../ with ./ (current directory)', () => {
it('reproduces issue #1254: should handle ../ with ./ (current directory)', () => {
// Mixed relative paths: Project/./Meetings/../Tasks
const folderPath = 'Project/./Meetings/../Tasks';
const result = processFolderTemplate(folderPath, { date: testDate });
@ -221,7 +221,7 @@ describe('Issue #1254: Instant Note Creation - Change File Name + Support ../ Pa
});
describe('Combined Scenarios', () => {
it.skip('reproduces issue #1254: should support both date in filename and ../ in folder', () => {
it('reproduces issue #1254: should support both date in filename and ../ in folder', () => {
// Full scenario from the issue:
// - Folder template: {{currentNotePath}}/../Tasks/{{currentNoteTitle}}
// - Filename template: {{date}} {{title}}