Skip to content

Commit 54bb3d8

Browse files
committed
fix(template): scope YAML placeholders to post-processing
1 parent 00f404a commit 54bb3d8

9 files changed

Lines changed: 181 additions & 52 deletions

src/engine/CaptureChoiceEngine.selection.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ vi.mock("../formatters/captureChoiceFormatter", () => ({
2323
}
2424
setDestinationFile() {}
2525
setDestinationSourcePath() {}
26+
async withTemplatePropertyCollection<T>(work: () => Promise<T>) {
27+
return await work();
28+
}
2629
async formatContentOnly(content: string) {
2730
return content;
2831
}

src/engine/CaptureChoiceEngine.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -628,18 +628,22 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {
628628
this.formatter.setDestinationFile(file);
629629

630630
// First format pass...
631-
const formatted = await this.formatter.formatContentOnly(content);
631+
const formatted = await this.formatter.withTemplatePropertyCollection(
632+
() => this.formatter.formatContentOnly(content),
633+
);
632634
this.mergeCapturePropertyVars(this.formatter.getAndClearTemplatePropertyVars());
633635

634636
const fileContent: string = await this.app.vault.read(file);
635637
// Second format pass, with the file content... User input (long running) should have been captured during first pass
636638
// So this pass is to insert the formatted capture value into the file content, depending on the user's settings
637639
const formattedFileContent: string =
638-
await this.formatter.formatContentWithFile(
639-
formatted,
640-
this.choice,
641-
fileContent,
642-
file,
640+
await this.formatter.withTemplatePropertyCollection(() =>
641+
this.formatter.formatContentWithFile(
642+
formatted,
643+
this.choice,
644+
fileContent,
645+
file,
646+
),
643647
);
644648
this.mergeCapturePropertyVars(this.formatter.getAndClearTemplatePropertyVars());
645649

@@ -685,7 +689,9 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {
685689
// This mirrors the logic used when the target file already exists and prevents the timing issue
686690
// where templater would run before the {{value}} placeholder is substituted (Issue #809).
687691
const formattedCaptureContent: string =
688-
await this.formatter.formatContentOnly(captureContent);
692+
await this.formatter.withTemplatePropertyCollection(() =>
693+
this.formatter.formatContentOnly(captureContent),
694+
);
689695
this.mergeCapturePropertyVars(this.formatter.getAndClearTemplatePropertyVars());
690696

691697
let fileContent = "";
@@ -743,12 +749,15 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {
743749
// after the initial Templater run on newly created files.
744750
const updatedFileContent: string = await this.app.vault.read(file);
745751
// Second formatting pass: embed the already-resolved capture content into the newly created file
746-
const newFileContent: string = await this.formatter.formatContentWithFile(
747-
formattedCaptureContent,
748-
this.choice,
749-
updatedFileContent,
750-
file,
751-
);
752+
const newFileContent: string =
753+
await this.formatter.withTemplatePropertyCollection(() =>
754+
this.formatter.formatContentWithFile(
755+
formattedCaptureContent,
756+
this.choice,
757+
updatedFileContent,
758+
file,
759+
),
760+
);
752761
this.mergeCapturePropertyVars(this.formatter.getAndClearTemplatePropertyVars());
753762

754763
return { file, newFileContent, captureContent: formattedCaptureContent };

src/engine/SingleTemplateEngine.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ export class SingleTemplateEngine extends TemplateEngine {
2121
log.logError(`Template ${this.templatePath} not found.`);
2222
}
2323

24-
templateContent = await this.formatter.formatFileContent(
25-
templateContent
24+
templateContent = await this.formatter.withTemplatePropertyCollection(
25+
() => this.formatter.formatFileContent(templateContent),
2626
);
2727

2828
return templateContent;

src/engine/TemplateChoiceEngine.notice.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ vi.mock("../formatters/completeFormatter", () => {
6666
async formatFileContent(...args: unknown[]) {
6767
return await formatFileContentMock(...args);
6868
}
69+
async withTemplatePropertyCollection<T>(work: () => Promise<T>) {
70+
return await work();
71+
}
6972
getAndClearTemplatePropertyVars() {
7073
return new Map<string, unknown>();
7174
}

src/engine/TemplateEngine.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,9 @@ export abstract class TemplateEngine extends QuickAddEngine {
480480
this.formatter.setTitle(fileBasename);
481481

482482
const formattedTemplateContent: string =
483-
await this.formatter.formatFileContent(templateContent);
483+
await this.formatter.withTemplatePropertyCollection(() =>
484+
this.formatter.formatFileContent(templateContent),
485+
);
484486

485487
// Get template variables before creating the file
486488
const templateVars = this.formatter.getAndClearTemplatePropertyVars();
@@ -537,7 +539,9 @@ export abstract class TemplateEngine extends QuickAddEngine {
537539
this.formatter.setTitle(fileBasename);
538540

539541
const formattedTemplateContent: string =
540-
await this.formatter.formatFileContent(templateContent);
542+
await this.formatter.withTemplatePropertyCollection(() =>
543+
this.formatter.formatFileContent(templateContent),
544+
);
541545

542546
// Get template variables before modifying the file
543547
const templateVars = this.formatter.getAndClearTemplatePropertyVars();

src/engine/templateEngine-title.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ vi.mock('../formatters/completeFormatter', () => {
1212
return {
1313
setTitle: vi.fn((t: string) => { title = t; }),
1414
getTitle: () => title,
15+
withTemplatePropertyCollection: vi.fn(
16+
async (work: () => Promise<unknown>) => await work(),
17+
),
1518
formatFileContent: vi.fn(async (content: string) => {
1619
// Simple mock that replaces {{title}} with the stored title
1720
return content.replace(/{{title}}/gi, title);

src/formatters/formatter-template-property-types.test.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,7 @@ class TemplatePropertyTypesTestFormatter extends Formatter {
2424
}
2525

2626
protected getVariableValue(variableName: string): string {
27-
const value = this.variables.get(variableName);
28-
return typeof value === 'string' ? value : '';
27+
return (this.variables.get(variableName) as string) ?? '';
2928
}
3029

3130
protected suggestForValue(
@@ -85,6 +84,14 @@ class TemplatePropertyTypesTestFormatter extends Formatter {
8584
public async testFormat(input: string): Promise<string> {
8685
return await this.format(input);
8786
}
87+
88+
public async testFormatWithTemplatePropertyCollection(
89+
input: string,
90+
): Promise<string> {
91+
return await this.withTemplatePropertyCollection(() =>
92+
this.testFormat(input),
93+
);
94+
}
8895
}
8996

9097
describe('Formatter template property type inference', () => {
@@ -110,37 +117,68 @@ describe('Formatter template property type inference', () => {
110117

111118
it('collects comma-separated values as YAML arrays', async () => {
112119
(formatter as any).variables.set('tags', 'tag1, tag2, awesomeproject');
113-
await formatter.testFormat('---\ntags: {{VALUE:tags}}\n---');
120+
await formatter.testFormatWithTemplatePropertyCollection(
121+
'---\ntags: {{VALUE:tags}}\n---',
122+
);
114123
const vars = formatter.getAndClearTemplatePropertyVars();
115124
expect(vars.get('tags')).toEqual(['tag1', 'tag2', 'awesomeproject']);
116125
});
117126

118127
it('does not collect comma text for scalar properties', async () => {
119128
(formatter as any).variables.set('description', 'Hello, world');
120-
await formatter.testFormat('---\ndescription: {{VALUE:description}}\n---');
129+
await formatter.testFormatWithTemplatePropertyCollection(
130+
'---\ndescription: {{VALUE:description}}\n---',
131+
);
121132
const vars = formatter.getAndClearTemplatePropertyVars();
122133
expect(vars.has('description')).toBe(false);
123134
});
124135

125136
it('collects bullet list values as YAML arrays', async () => {
126137
(formatter as any).variables.set('projects', '- project1\n- project2');
127-
await formatter.testFormat('---\nprojects: {{VALUE:projects}}\n---');
138+
await formatter.testFormatWithTemplatePropertyCollection(
139+
'---\nprojects: {{VALUE:projects}}\n---',
140+
);
128141
const vars = formatter.getAndClearTemplatePropertyVars();
129142
expect(vars.get('projects')).toEqual(['project1', 'project2']);
130143
});
131144

132-
it('uses a YAML-safe placeholder for collected arrays before post-processing', async () => {
145+
it('preserves raw structured replacements outside template property collection', async () => {
133146
(formatter as any).variables.set('tags', ['[[John Doe]]', '[[Jane Doe]]']);
134147
const output = await formatter.testFormat('---\ntags: {{VALUE:tags}}\n---');
135148
const vars = formatter.getAndClearTemplatePropertyVars();
136149

150+
expect(output).toBe('---\ntags: [[John Doe]],[[Jane Doe]]\n---');
151+
expect(vars.size).toBe(0);
152+
});
153+
154+
it('uses a YAML-safe placeholder for collected arrays before post-processing', async () => {
155+
(formatter as any).variables.set('tags', ['[[John Doe]]', '[[Jane Doe]]']);
156+
const output = await formatter.testFormatWithTemplatePropertyCollection(
157+
'---\ntags: {{VALUE:tags}}\n---',
158+
);
159+
const vars = formatter.getAndClearTemplatePropertyVars();
160+
137161
expect(output).toBe('---\ntags: []\n---');
138162
expect(vars.get('tags')).toEqual(['[[John Doe]]', '[[Jane Doe]]']);
139163
});
140164

165+
it('does not apply case transforms to YAML placeholders', async () => {
166+
(formatter as any).variables.set('done', null);
167+
const output =
168+
await formatter.testFormatWithTemplatePropertyCollection(
169+
'---\ndone: {{VALUE:done|case:upper}}\n---',
170+
);
171+
const vars = formatter.getAndClearTemplatePropertyVars();
172+
173+
expect(output).toBe('---\ndone: null\n---');
174+
expect(vars.get('done')).toBeNull();
175+
});
176+
141177
it('ignores wiki links with commas to avoid incorrect splitting', async () => {
142178
(formatter as any).variables.set('source', '[[test, a]]');
143-
await formatter.testFormat('---\nsource: {{VALUE:source}}\n---');
179+
await formatter.testFormatWithTemplatePropertyCollection(
180+
'---\nsource: {{VALUE:source}}\n---',
181+
);
144182
const vars = formatter.getAndClearTemplatePropertyVars();
145183
expect(vars.has('source')).toBe(false);
146184
});

src/formatters/formatter.ts

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ export abstract class Formatter {
5959

6060
// Tracks variables collected for YAML property post-processing
6161
private readonly propertyCollector: TemplatePropertyCollector;
62+
private templatePropertyCollectionDepth = 0;
6263

6364
protected constructor(protected readonly app?: App) {
6465
this.propertyCollector = new TemplatePropertyCollector(app);
@@ -303,6 +304,23 @@ export abstract class Formatter {
303304
return this.propertyCollector.drain();
304305
}
305306

307+
/**
308+
* Runs a formatting operation in a scope where structured YAML values should
309+
* be collected and replaced with temporary placeholders for later
310+
* `processFrontMatter()` post-processing.
311+
*/
312+
public async withTemplatePropertyCollection<T>(
313+
work: () => Promise<T>,
314+
): Promise<T> {
315+
this.templatePropertyCollectionDepth += 1;
316+
317+
try {
318+
return await work();
319+
} finally {
320+
this.templatePropertyCollectionDepth -= 1;
321+
}
322+
}
323+
306324
protected abstract getCurrentFileLink(): string | null;
307325
protected abstract getCurrentFileName(): string | null;
308326

@@ -393,15 +411,16 @@ export abstract class Formatter {
393411
matchEnd: match.index + match[0].length,
394412
rawValue: rawValueForCollector,
395413
fallbackKey: variableName,
396-
featureEnabled: propertyTypesEnabled,
414+
featureEnabled:
415+
propertyTypesEnabled &&
416+
this.templatePropertyCollectionDepth > 0,
397417
});
398418

399419
// Keep the interim frontmatter YAML-parseable until post-processing
400420
// writes the real structured value back through Obsidian.
401-
const rawReplacement =
402-
getYamlPlaceholder(structuredYamlValue) ??
403-
this.getVariableValue(effectiveKey);
404-
const replacement = transformCase(rawReplacement, caseStyle);
421+
const placeholder = getYamlPlaceholder(structuredYamlValue);
422+
const replacement = placeholder ??
423+
transformCase(this.getVariableValue(effectiveKey), caseStyle);
405424

406425
// Replace in output and adjust regex position
407426
output = output.slice(0, match.index) + replacement + output.slice(match.index + match[0].length);

0 commit comments

Comments
 (0)