mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
Normalize relative folder template paths
This commit is contained in:
parent
ad87647918
commit
da246c2f21
6 changed files with 53 additions and 17 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
|
|
|||
Loading…
Reference in a new issue