Skip to content

Commit 1c7def1

Browse files
authored
fix: normalize leading slashes in capture/template paths (#1050)
* fix: normalize leading slashes in capture/template paths * test: allow QuickAddEngine test instantiation
1 parent 0140556 commit 1c7def1

File tree

7 files changed

+60
-17
lines changed

7 files changed

+60
-17
lines changed

docs/docs/Choices/CaptureChoice.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ This also supports the [format syntax](/FormatSyntax.md). You can even write a f
2828
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.
2929

3030
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.
31+
Paths are vault-relative. A leading `/` is ignored (except a lone `/`, which opens the file picker for the whole vault).
3132

3233
Capturing to a folder will show all files in that folder. This means that files in nested folders will also appear.
3334

docs/docs/Choices/TemplateChoice.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ title: Template
55
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.
66

77
## Mandatory
8-
**Template Path**. This is a path to the template you wish to insert.
8+
**Template Path**. This is a path to the template you wish to insert. Paths are vault-relative; a leading `/` is ignored.
99

1010
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.
1111

src/engine/QuickAddEngine.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { describe, expect, it } from "vitest";
2+
import { QuickAddEngine } from "./QuickAddEngine";
3+
4+
class TestEngine extends QuickAddEngine {
5+
public constructor() {
6+
super({} as any);
7+
}
8+
9+
public normalize(folderPath: string, fileName: string): string {
10+
return this.normalizeMarkdownFilePath(folderPath, fileName);
11+
}
12+
13+
public run(): void {}
14+
}
15+
16+
describe("QuickAddEngine path normalization", () => {
17+
const engine = new TestEngine();
18+
19+
it("strips leading slashes from folder and file", () => {
20+
expect(engine.normalize("/daily", "/note")).toBe("daily/note.md");
21+
});
22+
23+
it("strips leading slashes from file-only paths", () => {
24+
expect(engine.normalize("", "/review/daily")).toBe("review/daily.md");
25+
});
26+
});

src/engine/QuickAddEngine.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -168,12 +168,17 @@ export abstract class QuickAddEngine {
168168
}
169169
}
170170

171+
protected stripLeadingSlash(path: string): string {
172+
return path.replace(/^\/+/, "");
173+
}
174+
171175
protected normalizeMarkdownFilePath(
172176
folderPath: string,
173177
fileName: string
174178
): string {
175-
const actualFolderPath: string = folderPath ? `${folderPath}/` : "";
176-
const formattedFileName: string = fileName.replace(
179+
const safeFolderPath = this.stripLeadingSlash(folderPath);
180+
const actualFolderPath: string = safeFolderPath ? `${safeFolderPath}/` : "";
181+
const formattedFileName: string = this.stripLeadingSlash(fileName).replace(
177182
MARKDOWN_FILE_EXTENSION_REGEX,
178183
""
179184
);

src/engine/TemplateEngine.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -99,12 +99,12 @@ export abstract class TemplateEngine extends QuickAddEngine {
9999
fileName: string,
100100
templatePath: string
101101
): string {
102-
const actualFolderPath: string = folderPath ? `${folderPath}/` : "";
102+
const safeFolderPath = this.stripLeadingSlash(folderPath);
103+
const actualFolderPath: string = safeFolderPath ? `${safeFolderPath}/` : "";
103104
const extension = this.getTemplateExtension(templatePath);
104-
const formattedFileName: string = fileName.replace(
105-
MARKDOWN_FILE_EXTENSION_REGEX,
106-
""
107-
).replace(CANVAS_FILE_EXTENSION_REGEX, "");
105+
const formattedFileName: string = this.stripLeadingSlash(fileName)
106+
.replace(MARKDOWN_FILE_EXTENSION_REGEX, "")
107+
.replace(CANVAS_FILE_EXTENSION_REGEX, "");
108108
return `${actualFolderPath}${formattedFileName}${extension}`;
109109
}
110110

@@ -284,7 +284,7 @@ export abstract class TemplateEngine extends QuickAddEngine {
284284
}
285285

286286
protected async getTemplateContent(templatePath: string): Promise<string> {
287-
let correctTemplatePath: string = templatePath;
287+
let correctTemplatePath: string = this.stripLeadingSlash(templatePath);
288288
if (!MARKDOWN_FILE_EXTENSION_REGEX.test(templatePath) &&
289289
!CANVAS_FILE_EXTENSION_REGEX.test(templatePath))
290290
correctTemplatePath += ".md";

src/engine/canvas-integration.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,10 @@ describe('Canvas Template Integration', () => {
5353
});
5454

5555
describe('File path normalization', () => {
56+
const stripLeadingSlash = (path: string): string => {
57+
return path.replace(/^\/+/, "");
58+
};
59+
5660
const normalizeTemplateFilePath = (
5761
folderPath: string,
5862
fileName: string,
@@ -61,9 +65,10 @@ describe('Canvas Template Integration', () => {
6165
const MARKDOWN_REGEX = new RegExp(/\.md$/);
6266
const CANVAS_REGEX = new RegExp(/\.canvas$/);
6367

64-
const actualFolderPath = folderPath ? `${folderPath}/` : "";
68+
const safeFolderPath = stripLeadingSlash(folderPath);
69+
const actualFolderPath = safeFolderPath ? `${safeFolderPath}/` : "";
6570
const extension = CANVAS_REGEX.test(templatePath) ? ".canvas" : ".md";
66-
const formattedFileName = fileName
71+
const formattedFileName = stripLeadingSlash(fileName)
6772
.replace(MARKDOWN_REGEX, "")
6873
.replace(CANVAS_REGEX, "");
6974
return `${actualFolderPath}${formattedFileName}${extension}`;
@@ -88,6 +93,11 @@ describe('Canvas Template Integration', () => {
8893
expect(normalizeTemplateFilePath('', 'MyFile.md', 'template.canvas'))
8994
.toBe('MyFile.canvas');
9095
});
96+
97+
it('should strip leading slashes from folder and file names', () => {
98+
expect(normalizeTemplateFilePath('/Templates', '/MyFile', 'template.md'))
99+
.toBe('Templates/MyFile.md');
100+
});
91101
});
92102

93103
describe('Template path processing logic', () => {

src/preflight/runOnePagePreflight.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,12 +94,13 @@ async function collectForCaptureChoice(
9494

9595
// If captureTo indicates a folder or tag, offer a file picker requirement
9696
const formattedTarget = choice.captureTo?.trim() ?? "";
97-
const isTagTarget = formattedTarget.startsWith("#");
98-
const trimmedPath = formattedTarget.replace(/\/$|\.md$/g, "");
97+
const normalizedTarget = formattedTarget.replace(/^\/+/, "");
98+
const isTagTarget = normalizedTarget.startsWith("#");
99+
const trimmedPath = normalizedTarget.replace(/\/$|\.md$/g, "");
99100
const isFolderTarget =
100-
!isTagTarget && (formattedTarget === "" || isFolder(app, trimmedPath));
101+
!isTagTarget && (normalizedTarget === "" || isFolder(app, trimmedPath));
101102
// Heuristics: if target ends with '/' or contains unresolved tokens, we likely need a picker
102-
const looksLikeFolderBySuffix = formattedTarget.endsWith("/");
103+
const looksLikeFolderBySuffix = normalizedTarget.endsWith("/");
103104
const containsFormatTokens = /{{[^}]+}}/.test(choice.captureTo ?? "");
104105

105106
if (
@@ -111,9 +112,9 @@ async function collectForCaptureChoice(
111112
) {
112113
let files: TFile[] = [];
113114
if (isTagTarget) {
114-
files = getMarkdownFilesWithTag(app, formattedTarget);
115+
files = getMarkdownFilesWithTag(app, normalizedTarget);
115116
} else {
116-
const folder = formattedTarget.replace(/^\/$|\/\.md$|^\.md$/, "");
117+
const folder = normalizedTarget.replace(/^\/$|\/\.md$|^\.md$/, "");
117118
const base =
118119
folder === "" ? "" : folder.endsWith("/") ? folder : `${folder}/`;
119120
files = getMarkdownFilesInFolder(app, base);

0 commit comments

Comments
 (0)