Copy block link, embed, footnote or URL with Templater

Sharing a few Templater templates for copying links to a block.

Due to the post length limit, the templates reside in the comments below.

Full disclosure

I made these scripts with Claude for my own use. What matters is that they work, and they work well. I put them through hell in my rigorous stress-testing. After many debug sessions, the end result is extremely sound. I find them to be more reliable than all the plugin solutions I’ve tried.

How to use

  1. Ensure your vault is backed up.
    • Obviously, this code wasn’t initially audited by the Obsidian team, unlike a dedicated community plugin. I’m not liable for any potential data loss.
  2. Add the script you need to your template folder.
  3. Select text or place cursor in a single block and run the template.

How to run these templates from the right-click menu

With Commander, it’s possible to run these templates from the right-click menu:

A couple notes on script logic

  1. The scripts use block IDs to link to headings. This is preferable because such links survive heading renames, unlike Note#Heading links.
  2. When generating standalone (i.e. on its own line) block IDs, a script may insert a blank line before and/or after the line with standalone block ID. This is done because a standalone block ID is only valid if the line after it is empty. The empty line inserted before the standalone block ID is decorative and matches the default Obsidian behavior when generating block IDs via the auto-suggester.
    • If this mistakenly results in multiple subsequent blank lines, Linter’s Consecutive blank lines rule can help remove them automatically.
  3. Code improvements welcome.

This should really be a plugin

It’d be best if these templates were implemented in a plugin or better yet, as a core feature. If you’re a dev, feel free to use any of the code provided here in your plugin.

See also

To add similar commands for whole notes, see here.

Part of the code is based on Copy Block Link.

1 Like

Copy link to block


Outputs: [[filename#^block-id]]

Templater script

Copy_Link_to_Block.md (11.1 KB)

<%* 
/**
 * Template Name: Copy Link to Block
 * Description: Extracts or generates a block ID from cursor position or selection and copies a reference as wikilink.
 * Version: 2.2
 * Author: Created via Claude
 * Source: https://forum.obsidian.md/t/copy-block-link-embed-footnote-or-url-with-templater/92600/1
 * Last Updated: 2025-01-17
 */

if (app.workspace.activeEditor.getMode() === "preview") {
    new Notice("Reading view isn't supported");
    return;
}
const editor = tp.app.workspace.getActiveViewOfType(tp.obsidian.MarkdownView).editor;
const selections = editor.listSelections();
if (selections.length > 1) {
    new Notice("Multiple selections or cursors aren't supported");
    tR = editor.getSelection();
    return;
}
function generateId() {
    return Math.random().toString(36).substr(2, 6);
}
function shouldInsertAfter(block) {
    if (block.type) {
        return ["blockquote","code","table","comment","footnoteDefinition"].includes(block.type);
    }
    return block.heading !== undefined;
}
function isValidBlockId(id) {
    return /^[a-zA-Z0-9-]+$/.test(id);
}
function findNearestNonEmptyLineAbove(editor, currentLine) {
    for (let i = currentLine - 1; i >= 0; i--) {
        const lineContent = editor.getLine(i).trim();
        if (lineContent !== '') {
            const isBlockId = lineContent.match(/^\^[a-zA-Z0-9-]+$/);
            if (!isBlockId) return i;
        }
    }
    return -1;
}
function findStandaloneBlockId(startLine, editor) {
    for (let i = startLine; i < editor.lineCount(); i++) {
        const line = editor.getLine(i).trim();
        if (line === '') continue;
        const match = line.match(/^\^([a-zA-Z0-9-]+)$/);
        if (match) return match[1];
        break;
    }
    return null;
}
function getInlineBlockId(line) {
    const match = line.match(/\s\^([a-zA-Z0-9-]+)$/);
    return match ? match[1] : null;
}
function getBlock(editor, fileCache) {
    const cursor = editor.getCursor("to");
    const cursorLine = cursor.line;
    const currentLineContent = editor.getLine(cursorLine).trim();
    const currentSection = fileCache?.sections?.find(section =>
        section.position.start.line <= cursorLine &&
        section.position.end.line >= cursorLine
    );
    if (currentSection?.type === "table") {
        const blockId = findStandaloneBlockId(currentSection.position.end.line + 1, editor);
        return {
            ...currentSection,
            id: blockId,
            type: blockId ? 'text-with-standalone-id' : 'table'
        };
    }
    if (currentLineContent.match(/^\^[a-zA-Z0-9-]+$/)) {
        const blockLine = findNearestNonEmptyLineAbove(editor, cursorLine);
        if (blockLine === -1) return null;
        const section = fileCache?.sections?.find(section => 
            section.type === "list" && section.position.start.line <= blockLine && 
            section.position.end.line >= blockLine
        );
        if (section?.type === "list") {
            return {
                ...section,
                id: currentLineContent.substring(1),
                type: 'text-with-standalone-id'
            };
        }
        return {
            position: {
                start: { line: blockLine, ch: 0 },
                end: { line: blockLine, ch: editor.getLine(blockLine).length }
            },
            type: 'text-with-standalone-id',
            id: currentLineContent.substring(1)
        };
    }
    if (currentSection?.type === "list") {
        const listItem = fileCache?.listItems?.find(item =>
            item.position.start.line <= cursorLine &&
            item.position.end.line >= cursorLine &&
            currentLineContent === editor.getLine(item.position.start.line).trim()
        );
        if (listItem) {
            const standaloneId = findStandaloneBlockId(listItem.position.end.line + 1, editor);
            if (currentLineContent === '' && standaloneId) {
                return {
                    ...currentSection,
                    id: standaloneId,
                    type: 'text-with-standalone-id'
                };
            }
            return listItem;
        } else {
            const standaloneId = findStandaloneBlockId(currentSection.position.end.line + 1, editor);
            if (standaloneId && currentLineContent === '') {
                return {
                    ...currentSection,
                    id: standaloneId,
                    type: 'text-with-standalone-id'
                };
            }
            return currentSection;
        }
    } else if (currentSection?.type === "heading") {
        const heading = fileCache.headings.find(heading =>
            heading.position.start.line === currentSection.position.start.line
        );
        if (heading) {
            const headingLine = editor.getLine(heading.position.start.line);
            const inlineId = getInlineBlockId(headingLine);
            if (inlineId) {
                return {...heading, id: inlineId, type: 'text-with-standalone-id'};
            }
            const standaloneId = findStandaloneBlockId(heading.position.start.line + 1, editor);
            if (standaloneId) {
                return {...heading, id: standaloneId, type: 'text-with-standalone-id'};
            }
            return heading;
        }
    } else if (currentSection) {
        const standaloneId = findStandaloneBlockId(currentSection.position.end.line + 1, editor);
        if (standaloneId) {
            return {
                ...currentSection,
                id: standaloneId,
                type: 'text-with-standalone-id'
            };
        }
        return currentSection;
    }
    const prevLine = cursorLine - 1;
    if (prevLine >= 0) {
        const prevSection = fileCache?.sections?.find(section => 
            section.type === "list" && section.position.end.line === prevLine
        );
        if (prevSection) {
            const standaloneId = findStandaloneBlockId(cursorLine + 1, editor);
            if (standaloneId && currentLineContent === '') {
                return {
                    ...prevSection,
                    id: standaloneId,
                    type: 'text-with-standalone-id'
                };
            }
            if (currentLineContent === '') {
                return {
                    position: {
                        start: { line: cursorLine, ch: 0 },
                        end: { line: cursorLine, ch: 0 }
                    },
                    type: 'empty-after-list'
                };
            }
        }
    }
    if (currentLineContent !== '') {
        const standaloneId = findStandaloneBlockId(cursorLine + 1, editor);
        return {
            position: {
                start: { line: cursorLine, ch: 0 },
                end: { line: cursorLine, ch: editor.getLine(cursorLine).length }
            },
            type: standaloneId ? 'text-with-standalone-id' : 'text',
            id: standaloneId
        };
    }
    return null;
}
function checkSelectionSpansBlocks(from, to, fileCache, editor) {
    const sections = fileCache?.sections || [];
    const blockquote = sections.find(section => 
        section.type === "blockquote" &&
        section.position.start.line <= from.line &&
        section.position.end.line >= to.line
    );
    if (blockquote) return false;
    const listItems = fileCache?.listItems || [];
    const selectedItems = listItems.filter(item => {
        const itemStart = item.position.start.line;
        let itemEnd = item.position.end.line;
        if (findStandaloneBlockId(itemEnd + 1, editor)) {
            itemEnd += 2;
        }
        return itemStart >= from.line && itemEnd <= to.line;
    });
    if (selectedItems.length > 1) {
        const firstItemLine = selectedItems[0].position.start.line;
        const section = sections.find(section => 
            section.type === "list" && 
            section.position.start.line <= firstItemLine && 
            section.position.end.line >= firstItemLine
        );
        if (section && selectedItems.every(item => 
            item.position.start.line >= section.position.start.line && 
            item.position.end.line <= section.position.end.line
        )) {
            return false;
        }
        return true;
    }
    const selectedSections = sections.filter(section => {
        const sectionStart = section.position.start.line;
        let sectionEnd = section.position.end.line;
        if (findStandaloneBlockId(sectionEnd + 1, editor)) {
            sectionEnd += 2;
        }
        return (sectionStart >= from.line && sectionStart <= to.line) || 
               (sectionEnd >= from.line && sectionEnd <= to.line) ||
               (sectionStart <= from.line && sectionEnd >= to.line);
    });
    return selectedSections.length > 1;
}
async function handleBlock(file, editor, block) {
    let blockId;
    if (block.type === 'text-with-standalone-id' || block.id) {
        blockId = block.id;
    } else {
        blockId = generateId();
        const currentLine = block.position.end.line;
        if (block.type === 'empty-after-list') {
            await editor.replaceRange("\n", {line: currentLine - 1, ch: editor.getLine(currentLine - 1).length});
            const nextLineEmpty = currentLine + 2 < editor.lineCount() && editor.getLine(currentLine + 2).trim() === '';
            await editor.replaceRange(`^${blockId}${nextLineEmpty ? '' : '\n'}`, {line: currentLine + 1, ch: 0});
        } else if (shouldInsertAfter(block)) {
            const nextLineEmpty = currentLine + 1 < editor.lineCount() && editor.getLine(currentLine + 1).trim() === '';
            if (!nextLineEmpty) {
                await editor.replaceRange("\n\n", {line: currentLine, ch: editor.getLine(currentLine).length});
                await editor.replaceRange(`^${blockId}\n`, {line: currentLine + 2, ch: 0});
            } else {
                await editor.replaceRange("\n", {line: currentLine + 1, ch: 0});
                await editor.replaceRange(`^${blockId}\n`, {line: currentLine + 2, ch: 0});
            }
        } else if (block.position.end.line > block.position.start.line) {
            const lastLineContent = editor.getLine(currentLine);
            await editor.replaceRange(` ^${blockId}`, {
                line: currentLine,
                ch: lastLineContent.length
            });
        } else {
            await editor.replaceRange(` ^${blockId}`, {
                line: currentLine,
                ch: block.position.end.col || editor.getLine(currentLine).length
            });
        }
    }
    return `[[${file.basename}#^${blockId}]]`;
}
const view = tp.app.workspace.getActiveViewOfType(tp.obsidian.MarkdownView);
if (!view) return;
const selection = editor.getSelection();
if (selection && checkSelectionSpansBlocks(editor.getCursor('from'), editor.getCursor('to'), tp.app.metadataCache.getFileCache(view.file), editor)) {
    new Notice("Selections spanning multiple blocks aren't supported");
    tR = selection;
    return;
}
const block = getBlock(editor, tp.app.metadataCache.getFileCache(view.file));
if (!block) {
    tR = selection;
    return;
}
const result = await handleBlock(view.file, editor, block);
if (result) {
    await navigator.clipboard.writeText(result);
    new Notice("Copied to your clipboard");
}
tR = selection;
%>

Alternative solutions

Copy Block Link and Block Link Plus add commands to copy link to block. Unlike the script, Block Link Plus can copy links to multiple blocks simultaneously. The advantage they both have over the script is that they can be run in page preview edit mode. (Though this can be circumvented if using the Hover Editor plugin.)

Copy block embed


Outputs: ![[filename#^block-id]]

Templater script

Copy_Block_Embed.md (11.2 KB)

<%* 
/**
 * Template Name: Copy Block Embed
 * Description: Extracts or generates a block ID from cursor position or selection and copies a reference as wikilink embed.
 * Version: 2.2
 * Author: Created via Claude
 * Source: https://forum.obsidian.md/t/copy-block-link-embed-footnote-or-url-with-templater/92600/2
 * Last Updated: 2025-01-17
 */

if (app.workspace.activeEditor.getMode() === "preview") {
    new Notice("Reading view isn't supported");
    return;
}

const editor = tp.app.workspace.getActiveViewOfType(tp.obsidian.MarkdownView).editor;
const selections = editor.listSelections();

if (selections.length > 1) {
    new Notice("Multiple selections or cursors aren't supported");
    tR = editor.getSelection();
    return;
}

function generateId() {
    return Math.random().toString(36).substr(2, 6);
}

function shouldInsertAfter(block) {
    if (block.type) {
        return [
            "blockquote",
            "code",
            "table",
            "comment",
            "footnoteDefinition",
        ].includes(block.type);
    }
    return block.heading !== undefined;
}

function isValidBlockId(id) {
    return /^[a-zA-Z0-9-]+$/.test(id);
}

function findNearestNonEmptyLineAbove(editor, currentLine) {
    for (let i = currentLine - 1; i >= 0; i--) {
        const lineContent = editor.getLine(i).trim();
        if (lineContent !== '') {
            const isBlockId = lineContent.match(/^\^[a-zA-Z0-9-]+$/);
            if (!isBlockId) return i;
        }
    }
    return -1;
}

function findStandaloneBlockId(startLine, editor) {
    for (let i = startLine; i < editor.lineCount(); i++) {
        const line = editor.getLine(i).trim();
        if (line === '') continue;
        const match = line.match(/^\^([a-zA-Z0-9-]+)$/);
        if (match) return match[1];
        break;
    }
    return null;
}

function getInlineBlockId(line) {
    const match = line.match(/\s\^([a-zA-Z0-9-]+)$/);
    return match ? match[1] : null;
}

function getBlock(editor, fileCache) {
    const cursor = editor.getCursor("to");
    const cursorLine = cursor.line;
    const currentLineContent = editor.getLine(cursorLine).trim();

    const currentSection = fileCache?.sections?.find(section =>
        section.position.start.line <= cursorLine &&
        section.position.end.line >= cursorLine
    );

    if (currentSection?.type === "table") {
        const blockId = findStandaloneBlockId(currentSection.position.end.line + 1, editor);
        return {
            ...currentSection,
            id: blockId,
            type: blockId ? 'text-with-standalone-id' : 'table'
        };
    }

    if (currentLineContent.match(/^\^[a-zA-Z0-9-]+$/)) {
        const blockLine = findNearestNonEmptyLineAbove(editor, cursorLine);
        if (blockLine === -1) return null;
        
        const section = fileCache?.sections?.find(section => 
            section.position.start.line <= blockLine && 
            section.position.end.line >= blockLine
        );

        if (section?.type === "list") {
            return {
                ...section,
                id: currentLineContent.substring(1),
                type: 'text-with-standalone-id'
            };
        }

        return {
            position: {
                start: { line: blockLine, ch: 0 },
                end: { line: blockLine, ch: editor.getLine(blockLine).length }
            },
            type: 'text-with-standalone-id',
            id: currentLineContent.substring(1)
        };
    }

    if (currentSection?.type === "list") {
        const listItem = fileCache?.listItems?.find(item =>
            item.position.start.line <= cursorLine &&
            item.position.end.line >= cursorLine &&
            currentLineContent === editor.getLine(item.position.start.line).trim()
        );

        if (listItem) {
            const standaloneId = findStandaloneBlockId(listItem.position.end.line + 1, editor);
            if (currentLineContent === '' && standaloneId) {
                return {
                    ...currentSection,
                    id: standaloneId,
                    type: 'text-with-standalone-id'
                };
            }
            return listItem;
        } else {
            const standaloneId = findStandaloneBlockId(currentSection.position.end.line + 1, editor);
            if (standaloneId && currentLineContent === '') {
                return {
                    ...currentSection,
                    id: standaloneId,
                    type: 'text-with-standalone-id'
                };
            }
            return currentSection;
        }
    } else if (currentSection?.type === "heading") {
        const heading = fileCache.headings.find(heading =>
            heading.position.start.line === currentSection.position.start.line
        );
        if (heading) {
            const headingLine = editor.getLine(heading.position.start.line);
            const inlineId = getInlineBlockId(headingLine);
            if (inlineId) {
                return {...heading, id: inlineId, type: 'text-with-standalone-id'};
            }
            const standaloneId = findStandaloneBlockId(heading.position.start.line + 1, editor);
            if (standaloneId) {
                return {...heading, id: standaloneId, type: 'text-with-standalone-id'};
            }
            return heading;
        }
    } else if (currentSection) {
        const standaloneId = findStandaloneBlockId(currentSection.position.end.line + 1, editor);
        if (standaloneId) {
            return {
                ...currentSection,
                id: standaloneId,
                type: 'text-with-standalone-id'
            };
        }
        return currentSection;
    }

    const prevLine = cursorLine - 1;
    if (prevLine >= 0) {
        const prevSection = fileCache?.sections?.find(section => 
            section.type === "list" && section.position.end.line === prevLine
        );
        if (prevSection) {
            const standaloneId = findStandaloneBlockId(cursorLine + 1, editor);
            if (standaloneId && currentLineContent === '') {
                return {
                    ...prevSection,
                    id: standaloneId,
                    type: 'text-with-standalone-id'
                };
            }
            if (currentLineContent === '') {
                return {
                    position: {
                        start: { line: cursorLine, ch: 0 },
                        end: { line: cursorLine, ch: 0 }
                    },
                    type: 'empty-after-list'
                };
            }
        }
    }

    if (currentLineContent !== '') {
        const standaloneId = findStandaloneBlockId(cursorLine + 1, editor);
        return {
            position: {
                start: { line: cursorLine, ch: 0 },
                end: { line: cursorLine, ch: editor.getLine(cursorLine).length }
            },
            type: standaloneId ? 'text-with-standalone-id' : 'text',
            id: standaloneId
        };
    }

    return null;
}

function checkSelectionSpansBlocks(from, to, fileCache, editor) {
    const sections = fileCache?.sections || [];
    const blockquote = sections.find(section => 
        section.type === "blockquote" &&
        section.position.start.line <= from.line &&
        section.position.end.line >= to.line
    );
    if (blockquote) return false;
    const listItems = fileCache?.listItems || [];
    const selectedItems = listItems.filter(item => {
        const itemStart = item.position.start.line;
        let itemEnd = item.position.end.line;
        if (findStandaloneBlockId(itemEnd + 1, editor)) {
            itemEnd += 2;
        }
        return itemStart >= from.line && itemEnd <= to.line;
    });
    if (selectedItems.length > 1) {
        const firstItemLine = selectedItems[0].position.start.line;
        const section = sections.find(section => 
            section.type === "list" && 
            section.position.start.line <= firstItemLine && 
            section.position.end.line >= firstItemLine
        );
        if (section && selectedItems.every(item => 
            item.position.start.line >= section.position.start.line && 
            item.position.end.line <= section.position.end.line
        )) {
            return false;
        }
        return true;
    }
    const selectedSections = sections.filter(section => {
        const sectionStart = section.position.start.line;
        let sectionEnd = section.position.end.line;
        if (findStandaloneBlockId(sectionEnd + 1, editor)) {
            sectionEnd += 2;
        }
        return (sectionStart >= from.line && sectionStart <= to.line) || 
               (sectionEnd >= from.line && sectionEnd <= to.line) ||
               (sectionStart <= from.line && sectionEnd >= to.line);
    });
    return selectedSections.length > 1;
}
async function handleBlock(file, editor, block) {
    let blockId;
    if (block.type === 'text-with-standalone-id' || block.id) {
        blockId = block.id;
    } else {
        blockId = generateId();
        const currentLine = block.position.end.line;
        if (block.type === 'empty-after-list') {
            await editor.replaceRange("\n", {line: currentLine - 1, ch: editor.getLine(currentLine - 1).length});
            const nextLineEmpty = currentLine + 2 < editor.lineCount() && editor.getLine(currentLine + 2).trim() === '';
            await editor.replaceRange(`^${blockId}${nextLineEmpty ? '' : '\n'}`, {line: currentLine + 1, ch: 0});
        } else if (shouldInsertAfter(block)) {
            const nextLineEmpty = currentLine + 1 < editor.lineCount() && editor.getLine(currentLine + 1).trim() === '';
            if (!nextLineEmpty) {
                await editor.replaceRange("\n\n", {line: currentLine, ch: editor.getLine(currentLine).length});
                await editor.replaceRange(`^${blockId}\n`, {line: currentLine + 2, ch: 0});
            } else {
                await editor.replaceRange("\n", {line: currentLine + 1, ch: 0});
                await editor.replaceRange(`^${blockId}\n`, {line: currentLine + 2, ch: 0});
            }
        } else if (block.position.end.line > block.position.start.line) {
            const lastLineContent = editor.getLine(currentLine);
            await editor.replaceRange(` ^${blockId}`, {
                line: currentLine,
                ch: lastLineContent.length
            });
        } else {
            await editor.replaceRange(` ^${blockId}`, {
                line: currentLine,
                ch: block.position.end.col || editor.getLine(currentLine).length
            });
        }
    }
    return `![[${file.basename}#^${blockId}]]`;
}

const view = tp.app.workspace.getActiveViewOfType(tp.obsidian.MarkdownView);
if (!view) return;

const selection = editor.getSelection();

if (selection && checkSelectionSpansBlocks(editor.getCursor('from'), editor.getCursor('to'), tp.app.metadataCache.getFileCache(view.file), editor)) {
    new Notice("Selections spanning multiple blocks aren't supported");
    tR = selection;
    return;
}

const block = getBlock(editor, tp.app.metadataCache.getFileCache(view.file));
if (!block) {
    tR = selection;
    return;
}

const result = await handleBlock(view.file, editor, block);
if (result) {
    await navigator.clipboard.writeText(result);
    new Notice("Copied to your clipboard");
}
tR = selection;
%>

Alternative solutions

Copy Block Link and Block Link Plus add commands to copy embed to block. Unlike the script, Block Link Plus can copy embeds to multiple blocks simultaneously. The advantage they both have over the script is that they can be run in page preview edit mode. (Though this can be circumvented if using the Hover Editor plugin.)

Copy link to block as footnote


Outputs: ^[[[filename#^block-id]]]

Templater script

Copy_Link_to_Block_as_Footnote.md (11.2 KB)

<%* 
/**
 * Template Name: Copy Link to Block as Footnote
 * Description: Extracts or generates a block ID from cursor position or selection and copies a reference as wikilink in footnote.
 * Version: 2.2
 * Author: Created via Claude
 * Source: https://forum.obsidian.md/t/copy-block-link-embed-footnote-or-url-with-templater/92600/3
 * Last Updated: 2025-01-17
 */

if (app.workspace.activeEditor.getMode() === "preview") {
    new Notice("Reading view isn't supported");
    return;
}

const editor = tp.app.workspace.getActiveViewOfType(tp.obsidian.MarkdownView).editor;
const selections = editor.listSelections();

if (selections.length > 1) {
    new Notice("Multiple selections or cursors aren't supported");
    tR = editor.getSelection();
    return;
}

function generateId() {
    return Math.random().toString(36).substr(2, 6);
}

function shouldInsertAfter(block) {
    if (block.type) {
        return [
            "blockquote",
            "code",
            "table",
            "comment",
            "footnoteDefinition",
        ].includes(block.type);
    }
    return block.heading !== undefined;
}

function isValidBlockId(id) {
    return /^[a-zA-Z0-9-]+$/.test(id);
}

function findNearestNonEmptyLineAbove(editor, currentLine) {
    for (let i = currentLine - 1; i >= 0; i--) {
        const lineContent = editor.getLine(i).trim();
        if (lineContent !== '') {
            const isBlockId = lineContent.match(/^\^[a-zA-Z0-9-]+$/);
            if (!isBlockId) return i;
        }
    }
    return -1;
}

function findStandaloneBlockId(startLine, editor) {
    for (let i = startLine; i < editor.lineCount(); i++) {
        const line = editor.getLine(i).trim();
        if (line === '') continue;
        const match = line.match(/^\^([a-zA-Z0-9-]+)$/);
        if (match) return match[1];
        break;
    }
    return null;
}

function getInlineBlockId(line) {
    const match = line.match(/\s\^([a-zA-Z0-9-]+)$/);
    return match ? match[1] : null;
}

function getBlock(editor, fileCache) {
    const cursor = editor.getCursor("to");
    const cursorLine = cursor.line;
    const currentLineContent = editor.getLine(cursorLine).trim();

    const currentSection = fileCache?.sections?.find(section =>
        section.position.start.line <= cursorLine &&
        section.position.end.line >= cursorLine
    );

    if (currentSection?.type === "table") {
        const blockId = findStandaloneBlockId(currentSection.position.end.line + 1, editor);
        return {
            ...currentSection,
            id: blockId,
            type: blockId ? 'text-with-standalone-id' : 'table'
        };
    }

    if (currentLineContent.match(/^\^[a-zA-Z0-9-]+$/)) {
        const blockLine = findNearestNonEmptyLineAbove(editor, cursorLine);
        if (blockLine === -1) return null;
        
        const section = fileCache?.sections?.find(section => 
            section.position.start.line <= blockLine && 
            section.position.end.line >= blockLine
        );

        if (section?.type === "list") {
            return {
                ...section,
                id: currentLineContent.substring(1),
                type: 'text-with-standalone-id'
            };
        }

        return {
            position: {
                start: { line: blockLine, ch: 0 },
                end: { line: blockLine, ch: editor.getLine(blockLine).length }
            },
            type: 'text-with-standalone-id',
            id: currentLineContent.substring(1)
        };
    }

    if (currentSection?.type === "list") {
        const listItem = fileCache?.listItems?.find(item =>
            item.position.start.line <= cursorLine &&
            item.position.end.line >= cursorLine &&
            currentLineContent === editor.getLine(item.position.start.line).trim()
        );

        if (listItem) {
            const standaloneId = findStandaloneBlockId(listItem.position.end.line + 1, editor);
            if (currentLineContent === '' && standaloneId) {
                return {
                    ...currentSection,
                    id: standaloneId,
                    type: 'text-with-standalone-id'
                };
            }
            return listItem;
        } else {
            const standaloneId = findStandaloneBlockId(currentSection.position.end.line + 1, editor);
            if (standaloneId && currentLineContent === '') {
                return {
                    ...currentSection,
                    id: standaloneId,
                    type: 'text-with-standalone-id'
                };
            }
            return currentSection;
        }
    } else if (currentSection?.type === "heading") {
        const heading = fileCache.headings.find(heading =>
            heading.position.start.line === currentSection.position.start.line
        );
        if (heading) {
            const headingLine = editor.getLine(heading.position.start.line);
            const inlineId = getInlineBlockId(headingLine);
            if (inlineId) {
                return {...heading, id: inlineId, type: 'text-with-standalone-id'};
            }
            const standaloneId = findStandaloneBlockId(heading.position.start.line + 1, editor);
            if (standaloneId) {
                return {...heading, id: standaloneId, type: 'text-with-standalone-id'};
            }
            return heading;
        }
    } else if (currentSection) {
        const standaloneId = findStandaloneBlockId(currentSection.position.end.line + 1, editor);
        if (standaloneId) {
            return {
                ...currentSection,
                id: standaloneId,
                type: 'text-with-standalone-id'
            };
        }
        return currentSection;
    }

    const prevLine = cursorLine - 1;
    if (prevLine >= 0) {
        const prevSection = fileCache?.sections?.find(section => 
            section.type === "list" && section.position.end.line === prevLine
        );
        if (prevSection) {
            const standaloneId = findStandaloneBlockId(cursorLine + 1, editor);
            if (standaloneId && currentLineContent === '') {
                return {
                    ...prevSection,
                    id: standaloneId,
                    type: 'text-with-standalone-id'
                };
            }
            if (currentLineContent === '') {
                return {
                    position: {
                        start: { line: cursorLine, ch: 0 },
                        end: { line: cursorLine, ch: 0 }
                    },
                    type: 'empty-after-list'
                };
            }
        }
    }

    if (currentLineContent !== '') {
        const standaloneId = findStandaloneBlockId(cursorLine + 1, editor);
        return {
            position: {
                start: { line: cursorLine, ch: 0 },
                end: { line: cursorLine, ch: editor.getLine(cursorLine).length }
            },
            type: standaloneId ? 'text-with-standalone-id' : 'text',
            id: standaloneId
        };
    }

    return null;
}

function checkSelectionSpansBlocks(from, to, fileCache, editor) {
    const sections = fileCache?.sections || [];
    const blockquote = sections.find(section => 
        section.type === "blockquote" &&
        section.position.start.line <= from.line &&
        section.position.end.line >= to.line
    );
    if (blockquote) return false;
    const listItems = fileCache?.listItems || [];
    const selectedItems = listItems.filter(item => {
        const itemStart = item.position.start.line;
        let itemEnd = item.position.end.line;
        if (findStandaloneBlockId(itemEnd + 1, editor)) {
            itemEnd += 2;
        }
        return itemStart >= from.line && itemEnd <= to.line;
    });
    if (selectedItems.length > 1) {
        const firstItemLine = selectedItems[0].position.start.line;
        const section = sections.find(section => 
            section.type === "list" && 
            section.position.start.line <= firstItemLine && 
            section.position.end.line >= firstItemLine
        );
        if (section && selectedItems.every(item => 
            item.position.start.line >= section.position.start.line && 
            item.position.end.line <= section.position.end.line
        )) {
            return false;
        }
        return true;
    }
    const selectedSections = sections.filter(section => {
        const sectionStart = section.position.start.line;
        let sectionEnd = section.position.end.line;
        if (findStandaloneBlockId(sectionEnd + 1, editor)) {
            sectionEnd += 2;
        }
        return (sectionStart >= from.line && sectionStart <= to.line) || 
               (sectionEnd >= from.line && sectionEnd <= to.line) ||
               (sectionStart <= from.line && sectionEnd >= to.line);
    });
    return selectedSections.length > 1;
}
async function handleBlock(file, editor, block) {
    let blockId;
    if (block.type === 'text-with-standalone-id' || block.id) {
        blockId = block.id;
    } else {
        blockId = generateId();
        const currentLine = block.position.end.line;
        if (block.type === 'empty-after-list') {
            await editor.replaceRange("\n", {line: currentLine - 1, ch: editor.getLine(currentLine - 1).length});
            const nextLineEmpty = currentLine + 2 < editor.lineCount() && editor.getLine(currentLine + 2).trim() === '';
            await editor.replaceRange(`^${blockId}${nextLineEmpty ? '' : '\n'}`, {line: currentLine + 1, ch: 0});
        } else if (shouldInsertAfter(block)) {
            const nextLineEmpty = currentLine + 1 < editor.lineCount() && editor.getLine(currentLine + 1).trim() === '';
            if (!nextLineEmpty) {
                await editor.replaceRange("\n\n", {line: currentLine, ch: editor.getLine(currentLine).length});
                await editor.replaceRange(`^${blockId}\n`, {line: currentLine + 2, ch: 0});
            } else {
                await editor.replaceRange("\n", {line: currentLine + 1, ch: 0});
                await editor.replaceRange(`^${blockId}\n`, {line: currentLine + 2, ch: 0});
            }
        } else if (block.position.end.line > block.position.start.line) {
            const lastLineContent = editor.getLine(currentLine);
            await editor.replaceRange(` ^${blockId}`, {
                line: currentLine,
                ch: lastLineContent.length
            });
        } else {
            await editor.replaceRange(` ^${blockId}`, {
                line: currentLine,
                ch: block.position.end.col || editor.getLine(currentLine).length
            });
        }
    }
    return `^[[[${file.basename}#^${blockId}]]]`;
}

const view = tp.app.workspace.getActiveViewOfType(tp.obsidian.MarkdownView);
if (!view) return;

const selection = editor.getSelection();

if (selection && checkSelectionSpansBlocks(editor.getCursor('from'), editor.getCursor('to'), tp.app.metadataCache.getFileCache(view.file), editor)) {
    new Notice("Selections spanning multiple blocks aren't supported");
    tR = selection;
    return;
}

const block = getBlock(editor, tp.app.metadataCache.getFileCache(view.file));
if (!block) {
    tR = selection;
    return;
}

const result = await handleBlock(view.file, editor, block);
if (result) {
    await navigator.clipboard.writeText(result);
    new Notice("Copied to your clipboard");
}
tR = selection;
%>

Alternative solutions

These community plugins achieve similar goals:

  1. Carry-Forward
  2. Text Transporter

Copy URL to block


Outputs: obsidian://open?vault=vault&file=filename%23%5Eblock-id

Templater script

Copy_URL_to_Block.md (11.4 KB)

<%* 
/**
 * Template Name: Copy URL to Block
 * Description: Extracts or generates a block ID from cursor position or selection and copies a URL to it.
 * Version: 2.2
 * Author: Created via Claude
 * Source: https://forum.obsidian.md/t/copy-block-link-embed-footnote-or-url-with-templater/92600/4
 * Last Updated: 2025-01-17
 */

if (app.workspace.activeEditor.getMode() === "preview") {
    new Notice("Reading view isn't supported");
    tR = selection;
    return;
}

const editor = tp.app.workspace.getActiveViewOfType(tp.obsidian.MarkdownView).editor;
const selections = editor.listSelections();

if (selections.length > 1) {
    new Notice("Multiple selections or cursors aren't supported");
    tR = editor.getSelection();
    return;
}

function generateId() {
    return Math.random().toString(36).substr(2, 6);
}

function shouldInsertAfter(block) {
    if (block.type) {
        return [
            "blockquote",
            "code",
            "table",
            "comment",
            "footnoteDefinition",
        ].includes(block.type);
    }
    return block.heading !== undefined;
}

function isValidBlockId(id) {
    return /^[a-zA-Z0-9-]+$/.test(id);
}

function findNearestNonEmptyLineAbove(editor, currentLine) {
    for (let i = currentLine - 1; i >= 0; i--) {
        const lineContent = editor.getLine(i).trim();
        if (lineContent !== '') {
            const isBlockId = lineContent.match(/^\^[a-zA-Z0-9-]+$/);
            if (!isBlockId) return i;
        }
    }
    return -1;
}

function findStandaloneBlockId(startLine, editor) {
    for (let i = startLine; i < editor.lineCount(); i++) {
        const line = editor.getLine(i).trim();
        if (line === '') continue;
        const match = line.match(/^\^([a-zA-Z0-9-]+)$/);
        if (match) return match[1];
        break;
    }
    return null;
}

function getInlineBlockId(line) {
    const match = line.match(/\s\^([a-zA-Z0-9-]+)$/);
    return match ? match[1] : null;
}

function getBlock(editor, fileCache) {
    const cursor = editor.getCursor("to");
    const cursorLine = cursor.line;
    const currentLineContent = editor.getLine(cursorLine).trim();

    const currentSection = fileCache?.sections?.find(section =>
        section.position.start.line <= cursorLine &&
        section.position.end.line >= cursorLine
    );

    if (currentSection?.type === "table") {
        const blockId = findStandaloneBlockId(currentSection.position.end.line + 1, editor);
        return {
            ...currentSection,
            id: blockId,
            type: blockId ? 'text-with-standalone-id' : 'table'
        };
    }

    if (currentLineContent.match(/^\^[a-zA-Z0-9-]+$/)) {
        const blockLine = findNearestNonEmptyLineAbove(editor, cursorLine);
        if (blockLine === -1) return null;
        
        const section = fileCache?.sections?.find(section => 
            section.position.start.line <= blockLine && 
            section.position.end.line >= blockLine
        );

        if (section?.type === "list") {
            return {
                ...section,
                id: currentLineContent.substring(1),
                type: 'text-with-standalone-id'
            };
        }

        return {
            position: {
                start: { line: blockLine, ch: 0 },
                end: { line: blockLine, ch: editor.getLine(blockLine).length }
            },
            type: 'text-with-standalone-id',
            id: currentLineContent.substring(1)
        };
    }

    if (currentSection?.type === "list") {
        const listItem = fileCache?.listItems?.find(item =>
            item.position.start.line <= cursorLine &&
            item.position.end.line >= cursorLine &&
            currentLineContent === editor.getLine(item.position.start.line).trim()
        );

        if (listItem) {
            const standaloneId = findStandaloneBlockId(listItem.position.end.line + 1, editor);
            if (currentLineContent === '' && standaloneId) {
                return {
                    ...currentSection,
                    id: standaloneId,
                    type: 'text-with-standalone-id'
                };
            }
            return listItem;
        } else {
            const standaloneId = findStandaloneBlockId(currentSection.position.end.line + 1, editor);
            if (standaloneId && currentLineContent === '') {
                return {
                    ...currentSection,
                    id: standaloneId,
                    type: 'text-with-standalone-id'
                };
            }
            return currentSection;
        }
    } else if (currentSection?.type === "heading") {
        const heading = fileCache.headings.find(heading =>
            heading.position.start.line === currentSection.position.start.line
        );
        if (heading) {
            const headingLine = editor.getLine(heading.position.start.line);
            const inlineId = getInlineBlockId(headingLine);
            if (inlineId) {
                return {...heading, id: inlineId, type: 'text-with-standalone-id'};
            }
            const standaloneId = findStandaloneBlockId(heading.position.start.line + 1, editor);
            if (standaloneId) {
                return {...heading, id: standaloneId, type: 'text-with-standalone-id'};
            }
            return heading;
        }
    } else if (currentSection) {
        const standaloneId = findStandaloneBlockId(currentSection.position.end.line + 1, editor);
        if (standaloneId) {
            return {
                ...currentSection,
                id: standaloneId,
                type: 'text-with-standalone-id'
            };
        }
        return currentSection;
    }

    const prevLine = cursorLine - 1;
    if (prevLine >= 0) {
        const prevSection = fileCache?.sections?.find(section => 
            section.type === "list" && section.position.end.line === prevLine
        );
        if (prevSection) {
            const standaloneId = findStandaloneBlockId(cursorLine + 1, editor);
            if (standaloneId && currentLineContent === '') {
                return {
                    ...prevSection,
                    id: standaloneId,
                    type: 'text-with-standalone-id'
                };
            }
            if (currentLineContent === '') {
                return {
                    position: {
                        start: { line: cursorLine, ch: 0 },
                        end: { line: cursorLine, ch: 0 }
                    },
                    type: 'empty-after-list'
                };
            }
        }
    }

    if (currentLineContent !== '') {
        const standaloneId = findStandaloneBlockId(cursorLine + 1, editor);
        return {
            position: {
                start: { line: cursorLine, ch: 0 },
                end: { line: cursorLine, ch: editor.getLine(cursorLine).length }
            },
            type: standaloneId ? 'text-with-standalone-id' : 'text',
            id: standaloneId
        };
    }

    return null;
}

function checkSelectionSpansBlocks(from, to, fileCache, editor) {
    const sections = fileCache?.sections || [];
    const blockquote = sections.find(section => 
        section.type === "blockquote" &&
        section.position.start.line <= from.line &&
        section.position.end.line >= to.line
    );
    if (blockquote) return false;
    const listItems = fileCache?.listItems || [];
    const selectedItems = listItems.filter(item => {
        const itemStart = item.position.start.line;
        let itemEnd = item.position.end.line;
        if (findStandaloneBlockId(itemEnd + 1, editor)) {
            itemEnd += 2;
        }
        return itemStart >= from.line && itemEnd <= to.line;
    });
    if (selectedItems.length > 1) {
        const firstItemLine = selectedItems[0].position.start.line;
        const section = sections.find(section => 
            section.type === "list" && 
            section.position.start.line <= firstItemLine && 
            section.position.end.line >= firstItemLine
        );
        if (section && selectedItems.every(item => 
            item.position.start.line >= section.position.start.line && 
            item.position.end.line <= section.position.end.line
        )) {
            return false;
        }
        return true;
    }
    const selectedSections = sections.filter(section => {
        const sectionStart = section.position.start.line;
        let sectionEnd = section.position.end.line;
        if (findStandaloneBlockId(sectionEnd + 1, editor)) {
            sectionEnd += 2;
        }
        return (sectionStart >= from.line && sectionStart <= to.line) || 
               (sectionEnd >= from.line && sectionEnd <= to.line) ||
               (sectionStart <= from.line && sectionEnd >= to.line);
    });
    return selectedSections.length > 1;
}
async function handleBlock(file, editor, block) {
    let blockId;
    if (block.type === 'text-with-standalone-id' || block.id) {
        blockId = block.id;
    } else {
        blockId = generateId();
        const currentLine = block.position.end.line;
        if (block.type === 'empty-after-list') {
            await editor.replaceRange("\n", {line: currentLine - 1, ch: editor.getLine(currentLine - 1).length});
            const nextLineEmpty = currentLine + 2 < editor.lineCount() && editor.getLine(currentLine + 2).trim() === '';
            await editor.replaceRange(`^${blockId}${nextLineEmpty ? '' : '\n'}`, {line: currentLine + 1, ch: 0});
        } else if (shouldInsertAfter(block)) {
            const nextLineEmpty = currentLine + 1 < editor.lineCount() && editor.getLine(currentLine + 1).trim() === '';
            if (!nextLineEmpty) {
                await editor.replaceRange("\n\n", {line: currentLine, ch: editor.getLine(currentLine).length});
                await editor.replaceRange(`^${blockId}\n`, {line: currentLine + 2, ch: 0});
            } else {
                await editor.replaceRange("\n", {line: currentLine + 1, ch: 0});
                await editor.replaceRange(`^${blockId}\n`, {line: currentLine + 2, ch: 0});
            }
        } else if (block.position.end.line > block.position.start.line) {
            const lastLineContent = editor.getLine(currentLine);
            await editor.replaceRange(` ^${blockId}`, {
                line: currentLine,
                ch: lastLineContent.length
            });
        } else {
            await editor.replaceRange(` ^${blockId}`, {
                line: currentLine,
                ch: block.position.end.col || editor.getLine(currentLine).length
            });
        }
    }
    const vaultName = app.vault.getName();
    const filePath = file.path.replace('.md', '');
    const encodedVault = encodeURIComponent(vaultName);
    const encodedPath = encodeURIComponent(filePath);
    return `obsidian://open?vault=${encodedVault}&file=${encodedPath}%23%5E${blockId}`;
}

const view = tp.app.workspace.getActiveViewOfType(tp.obsidian.MarkdownView);
if (!view) return;

const selection = editor.getSelection();

if (selection && checkSelectionSpansBlocks(editor.getCursor('from'), editor.getCursor('to'), tp.app.metadataCache.getFileCache(view.file), editor)) {
    new Notice("Selections spanning multiple blocks aren't supported");
    tR = selection;
    return;
}

const block = getBlock(editor, tp.app.metadataCache.getFileCache(view.file));
if (!block) {
    tR = selection;
    return;
}

const result = await handleBlock(view.file, editor, block);
if (result) {
    await navigator.clipboard.writeText(result);
    new Notice("URL copied to your clipboard");
}
tR = selection;
%>
  1. Create a macro in QuickAdd.
  2. As step one, add Obsidian command: “Copy Block Link: Copy link to current block or heading”.
  3. As step two, add user script: Copy_URL_to_block.js.

Copy Block Link cannot generate block IDs for headings, so this only works for non-heading blocks.


Room for improvement

Could create a modified version of the script that would, when run:

  1. Insert 🔗 before the block ID–to mark that the block is linked to outside the vault, and that either the block ID shouldn’t be renamed, or the URL to it should be updated on block ID rename.
  2. Add a task-type property value to frontmatter titled linked to and set its key to true–to mark that either the note shouldn’t be renamed, or the URLs to it should be updated on note rename.
  3. Insert the current timestamp after the emoji in block e.g. Regular block text. 🔗 %%YYYY-MM-DDTHH:mmZ%% ^block-id–to capture when the URL was generated.

Alternative solution

Block Link Plus has added a command to copy URL to block in ver. 1.0.6.

Do note that a URL will break if the vault, note or block ID get renamed. See feature request for permanent URLs.

Copy Link to Block with Selection as Alias


Outputs: [[Filename#^id|ipsum]]

for selection ipsum

in Lorem ipsum ^id.

Templater script

Copy_Link_to_Block_with_Selection_as_Alias.md (10.5 KB)

<%* 
/**
 * Template Name: Copy Link to Block with Selection as Alias
 * Description: Extracts or generates a block ID from cursor position or selection and copies a reference as wikilink, adds alias if text is selected.
 * Version: 2.2
 * Author: Created via Claude
 * Source: https://forum.obsidian.md/t/copy-block-link-embed-footnote-or-url-with-templater/92600/6
 * Last Updated: 2025-01-17
 */

if (app.workspace.activeEditor.getMode() === "preview") {
    new Notice("Reading view isn't supported");
    return;
}

const editor = tp.app.workspace.getActiveViewOfType(tp.obsidian.MarkdownView).editor;
const selections = editor.listSelections();

if (selections.length > 1) {
    new Notice("Multiple selections or cursors aren't supported");
    tR = editor.getSelection();
    return;
}

function generateId() {
    return Math.random().toString(36).substr(2, 6);
}

function shouldInsertAfter(block) {
    if (block.type) {
        return [
            "blockquote",
            "code",
            "table",
            "comment",
            "footnoteDefinition",
        ].includes(block.type);
    }
    return block.heading !== undefined;
}

function isValidBlockId(id) {
    return /^[a-zA-Z0-9-]+$/.test(id);
}

function findNearestNonEmptyLineAbove(editor, currentLine) {
    for (let i = currentLine - 1; i >= 0; i--) {
        const lineContent = editor.getLine(i).trim();
        if (lineContent !== '') {
            const isBlockId = lineContent.match(/^\^[a-zA-Z0-9-]+$/);
            if (!isBlockId) return i;
        }
    }
    return -1;
}

function findStandaloneBlockId(startLine, editor) {
    for (let i = startLine; i < editor.lineCount(); i++) {
        const line = editor.getLine(i).trim();
        if (line === '') continue;
        const match = line.match(/^\^([a-zA-Z0-9-]+)$/);
        if (match) return match[1];
        break;
    }
    return null;
}

function getInlineBlockId(line) {
    const match = line.match(/\s\^([a-zA-Z0-9-]+)$/);
    return match ? match[1] : null;
}

function getBlock(editor, fileCache) {
    const cursor = editor.getCursor("to");
    const cursorLine = cursor.line;
    const currentLineContent = editor.getLine(cursorLine).trim();

    const currentSection = fileCache?.sections?.find(section =>
        section.position.start.line <= cursorLine &&
        section.position.end.line >= cursorLine
    );

    if (currentSection?.type === "table") {
        const blockId = findStandaloneBlockId(currentSection.position.end.line + 1, editor);
        return {
            ...currentSection,
            id: blockId,
            type: blockId ? 'text-with-standalone-id' : 'table'
        };
    }

    if (currentLineContent.match(/^\^[a-zA-Z0-9-]+$/)) {
        const blockLine = findNearestNonEmptyLineAbove(editor, cursorLine);
        if (blockLine === -1) return null;
        
        const section = fileCache?.sections?.find(section => 
            section.position.start.line <= blockLine && 
            section.position.end.line >= blockLine
        );

        if (section?.type === "list") {
            return {
                ...section,
                id: currentLineContent.substring(1),
                type: 'text-with-standalone-id'
            };
        }

        return {
            position: {
                start: { line: blockLine, ch: 0 },
                end: { line: blockLine, ch: editor.getLine(blockLine).length }
            },
            type: 'text-with-standalone-id',
            id: currentLineContent.substring(1)
        };
    }

    if (currentSection?.type === "list") {
        const listItem = fileCache?.listItems?.find(item =>
            item.position.start.line <= cursorLine &&
            item.position.end.line >= cursorLine &&
            currentLineContent === editor.getLine(item.position.start.line).trim()
        );

        if (listItem) {
            const standaloneId = findStandaloneBlockId(listItem.position.end.line + 1, editor);
            if (currentLineContent === '' && standaloneId) {
                return {
                    ...currentSection,
                    id: standaloneId,
                    type: 'text-with-standalone-id'
                };
            }
            return listItem;
        } else {
            const standaloneId = findStandaloneBlockId(currentSection.position.end.line + 1, editor);
            if (standaloneId && currentLineContent === '') {
                return {
                    ...currentSection,
                    id: standaloneId,
                    type: 'text-with-standalone-id'
                };
            }
            return currentSection;
        }
    } else if (currentSection?.type === "heading") {
        const heading = fileCache.headings.find(heading =>
            heading.position.start.line === currentSection.position.start.line
        );
        if (heading) {
            const headingLine = editor.getLine(heading.position.start.line);
            const inlineId = getInlineBlockId(headingLine);
            if (inlineId) {
                return {...heading, id: inlineId, type: 'text-with-standalone-id'};
            }
            const standaloneId = findStandaloneBlockId(heading.position.start.line + 1, editor);
            if (standaloneId) {
                return {...heading, id: standaloneId, type: 'text-with-standalone-id'};
            }
            return heading;
        }
    } else if (currentSection) {
        const standaloneId = findStandaloneBlockId(currentSection.position.end.line + 1, editor);
        if (standaloneId) {
            return {
                ...currentSection,
                id: standaloneId,
                type: 'text-with-standalone-id'
            };
        }
        return currentSection;
    }

    const prevLine = cursorLine - 1;
    if (prevLine >= 0) {
        const prevSection = fileCache?.sections?.find(section => 
            section.type === "list" && section.position.end.line === prevLine
        );
        if (prevSection) {
            const standaloneId = findStandaloneBlockId(cursorLine + 1, editor);
            if (standaloneId && currentLineContent === '') {
                return {
                    ...prevSection,
                    id: standaloneId,
                    type: 'text-with-standalone-id'
                };
            }
            if (currentLineContent === '') {
                return {
                    position: {
                        start: { line: cursorLine, ch: 0 },
                        end: { line: cursorLine, ch: 0 }
                    },
                    type: 'empty-after-list'
                };
            }
        }
    }

    if (currentLineContent !== '') {
        const standaloneId = findStandaloneBlockId(cursorLine + 1, editor);
        return {
            position: {
                start: { line: cursorLine, ch: 0 },
                end: { line: cursorLine, ch: editor.getLine(cursorLine).length }
            },
            type: standaloneId ? 'text-with-standalone-id' : 'text',
            id: standaloneId
        };
    }

    return null;
}

function checkSelectionSpansBlocks(from, to, fileCache, editor) {
    const listItems = fileCache?.listItems || [];
    const selectedItems = listItems.filter(item => {
        const itemStart = item.position.start.line;
        let itemEnd = item.position.end.line;
        
        if (findStandaloneBlockId(itemEnd + 1, editor)) {
            itemEnd += 2;
        }
        
        return (itemStart >= from.line && itemStart <= to.line) || 
               (itemEnd >= from.line && itemEnd <= to.line) ||
               (itemStart <= from.line && itemEnd >= to.line);
    });

    if (selectedItems.length > 1) return true;

    const sections = fileCache?.sections || [];
    const selectedSections = sections.filter(section => {
        const sectionStart = section.position.start.line;
        let sectionEnd = section.position.end.line;
        
        if (findStandaloneBlockId(sectionEnd + 1, editor)) {
            sectionEnd += 2;
        }
        
        return (sectionStart >= from.line && sectionStart <= to.line) || 
               (sectionEnd >= from.line && sectionEnd <= to.line) ||
               (sectionStart <= from.line && sectionEnd >= to.line);
    });

    return selectedSections.length > 1;
}

async function handleBlock(file, editor, block) {
    let blockId;
    if (block.type === 'text-with-standalone-id' || block.id) {
        blockId = block.id;
    } else {
        blockId = generateId();
        const currentLine = block.position.end.line;
        
        if (block.type === 'empty-after-list') {
            await editor.replaceRange("\n", {line: currentLine - 1, ch: editor.getLine(currentLine - 1).length});
            const nextLineEmpty = currentLine + 2 < editor.lineCount() && editor.getLine(currentLine + 2).trim() === '';
            await editor.replaceRange(`^${blockId}${nextLineEmpty ? '' : '\n'}`, {line: currentLine + 1, ch: 0});
        } else if (shouldInsertAfter(block)) {
            const nextLineEmpty = currentLine + 1 < editor.lineCount() && editor.getLine(currentLine + 1).trim() === '';
            if (!nextLineEmpty) {
                await editor.replaceRange("\n\n", {line: currentLine, ch: editor.getLine(currentLine).length});
                await editor.replaceRange(`^${blockId}\n`, {line: currentLine + 2, ch: 0});
            } else {
                await editor.replaceRange("\n", {line: currentLine + 1, ch: 0});
                await editor.replaceRange(`^${blockId}\n`, {line: currentLine + 2, ch: 0});
            }
        } else {
            await editor.replaceRange(` ^${blockId}`, {
                line: currentLine,
                ch: block.position.end.col || editor.getLine(currentLine).length
            });
        }
    }
    const selectedText = editor.getSelection().trim();
    return selectedText ? `[[${file.basename}#^${blockId}|~ ${selectedText}]]` : `[[${file.basename}#^${blockId}]]`;
}

const view = tp.app.workspace.getActiveViewOfType(tp.obsidian.MarkdownView);
if (!view) return;

const selection = editor.getSelection();

if (selection && checkSelectionSpansBlocks(editor.getCursor('from'), editor.getCursor('to'), tp.app.metadataCache.getFileCache(view.file), editor)) {
    new Notice("Selections spanning multiple blocks aren't supported");
    tR = selection;
    return;
}

const block = getBlock(editor, tp.app.metadataCache.getFileCache(view.file));
if (!block) {
    tR = selection;
    return;
}

const result = await handleBlock(view.file, editor, block);
if (result) {
    await navigator.clipboard.writeText(result);
    new Notice("Copied to your clipboard");
}
tR = selection;
%>

Alternative solution

Carry-Forward has a command that achieves this but it seems to be bugged.

1 Like