Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/engine/CaptureChoiceEngine.notice.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ vi.mock("../formatters/captureChoiceFormatter", () => {
async formatFileName(name: string) {
return name;
}
getAndClearTemplatePropertyVars() {
return new Map();
}
}
return {
CaptureChoiceFormatter: CaptureChoiceFormatterMock,
Expand Down
271 changes: 271 additions & 0 deletions src/engine/CaptureChoiceEngine.template-property-types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { App, TFile, TFolder } from "obsidian";
import { TFolder as ObsidianTFolder } from "obsidian";
import { CaptureChoiceEngine } from "./CaptureChoiceEngine";
import type ICaptureChoice from "../types/choices/ICaptureChoice";
import type { IChoiceExecutor } from "../IChoiceExecutor";

vi.mock("../quickAddSettingsTab", () => {
const defaultSettings = {
choices: [],
inputPrompt: "single-line",
devMode: false,
templateFolderPath: "",
announceUpdates: true,
version: "0.0.0",
globalVariables: {},
onePageInputEnabled: false,
disableOnlineFeatures: true,
enableRibbonIcon: false,
showCaptureNotification: false,
showInputCancellationNotification: true,
enableTemplatePropertyTypes: true,
ai: {
defaultModel: "Ask me",
defaultSystemPrompt: "",
promptTemplatesFolderPath: "",
showAssistant: true,
providers: [],
},
migrations: {
migrateToMacroIDFromEmbeddedMacro: true,
useQuickAddTemplateFolder: false,
incrementFileNameSettingMoveToDefaultBehavior: false,
mutualExclusionInsertAfterAndWriteToBottomOfFile: false,
setVersionAfterUpdateModalRelease: false,
addDefaultAIProviders: false,
removeMacroIndirection: false,
migrateFileOpeningSettings: false,
},
};

return {
DEFAULT_SETTINGS: defaultSettings,
QuickAddSettingsTab: class {},
};
});

vi.mock("src/gui/InputSuggester/inputSuggester", () => ({
default: class {
static Suggest = vi.fn().mockResolvedValue("");
},
}));

vi.mock("src/gui/GenericSuggester/genericSuggester", () => ({
__esModule: true,
default: class {
static Suggest = vi.fn().mockResolvedValue("");
},
}));

vi.mock("src/gui/InputPrompt", () => ({
__esModule: true,
default: class {
factory() {
return {
Prompt: vi.fn().mockResolvedValue(""),
PromptWithContext: vi.fn().mockResolvedValue(""),
};
}
},
}));

vi.mock("src/gui/GenericInputPrompt/GenericInputPrompt", () => ({
__esModule: true,
default: class {},
}));

vi.mock("src/gui/VDateInputPrompt/VDateInputPrompt", () => ({
__esModule: true,
default: {
Prompt: vi.fn().mockResolvedValue(""),
},
}));

vi.mock("src/gui/MathModal", () => ({
__esModule: true,
MathModal: {
Prompt: vi.fn().mockResolvedValue(""),
},
}));

vi.mock("../engine/SingleInlineScriptEngine", () => ({
__esModule: true,
SingleInlineScriptEngine: class {
public params = { variables: {} as Record<string, unknown> };
async runAndGetOutput() {
return "";
}
},
}));

vi.mock("../engine/SingleMacroEngine", () => ({
__esModule: true,
SingleMacroEngine: class {
async runAndGetOutput() {
return "";
}
},
}));

vi.mock("obsidian-dataview", () => ({
getAPI: vi.fn().mockReturnValue(null),
}));

vi.mock("../gui/choiceList/ChoiceView.svelte", () => ({
default: class {},
}));

vi.mock("../utilityObsidian", () => ({
appendToCurrentLine: vi.fn(),
getMarkdownFilesInFolder: vi.fn().mockResolvedValue([]),
getMarkdownFilesWithTag: vi.fn().mockResolvedValue([]),
insertFileLinkToActiveView: vi.fn(),
insertOnNewLineAbove: vi.fn(),
insertOnNewLineBelow: vi.fn(),
isFolder: vi.fn().mockReturnValue(false),
openExistingFileTab: vi.fn().mockReturnValue(false),
openFile: vi.fn(),
overwriteTemplaterOnce: vi.fn().mockResolvedValue(undefined),
templaterParseTemplate: vi.fn(async (_app, content) => content),
getTemplater: vi.fn(() => ({})),
}));

describe("CaptureChoiceEngine template property types", () => {
beforeEach(() => {
vi.clearAllMocks();
(global as any).navigator = {
clipboard: {
readText: vi.fn().mockResolvedValue(""),
},
};
});

it("post-processes capture frontmatter arrays into YAML lists", async () => {
const targetPath = "Journal/Test.md";
const createdContent: Record<string, string> = {};
let writtenContent = "";
let appliedFrontmatter: Record<string, unknown> | undefined;

const folder = { path: "Journal" } as unknown as TFolder;
if (typeof ObsidianTFolder === "function") {
Object.setPrototypeOf(folder as unknown as object, ObsidianTFolder.prototype);
}

const processFrontMatter = vi.fn(async (_file: TFile, updater: (frontmatter: Record<string, unknown>) => void) => {
const fm: Record<string, unknown> = { tags: "foo,bar" };
updater(fm);
appliedFrontmatter = fm;
});

const tFile = {
path: targetPath,
name: "Test.md",
basename: "Test",
extension: "md",
} as unknown as TFile;

const app = {
vault: {
adapter: {
exists: vi.fn(async (path: string) => {
if (path === targetPath) return false;
if (path === "Journal") return true;
return false;
}),
},
getAbstractFileByPath: vi.fn((path: string) => {
if (path === "Journal") return folder;
return null;
}),
createFolder: vi.fn(),
create: vi.fn(async (path: string, content: string) => {
createdContent[path] = content;
writtenContent = content;
return tFile;
}),
read: vi.fn(async (file: TFile) => createdContent[file.path] ?? ""),
modify: vi.fn(async (_file: TFile, content: string) => {
writtenContent = content;
createdContent[_file.path] = content;
}),
cachedRead: vi.fn(),
},
fileManager: {
generateMarkdownLink: vi.fn().mockReturnValue(""),
processFrontMatter,
},
workspace: {
getActiveFile: vi.fn().mockReturnValue(null),
getActiveViewOfType: vi.fn().mockReturnValue(null),
},
metadataCache: {
getFileCache: vi.fn().mockReturnValue(null),
},
} as unknown as App;

const plugin = {
settings: {
enableTemplatePropertyTypes: true,
globalVariables: {},
showCaptureNotification: false,
showInputCancellationNotification: false,
},
} as any;

const choice: ICaptureChoice = {
id: "capture",
name: "Test Capture",
type: "Capture",
command: false,
captureTo: targetPath,
captureToActiveFile: false,
createFileIfItDoesntExist: {
enabled: true,
createWithTemplate: false,
template: "",
},
format: {
enabled: true,
format: ["---", "tags: {{VALUE:tags}}", "---", ""].join("\n"),
},
prepend: false,
appendLink: false,
task: false,
insertAfter: {
enabled: false,
after: "",
insertAtEnd: false,
considerSubsections: false,
createIfNotFound: false,
createIfNotFoundLocation: "",
},
newLineCapture: {
enabled: false,
direction: "below",
},
openFile: false,
fileOpening: {
location: "tab",
direction: "vertical",
mode: "source",
focus: false,
},
};

const choiceExecutor: IChoiceExecutor = {
execute: vi.fn(),
variables: new Map<string, unknown>([
["tags", ["foo", "bar"]],
]),
};

const engine = new CaptureChoiceEngine(app, plugin, choice, choiceExecutor);

await engine.run();

expect(writtenContent).toContain("tags: foo,bar");
expect(processFrontMatter).toHaveBeenCalledTimes(1);
expect(appliedFrontmatter?.tags).toEqual(["foo", "bar"]);
});
});
39 changes: 39 additions & 0 deletions src/engine/CaptureChoiceEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {
private formatter: CaptureChoiceFormatter;
private readonly plugin: QuickAdd;
private templatePropertyVars?: Map<string, unknown>;
private capturePropertyVars: Map<string, unknown> = new Map();

constructor(
app: App,
Expand Down Expand Up @@ -91,6 +92,8 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {

async run(): Promise<void> {
try {
// Reset any pending structured values before starting a new capture run
this.capturePropertyVars.clear();
const linkOptions = normalizeAppendLinkOptions(this.choice.appendLink);
this.formatter.setLinkToCurrentFileBehavior(
linkOptions.enabled && !linkOptions.requireActiveFile
Expand Down Expand Up @@ -150,6 +153,7 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {
} else {
await this.app.vault.modify(file, newFileContent);
await overwriteTemplaterOnce(this.app, file);
await this.applyCapturePropertyVars(file);
}

// Show success notification
Expand Down Expand Up @@ -343,6 +347,7 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {

// First format pass...
const formatted = await this.formatter.formatContentOnly(content);
this.mergeCapturePropertyVars(this.formatter.getAndClearTemplatePropertyVars());

const fileContent: string = await this.app.vault.read(file);
// Second format pass, with the file content... User input (long running) should have been captured during first pass
Expand All @@ -354,6 +359,7 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {
fileContent,
file,
);
this.mergeCapturePropertyVars(this.formatter.getAndClearTemplatePropertyVars());

const secondReadFileContent: string = await this.app.vault.read(file);

Expand Down Expand Up @@ -398,6 +404,7 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {
// where templater would run before the {{value}} placeholder is substituted (Issue #809).
const formattedCaptureContent: string =
await this.formatter.formatContentOnly(captureContent);
this.mergeCapturePropertyVars(this.formatter.getAndClearTemplatePropertyVars());

let fileContent = "";
if (this.choice.createFileIfItDoesntExist.createWithTemplate) {
Expand Down Expand Up @@ -455,6 +462,7 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {
updatedFileContent,
file,
);
this.mergeCapturePropertyVars(this.formatter.getAndClearTemplatePropertyVars());

return { file, newFileContent, captureContent: formattedCaptureContent };
}
Expand All @@ -467,4 +475,35 @@ export class CaptureChoiceEngine extends QuickAddChoiceEngine {

return this.normalizeMarkdownFilePath("", formattedCaptureTo);
}

private mergeCapturePropertyVars(vars: Map<string, unknown>): void {
if (!vars || vars.size === 0) {
return;
}

for (const [key, value] of vars) {
this.capturePropertyVars.set(key, value);
}

log.logMessage(
`CaptureChoiceEngine: Accumulated ${this.capturePropertyVars.size} structured capture variables`
);
}

private async applyCapturePropertyVars(file: TFile): Promise<void> {
if (this.capturePropertyVars.size === 0) {
return;
}

if (!this.shouldPostProcessFrontMatter(file, this.capturePropertyVars)) {
this.capturePropertyVars.clear();
return;
}

log.logMessage(
`CaptureChoiceEngine: Post-processing front matter with ${this.capturePropertyVars.size} capture variables`
);
await this.postProcessFrontMatter(file, this.capturePropertyVars);
this.capturePropertyVars.clear();
}
}