From da246c2f21aebe3735df45f8039b96b430a4dda8 Mon Sep 17 00:00:00 2001 From: callumalpass Date: Sun, 17 May 2026 23:15:53 +1000 Subject: [PATCH] Normalize relative folder template paths --- docs/releases/unreleased.md | 1 + docs/settings/general.md | 2 +- docs/settings/inline-task-settings.md | 4 ++- docs/settings/task-defaults.md | 3 +- src/utils/folderTemplateProcessor.ts | 34 ++++++++++++++++++- ...stantTaskConvertService.issue-1254.test.ts | 26 +++++++------- 6 files changed, 53 insertions(+), 17 deletions(-) diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index 51059ebe..5e18a79a 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -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. diff --git a/docs/settings/general.md b/docs/settings/general.md index 220bd8ba..0b766c3a 100644 --- a/docs/settings/general.md +++ b/docs/settings/general.md @@ -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 diff --git a/docs/settings/inline-task-settings.md b/docs/settings/inline-task-settings.md index 81d1ff45..fc7e5c07 100644 --- a/docs/settings/inline-task-settings.md +++ b/docs/settings/inline-task-settings.md @@ -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 diff --git a/docs/settings/task-defaults.md b/docs/settings/task-defaults.md index 3e221b5d..e248f0b4 100644 --- a/docs/settings/task-defaults.md +++ b/docs/settings/task-defaults.md @@ -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 diff --git a/src/utils/folderTemplateProcessor.ts b/src/utils/folderTemplateProcessor.ts index 51c4b663..dfa31a39 100644 --- a/src/utils/folderTemplateProcessor.ts +++ b/src/utils/folderTemplateProcessor.ts @@ -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; } diff --git a/tests/unit/services/InstantTaskConvertService.issue-1254.test.ts b/tests/unit/services/InstantTaskConvertService.issue-1254.test.ts index 74e36856..408eda77 100644 --- a/tests/unit/services/InstantTaskConvertService.issue-1254.test.ts +++ b/tests/unit/services/InstantTaskConvertService.issue-1254.test.ts @@ -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}}