Regex Search And/Or Replace in Current File (Extended)

The script can be extended with some built-in replacements (two of them are included within) and other functionality:

<%*
// Switch to Live Preview
const view = app.workspace.activeLeaf.getViewState()
view.state.mode = 'source'
view.state.source = false
app.workspace.activeLeaf.setViewState(view)

// Get the current file
const currentFile = app.workspace.getActiveFile();
if (!currentFile) return;

// Read the content of the current file
const fileContent = await app.vault.read(currentFile);
const separator = '---';
const firstSeparatorPos = fileContent.indexOf(separator);
const secondSeparatorPos = fileContent.indexOf(separator, firstSeparatorPos + 1);

// Define regex pattern for removing footnote references
const removeFootnoteRefsPattern = /\[\^[0-9]{1,3}\]/gm;

// Function to apply cleanup rules to text
function applyRules(text) {
    const rules = [
        {
            from: /\s+$/gm,
            to: "",
        },
        {
            from: /(\r\n)+|\r+|\n+/gm,
            to: " ",
        },
        {
            from: /([a-zžáàäæãééíóöőüűčñßðđŋħjĸłß])(-\s{1})([a-zžáàäæãééíóöőüűčñßðđŋħjĸłß])/gm,
            to: "$1$3",
        }
    ];
    for (const rule of rules) {
        text = text.replace(rule.from, rule.to);
    }
    return text;
}

// Define Obsidian current leaf editor and current selection
const editor = app.workspace.activeLeaf.view.editor;
const selection = editor.getSelection();

// Function to perform replacements on selected text
const performReplacementsOnSelection = async (pattern, replacement) => {
    // Get the selected text from the editor
    const selectedText = editor.getSelection();
    if (pattern.test(selectedText)) {
        // Include 'g' and 'm' flags for global and multiline matching
        const regex = new RegExp(pattern.source, 'gm');
        // Perform replacements on the selected text
        const updatedText = selectedText.replace(regex, replacement.replace(/\\n/g, '\n'));
        // Replace the selected text in the editor with the modified text
        await editor.replaceSelection(updatedText);
    }
};

// Ask user whether to search, replace, or use built-in replacements
const userChoice = await tp.system.suggester(
    ["Search Current File", "Replace in Current File", "Built-in Replacements to Be Performed in Current File", "DataView Full-vault Query for Content by Dynamic Search Term"],
    ["Search Current File", "Replace in Current File", "Built-in Replacements to Be Performed in Current File", "DataView Full-vault Query for Content by Dynamic Search Term"],
    false,
    "Choose an option:"
);
if (!userChoice) return;

if (userChoice === "Replace in Current File") {
    // Ask user for a search pattern – regex pattern is accepted
    const userPattern = await tp.system.prompt("Enter search pattern:");
    if (!userPattern) return;

    // Ask user for a replacement pattern
    const userReplace = await tp.system.prompt("Enter replacement pattern (or leave empty for zero replacement):");
    if (userReplace === undefined) return;

    // Perform search and replace on selected text or entire file
    if (selection.length > 0) {
        await performReplacementsOnSelection(new RegExp(userPattern, 'gm'), userReplace);
    } else {
        const updatedContent = fileContent.replace(new RegExp(userPattern, 'gm'), userReplace.replace(/\\n/g, '\n'));
        await app.vault.modify(currentFile, updatedContent);
    }

    // Update date modified if YAML front matter exists
    const yamlRegex = /^---\s*\n[\s\S]*?\n---/;
    const hasYaml = yamlRegex.test(fileContent);
    if (hasYaml) {
        const modDateTime = await tp.date.now("YYYY-MM-DDTHH:mm");
        await app.fileManager.processFrontMatter(tp.config.target_file, frontmatter => {
            frontmatter['date modified'] = modDateTime;
        });
    }
} else if (userChoice === "Built-in Replacements to Be Performed in Current File") {
    // Ask user to select a replacement to be done on the current file
    const builtinReplacementChoice = await tp.system.suggester(
        ["Remove all footnote references (globally or on selection)", "Clean up pasted text (globally or on selection)", "Remove InlineQueries"],
        ["Remove all footnote references (globally or on selection)", "Clean up pasted text (globally or on selection)", "Remove InlineQueries"],
        false,
        "Select which to perform on current file (globally or on selected text):"
    );

    if (builtinReplacementChoice === "Remove InlineQueries") {
        // Include the separate Templater template file
        await tp.file.include("[[Remove InLineQueries Template]]");
    } else if (builtinReplacementChoice === "Remove all footnote references (globally or on selection)") {
        // Perform regex replacement for removing all footnote references
        if (selection.length > 0) {
            await performReplacementsOnSelection(removeFootnoteRefsPattern, '');
        } else {
            const updatedContent = fileContent.replace(removeFootnoteRefsPattern, '');
            await app.vault.modify(currentFile, updatedContent);
        }
    } else if (builtinReplacementChoice === "Clean up pasted text (globally or on selection)") {
        // Perform cleanup of pasted text rules
        if (selection.length > 0) {
            const modifiedText = applyRules(selection).replace(/\\n/g, '\n');
            editor.replaceSelection(modifiedText);
        } else {
            let updatedContent = fileContent;
            // Applying cleanup rules globally
            updatedContent = applyRules(updatedContent);
            await app.vault.modify(currentFile, updatedContent);
        }
    }
} else if (userChoice === "DataView Full-vault Query for Content by Dynamic Search Term") {
    // Include the separate Templater template file
    await tp.file.include("[[Query Paragraphs Based on Searchterm with User Interaction Template]]");
} else if (userChoice === "Search Current File") {
    // Ask user for a search pattern – regex pattern is accepted
    const userPattern = await tp.system.prompt("Enter search pattern:");
    if (!userPattern) return;

    // Prepare dynamic content with the user's regex pattern
    const dynamicContent = `
\%\%
\`\`\`query
file: /^${currentFile.basename}.md/ /${userPattern}/
\`\`\`
\%\%`;

    // Ensure an extra empty line after the YAML separator for search
    const updatedContent = fileContent.slice(0, secondSeparatorPos + separator.length) +
        `\n${dynamicContent}` +
        fileContent.slice(secondSeparatorPos + separator.length);

    // Write back changes
    await app.vault.modify(currentFile, updatedContent);

    // Set the cursor to the beginning of the query
    this.app.workspace.activeLeaf.view.editor.setCursor({ line: 1, ch: 0 });
} else if (userChoice === "Go to specific line") {
	await tp.file.include("[[Go to Specific Line Number Template]]");
}
_%>

From the built-in replacements, one can remove footnote references (handy when one’s pasting in Wikipedia passages) and can clean up pasted text talked about here.
Script supports full note replacements but more importantly, if you have selected text before launching the script, the replacements will only be done on the selected text. Thus we can make sure we don’t remove our own footnote references or we don’t make the mistake of merging our YAML with the rest of the note. You can try what it does and CTRL+Z to undo changes.

The inline query we pasted (or it can be a DV query anywhere in the file) is easily removed with the following (make sure you save this as Remove InLineQueries Template.md):

<%*
(async () => {
    const currentFile = app.workspace.getActiveFile();
    if (!currentFile) return;
    let updatedContent;

    await this.app.workspace.activeLeaf.view.editor.setCursor({line: 1, ch: 0});
    const pattern = /^(?:%%\n)?```(?:query|dataview)[\s\S]*?```\n?(?:%%\n)?(?:\s*\n)*\s*/gm;

    const fileContent = await app.vault.read(currentFile);
    
    if (pattern.test(fileContent)) {
        updatedContent = fileContent.replace(pattern, "");
        await app.vault.modify(currentFile, updatedContent);
    }
})();
-%>

I also referenced a full-vault/unique folder Dataview search I created here. This is also not included in the main script. You need to save this script as Query Paragraphs Based on Searchterm with User Interaction Template.md again in your Templater folder.

  • Again, we seamlessly call/switch to that script with await tp.file.include("[[Query Paragraphs Based on Searchterm with User Interaction Template]]"); as if it was part of the above script.
  • You can save both scripts with more descriptive filenames and then use them in above commands to make sure they get called.

All this is (just tinkering but) useful when one has many Templater scripts and can no longer remember the hotkeys or don’t want to put dozens of icons here and there on the workspace or mobile toolbar.

The above can be further extended rather easily so one can add more items one can call with the command I mentioned.

Make sure you launch the script with one markdown file currently open and active, otherwise Templater won’t budge.

EDIT. I added a missing script for removing inline queries.

EDIT 2-3.
10-03-2024: Fixed a bug where replacement side was not accepting \n for new line character.

By the way, the Date-Time format can be changed to your own preferred format.
The use of the word ‘globally’ in the pop-up denotes whole file of course, not full-vault, as we’re dealing with current file when doing replacements.

1 Like