Zotero一键复制带有页码指向的Markdown链接

Zotero Copy Link with Current Page

https://github.com/windingwind/zotero-actions-tags/discussions/643

A customized Zotero Actions & Tags script that copies Zotero links with automatic PDF page detection.

Features

  • Automatic Page Detection: Retrieves the current page number from the active Zotero PDF reader.
  • Direct PDF Navigation: Generates zotero://open-pdf links with the current page parameter.
  • Annotation Support: Preserves annotation-specific page numbers and annotation IDs.
  • Smart Link Resolution: Automatically determines whether to open a PDF or select a library item.
  • Markdown Support: Generates Markdown links that can be pasted directly into Obsidian.
  • Collection Support: Optionally includes the currently selected collection in the link.
  • Better Notes Integration: Supports Better Notes links for note items.

Usage

  1. Install and configure the Zotero Actions & Tags plugin.
  2. Import this script as a custom action.
  3. Open a PDF in Zotero.
  4. Navigate to the page you want to reference.
  5. Select the corresponding library item or PDF attachment.
  6. Execute the assigned shortcut.
  7. Paste the generated Markdown link into Obsidian.

Example

[Research Paper](zotero://open-pdf/library/items/ABC123?page=15)

Clicking the link opens the corresponding PDF directly at page 15.

Configuration

Setting Description Default
linkTextField Field used as link text title
linkType Output format md
useColl Include collection information false
useNoteLink Enable Better Notes links false
linkAction Link resolution strategy auto

Notes

  • Page numbers refer to physical PDF page numbers.
  • The current page is detected only when the active reader matches the target PDF.
  • Annotation links use their saved annotation positions.
  • Reader page detection relies on Zotero's internal reader properties and may require adjustments after Zotero updates.
/**
 * A description of this script.
 * @author HelloNVIC ,windingwind, garth74
 * @usage Select an item in the library and press the assigned shortcut keys
 * @link https://github.com/windingwind/zotero-actions-tags/discussions/643
 * @see https://github.com/windingwind/zotero-actions-tags/discussions/643
 */

// ==================================================
// 1. EDITABLE SETTINGS
// ==================================================

/** @type {string} Field used as the link text. Use "citationKey" for citation keys. */
let linkTextField = "title";

/** @type {'html' | 'md' | 'plain'} Link output format. */
let linkType = "md";

/** @type {boolean} Whether to include the currently selected collection in the link. */
let useColl = false;

/** @type {boolean} Whether to use Better Notes links for note items. */
let useNoteLink = false;

/** @type {'select' | 'open-pdf' | 'auto'} Link action. */
let linkAction = "auto";

// ==================================================
// 2. GET CURRENT PDF READER INFORMATION
// ==================================================

/**
 * Get the currently active PDF reader and its current page number.
 *
 * @returns {{reader: object, pdfItem: object, page: number|null}|null}
 */
function getActivePDFReaderInfo() {
  try {
    const mainWindow = Zotero.getMainWindow();
    const tabID = mainWindow?.Zotero_Tabs?.selectedID;

    if (!tabID) return null;

    const reader = Zotero.Reader.getByTabID(tabID);

    if (!reader || !reader.itemID) return null;

    // Get the PDF attachment associated with the reader.
    const pdfItem = Zotero.Items.get(reader.itemID);

    if (!pdfItem || !pdfItem.isPDFAttachment()) {
      return null;
    }

    // Access the PDF.js application instance.
    let pdfViewerApplication =
      reader._iframeWindow
        ?.wrappedJSObject
        ?.PDFViewerApplication;

    // Try an alternative internal reader path.
    if (!pdfViewerApplication) {
      pdfViewerApplication =
        reader._internalReader
          ?._primaryView
          ?._iframeWindow
          ?.PDFViewerApplication;
    }

    // Try the direct iframe window as a fallback.
    if (!pdfViewerApplication) {
      pdfViewerApplication =
        reader._iframeWindow
          ?.PDFViewerApplication;
    }

    // Get the current PDF page number (1-based).
    let page = Number(
      pdfViewerApplication?.pdfViewer?.currentPageNumber
    );

    // Fall back to the page number input field if necessary.
    if (!Number.isInteger(page) || page < 1) {
      const pageInput =
        reader._iframeWindow
          ?.document
          ?.querySelector("input#pageNumber");

      page = Number.parseInt(pageInput?.value, 10);
    }

    return {
      reader,
      pdfItem,
      page:
        Number.isInteger(page) && page > 0
          ? page
          : null
    };

  } catch (e) {
    Zotero.warn(
      "[Copy Zotero Link] Failed to retrieve current PDF page: " + e
    );
    return null;
  }
}

/**
 * Check whether the active PDF reader is displaying
 * an attachment belonging to the specified library item.
 *
 * @param {object|null} readerInfo
 * @param {object|null} candidateItem
 * @returns {boolean}
 */
function isReaderPDFForItem(readerInfo, candidateItem) {
  if (!readerInfo?.pdfItem || !candidateItem) {
    return false;
  }

  const pdfItem = readerInfo.pdfItem;

  const parentID =
    pdfItem.parentItem?.id ??
    pdfItem.parentID;

  return (
    pdfItem.id === candidateItem.id ||
    parentID === candidateItem.id
  );
}

// ==================================================
// 3. INITIALIZE
// ==================================================

// Execute only once when multiple items are selected.
if (item) return;

item = items[0];

if (!item && !collection) {
  return "[Copy Zotero Link] item is empty";
}

// Retrieve the currently active PDF reader.
const activeReaderInfo = getActivePDFReaderInfo();

// Collection links always use the select action.
if (collection) {
  linkAction = "select";
  useColl = true;
}

// ==================================================
// 4. DETERMINE LINK ACTION
// ==================================================

if (linkAction === "auto") {

  if (
    item.isPDFAttachment() ||
    item.isAnnotation()
  ) {
    linkAction = "open-pdf";

  } else if (
    item.isRegularItem() &&
    isReaderPDFForItem(activeReaderInfo, item)
  ) {
    // Open the PDF directly when its parent item
    // matches the document currently being read.
    linkAction = "open-pdf";

  } else {
    linkAction = "select";
  }
}

// ==================================================
// 5. BUILD ZOTERO URI
// ==================================================

const uriParts = [];
let uriParams = "";

let targetItem = item;

if (linkAction === "open-pdf") {

  uriParts.push("zotero://open-pdf");

  if (item.isRegularItem()) {

    // Prefer the PDF currently open in the reader.
    if (isReaderPDFForItem(activeReaderInfo, item)) {

      targetItem = activeReaderInfo.pdfItem;

    } else {

      // Otherwise, use the best available PDF attachment.
      targetItem = (await item.getBestAttachments()).find(
        (att) => att.isPDFAttachment()
      );
    }

  } else if (item.isAnnotation()) {

    // Use the parent PDF attachment of the annotation.
    targetItem = item.parentItem;

    // Retrieve the annotation's saved page number.
    let pageIndex = 1;

    try {
      pageIndex =
        JSON.parse(item.annotationPosition).pageIndex + 1;
    } catch (e) {
      Zotero.warn(e);
    }

    // Preserve the annotation's page and annotation key.
    uriParams =
      `?page=${pageIndex}&annotation=${item.key}`;
  }

  // ==================================================
  // 6. ADD CURRENT PDF READER PAGE
  // ==================================================

  // For regular items and PDF attachments, use the
  // current reader page when the active PDF matches
  // the target attachment.
  //
  // Annotation links retain their own saved page number.

  if (
    !item.isAnnotation() &&
    targetItem?.isPDFAttachment() &&
    activeReaderInfo?.pdfItem?.id === targetItem.id &&
    activeReaderInfo.page
  ) {

    uriParams = `?page=${activeReaderInfo.page}`;
  }

} else {

  uriParts.push("zotero://select");

  if (item?.isAnnotation()) {
    targetItem = item.parentItem;
  }
}

// Validate the target item.
if (!targetItem && !collection) {
  return "[Copy Zotero Link] item is invalid";
}

// ==================================================
// 7. GENERATE LINK TEXT
// ==================================================

let linkText;

if (collection) {

  linkText = collection.name;

} else if (item.isAttachment()) {

  // Use the top-level library item's field as link text.
  const topLevelItem =
    Zotero.Items.getTopLevel([item])[0];

  linkText = topLevelItem?.getField(linkTextField);

} else if (item.isAnnotation()) {

  // Include the annotation text in the link label.
  linkText = `${targetItem.getField(linkTextField)}(${
    item.annotationComment ||
    item.annotationText ||
    "annotation"
  })`;

} else {

  // Use the configured field of the selected item.
  linkText = item.getField(linkTextField);
}

// ==================================================
// 8. BUILD LIBRARY OR GROUP URI
// ==================================================

const libraryType =
  (collection || item).library.libraryType;

if (libraryType === "user") {

  uriParts.push("library");

} else {

  uriParts.push(
    `groups/${
      Zotero.Libraries.get(
        (collection || item).libraryID
      ).groupID
    }`
  );
}

// ==================================================
// 9. ADD COLLECTION URI
// ==================================================

if (useColl) {

  const coll =
    collection ||
    Zotero.getActiveZoteroPane().getSelectedCollection();

  if (coll) {
    uriParts.push(`collections/${coll.key}`);
  }
}

// ==================================================
// 10. ADD ITEM URI
// ==================================================

if (!collection) {
  uriParts.push(`items/${targetItem.key}`);
}

// Combine URI components.
let uri = uriParts.join("/");

// Append page and annotation parameters.
if (uriParams) {
  uri += uriParams;
}

// Use Better Notes URI for note items when enabled.
if (
  useNoteLink &&
  item?.isNote() &&
  Zotero.BetterNotes
) {
  uri = Zotero.BetterNotes.api.convert.note2link(item);
}

// ==================================================
// 11. COPY TO CLIPBOARD
// ==================================================

const clipboard =
  new Zotero.ActionsTags.api.utils.ClipboardHelper();

if (linkType === "html") {

  const html = `<a href="${uri}">${linkText}</a>`;

  clipboard.addText(html, "text/unicode");
  clipboard.addText(html, "text/html");

} else if (linkType === "md") {

  // Generate a Markdown link for Obsidian.
  clipboard.addText(
    `[${linkText}](${uri})`,
    "text/unicode"
  );

} else {

  // Copy the raw Zotero URI.
  clipboard.addText(uri, "text/unicode");
}

clipboard.copy();

// ==================================================
// 12. RETURN RESULT
// ==================================================

return `[Copy Zotero Link] link ${uri} copied.`;

Zotero一键复制带有页码指向的Markdown链接

https://blog.ckh-cn.site/index.php/2026/11/12/284.html

作者

CKH

发布时间

2026-11-12

许可协议

CC BY 4.0

OS: Linux 115a654af43f 5.15.0-113-generic #123-Ubuntu SMP Mon Jun 10 08:16:17 UTC 2024 x86_64
CPU Info:
Memory Info:
评论