From 9f3401af2f49f243251719922b42b0eaf585f681 Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Fri, 19 Dec 2025 22:26:14 +0100 Subject: [PATCH 1/2] fix: normalize leading slashes in capture/template paths --- docs/docs/Choices/CaptureChoice.md | 1 + docs/docs/Choices/TemplateChoice.md | 2 +- src/engine/QuickAddEngine.test.ts | 22 ++++++++++++++++++++++ src/engine/QuickAddEngine.ts | 9 +++++++-- src/engine/TemplateEngine.ts | 12 ++++++------ src/engine/canvas-integration.test.ts | 14 ++++++++++++-- src/preflight/runOnePagePreflight.ts | 13 +++++++------ 7 files changed, 56 insertions(+), 17 deletions(-) create mode 100644 src/engine/QuickAddEngine.test.ts diff --git a/docs/docs/Choices/CaptureChoice.md b/docs/docs/Choices/CaptureChoice.md index 7c1d393a..6e0245ab 100644 --- a/docs/docs/Choices/CaptureChoice.md +++ b/docs/docs/Choices/CaptureChoice.md @@ -28,6 +28,7 @@ This also supports the [format syntax](/FormatSyntax.md). You can even write a f For example, you might have a folder called `CRM/people`. In this folder, you have a note for the people in your life. You can type `CRM/people` in the _Capture To_ field, and QuickAdd will ask you which file to capture to. You can then type `John Doe` in the suggester, and QuickAdd will create a file called `John Doe.md` in the `CRM/people` folder. You could also write nothing - or `/` - in the _Capture To_ field. This will open the suggester with all of your files in it, and you can select or type the name of the file you want to capture to. +Paths are vault-relative. A leading `/` is ignored (except a lone `/`, which opens the file picker for the whole vault). Capturing to a folder will show all files in that folder. This means that files in nested folders will also appear. diff --git a/docs/docs/Choices/TemplateChoice.md b/docs/docs/Choices/TemplateChoice.md index 19bb1c32..14561b08 100644 --- a/docs/docs/Choices/TemplateChoice.md +++ b/docs/docs/Choices/TemplateChoice.md @@ -5,7 +5,7 @@ title: Template The template choice type is not meant to be a replacement for [Templater](https://github.com/SilentVoid13/Templater/) plugin or core `Templates`. It's meant to augment them, to add more possibilities. You can use both QuickAdd format syntax in a Templater template - and both will work. ## Mandatory -**Template Path**. This is a path to the template you wish to insert. +**Template Path**. This is a path to the template you wish to insert. Paths are vault-relative; a leading `/` is ignored. QuickAdd supports both markdown (`.md`) and canvas (`.canvas`) templates. When using a canvas template, the created file will also be a canvas file with the same extension. diff --git a/src/engine/QuickAddEngine.test.ts b/src/engine/QuickAddEngine.test.ts new file mode 100644 index 00000000..da1b6b87 --- /dev/null +++ b/src/engine/QuickAddEngine.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; +import { QuickAddEngine } from "./QuickAddEngine"; + +class TestEngine extends QuickAddEngine { + public normalize(folderPath: string, fileName: string): string { + return this.normalizeMarkdownFilePath(folderPath, fileName); + } + + public run(): void {} +} + +describe("QuickAddEngine path normalization", () => { + const engine = new TestEngine({} as any); + + it("strips leading slashes from folder and file", () => { + expect(engine.normalize("/daily", "/note")).toBe("daily/note.md"); + }); + + it("strips leading slashes from file-only paths", () => { + expect(engine.normalize("", "/review/daily")).toBe("review/daily.md"); + }); +}); diff --git a/src/engine/QuickAddEngine.ts b/src/engine/QuickAddEngine.ts index da0e7c09..e5a6ea8b 100644 --- a/src/engine/QuickAddEngine.ts +++ b/src/engine/QuickAddEngine.ts @@ -168,12 +168,17 @@ export abstract class QuickAddEngine { } } + protected stripLeadingSlash(path: string): string { + return path.replace(/^\/+/, ""); + } + protected normalizeMarkdownFilePath( folderPath: string, fileName: string ): string { - const actualFolderPath: string = folderPath ? `${folderPath}/` : ""; - const formattedFileName: string = fileName.replace( + const safeFolderPath = this.stripLeadingSlash(folderPath); + const actualFolderPath: string = safeFolderPath ? `${safeFolderPath}/` : ""; + const formattedFileName: string = this.stripLeadingSlash(fileName).replace( MARKDOWN_FILE_EXTENSION_REGEX, "" ); diff --git a/src/engine/TemplateEngine.ts b/src/engine/TemplateEngine.ts index b1f67996..9f8f3175 100644 --- a/src/engine/TemplateEngine.ts +++ b/src/engine/TemplateEngine.ts @@ -99,12 +99,12 @@ export abstract class TemplateEngine extends QuickAddEngine { fileName: string, templatePath: string ): string { - const actualFolderPath: string = folderPath ? `${folderPath}/` : ""; + const safeFolderPath = this.stripLeadingSlash(folderPath); + const actualFolderPath: string = safeFolderPath ? `${safeFolderPath}/` : ""; const extension = this.getTemplateExtension(templatePath); - const formattedFileName: string = fileName.replace( - MARKDOWN_FILE_EXTENSION_REGEX, - "" - ).replace(CANVAS_FILE_EXTENSION_REGEX, ""); + const formattedFileName: string = this.stripLeadingSlash(fileName) + .replace(MARKDOWN_FILE_EXTENSION_REGEX, "") + .replace(CANVAS_FILE_EXTENSION_REGEX, ""); return `${actualFolderPath}${formattedFileName}${extension}`; } @@ -284,7 +284,7 @@ export abstract class TemplateEngine extends QuickAddEngine { } protected async getTemplateContent(templatePath: string): Promise { - let correctTemplatePath: string = templatePath; + let correctTemplatePath: string = this.stripLeadingSlash(templatePath); if (!MARKDOWN_FILE_EXTENSION_REGEX.test(templatePath) && !CANVAS_FILE_EXTENSION_REGEX.test(templatePath)) correctTemplatePath += ".md"; diff --git a/src/engine/canvas-integration.test.ts b/src/engine/canvas-integration.test.ts index 376893fa..ad5a2f76 100644 --- a/src/engine/canvas-integration.test.ts +++ b/src/engine/canvas-integration.test.ts @@ -53,6 +53,10 @@ describe('Canvas Template Integration', () => { }); describe('File path normalization', () => { + const stripLeadingSlash = (path: string): string => { + return path.replace(/^\/+/, ""); + }; + const normalizeTemplateFilePath = ( folderPath: string, fileName: string, @@ -61,9 +65,10 @@ describe('Canvas Template Integration', () => { const MARKDOWN_REGEX = new RegExp(/\.md$/); const CANVAS_REGEX = new RegExp(/\.canvas$/); - const actualFolderPath = folderPath ? `${folderPath}/` : ""; + const safeFolderPath = stripLeadingSlash(folderPath); + const actualFolderPath = safeFolderPath ? `${safeFolderPath}/` : ""; const extension = CANVAS_REGEX.test(templatePath) ? ".canvas" : ".md"; - const formattedFileName = fileName + const formattedFileName = stripLeadingSlash(fileName) .replace(MARKDOWN_REGEX, "") .replace(CANVAS_REGEX, ""); return `${actualFolderPath}${formattedFileName}${extension}`; @@ -88,6 +93,11 @@ describe('Canvas Template Integration', () => { expect(normalizeTemplateFilePath('', 'MyFile.md', 'template.canvas')) .toBe('MyFile.canvas'); }); + + it('should strip leading slashes from folder and file names', () => { + expect(normalizeTemplateFilePath('/Templates', '/MyFile', 'template.md')) + .toBe('Templates/MyFile.md'); + }); }); describe('Template path processing logic', () => { diff --git a/src/preflight/runOnePagePreflight.ts b/src/preflight/runOnePagePreflight.ts index 066e3891..195c9f1c 100644 --- a/src/preflight/runOnePagePreflight.ts +++ b/src/preflight/runOnePagePreflight.ts @@ -94,12 +94,13 @@ async function collectForCaptureChoice( // If captureTo indicates a folder or tag, offer a file picker requirement const formattedTarget = choice.captureTo?.trim() ?? ""; - const isTagTarget = formattedTarget.startsWith("#"); - const trimmedPath = formattedTarget.replace(/\/$|\.md$/g, ""); + const normalizedTarget = formattedTarget.replace(/^\/+/, ""); + const isTagTarget = normalizedTarget.startsWith("#"); + const trimmedPath = normalizedTarget.replace(/\/$|\.md$/g, ""); const isFolderTarget = - !isTagTarget && (formattedTarget === "" || isFolder(app, trimmedPath)); + !isTagTarget && (normalizedTarget === "" || isFolder(app, trimmedPath)); // Heuristics: if target ends with '/' or contains unresolved tokens, we likely need a picker - const looksLikeFolderBySuffix = formattedTarget.endsWith("/"); + const looksLikeFolderBySuffix = normalizedTarget.endsWith("/"); const containsFormatTokens = /{{[^}]+}}/.test(choice.captureTo ?? ""); if ( @@ -111,9 +112,9 @@ async function collectForCaptureChoice( ) { let files: TFile[] = []; if (isTagTarget) { - files = getMarkdownFilesWithTag(app, formattedTarget); + files = getMarkdownFilesWithTag(app, normalizedTarget); } else { - const folder = formattedTarget.replace(/^\/$|\/\.md$|^\.md$/, ""); + const folder = normalizedTarget.replace(/^\/$|\/\.md$|^\.md$/, ""); const base = folder === "" ? "" : folder.endsWith("/") ? folder : `${folder}/`; files = getMarkdownFilesInFolder(app, base); From b4c7736325c1906ab8e06ba64b34db62c9b184ae Mon Sep 17 00:00:00 2001 From: Christian Bager Bach Houmann Date: Fri, 19 Dec 2025 22:29:51 +0100 Subject: [PATCH 2/2] test: allow QuickAddEngine test instantiation --- src/engine/QuickAddEngine.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/engine/QuickAddEngine.test.ts b/src/engine/QuickAddEngine.test.ts index da1b6b87..757a8f8e 100644 --- a/src/engine/QuickAddEngine.test.ts +++ b/src/engine/QuickAddEngine.test.ts @@ -2,6 +2,10 @@ import { describe, expect, it } from "vitest"; import { QuickAddEngine } from "./QuickAddEngine"; class TestEngine extends QuickAddEngine { + public constructor() { + super({} as any); + } + public normalize(folderPath: string, fileName: string): string { return this.normalizeMarkdownFilePath(folderPath, fileName); } @@ -10,7 +14,7 @@ class TestEngine extends QuickAddEngine { } describe("QuickAddEngine path normalization", () => { - const engine = new TestEngine({} as any); + const engine = new TestEngine(); it("strips leading slashes from folder and file", () => { expect(engine.normalize("/daily", "/note")).toBe("daily/note.md");