feat: add dynamic template variables for vault-level data
Expose backlinks, forward links, recent notes, shared tags, folder
siblings, and smart context directly in templates via {{variable}}
and {{variable:N}} syntax. Makes processTemplate async to support
the new resolvers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
acb82971b4
commit
06a228847f
4 changed files with 187 additions and 12 deletions
|
|
@ -96,12 +96,12 @@ export class BacklinkResolver {
|
||||||
|
|
||||||
// === Forward Link Resolution (multi-depth) ===
|
// === Forward Link Resolution (multi-depth) ===
|
||||||
|
|
||||||
interface ForwardLinkResult {
|
export interface ForwardLinkResult {
|
||||||
path: string;
|
path: string;
|
||||||
depth: number;
|
depth: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveForwardLinks(
|
export function resolveForwardLinks(
|
||||||
app: App,
|
app: App,
|
||||||
startPath: string,
|
startPath: string,
|
||||||
maxDepth: number
|
maxDepth: number
|
||||||
|
|
|
||||||
10
src/main.ts
10
src/main.ts
|
|
@ -243,7 +243,7 @@ export default class PromptfirePlugin extends Plugin {
|
||||||
if (template) {
|
if (template) {
|
||||||
const engine = new TemplateEngine(this.app);
|
const engine = new TemplateEngine(this.app);
|
||||||
const context = await engine.buildContext(combined);
|
const context = await engine.buildContext(combined);
|
||||||
combined = engine.processTemplate(template.content, context);
|
combined = await engine.processTemplate(template.content, context);
|
||||||
templateName = template.name;
|
templateName = template.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -429,7 +429,7 @@ export default class PromptfirePlugin extends Plugin {
|
||||||
if (template) {
|
if (template) {
|
||||||
const engine = new TemplateEngine(this.app);
|
const engine = new TemplateEngine(this.app);
|
||||||
const context = await engine.buildContext(combined);
|
const context = await engine.buildContext(combined);
|
||||||
combined = engine.processTemplate(template.content, context);
|
combined = await engine.processTemplate(template.content, context);
|
||||||
templateName = template.name;
|
templateName = template.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -603,7 +603,7 @@ export default class PromptfirePlugin extends Plugin {
|
||||||
if (template) {
|
if (template) {
|
||||||
const engine = new TemplateEngine(this.app);
|
const engine = new TemplateEngine(this.app);
|
||||||
const context = await engine.buildContext(combined);
|
const context = await engine.buildContext(combined);
|
||||||
combined = engine.processTemplate(template.content, context);
|
combined = await engine.processTemplate(template.content, context);
|
||||||
templateName = template.name;
|
templateName = template.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -800,7 +800,7 @@ export default class PromptfirePlugin extends Plugin {
|
||||||
templateName = template.name;
|
templateName = template.name;
|
||||||
const engine = new TemplateEngine(this.app);
|
const engine = new TemplateEngine(this.app);
|
||||||
const context = await engine.buildContext(combined);
|
const context = await engine.buildContext(combined);
|
||||||
combined = engine.processTemplate(template.content, context);
|
combined = await engine.processTemplate(template.content, context);
|
||||||
} else {
|
} else {
|
||||||
new Notice(`Template "${preset.template}" not found`, 5000);
|
new Notice(`Template "${preset.template}" not found`, 5000);
|
||||||
}
|
}
|
||||||
|
|
@ -811,7 +811,7 @@ export default class PromptfirePlugin extends Plugin {
|
||||||
templateName = template.name;
|
templateName = template.name;
|
||||||
const engine = new TemplateEngine(this.app);
|
const engine = new TemplateEngine(this.app);
|
||||||
const context = await engine.buildContext(combined);
|
const context = await engine.buildContext(combined);
|
||||||
combined = engine.processTemplate(template.content, context);
|
combined = await engine.processTemplate(template.content, context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ export class TemplateModal extends Modal {
|
||||||
try {
|
try {
|
||||||
const engine = new TemplateEngine(this.app);
|
const engine = new TemplateEngine(this.app);
|
||||||
const context = await engine.buildContext('[Generated context would appear here]');
|
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
|
// Show preview in a simple modal
|
||||||
const previewModal = new Modal(this.app);
|
const previewModal = new Modal(this.app);
|
||||||
|
|
|
||||||
183
src/templates.ts
183
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 ===
|
// === Types ===
|
||||||
|
|
||||||
|
|
@ -187,10 +194,10 @@ export class TemplateEngine {
|
||||||
/**
|
/**
|
||||||
* Process a template with the given context
|
* Process a template with the given context
|
||||||
*/
|
*/
|
||||||
processTemplate(template: string, context: TemplateContext): string {
|
async processTemplate(template: string, context: TemplateContext): Promise<string> {
|
||||||
let result = template;
|
let result = template;
|
||||||
|
|
||||||
// Simple variable replacement
|
// 1. Static variable replacement
|
||||||
result = result.replace(/\{\{context\}\}/g, context.context);
|
result = result.replace(/\{\{context\}\}/g, context.context);
|
||||||
result = result.replace(/\{\{selection\}\}/g, context.selection);
|
result = result.replace(/\{\{selection\}\}/g, context.selection);
|
||||||
result = result.replace(/\{\{active_note\}\}/g, context.activeNote);
|
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(/\{\{datetime\}\}/g, context.datetime);
|
||||||
result = result.replace(/\{\{vault_name\}\}/g, context.vaultName);
|
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);
|
result = this.processConditionals(result, context);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async resolveDynamicVariables(template: string): Promise<string> {
|
||||||
|
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<string> {
|
||||||
|
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<string> {
|
||||||
|
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<string> {
|
||||||
|
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<string> {
|
||||||
|
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<string> {
|
||||||
|
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<string> {
|
||||||
|
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<string> {
|
||||||
|
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
|
* Process conditional blocks
|
||||||
* Supports: {{#if variable}}content{{/if}}
|
* Supports: {{#if variable}}content{{/if}}
|
||||||
|
|
@ -323,6 +492,12 @@ export function getAvailablePlaceholders(): { name: string; description: string
|
||||||
{ name: '{{time}}', description: 'Current time (HH:MM)' },
|
{ name: '{{time}}', description: 'Current time (HH:MM)' },
|
||||||
{ name: '{{datetime}}', description: 'Current date and time' },
|
{ name: '{{datetime}}', description: 'Current date and time' },
|
||||||
{ name: '{{vault_name}}', description: 'Name of the vault' },
|
{ 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}}...{{/if}}', description: 'Conditional block' },
|
||||||
{ name: '{{#if variable}}...{{else}}...{{/if}}', description: 'Conditional with else' },
|
{ name: '{{#if variable}}...{{else}}...{{/if}}', description: 'Conditional with else' },
|
||||||
];
|
];
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue