Copy block link, embed, footnote or URL with Templater

Sharing a few Templater templates for copying links to a block. To use them, select text or place cursor in a single block and run the template. Due to the post length limit, the templates reside in the comments below.

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

A few notes


  1. Using block IDs to link to headings is preferable as they survive heading renames, unlike Note#Heading links.
  2. These scripts were made for own personal use.
  3. Most of the code here was written by AI (Claude 3.5 Sonnet Oct’24). The scripts have been extensively stress-tested and refined to handle all sorts of block types and edge cases. But YMMV.
  4. Ensure your vault is backed up before running these templates. 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.
  5. 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 an auto-suggester.
    • If this mistakenly results in multiple subsequent blank lines, Linter’s Consecutive blank lines rule can help remove them automatically.
  6. To add similar commands for whole notes, see here.
  7. These solutions—while they get the job done—are a tad overengineered and may need ongoing maintenance, which I may not be willing to provide. It’d be best if this was 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.
  8. Code improvements welcome.

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

Unfortunately, links in inline footnotes don’t get updated as of 1.7.7 due to a bug.

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.

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