you can use obsidian query language plugin with templater as well
what you need:
- the two plugins mentioned
optionally installed:
- force note view mode plugin (needed for table to be seen in live preview)
- meta bind plugin (needed to fire script with button)
so the drill:
- create an empty file
with contents:
---
obsidianUIMode: source
obsidianEditingMode: live
---
```meta-bind-button
style: primary
label: Press to fire OQL query
id: templater-obsidian-OQL-query
action:
type: command
command: templater-obsidian:<path to and name of templater file you save the script below>.md
```
---
-
for example:
command: templater-obsidian:templater/OQL query to print results to end of current file.md
- no quotes are needed as you see
-
if you set up the empty file with these contents, with each subsequent query, the table results from last query will be automatically deleted so no need to CTRL+Z in here
you create and save the following script to filename given in the command above:
<%*
// Ensure that the 'obsidian-query-language' plugin is available
if ('obsidian-query-language' in this.app.plugins.plugins) {
const oql = this.app.plugins.plugins['obsidian-query-language'];
// Get user input for the query
const userQuery = await tp.system.prompt("Enter search terms");
if (!userQuery) return;
// Convert the query into extended search format
const extendedQuery = userQuery.split(' ').map(word => `'${word}`).join(' ');
// Setup and execute search with extended search enabled
const options = { useExtendedSearch: true };
const searchResults = await oql.search(extendedQuery, options);
// Filter away files starting with the query
const filteredList = searchResults
.filter(t => !t.path.split("/").pop().startsWith(extendedQuery.split(' ')[0].substring(1)))
.map(t => t.path);
// Define a function to extract a snippet of content with limited context
function extractContentSnippet(content, searchTerm) {
if (!content) return "";
if (searchTerm.includes('_image')) {
const regex = /(!\[\[.+_image[0-9]{1,3}\.(jpg|jpeg|gif|png)(.*?)\]\])/gi;
const matches = [...content.matchAll(regex)];
if (matches.length === 0) return "";
return matches.map(match => match[0]).join(' ');
}
const contextRadius = 120; // Number of characters before and after the match
const regex = new RegExp(searchTerm, 'gi');
const matches = [...content.matchAll(regex)];
if (matches.length === 0) return "";
// Take the first match for simplicity
const match = matches[0];
const matchIndex = match.index;
const start = Math.max(0, matchIndex - contextRadius);
const end = Math.min(content.length, matchIndex + match[0].length + contextRadius);
let snippet = content.slice(start, end);
// Highlight the match if not containing "_image"
if (!searchTerm.includes('_image')) {
snippet = snippet.replace(new RegExp(searchTerm, 'gi'), '==$&==');
}
return snippet;
}
// Process data to get context
const processedData = [];
for (const path of filteredList) {
const content = await this.app.vault.read(this.app.vault.getAbstractFileByPath(path));
const paragraphs = content.split(/\n\s*\n/);
const fileName = `[[${path.split('/').pop().replace('.md', '')}]]`;
const snippets = paragraphs
.filter(paragraph => new RegExp(userQuery, 'i').test(paragraph))
.map(paragraph => extractContentSnippet(paragraph, userQuery));
if (snippets.length > 0) {
processedData.push({
link: fileName,
content: snippets.join('<br><br>') // Separate each processed match with two line breaks
});
}
}
// Construct a markdown table
let markdownTable = `| Source File | Context |\n| --- | --- |\n`;
processedData.forEach(p => {
markdownTable += `| ${p.link} | ${p.content.replace(/\n/g, ' ').replace(/\|/g, '\\|')} |\n`;
});
const currentFile = app.workspace.getActiveFile();
if (currentFile) {
const currentFileName = currentFile.name;
if (userQuery === "_image") {
// Limit search to the current file and append results
const content = await this.app.vault.read(currentFile);
const paragraphs = content.split(/\n\s*\n/);
const fileName = `[[${currentFileName.replace('.md', '')}]]`;
const snippets = paragraphs
.filter(paragraph => new RegExp(userQuery, 'i').test(paragraph))
.map(paragraph => extractContentSnippet(paragraph, userQuery));
let markdownTableImage = `| Source File | Context |\n| --- | --- |\n`;
if (snippets.length > 0) {
markdownTableImage += `| ${fileName} | ${snippets.join('<br><br>').replace(/\n/g, ' ').replace(/\|/g, '\\|')} |\n`;
}
// Switch to Live Preview if user fired script in Source Mode to able to see the generated table
const view = app.workspace.activeLeaf.getViewState()
view.state.mode = 'source'
view.state.source = false
app.workspace.activeLeaf.setViewState(view)
// Append to the current file without deleting existing content
await this.app.vault.modify(currentFile, content + "\n\n\n" + markdownTableImage);
// Take me to the generated table -- Move the cursor to the last line
this.app.workspace.activeLeaf.view.editor.setCursor({ line: 99999, ch: 0 });
} else {
// Append the markdown table to the current note
let content = await this.app.vault.read(currentFile);
// Remove everything after the third "---\n\n"
content = content.replace(/(---[\s\S]*?[.]md\n[`]{3}\n\n)(---\n\n\n)([\s\S]*)$/, '$1$2');
await this.app.vault.modify(currentFile, content + "\n" + markdownTable);
}
}
// Notify the user that the table has been appended
new Notice(`Table of files containing "${userQuery}" has been added to the end of the current note.`);
} else {
new Notice('The obsidian-query-language plugin is not installed and enabled.');
}
_%>
multi-string search is supported (e.g. chicken crossed the road
) and you can use OR operator like so: string1|string2
for either/or search functionality
using this script is beneficial for following reasons:
- instant! results
- copiable content from table
context radius is set to 120 currently – you can look for this number and increase it to give more context where search term was found
- personally, I don’t like long paragraphs printed into the table
table results come in the order of filename contains first and fuzzy matched equivalent of filename(s) first
with large vaults OQL can take a while to initialize on startup but may be quicker than e.g. omnisearch plugin
if script throws some error, go to command palette and rebuild OQL search index
the script currently does way more for my own use case: it can print images into table of current (any) markdown file – if your image embed references differ from mine, you cannot use this of course
- my images are like this in all my files:
parentmarkdownfilename_image1,png
,parentmarkdownfilename_image99.jpg
, etc.
disadvantage compared to the script using dataview linked above: the OQL plugin uses fuse.js which cannot be used with regex search…so no way to match ranges as you can do with regex, such as string1.*?string2
or string1[\s\S]*?string2
… sigh
advantage over that script: can be used with large vaults on mobile as well as dataview may not work on mobile…and: results are instant (only the table building will take some time)
p.s.
the new chatgpt4o model is pretty handy with templater scripts and with proper explanatory prompts you can tailor script to handle your own images and other needs
have fun, guys
credits to original dataview script by @holroy: