Skip to content

adding menu option to copy selected text as a link alias #33

@AlfonsoRReyes

Description

@AlfonsoRReyes

I made couple of modifications in the code to be able to select text when copying a block link so when it is pasted the text is shown as an alias of the file link.

Case

  1. We want to copy a unique text "field-to-field discrete PINNs by leveraging convolution operations and numerical discretizations" as a block link

image

  1. We select the text and right-click and pick the new menu option "Copy text link to block":

image

  1. Paste the block link at the desired location
    image

Voila!

Changes required

  • Line 45, add this line: const aliasText = editor.getSelection();
  • Line 55 add a comma at the end; line 56 add aliasText
  • Line 68, add this menu.addItem
        menu.addItem((item) => {
          item
            .setTitle(isHeading ? "Copy link to heading" : "Copy text link to block")
            .setIcon("links-coming-in")
            .onClick(() => onClick(false));
        }); 
  • Line 88, add let aliasText = ""

  • Line 93, add this addCommand

    this.addCommand({
      id: "copy-text-link-to-block",
      name: "Copy text as link to current block or heading",
      editorCheckCallback: (isChecking, editor: Editor, view: MarkdownView) => {
        let aliasText = editor.getSelection();
        return this.handleCommand(isChecking, editor, view, false, aliasText);
      },
    });   
  • Line 106, add let aliasText = ""

  • Line 117, add aliasText: string

  • Line 137, add aliasText

  • Line 185, 186, add a comma and aliasText: string

  • Line 196

"#^" + blockId, (aliasText.length > 0 ? aliasText : "")
  • Line 217
"#^" + id, (aliasText.length > 0 ? aliasText : "")

Modified main.ts attached:

import {
  Editor,
  EditorPosition,
  HeadingCache,
  ListItemCache,
  MarkdownView,
  Plugin,
  SectionCache,
  TFile,
} from "obsidian";

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

const illegalHeadingCharsRegex = /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\]/g;
function sanitizeHeading(heading: string) {
  return heading
    .replace(illegalHeadingCharsRegex, " ")
    .replace(/\s+/g, " ")
    .trim();
}

function shouldInsertAfter(block: ListItemCache | SectionCache) {
  if ((block as any).type) {
    return [
      "blockquote",
      "code",
      "table",
      "comment",
      "footnoteDefinition",
    ].includes((block as SectionCache).type);
  }
}

export default class MyPlugin extends Plugin {
  async onload() {
    this.registerEvent(
      this.app.workspace.on("editor-menu", (menu, editor, view) => {
        const block = this.getBlock(editor, view.file);

        if (!block) return;

        const isHeading = !!(block as any).heading;
        const aliasText = editor.getSelection();

        const onClick = (isEmbed: boolean) => {
          if (isHeading) {
            this.handleHeading(view.file, block as HeadingCache, isEmbed);
          } else {
            this.handleBlock(
              view.file,
              editor,
              block as SectionCache | ListItemCache,
              isEmbed,
              aliasText
            );
          }
        };

        menu.addItem((item) => {
          item
            .setTitle(isHeading ? "Copy link to heading" : "Copy link to block")
            .setIcon("links-coming-in")
            .onClick(() => onClick(false));
        });

        menu.addItem((item) => {
          item
            .setTitle(isHeading ? "Copy link to heading" : "Copy text link to block")
            .setIcon("links-coming-in")
            .onClick(() => onClick(false));
        });        

        menu.addItem((item) => {
          item
            .setTitle(isHeading ? "Copy heading embed" : "Copy block embed")
            .setIcon("links-coming-in")
            .onClick(() => onClick(true));
        });
      })
    );

    this.addCommand({
      id: "copy-link-to-block",
      name: "Copy link to current block or heading",
      editorCheckCallback: (isChecking, editor, view) => {
        let aliasText = ""
        return this.handleCommand(isChecking, editor, view, false, aliasText);
      },
    });

    this.addCommand({
      id: "copy-text-link-to-block",
      name: "Copy text as link to current block or heading",
      editorCheckCallback: (isChecking, editor: Editor, view: MarkdownView) => {
        let aliasText = editor.getSelection();
        return this.handleCommand(isChecking, editor, view, false, aliasText);
      },
    });    

    this.addCommand({
      id: "copy-embed-to-block",
      name: "Copy embed to current block or heading",
      editorCheckCallback: (isChecking, editor, view) => {
        let aliasText = ""
        return this.handleCommand(isChecking, editor, view, true, aliasText);
      },
    });
  }

  handleCommand(
    isChecking: boolean,
    editor: Editor,
    view: MarkdownView,
    isEmbed: boolean,
    aliasText: string
  ) {
    if (isChecking) {
      return !!this.getBlock(editor, view.file);
    }

    const block = this.getBlock(editor, view.file);

    if (!block) return;

    const isHeading = !!(block as any).heading;

    if (isHeading) {
      this.handleHeading(view.file, block as HeadingCache, isEmbed);
    } else {
      this.handleBlock(
        view.file,
        editor,
        block as SectionCache | ListItemCache,
        isEmbed,
        aliasText
      );
    }
  }

  getBlock(editor: Editor, file: TFile) {
    const cursor = editor.getCursor("to");
    const fileCache = this.app.metadataCache.getFileCache(file);

    let block: ListItemCache | HeadingCache | SectionCache = (
      fileCache?.sections || []
    ).find((section) => {
      return (
        section.position.start.line <= cursor.line &&
        section.position.end.line >= cursor.line
      );
    });

    if (block?.type === "list") {
      block = (fileCache?.listItems || []).find((item) => {
        return (
          item.position.start.line <= cursor.line &&
          item.position.end.line >= cursor.line
        );
      });
    } else if (block?.type === "heading") {
      block = fileCache.headings.find((heading) => {
        return heading.position.start.line === block.position.start.line;
      });
    }

    return block;
  }

  handleHeading(file: TFile, block: HeadingCache, isEmbed: boolean) {
    navigator.clipboard.writeText(
      `${isEmbed ? "!" : ""}${this.app.fileManager.generateMarkdownLink(
        file,
        "",
        "#" + sanitizeHeading(block.heading)
      )}`
    );
  }

  handleBlock(
    file: TFile,
    editor: Editor,
    block: ListItemCache | SectionCache,
    isEmbed: boolean,
    aliasText: string
  ) {
    const blockId = block.id;

    // Copy existing block id
    if (blockId) {
      return navigator.clipboard.writeText(
        `${isEmbed ? "!" : ""}${this.app.fileManager.generateMarkdownLink(
          file,
          "",
          "#^" + blockId, (aliasText.length > 0 ? aliasText : "")
        )}`
      );
    }

    // Add a block id
    const sectionEnd = block.position.end;
    const end: EditorPosition = {
      ch: sectionEnd.col,
      line: sectionEnd.line,
    };

    const id = generateId();
    const spacer = shouldInsertAfter(block) ? "\n\n" : " ";

    editor.replaceRange(`${spacer}^${id}`, end);
    navigator.clipboard.writeText(
      `${isEmbed ? "!" : ""}${this.app.fileManager.generateMarkdownLink(
        file,
        "",
        // "#^" + id, "add-block-id"
        "#^" + id, (aliasText.length > 0 ? aliasText : "")
      )}`
    );
  }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions