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-pdflinks 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
- Install and configure the Zotero Actions & Tags plugin.
- Import this script as a custom action.
- Open a PDF in Zotero.
- Navigate to the page you want to reference.
- Select the corresponding library item or PDF attachment.
- Execute the assigned shortcut.
- 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.`;