Skip to content

Commit 11e6490

Browse files
committed
feat: support .base template files for template choices
1 parent e7cbbf2 commit 11e6490

12 files changed

+220
-18
lines changed

docs/docs/Choices/TemplateChoice.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ The template choice type is not meant to be a replacement for [Templater](https:
77
## Mandatory
88
**Template Path**. This is a path to the template you wish to insert. Paths are vault-relative; a leading `/` is ignored.
99

10-
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.
10+
QuickAdd supports markdown (`.md`), canvas (`.canvas`), and base (`.base`) templates. The created file uses the same extension as the template.
1111

1212
## Optional
1313
**File Name Format**. You can specify a format for the file name, which is based on the format syntax - which you can see further down this page.

src/constants.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,12 @@ export const LINK_TO_CURRENT_FILE_REGEX = new RegExp(/{{LINKCURRENT}}/i);
9898
export const FILE_NAME_OF_CURRENT_FILE_REGEX = new RegExp(/{{FILENAMECURRENT}}/i);
9999
export const MARKDOWN_FILE_EXTENSION_REGEX = new RegExp(/\.md$/);
100100
export const CANVAS_FILE_EXTENSION_REGEX = new RegExp(/\.canvas$/);
101+
export const BASE_FILE_EXTENSION_REGEX = new RegExp(/\.base$/);
101102
export const JAVASCRIPT_FILE_EXTENSION_REGEX = new RegExp(/\.js$/);
102103
export const MACRO_REGEX = new RegExp(/{{MACRO:([^\n\r}]*)}}/i);
103-
export const TEMPLATE_REGEX = new RegExp(/{{TEMPLATE:([^\n\r}]*.md)}}/i);
104+
export const TEMPLATE_REGEX = new RegExp(
105+
/{{TEMPLATE:([^\n\r}]*\.(?:md|canvas|base))}}/i,
106+
);
104107
export const GLOBAL_VAR_REGEX = new RegExp(/{{GLOBAL_VAR:([^\n\r}]*)}}/i);
105108
export const INLINE_JAVASCRIPT_REGEX = new RegExp(
106109
/`{3,}js quickadd([\s\S]*?)`{3,}/,

src/engine/TemplateChoiceEngine.notice.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,50 @@ describe("TemplateChoiceEngine file casing resolution", () => {
332332
engine.choice.templatePath,
333333
);
334334
});
335+
336+
it("supports existing .base files for overwrite mode", async () => {
337+
const { engine, app } = createEngine("ignored", {
338+
throwDuringFileName: false,
339+
stubTemplateContent: true,
340+
});
341+
342+
const existingFile = new TFile();
343+
existingFile.path = "Board.base";
344+
existingFile.name = "Board.base";
345+
existingFile.extension = "base";
346+
existingFile.basename = "Board";
347+
348+
engine.choice.templatePath = "Templates/Board.base";
349+
engine.choice.fileExistsMode = fileExistsOverwriteFile;
350+
engine.choice.setFileExistsBehavior = true;
351+
formatFileNameMock.mockResolvedValueOnce("Board");
352+
353+
(app.vault.adapter.exists as ReturnType<typeof vi.fn>).mockResolvedValue(
354+
true,
355+
);
356+
(app.vault.getAbstractFileByPath as ReturnType<typeof vi.fn>).mockReturnValue(
357+
existingFile,
358+
);
359+
360+
const overwriteSpy = vi
361+
.spyOn(
362+
engine as unknown as {
363+
overwriteFileWithTemplate: (
364+
file: TFile,
365+
templatePath: string,
366+
) => Promise<TFile | null>;
367+
},
368+
"overwriteFileWithTemplate",
369+
)
370+
.mockResolvedValue(existingFile);
371+
372+
await engine.run();
373+
374+
expect(overwriteSpy).toHaveBeenCalledWith(
375+
existingFile,
376+
"Templates/Board.base",
377+
);
378+
});
335379
});
336380

337381
describe("TemplateChoiceEngine destination path resolution", () => {

src/engine/TemplateChoiceEngine.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,12 @@ export class TemplateChoiceEngine extends TemplateEngine {
105105
const file = this.findExistingFile(filePath);
106106
if (
107107
!(file instanceof TFile) ||
108-
(file.extension !== "md" && file.extension !== "canvas")
108+
(file.extension !== "md" &&
109+
file.extension !== "canvas" &&
110+
file.extension !== "base")
109111
) {
110112
log.logError(
111-
`'${filePath}' already exists but could not be resolved as a markdown or canvas file.`,
113+
`'${filePath}' already exists but could not be resolved as a markdown, canvas, or base file.`,
112114
);
113115
return;
114116
}

src/engine/TemplateEngine.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ import {
1111
} from "../utilityObsidian";
1212
import GenericSuggester from "../gui/GenericSuggester/genericSuggester";
1313
import InputSuggester from "../gui/InputSuggester/inputSuggester";
14-
import { MARKDOWN_FILE_EXTENSION_REGEX, CANVAS_FILE_EXTENSION_REGEX } from "../constants";
14+
import {
15+
BASE_FILE_EXTENSION_REGEX,
16+
CANVAS_FILE_EXTENSION_REGEX,
17+
MARKDOWN_FILE_EXTENSION_REGEX,
18+
} from "../constants";
1519
import { reportError } from "../utils/errorUtils";
1620
import { basenameWithoutMdOrCanvas } from "../utils/pathUtils";
1721
import {
@@ -441,6 +445,9 @@ export abstract class TemplateEngine extends QuickAddEngine {
441445
if (CANVAS_FILE_EXTENSION_REGEX.test(templatePath)) {
442446
return ".canvas";
443447
}
448+
if (BASE_FILE_EXTENSION_REGEX.test(templatePath)) {
449+
return ".base";
450+
}
444451
return ".md";
445452
}
446453

@@ -454,7 +461,8 @@ export abstract class TemplateEngine extends QuickAddEngine {
454461
const extension = this.getTemplateExtension(templatePath);
455462
const formattedFileName: string = this.stripLeadingSlash(fileName)
456463
.replace(MARKDOWN_FILE_EXTENSION_REGEX, "")
457-
.replace(CANVAS_FILE_EXTENSION_REGEX, "");
464+
.replace(CANVAS_FILE_EXTENSION_REGEX, "")
465+
.replace(BASE_FILE_EXTENSION_REGEX, "");
458466
return `${actualFolderPath}${formattedFileName}${extension}`;
459467
}
460468

@@ -463,7 +471,12 @@ export abstract class TemplateEngine extends QuickAddEngine {
463471
let newFileName = fileName;
464472

465473
// Determine the extension from the filename and construct a matching regex
466-
const extension = CANVAS_FILE_EXTENSION_REGEX.test(fileName) ? ".canvas" : ".md";
474+
let extension = ".md";
475+
if (CANVAS_FILE_EXTENSION_REGEX.test(fileName)) {
476+
extension = ".canvas";
477+
} else if (BASE_FILE_EXTENSION_REGEX.test(fileName)) {
478+
extension = ".base";
479+
}
467480
const extPattern = extension.replace(/\./g, "\\.");
468481
const numberWithExtRegex = new RegExp(`(\\d*)${extPattern}$`);
469482
const exec = numberWithExtRegex.exec(fileName);
@@ -501,8 +514,8 @@ export abstract class TemplateEngine extends QuickAddEngine {
501514
templatePath
502515
);
503516

504-
// Extract filename without extension from the full path (supports .md and .canvas)
505-
const fileBasename = basenameWithoutMdOrCanvas(filePath);
517+
// Extract filename without extension from the full path.
518+
const fileBasename = basenameWithoutMdOrCanvas(filePath);
506519
this.formatter.setTitle(fileBasename);
507520

508521
const formattedTemplateContent: string =
@@ -636,7 +649,8 @@ export abstract class TemplateEngine extends QuickAddEngine {
636649
protected async getTemplateContent(templatePath: string): Promise<string> {
637650
let correctTemplatePath: string = this.stripLeadingSlash(templatePath);
638651
if (!MARKDOWN_FILE_EXTENSION_REGEX.test(templatePath) &&
639-
!CANVAS_FILE_EXTENSION_REGEX.test(templatePath))
652+
!CANVAS_FILE_EXTENSION_REGEX.test(templatePath) &&
653+
!BASE_FILE_EXTENSION_REGEX.test(templatePath))
640654
correctTemplatePath += ".md";
641655

642656
const templateFile =

src/engine/canvas-integration.test.ts

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ describe('Canvas Template Integration', () => {
55
// Test the actual regex patterns used in the implementation
66
const MARKDOWN_REGEX = new RegExp(/\.md$/);
77
const CANVAS_REGEX = new RegExp(/\.canvas$/);
8+
const BASE_REGEX = new RegExp(/\.base$/);
89

910
it('should correctly identify markdown files', () => {
1011
expect(MARKDOWN_REGEX.test('file.md')).toBe(true);
@@ -21,22 +22,29 @@ describe('Canvas Template Integration', () => {
2122
});
2223

2324
it('should have mutually exclusive patterns', () => {
24-
const testFiles = ['file.md', 'file.canvas', 'file.txt', 'file'];
25+
const testFiles = ['file.md', 'file.canvas', 'file.base', 'file.txt', 'file'];
2526

2627
testFiles.forEach(file => {
2728
const matchesMd = MARKDOWN_REGEX.test(file);
2829
const matchesCanvas = CANVAS_REGEX.test(file);
30+
const matchesBase = BASE_REGEX.test(file);
2931
expect(matchesMd && matchesCanvas).toBe(false);
32+
expect(matchesMd && matchesBase).toBe(false);
33+
expect(matchesCanvas && matchesBase).toBe(false);
3034
});
3135
});
3236
});
3337

3438
describe('Template extension logic', () => {
3539
const getTemplateExtension = (templatePath: string): string => {
3640
const CANVAS_REGEX = new RegExp(/\.canvas$/);
41+
const BASE_REGEX = new RegExp(/\.base$/);
3742
if (CANVAS_REGEX.test(templatePath)) {
3843
return ".canvas";
3944
}
45+
if (BASE_REGEX.test(templatePath)) {
46+
return ".base";
47+
}
4048
return ".md";
4149
};
4250

@@ -50,6 +58,11 @@ describe('Canvas Template Integration', () => {
5058
expect(getTemplateExtension('template')).toBe('.md');
5159
expect(getTemplateExtension('template.txt')).toBe('.md');
5260
});
61+
62+
it('should return .base for base templates', () => {
63+
expect(getTemplateExtension('template.base')).toBe('.base');
64+
expect(getTemplateExtension('path/to/template.base')).toBe('.base');
65+
});
5366
});
5467

5568
describe('File path normalization', () => {
@@ -64,13 +77,20 @@ describe('Canvas Template Integration', () => {
6477
): string => {
6578
const MARKDOWN_REGEX = new RegExp(/\.md$/);
6679
const CANVAS_REGEX = new RegExp(/\.canvas$/);
80+
const BASE_REGEX = new RegExp(/\.base$/);
6781

6882
const safeFolderPath = stripLeadingSlash(folderPath);
6983
const actualFolderPath = safeFolderPath ? `${safeFolderPath}/` : "";
70-
const extension = CANVAS_REGEX.test(templatePath) ? ".canvas" : ".md";
84+
let extension = ".md";
85+
if (CANVAS_REGEX.test(templatePath)) {
86+
extension = ".canvas";
87+
} else if (BASE_REGEX.test(templatePath)) {
88+
extension = ".base";
89+
}
7190
const formattedFileName = stripLeadingSlash(fileName)
7291
.replace(MARKDOWN_REGEX, "")
73-
.replace(CANVAS_REGEX, "");
92+
.replace(CANVAS_REGEX, "")
93+
.replace(BASE_REGEX, "");
7494
return `${actualFolderPath}${formattedFileName}${extension}`;
7595
};
7696

@@ -98,18 +118,27 @@ describe('Canvas Template Integration', () => {
98118
expect(normalizeTemplateFilePath('/Templates', '/MyFile', 'template.md'))
99119
.toBe('Templates/MyFile.md');
100120
});
121+
122+
it('should create base paths for base templates', () => {
123+
expect(normalizeTemplateFilePath('Templates', 'Board', 'template.base'))
124+
.toBe('Templates/Board.base');
125+
});
101126
});
102127

103128
describe('Template path processing logic', () => {
104129
const shouldAppendMdExtension = (templatePath: string): boolean => {
105130
const MARKDOWN_REGEX = new RegExp(/\.md$/);
106131
const CANVAS_REGEX = new RegExp(/\.canvas$/);
107-
return !MARKDOWN_REGEX.test(templatePath) && !CANVAS_REGEX.test(templatePath);
132+
const BASE_REGEX = new RegExp(/\.base$/);
133+
return !MARKDOWN_REGEX.test(templatePath) &&
134+
!CANVAS_REGEX.test(templatePath) &&
135+
!BASE_REGEX.test(templatePath);
108136
};
109137

110138
it('should not append .md to recognized extensions', () => {
111139
expect(shouldAppendMdExtension('template.canvas')).toBe(false);
112140
expect(shouldAppendMdExtension('template.md')).toBe(false);
141+
expect(shouldAppendMdExtension('template.base')).toBe(false);
113142
});
114143

115144
it('should append .md to unrecognized paths', () => {
@@ -120,12 +149,13 @@ describe('Canvas Template Integration', () => {
120149

121150
describe('File validation logic', () => {
122151
const isValidFileType = (extension: string): boolean => {
123-
return extension === 'md' || extension === 'canvas';
152+
return extension === 'md' || extension === 'canvas' || extension === 'base';
124153
};
125154

126155
it('should accept markdown and canvas files', () => {
127156
expect(isValidFileType('md')).toBe(true);
128157
expect(isValidFileType('canvas')).toBe(true);
158+
expect(isValidFileType('base')).toBe(true);
129159
});
130160

131161
it('should reject other file types', () => {

src/engine/templateEngine-increment-canvas.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,12 @@ describe('TemplateEngine - incrementFileName for .canvas', () => {
8585
const out = await engine.testIncrement('Doc.md');
8686
expect(out).toBe('Doc1.md');
8787
});
88+
89+
it('works similarly for .base', async () => {
90+
(mockApp.vault.adapter.exists as any)
91+
.mockResolvedValueOnce(true) // Board.base exists
92+
.mockResolvedValueOnce(false);
93+
const out = await engine.testIncrement('Board.base');
94+
expect(out).toBe('Board1.base');
95+
});
8896
});

src/engine/templateEngine-title.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ describe('TemplateEngine - Title Handling', () => {
125125
expect(engine.getFormatterTitle()).toBe('CanvasDoc');
126126
});
127127

128+
it('should extract title from .base filename', async () => {
129+
await engine.testCreateFileWithTemplate('folder/Kanban.base', 'template.base');
130+
expect(engine.getFormatterTitle()).toBe('Kanban');
131+
});
132+
128133
it('should format content with title replacement', async () => {
129134
const mockFormatter = (engine as any).formatter;
130135

src/preflight/RequirementCollector.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,15 @@ describe("RequirementCollector", () => {
9595
expect(rc.templatesToScan.size === 0 || rc.templatesToScan.has("Templates/Note")).toBe(true);
9696
});
9797

98+
it("records .base TEMPLATE references for recursive scanning", async () => {
99+
const app = makeApp();
100+
const plugin = makePlugin();
101+
const rc = new RequirementCollector(app, plugin);
102+
await rc.scanString("{{TEMPLATE:Templates/Kanban.base}}" );
103+
104+
expect(rc.templatesToScan.has("Templates/Kanban.base")).toBe(true);
105+
});
106+
98107
it("uses textarea for VALUE tokens with type:multiline", async () => {
99108
const app = makeApp();
100109
const plugin = makePlugin({ inputPrompt: "single-line" });

0 commit comments

Comments
 (0)