feat: add FormProcessor for note creation and frontmatter updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca Oelfke 2026-02-13 13:14:48 +01:00
parent 1a5a7232bb
commit ec3f1bc404

135
src/core/form-processor.ts Normal file
View file

@ -0,0 +1,135 @@
import { App, TFile, TFolder, Notice, normalizePath } from 'obsidian';
import { FormDefinition } from '../types';
import { renderTemplate, sanitizeFileName } from '../utils/template-engine';
/**
* Processes a submitted form: creates a new note or updates frontmatter.
*/
export class FormProcessor {
constructor(private app: App) {}
/**
* Process form submission based on mode.
* Returns the file that was created or updated.
*/
async process(
form: FormDefinition,
values: Record<string, unknown>,
targetFile?: TFile,
): Promise<TFile> {
if (form.mode === 'create') {
return this.createNote(form, values);
} else {
return this.updateNote(form, values, targetFile);
}
}
private async createNote(
form: FormDefinition,
values: Record<string, unknown>,
): Promise<TFile> {
const stringValues = this.toStringValues(values);
// Build file name
const rawName = form.fileNameTemplate
? renderTemplate(form.fileNameTemplate, stringValues)
: `${stringValues['date'] ?? new Date().toISOString().slice(0, 10)}-untitled`;
const fileName = sanitizeFileName(rawName);
// Ensure target folder exists
const folder = normalizePath(form.targetFolder ?? '/');
await this.ensureFolder(folder);
// Build file path (avoid duplicates)
let filePath = normalizePath(`${folder}/${fileName}.md`);
let counter = 1;
while (this.app.vault.getAbstractFileByPath(filePath)) {
filePath = normalizePath(`${folder}/${fileName}-${counter}.md`);
counter++;
}
// Build content
const frontmatter = this.buildFrontmatter(form, values);
const body = form.bodyTemplate
? renderTemplate(form.bodyTemplate, stringValues)
: '';
const content = `---\n${frontmatter}---\n${body}`;
const file = await this.app.vault.create(filePath, content);
new Notice(`Created: ${file.path}`);
return file;
}
private async updateNote(
form: FormDefinition,
values: Record<string, unknown>,
targetFile?: TFile,
): Promise<TFile> {
if (!targetFile) {
throw new Error('No target file specified for update.');
}
await this.app.fileManager.processFrontMatter(targetFile, (fm) => {
for (const field of form.fields) {
const value = values[field.id];
if (value !== undefined) {
fm[field.id] = value;
}
}
});
new Notice(`Updated: ${targetFile.path}`);
return targetFile;
}
private buildFrontmatter(
form: FormDefinition,
values: Record<string, unknown>,
): string {
let yaml = '';
for (const field of form.fields) {
const value = values[field.id];
if (value === undefined || value === null) continue;
if (Array.isArray(value)) {
yaml += `${field.id}:\n`;
for (const item of value) {
yaml += ` - ${this.yamlEscape(String(item))}\n`;
}
} else if (typeof value === 'boolean') {
yaml += `${field.id}: ${value}\n`;
} else if (typeof value === 'number') {
yaml += `${field.id}: ${value}\n`;
} else {
yaml += `${field.id}: ${this.yamlEscape(String(value))}\n`;
}
}
return yaml;
}
private yamlEscape(value: string): string {
if (/[:#{}[\],&*?|>!%@`]/.test(value) || value.includes('\n')) {
return `"${value.replace(/"/g, '\\"')}"`;
}
return value;
}
private toStringValues(values: Record<string, unknown>): Record<string, string> {
const result: Record<string, string> = {};
for (const [key, value] of Object.entries(values)) {
if (Array.isArray(value)) {
result[key] = value.join(', ');
} else {
result[key] = String(value ?? '');
}
}
return result;
}
private async ensureFolder(path: string): Promise<void> {
if (path === '/' || path === '') return;
const existing = this.app.vault.getAbstractFileByPath(path);
if (existing instanceof TFolder) return;
await this.app.vault.createFolder(path);
}
}