feat: add FormProcessor for note creation and frontmatter updates
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1a5a7232bb
commit
ec3f1bc404
1 changed files with 135 additions and 0 deletions
135
src/core/form-processor.ts
Normal file
135
src/core/form-processor.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue