Possible to copy paste search results beyond just links?

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]*?string2sigh

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 :slight_smile:



credits to original dataview script by @holroy: