diff --git a/src/context-intelligence.ts b/src/context-intelligence.ts index 59b5293..8c2cf83 100644 --- a/src/context-intelligence.ts +++ b/src/context-intelligence.ts @@ -96,12 +96,12 @@ export class BacklinkResolver { // === Forward Link Resolution (multi-depth) === -interface ForwardLinkResult { +export interface ForwardLinkResult { path: string; depth: number; } -function resolveForwardLinks( +export function resolveForwardLinks( app: App, startPath: string, maxDepth: number diff --git a/src/main.ts b/src/main.ts index b09ea85..4b171e5 100644 --- a/src/main.ts +++ b/src/main.ts @@ -243,7 +243,7 @@ export default class PromptfirePlugin extends Plugin { if (template) { const engine = new TemplateEngine(this.app); const context = await engine.buildContext(combined); - combined = engine.processTemplate(template.content, context); + combined = await engine.processTemplate(template.content, context); templateName = template.name; } } @@ -429,7 +429,7 @@ export default class PromptfirePlugin extends Plugin { if (template) { const engine = new TemplateEngine(this.app); const context = await engine.buildContext(combined); - combined = engine.processTemplate(template.content, context); + combined = await engine.processTemplate(template.content, context); templateName = template.name; } } @@ -603,7 +603,7 @@ export default class PromptfirePlugin extends Plugin { if (template) { const engine = new TemplateEngine(this.app); const context = await engine.buildContext(combined); - combined = engine.processTemplate(template.content, context); + combined = await engine.processTemplate(template.content, context); templateName = template.name; } } @@ -800,7 +800,7 @@ export default class PromptfirePlugin extends Plugin { templateName = template.name; const engine = new TemplateEngine(this.app); const context = await engine.buildContext(combined); - combined = engine.processTemplate(template.content, context); + combined = await engine.processTemplate(template.content, context); } else { new Notice(`Template "${preset.template}" not found`, 5000); } @@ -811,7 +811,7 @@ export default class PromptfirePlugin extends Plugin { templateName = template.name; const engine = new TemplateEngine(this.app); const context = await engine.buildContext(combined); - combined = engine.processTemplate(template.content, context); + combined = await engine.processTemplate(template.content, context); } } diff --git a/src/template-modal.ts b/src/template-modal.ts index ef1863c..3a5dca7 100644 --- a/src/template-modal.ts +++ b/src/template-modal.ts @@ -116,7 +116,7 @@ export class TemplateModal extends Modal { try { const engine = new TemplateEngine(this.app); const context = await engine.buildContext('[Generated context would appear here]'); - const processed = engine.processTemplate(this.content, context); + const processed = await engine.processTemplate(this.content, context); // Show preview in a simple modal const previewModal = new Modal(this.app); diff --git a/src/templates.ts b/src/templates.ts index f246030..91fdb31 100644 --- a/src/templates.ts +++ b/src/templates.ts @@ -1,4 +1,11 @@ -import { App, MarkdownView } from 'obsidian'; +import { App, MarkdownView, TFile } from 'obsidian'; +import { + BacklinkResolver, + ContextIntelligence, + DEFAULT_INTELLIGENCE_SETTINGS, + resolveForwardLinks, +} from './context-intelligence'; +import { TagCollector } from './presets'; // === Types === @@ -187,10 +194,10 @@ export class TemplateEngine { /** * Process a template with the given context */ - processTemplate(template: string, context: TemplateContext): string { + async processTemplate(template: string, context: TemplateContext): Promise { let result = template; - // Simple variable replacement + // 1. Static variable replacement result = result.replace(/\{\{context\}\}/g, context.context); result = result.replace(/\{\{selection\}\}/g, context.selection); result = result.replace(/\{\{active_note\}\}/g, context.activeNote); @@ -200,12 +207,174 @@ export class TemplateEngine { result = result.replace(/\{\{datetime\}\}/g, context.datetime); result = result.replace(/\{\{vault_name\}\}/g, context.vaultName); - // Process conditionals: {{#if variable}}...{{/if}} + // 2. Dynamic variable resolution + result = await this.resolveDynamicVariables(result); + + // 3. Process conditionals: {{#if variable}}...{{/if}} result = this.processConditionals(result, context); return result; } + private async resolveDynamicVariables(template: string): Promise { + const pattern = /\{\{(backlinks|forward_links|recent_modified|shared_tags|folder_siblings|smart_context)(?::(\d+))?\}\}/g; + const matches: { full: string; name: string; limit: number }[] = []; + + let match; + while ((match = pattern.exec(template)) !== null) { + matches.push({ + full: match[0], + name: match[1] as string, + limit: match[2] ? parseInt(match[2], 10) : 10, + }); + } + + for (const m of matches) { + let resolved = ''; + switch (m.name) { + case 'backlinks': + resolved = await this.resolveBacklinks(m.limit); + break; + case 'forward_links': + resolved = await this.resolveForwardLinksDynamic(m.limit); + break; + case 'recent_modified': + resolved = await this.resolveRecentModified(m.limit); + break; + case 'shared_tags': + resolved = await this.resolveSharedTags(m.limit); + break; + case 'folder_siblings': + resolved = await this.resolveFolderSiblings(m.limit); + break; + case 'smart_context': + resolved = await this.resolveSmartContext(m.limit); + break; + } + template = template.replace(m.full, resolved); + } + + return template; + } + + private getActiveFile(): TFile | null { + const view = this.app.workspace.getActiveViewOfType(MarkdownView); + return view?.file ?? null; + } + + private async formatNoteList(files: TFile[], limit: number): Promise { + const limited = files.slice(0, limit); + const parts: string[] = []; + + for (const file of limited) { + const content = await this.app.vault.cachedRead(file); + parts.push(`# === ${file.name} ===\n\n${content}`); + } + + return parts.join('\n\n---\n\n'); + } + + private async resolveBacklinks(limit: number): Promise { + const active = this.getActiveFile(); + if (!active) return ''; + + const resolver = new BacklinkResolver(this.app); + const paths = resolver.getBacklinks(active.path); + const files: TFile[] = []; + + for (const path of paths) { + const file = this.app.vault.getAbstractFileByPath(path); + if (file instanceof TFile) files.push(file); + } + + return this.formatNoteList(files, limit); + } + + private async resolveForwardLinksDynamic(limit: number): Promise { + const active = this.getActiveFile(); + if (!active) return ''; + + const results = resolveForwardLinks(this.app, active.path, 1); + const files: TFile[] = []; + + for (const r of results) { + const file = this.app.vault.getAbstractFileByPath(r.path); + if (file instanceof TFile) files.push(file); + } + + return this.formatNoteList(files, limit); + } + + private async resolveRecentModified(limit: number): Promise { + const active = this.getActiveFile(); + const activePath = active?.path ?? ''; + + const files = this.app.vault.getMarkdownFiles() + .filter(f => { + if (f.path === activePath) return false; + if (f.path.startsWith('.obsidian')) return false; + if (f.path.startsWith('_context')) return false; + return true; + }) + .sort((a, b) => b.stat.mtime - a.stat.mtime); + + return this.formatNoteList(files, limit); + } + + private async resolveSharedTags(limit: number): Promise { + const active = this.getActiveFile(); + if (!active) return ''; + + const cache = this.app.metadataCache.getFileCache(active); + if (!cache) return ''; + + const tags: string[] = []; + if (cache.frontmatter?.tags) { + const fmTags = cache.frontmatter.tags; + if (Array.isArray(fmTags)) { + tags.push(...fmTags.map((t: unknown) => typeof t === 'string' ? t : String(t))); + } else if (typeof fmTags === 'string') { + tags.push(fmTags); + } + } + if (cache.tags) { + tags.push(...cache.tags.map(t => t.tag.startsWith('#') ? t.tag.substring(1) : t.tag)); + } + + if (tags.length === 0) return ''; + + const collector = new TagCollector(this.app); + const files = collector.collectFilesWithTags(tags) + .filter(f => f.path !== active.path); + + return this.formatNoteList(files, limit); + } + + private async resolveFolderSiblings(limit: number): Promise { + const active = this.getActiveFile(); + if (!active) return ''; + + const parentPath = active.parent?.path ?? ''; + const files = this.app.vault.getMarkdownFiles() + .filter(f => f.path !== active.path && (f.parent?.path ?? '') === parentPath); + + return this.formatNoteList(files, limit); + } + + private async resolveSmartContext(limit: number): Promise { + const active = this.getActiveFile(); + if (!active) return ''; + + const intelligence = new ContextIntelligence(this.app); + const result = await intelligence.analyze(active, DEFAULT_INTELLIGENCE_SETTINGS); + + const files = result.scoredNotes + .filter(n => n.selected) + .map(n => n.file); + + return this.formatNoteList(files, limit); + } + /** * Process conditional blocks * Supports: {{#if variable}}content{{/if}} @@ -323,6 +492,12 @@ export function getAvailablePlaceholders(): { name: string; description: string { name: '{{time}}', description: 'Current time (HH:MM)' }, { name: '{{datetime}}', description: 'Current date and time' }, { name: '{{vault_name}}', description: 'Name of the vault' }, + { name: '{{backlinks}}', description: 'Content of notes linking to active note' }, + { name: '{{forward_links}}', description: 'Content of notes linked from active note' }, + { name: '{{recent_modified:N}}', description: 'Content of N most recently modified notes' }, + { name: '{{shared_tags}}', description: 'Content of notes sharing tags with active note' }, + { name: '{{folder_siblings}}', description: 'Content of notes in the same folder' }, + { name: '{{smart_context}}', description: 'Top-scored related notes via intelligence engine' }, { name: '{{#if variable}}...{{/if}}', description: 'Conditional block' }, { name: '{{#if variable}}...{{else}}...{{/if}}', description: 'Conditional with else' }, ];