Zotero

name

Zotero

icon

menu_book

setting

Zotero User ID

setting

Zotero API Key (secureText)

setting

Library Type (select: user, group)

setting

Default Citation Style (select: apa, chicago-author-date, modern-language-association, ieee, harvard-cite-them-right, nature, vancouver, bibtex)

setting

Sync Tag Prefix (default: -reports/-zotero)

setting

Auto Attach PDF (select: true, false)

description

Search Zotero references, insert formatted citations, and link research items.

instructions

- { Insert Citation } & { Insert Bibliography }& { Insert Note Link }& { Insert BibTeX }

- appOption["Import Zotero Reference to Note"]

- appOption["Sync Zotero Collection"]

- appOption["Open Zotero Explorer"]
- appOption["Configure Zotero Settings"]
Note: More options are on its way. Comment in the Installation page for any feedback or suggestion.
GitHub Link: https://github.com/krishnakanthb13/anp-25-zotero
YouTube: (Coming Soon)


(() => {
// anp-25-zotero/lib/constants.js
var ZOTERO_API_BASE = "https://api.zotero.org";
var ZOTERO_API_VERSION = "3";
var CITATION_STYLES = {
APA: "apa",
CHICAGO_AUTHOR_DATE: "chicago-author-date",
CHICAGO_FULLNOTE: "chicago-fullnote-bibliography",
MLA: "modern-language-association",
IEEE: "ieee",
HARVARD: "harvard-cite-them-right",
NATURE: "nature",
VANCOUVER: "vancouver",
BIBTEX: "bibtex"
};
var CITATION_STYLE_LABELS = [
{ label: "APA 7th Edition", value: CITATION_STYLES.APA },
{ label: "Chicago (Author-Date)", value: CITATION_STYLES.CHICAGO_AUTHOR_DATE },
{ label: "Chicago (Full Note)", value: CITATION_STYLES.CHICAGO_FULLNOTE },
{ label: "MLA 9th Edition", value: CITATION_STYLES.MLA },
{ label: "IEEE", value: CITATION_STYLES.IEEE },
{ label: "Harvard", value: CITATION_STYLES.HARVARD },
{ label: "Nature", value: CITATION_STYLES.NATURE },
{ label: "Vancouver", value: CITATION_STYLES.VANCOUVER },
{ label: "BibTeX Entry", value: CITATION_STYLES.BIBTEX }
];
var DEFAULT_SETTINGS = {
userId: "",
apiKey: "",
libraryType: "user",
// "user" or "group"
defaultStyle: CITATION_STYLES.APA,
syncTagPrefix: "-reports/-zotero",
autoAttachPDF: true,
itemsPerPage: 25,
maxSearchItems: 50
};
var ITEM_TYPE_ICONS = {
journalArticle: "article",
book: "menu_book",
bookSection: "auto_stories",
conferencePaper: "groups",
preprint: "description",
thesis: "school",
report: "assessment",
webpage: "language",
document: "description",
newspaperArticle: "newspaper",
magazineArticle: "auto_stories"
};
var ERROR_MESSAGES = {
MISSING_USER_ID: "Zotero User ID / Library ID is required in Plugin Settings.",
MISSING_API_KEY: "Zotero API Key is required in Plugin Settings.",
INVALID_CREDENTIALS: "Authentication failed. Please verify your Zotero User ID and API Key.",
RATE_LIMIT_EXCEEDED: "Zotero API rate limit reached. Please wait a few moments and retry.",
NETWORK_ERROR: "Unable to reach Zotero API. Please check your internet connection.",
ITEM_NOT_FOUND: "The requested Zotero reference could not be found.",
EMPTY_QUERY: "Search query cannot be empty."
};
 
// anp-25-zotero/lib/settings.js
function getPluginSettings(app) {
if (!app || !app.settings) {
return { ...DEFAULT_SETTINGS };
}
const rawUserId = app.settings["Zotero User ID"] || app.settings["User ID"] || "";
const rawApiKey = app.settings["Zotero API Key"] || app.settings["API Key"] || "";
const rawLibraryType = (app.settings["Library Type"] || "user").trim().toLowerCase();
const rawDefaultStyle = app.settings["Default Citation Style"] || app.settings["Citation Style"] || DEFAULT_SETTINGS.defaultStyle;
const rawSyncTagPrefix = app.settings["Sync Tag Prefix"] || DEFAULT_SETTINGS.syncTagPrefix;
const rawAutoAttachPDF = app.settings["Auto Attach PDF"] !== "false" && app.settings["Auto Attach PDF"] !== false;
return {
userId: String(rawUserId).trim(),
apiKey: String(rawApiKey).trim(),
libraryType: rawLibraryType === "group" ? "group" : "user",
defaultStyle: validateCitationStyle(rawDefaultStyle),
syncTagPrefix: String(rawSyncTagPrefix).trim().replace(/^\/+|\/+$/g, ""),
autoAttachPDF: Boolean(rawAutoAttachPDF),
itemsPerPage: DEFAULT_SETTINGS.itemsPerPage,
maxSearchItems: DEFAULT_SETTINGS.maxSearchItems
};
}
function validateCitationStyle(style) {
if (!style || typeof style !== "string") {
return DEFAULT_SETTINGS.defaultStyle;
}
const cleanStyle = style.trim().toLowerCase();
const validStyles = Object.values(CITATION_STYLES);
return validStyles.includes(cleanStyle) ? cleanStyle : DEFAULT_SETTINGS.defaultStyle;
}
function validateCredentials(settings) {
if (!settings || !settings.userId) {
return {
valid: false,
error: "Missing Zotero User ID. Your numeric User ID can be found at: https://www.zotero.org/settings/keys"
};
}
if (!settings.apiKey) {
return {
valid: false,
error: "Missing Zotero API Key. Create a private key at: https://www.zotero.org/settings/keys"
};
}
return { valid: true, error: null };
}
async function promptConfigureSettings(app, isFirstTime = false) {
if (!app || !app.prompt) return null;
const current = getPluginSettings(app);
const message = isFirstTime ? "Welcome to Zotero for Amplenote!\n\nTo connect your research library, please enter your Zotero User ID and API Key below.\nGet your User ID & create an API Key here: https://www.zotero.org/settings/keys\n(Ensure your API Key has 'Allow library access' enabled)" : "Configure Zotero Plugin Settings\n\nUpdate your Zotero API credentials, default citation style, and tag hierarchy.\nManage your keys at: https://www.zotero.org/settings/keys";
const result = await app.prompt(message, {
inputs: [
{
label: "1. Zotero User ID (Find at zotero.org/settings/keys)",
placeholder: "e.g. 12345678",
type: "string",
value: current.userId || ""
},
{
label: "2. Zotero API Key (Private Key with Read access)",
placeholder: "e.g. abcd1234efgh5678...",
type: "secureText",
value: current.apiKey || ""
},
{
label: "3. Library Type",
type: "select",
value: current.libraryType || "user",
options: [
{ label: "Personal Library (User)", value: "user" },
{ label: "Group Library", value: "group" }
]
},
{
label: "4. Default Citation Style",
type: "select",
value: current.defaultStyle || DEFAULT_SETTINGS.defaultStyle,
options: CITATION_STYLE_LABELS
},
{
label: "5. Sync Tag Prefix (Base tag for all literature notes)",
placeholder: "-reports/-zotero",
type: "string",
value: current.syncTagPrefix || DEFAULT_SETTINGS.syncTagPrefix
},
{
label: "6. Auto Attach PDF files to notes",
type: "select",
value: current.autoAttachPDF ? "true" : "false",
options: [
{ label: "Yes (Download & attach PDFs if available in cloud)", value: "true" },
{ label: "No (Use desktop deep links only)", value: "false" }
]
}
]
});
if (!result) return null;
const [userId, apiKey, libraryType, defaultStyle, syncTagPrefix, autoAttachPDF] = result;
const newUserId = String(userId || "").trim();
const newApiKey = String(apiKey || "").trim();
const newLibraryType = libraryType === "group" ? "group" : "user";
const newStyle = validateCitationStyle(defaultStyle);
const newTagPrefix = String(syncTagPrefix || DEFAULT_SETTINGS.syncTagPrefix).trim().replace(/^\/+|\/+$/g, "");
const newAutoAttach = autoAttachPDF === "true" || autoAttachPDF === true;
if (app.setSetting) {
await app.setSetting("Zotero User ID", newUserId);
await app.setSetting("Zotero API Key", newApiKey);
await app.setSetting("Library Type", newLibraryType);
await app.setSetting("Default Citation Style", newStyle);
await app.setSetting("Sync Tag Prefix", newTagPrefix);
await app.setSetting("Auto Attach PDF", newAutoAttach ? "true" : "false");
}
if (app.settings) {
app.settings["Zotero User ID"] = newUserId;
app.settings["Zotero API Key"] = newApiKey;
app.settings["Library Type"] = newLibraryType;
app.settings["Default Citation Style"] = newStyle;
app.settings["Sync Tag Prefix"] = newTagPrefix;
app.settings["Auto Attach PDF"] = newAutoAttach ? "true" : "false";
}
const updatedSettings = {
userId: newUserId,
apiKey: newApiKey,
libraryType: newLibraryType,
defaultStyle: newStyle,
syncTagPrefix: newTagPrefix,
autoAttachPDF: newAutoAttach,
itemsPerPage: DEFAULT_SETTINGS.itemsPerPage,
maxSearchItems: DEFAULT_SETTINGS.maxSearchItems
};
if (!isFirstTime && app.alert) {
await app.alert("Zotero Plugin Settings updated successfully!");
}
return updatedSettings;
}
async function ensureCredentials(app) {
let settings = getPluginSettings(app);
let validation = validateCredentials(settings);
if (!validation.valid) {
const prompted = await promptConfigureSettings(app, true);
if (!prompted) return null;
settings = prompted;
validation = validateCredentials(settings);
if (!validation.valid) {
if (app.alert) {
await app.alert(`Setup Incomplete:
 
${validation.error}`);
}
return null;
}
}
return settings;
}
 
// anp-25-zotero/lib/zotero-client.js
var ZoteroClient = class {
/**
* @param {Object} config - Client configuration.
* @param {string} config.userId - Zotero User ID or Group ID.
* @param {string} config.apiKey - Zotero API Key.
* @param {string} [config.libraryType="user"] - "user" or "group".
* @param {Function} [config.fetchFn=globalThis.fetch] - Pluggable fetch implementation (for testing).
*/
constructor({ userId, apiKey, libraryType = "user", fetchFn = globalThis.fetch } = {}) {
if (!userId) throw new Error(ERROR_MESSAGES.MISSING_USER_ID);
if (!apiKey) throw new Error(ERROR_MESSAGES.MISSING_API_KEY);
this.userId = String(userId).trim();
this.apiKey = String(apiKey).trim();
this.libraryType = libraryType === "group" ? "groups" : "users";
this.fetch = fetchFn || globalThis.fetch;
this.baseUrl = `${ZOTERO_API_BASE}/${this.libraryType}/${this.userId}`;
}
/**
* Common request handler with standardized headers, auth, and error mapping.
*
* @param {string} endpoint - API path relative to the base URL or full path.
* @param {Object} [options={}] - Request options and query parameters.
* @returns {Promise<{ data: any, headers: Headers, totalResults: number }>}
*/
async request(endpoint, options = {}) {
const url = new URL(
endpoint.startsWith("http") ? endpoint : `${this.baseUrl}${endpoint.startsWith("/") ? "" : "/"}${endpoint}`
);
if (options.params) {
Object.entries(options.params).forEach(([key, val]) => {
if (val !== void 0 && val !== null && val !== "") {
url.searchParams.append(key, String(val));
}
});
}
const headers = new Headers({
"Zotero-API-Key": this.apiKey,
"Zotero-API-Version": ZOTERO_API_VERSION,
...options.headers || {}
});
try {
const response = await this.fetch(url.toString(), {
method: options.method || "GET",
headers,
body: options.body
});
if (!response.ok) {
await this._handleErrorResponse(response);
}
const totalResults = parseInt(response.headers.get("Total-Results") || "0", 10);
const contentType = response.headers.get("Content-Type") || "";
let data;
if (contentType.includes("application/json")) {
data = await response.json();
} else {
data = await response.text();
}
return { data, headers: response.headers, totalResults };
} catch (err) {
if (err.message && err.message.startsWith("Zotero API Error")) {
throw err;
}
throw new Error(`${ERROR_MESSAGES.NETWORK_ERROR} (${err.message})`);
}
}
/**
* Handles non-2xx HTTP responses.
* @private
*/
async _handleErrorResponse(response) {
let details = "";
try {
details = await response.text();
} catch {
}
switch (response.status) {
case 401:
case 403:
throw new Error(`Zotero API Error [${response.status}]: ${ERROR_MESSAGES.INVALID_CREDENTIALS}`);
case 404:
throw new Error(`Zotero API Error [404]: ${ERROR_MESSAGES.ITEM_NOT_FOUND}`);
case 429: {
const retryAfter = response.headers.get("Retry-After") || "a few";
throw new Error(`Zotero API Error [429]: ${ERROR_MESSAGES.RATE_LIMIT_EXCEEDED} (Retry after ${retryAfter}s)`);
}
default:
throw new Error(`Zotero API Error [${response.status}]: ${details || response.statusText}`);
}
}
/**
* Searches top-level items in the library matching a text query.
*
* @param {Object} params - Search parameters.
* @param {string} params.query - Search query string.
* @param {number} [params.limit=25] - Number of items to return.
* @param {number} [params.start=0] - Offset index for pagination.
* @param {string} [params.itemType] - Specific item type filter (e.g. "journalArticle -attachment").
* @param {string} [params.sort="dateModified"] - Sort field.
* @param {string} [params.direction="desc"] - Sort direction ("asc" or "desc").
* @returns {Promise<Array<Object>>} List of Zotero item objects.
*/
async searchItems({ query, limit = 25, start = 0, itemType = "-attachment", sort = "dateModified", direction = "desc" } = {}) {
if (!query || typeof query !== "string" || !query.trim()) {
throw new Error(ERROR_MESSAGES.EMPTY_QUERY);
}
const res = await this.request("/items/top", {
params: {
q: query.trim(),
limit,
start,
itemType,
sort,
direction
}
});
return res.data;
}
/**
* Retrieves top-level items with pagination.
*
* @param {Object} [params={}] - Query parameters.
* @returns {Promise<{ items: Array<Object>, total: number }>}
*/
async getTopItems({ limit = 25, start = 0, sort = "dateModified", direction = "desc", itemType = "-attachment" } = {}) {
const res = await this.request("/items/top", {
params: { limit, start, sort, direction, itemType }
});
return { items: res.data, total: res.totalResults };
}
/**
* Retrieves a single item by key.
*
* @param {string} itemKey - Zotero item key.
* @param {Object} [params={}] - Options (format, style, include).
* @returns {Promise<Object>}
*/
async getItem(itemKey, params = {}) {
if (!itemKey) throw new Error("Item key is required.");
const res = await this.request(`/items/${itemKey}`, { params });
return res.data;
}
/**
* Retrieves child items (notes, annotations, attachments) for a parent item.
*
* @param {string} itemKey - Parent item key.
* @returns {Promise<Array<Object>>}
*/
async getItemChildren(itemKey) {
if (!itemKey) throw new Error("Item key is required.");
const res = await this.request(`/items/${itemKey}/children`);
return res.data;
}
/**
* Retrieves formatted CSL-JSON for an item.
*
* @param {string} itemKey - Zotero item key.
* @returns {Promise<Object>}
*/
async getItemCSL(itemKey) {
const res = await this.request(`/items/${itemKey}`, {
params: { format: "csljson" }
});
return res.data;
}
/**
* Retrieves formatted HTML/Text bibliography for an item in a specific CSL style.
*
* @param {string} itemKey - Zotero item key.
* @param {string} [style="apa"] - CSL citation style.
* @returns {Promise<string>}
*/
async getFormattedBibliography(itemKey, style = "apa") {
const res = await this.request(`/items/${itemKey}`, {
params: { format: "bib", style }
});
return typeof res.data === "string" ? res.data : JSON.stringify(res.data);
}
/**
* Retrieves user collections.
*
* @returns {Promise<Array<Object>>}
*/
async getCollections() {
const res = await this.request("/collections");
return res.data;
}
/**
* Retrieves items inside a specific collection.
*
* @param {string} collectionKey - Collection key.
* @param {Object} [params={}] - Query params.
* @returns {Promise<Array<Object>>}
*/
async getCollectionItems(collectionKey, { limit = 50, start = 0 } = {}) {
if (!collectionKey) throw new Error("Collection key is required.");
const res = await this.request(`/collections/${collectionKey}/items/top`, {
params: { limit, start, itemType: "-attachment" }
});
return res.data;
}
/**
* Retrieves all tags in the library.
*
* @returns {Promise<Array<Object>>}
*/
async getTags() {
const res = await this.request("/tags");
return res.data;
}
};
 
// anp-25-zotero/lib/citation-service.js
function formatCreators(itemData = {}) {
const creators = itemData.creators || [];
const authors = creators.map((c) => {
if (c.lastName && c.firstName) {
return `${c.firstName} ${c.lastName}`;
}
return c.name || c.lastName || "Unknown Author";
});
const lastNames = creators.map((c) => c.lastName || c.name || "Unknown");
let shortCitationAuthor = "Unknown";
if (lastNames.length === 1) {
shortCitationAuthor = lastNames[0];
} else if (lastNames.length === 2) {
shortCitationAuthor = `${lastNames[0]} & ${lastNames[1]}`;
} else if (lastNames.length > 2) {
shortCitationAuthor = `${lastNames[0]} et al.`;
}
return { authors, shortCitationAuthor };
}
function extractYear(dateStr) {
if (!dateStr || typeof dateStr !== "string") return "n.d.";
const match = dateStr.match(/\b(18|19|20)\d{2}\b/);
return match ? match[0] : "n.d.";
}
function formatInTextCitation(item = {}) {
const data = item.data || item;
const { shortCitationAuthor } = formatCreators(data);
const year = extractYear(data.date);
return `(${shortCitationAuthor}, ${year})`;
}
function formatNoteLinkCitation(item = {}, targetNoteTitle = null) {
const data = item.data || item;
const noteTitle = targetNoteTitle || data.title || "Untitled Reference";
const { shortCitationAuthor } = formatCreators(data);
const year = extractYear(data.date);
const label = `${shortCitationAuthor}, ${year}`;
return `[[${noteTitle}|${label}]]`;
}
function generateCitationKey(item = {}) {
const data = item.data || item;
if (data.citationKey) return data.citationKey;
if (data.extra) {
const match = data.extra.match(/Citation Key:\s*([^\s\n]+)/i);
if (match) return match[1];
}
const creators = data.creators || [];
const firstAuthor = (creators[0]?.lastName || creators[0]?.name || "unknown").toLowerCase().replace(/[^a-z0-9]/g, "");
const year = extractYear(data.date).replace(/[^0-9]/g, "") || "nd";
const firstWord = (data.title || "item").toLowerCase().replace(/^(the|a|an)\s+/i, "").split(/\s+/)[0].replace(/[^a-z0-9]/g, "").slice(0, 10);
return `${firstAuthor}${year}${firstWord}`;
}
function htmlBibToMarkdown(htmlBib) {
if (!htmlBib || typeof htmlBib !== "string") return "";
let md = htmlBib.replace(/<div class="csl-bib-body"[^>]*>/gi, "").replace(/<div class="csl-entry"[^>]*>/gi, "").replace(/<\/div>/gi, "\n").replace(/<i>(.*?)<\/i>/gi, "*$1*").replace(/<em>(.*?)<\/em>/gi, "*$1*").replace(/<b>(.*?)<\/b>/gi, "**$1**").replace(/<strong>(.*?)<\/strong>/gi, "**$1**").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/<[^>]+>/g, "").trim();
return md;
}
function formatFallbackBibliography(item = {}) {
const data = item.data || item;
const { authors } = formatCreators(data);
const authorStr = authors.length > 0 ? authors.join(", ") : "Unknown Author";
const year = extractYear(data.date);
const title = data.title || "Untitled";
const publication = data.publicationTitle || data.publisher || data.institution || "";
const volume = data.volume ? ` ${data.volume}` : "";
const issue = data.issue ? `(${data.issue})` : "";
const pages = data.pages ? `, ${data.pages}` : "";
const doi = data.DOI ? ` https://doi.org/${data.DOI}` : data.url ? ` ${data.url}` : "";
let bib = `${authorStr} (${year}). *${title}*.`;
if (publication) {
bib += ` ${publication},${volume}${issue}${pages}.`;
}
if (doi) {
bib += doi;
}
return bib.trim();
}
function formatBibTeX(item = {}) {
const data = item.data || item;
const citeKey = generateCitationKey(item);
const itemType = data.itemType === "journalArticle" ? "article" : data.itemType || "misc";
const fields = [];
if (data.title) fields.push(` title = {${data.title}}`);
if (data.creators?.length) {
const authorList = data.creators.map((c) => c.lastName ? `${c.lastName}, ${c.firstName || ""}` : c.name).join(" and ");
fields.push(` author = {${authorList}}`);
}
const year = extractYear(data.date);
if (year !== "n.d.") fields.push(` year = {${year}}`);
if (data.publicationTitle) fields.push(` journal = {${data.publicationTitle}}`);
if (data.volume) fields.push(` volume = {${data.volume}}`);
if (data.issue) fields.push(` number = {${data.issue}}`);
if (data.pages) fields.push(` pages = {${data.pages}}`);
if (data.DOI) fields.push(` doi = {${data.DOI}}`);
if (data.url) fields.push(` url = {${data.url}}`);
return `@${itemType}{${citeKey},
${fields.join(",\n")}
}`;
}
 
// anp-25-zotero/lib/insertCitation.js
async function handleInsertCitation(app, defaultFormat = "intext") {
const settings = await ensureCredentials(app);
if (!settings) return null;
const queryResult = await app.prompt("Search Zotero Library", {
inputs: [
{
label: "Search query (Author, Title, or Keyword)",
placeholder: "e.g. attention is all you need, vaswani, 2017",
type: "string"
},
{
label: "Citation Format",
type: "select",
value: defaultFormat,
options: [
{ label: "In-text Citation (Author, Year)", value: "intext" },
{ label: "Amplenote Note Link [[Title|Author, Year]]", value: "notelink" },
{ label: "Full Bibliography Entry", value: "bib" },
{ label: "BibTeX Code Block", value: "bibtex" }
]
},
{
label: "Citation Style (for bibliography)",
type: "select",
value: settings.defaultStyle,
options: CITATION_STYLE_LABELS
}
]
});
if (!queryResult) return null;
const [query, selectedFormat, selectedStyle] = queryResult;
if (!query || !query.trim()) {
await app.alert("Please enter a valid search query.");
return null;
}
const client = new ZoteroClient({
userId: settings.userId,
apiKey: settings.apiKey,
libraryType: settings.libraryType
});
let items = [];
try {
items = await client.searchItems({ query: query.trim(), limit: 15 });
} catch (err) {
await app.alert(`Zotero Search Error:
 
${err.message}`);
return null;
}
if (!items || items.length === 0) {
await app.alert(`No Zotero references found matching "${query.trim()}".`);
return null;
}
let selectedItem = items[0];
if (items.length > 1) {
const itemOptions = items.map((it, idx) => {
const data = it.data || it;
const { shortCitationAuthor } = formatCreators(data);
const year = extractYear(data.date);
const title = data.title ? data.title.slice(0, 60) : "Untitled";
return {
label: `[${shortCitationAuthor}, ${year}] ${title}`,
value: idx
};
});
const selectResult = await app.prompt("Select Reference to Insert", {
inputs: [
{
label: `Found ${items.length} references:`,
type: "select",
value: 0,
options: itemOptions
}
]
});
if (selectResult === null || selectResult === void 0) return null;
const selectedIdx = Array.isArray(selectResult) ? selectResult[0] : selectResult;
selectedItem = items[selectedIdx] || items[0];
}
switch (selectedFormat) {
case "intext":
return formatInTextCitation(selectedItem);
case "notelink":
return formatNoteLinkCitation(selectedItem);
case "bibtex":
return `\`\`\`bibtex
${formatBibTeX(selectedItem)}
\`\`\``;
case "bib":
default: {
try {
const rawBib = await client.getFormattedBibliography(selectedItem.key, selectedStyle || settings.defaultStyle);
const mdBib = htmlBibToMarkdown(rawBib);
return mdBib || formatFallbackBibliography(selectedItem);
} catch {
return formatFallbackBibliography(selectedItem);
}
}
}
}
 
// anp-25-zotero/lib/template-engine.js
function formatLiteratureNote(item = {}, options = {}) {
const data = item.data || item;
const key = item.key || data.key || "";
const title = data.title || "Untitled Literature Note";
const { authors } = formatCreators(data);
const year = extractYear(data.date);
const citeKey = generateCitationKey(item);
const itemType = data.itemType || "journalArticle";
const typeIcon = ITEM_TYPE_ICONS[itemType] || "description";
const {
childNotes = [],
childAttachments = [],
renderedBib = "",
pdfMediaUrl = "",
collectionName = ""
} = options;
const sections = [];
sections.push(`# ${title}
`);
const metaRows = [
`| Attribute | Value |`,
`| :--- | :--- |`,
`| **Type** | \`${itemType}\` |`,
`| **Authors** | ${authors.length > 0 ? authors.join(", ") : "Unknown"} |`,
`| **Year** | ${year} |`,
`| **Cite Key** | \`${citeKey}\` |`
];
if (data.publicationTitle) metaRows.push(`| **Publication** | ${data.publicationTitle} |`);
if (data.publisher) metaRows.push(`| **Publisher** | ${data.publisher} |`);
if (data.volume) metaRows.push(`| **Volume** | ${data.volume} |`);
if (data.issue) metaRows.push(`| **Issue** | ${data.issue} |`);
if (data.pages) metaRows.push(`| **Pages** | ${data.pages} |`);
if (data.DOI) metaRows.push(`| **DOI** | [${data.DOI}](https://doi.org/${data.DOI}) |`);
if (data.url) metaRows.push(`| **URL** | [${data.url}](${data.url}) |`);
if (collectionName) metaRows.push(`| **Collection** | \`${collectionName}\` |`);
if (key) {
metaRows.push(`| **Zotero Link** | [Open in Zotero App](zotero://select/items/${key}) |`);
}
sections.push(metaRows.join("\n") + "\n");
const attachmentLinks = [];
if (key) {
attachmentLinks.push(`- \u{1F4C4} **Zotero PDF**: [Open PDF in Zotero](zotero://open-pdf/library/items/${key})`);
}
childAttachments.forEach((att) => {
const attData = att.data || att;
const attKey = att.key || attData.key;
const attTitle = attData.title || attData.filename || "Attachment";
if (attData.contentType === "application/pdf" || attData.filename?.endsWith(".pdf")) {
attachmentLinks.push(`- \u{1F4D1} **PDF File**: [${attTitle}](zotero://open-pdf/library/items/${attKey})`);
} else if (attData.url) {
attachmentLinks.push(`- \u{1F517} **Web Snapshot**: [${attTitle}](${attData.url})`);
}
});
if (pdfMediaUrl) {
attachmentLinks.push(`- \u{1F4E5} **Amplenote Local Media**: [View Attached File](${pdfMediaUrl})`);
}
if (attachmentLinks.length > 0) {
sections.push(`## \u{1F4CE} Attachments & Links
 
${attachmentLinks.join("\n")}
`);
}
if (data.abstractNote && data.abstractNote.trim()) {
sections.push(`## \u{1F4DD} Abstract
 
> ${data.abstractNote.trim().replace(/\n/g, "\n> ")}
`);
}
if (childNotes.length > 0) {
const notesContent = childNotes.map((n) => {
const nData = n.data || n;
const rawNote = nData.note || "";
const cleanNote = rawNote.replace(/<p>/gi, "").replace(/<\/p>/gi, "\n\n").replace(/<b>(.*?)<\/b>/gi, "**$1**").replace(/<strong>(.*?)<\/strong>/gi, "**$1**").replace(/<i>(.*?)<\/i>/gi, "*$1*").replace(/<em>(.*?)<\/em>/gi, "*$1*").replace(/<br\s*\/?>/gi, "\n").replace(/<[^>]+>/g, "").trim();
return cleanNote ? `### Note (${nData.dateModified ? nData.dateModified.slice(0, 10) : "Imported"})
 
${cleanNote}` : "";
}).filter(Boolean).join("\n\n");
if (notesContent) {
sections.push(`## \u{1F4A1} Zotero Notes & Highlights
 
${notesContent}
`);
}
}
const finalBib = renderedBib ? htmlBibToMarkdown(renderedBib) : formatFallbackBibliography(item);
sections.push(`## \u{1F4DA} Reference
 
${finalBib}
`);
sections.push(`## \u270D\uFE0F Key Takeaways & Synthesis
 
-
`);
return sections.join("\n");
}
function sanitizeTag(tag) {
if (!tag || typeof tag !== "string") return "";
return tag.trim().toLowerCase().replace(/[\\^#?*[\]()]/g, "").replace(/\s+/g, "-").replace(/\/+/g, "/").replace(/^\/+|\/+$/g, "");
}
function mapItemTags(itemData = {}, prefix = "-reports/-zotero", collectionName = "") {
const tags = /* @__PURE__ */ new Set();
const basePrefix = sanitizeTag(prefix) || "-reports/-zotero";
tags.add(basePrefix);
if (collectionName) {
const colTag = sanitizeTag(collectionName);
if (colTag) tags.add(`${basePrefix}/${colTag}`);
}
const rawTags = itemData.tags || [];
rawTags.forEach((t) => {
const tagStr = typeof t === "string" ? t : t.tag;
const clean = sanitizeTag(tagStr);
if (clean) {
tags.add(`${basePrefix}/${clean}`);
}
});
return Array.from(tags);
}
 
// anp-25-zotero/lib/attachment-manager.js
function buildZoteroUri(itemKey, action = "select") {
if (!itemKey) return "";
const cleanKey = String(itemKey).trim();
if (action === "open-pdf") {
return `zotero://open-pdf/library/items/${cleanKey}`;
}
return `zotero://select/items/${cleanKey}`;
}
function categorizeChildItems(children = []) {
const childNotes = [];
const childAttachments = [];
let pdfAttachment = null;
children.forEach((child) => {
const data = child.data || child;
if (data.itemType === "note") {
childNotes.push(child);
} else if (data.itemType === "attachment") {
childAttachments.push(child);
if (data.contentType === "application/pdf" || data.filename && data.filename.toLowerCase().endsWith(".pdf")) {
if (!pdfAttachment) pdfAttachment = child;
}
}
});
return { childNotes, childAttachments, pdfAttachment };
}
function blobToDataURL(blob) {
return new Promise((resolve, reject) => {
if (!blob) {
return reject(new Error("Cannot convert null or empty Blob to Data URL."));
}
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(new Error("Failed to read Blob as Data URL."));
reader.readAsDataURL(blob);
});
}
async function downloadAndAttachPDF(app, noteHandle, client, attachmentKey) {
if (!app || !app.attachNoteMedia || !noteHandle || !client || !attachmentKey) {
return null;
}
try {
const url = `${client.baseUrl}/items/${attachmentKey}/file`;
const response = await client.fetch(url, {
method: "GET",
headers: {
"Zotero-API-Key": client.apiKey,
"Zotero-API-Version": "3"
}
});
if (!response.ok) {
return null;
}
const blob = await response.blob();
const dataURL = await blobToDataURL(blob);
const mediaUrl = await app.attachNoteMedia(noteHandle, dataURL);
return mediaUrl;
} catch {
return null;
}
}
 
// anp-25-zotero/lib/sync-engine.js
async function syncItemToNote(app, client, item, settings, options = {}) {
const data = item.data || item;
const itemKey = item.key || data.key;
const rawTitle = data.title || "Untitled Literature Note";
const title = `[Zotero] ${rawTitle}`;
let childNotes = [];
let childAttachments = [];
let pdfAttachment = null;
try {
const children = await client.getItemChildren(itemKey);
const categorized = categorizeChildItems(children);
childNotes = categorized.childNotes;
childAttachments = categorized.childAttachments;
pdfAttachment = categorized.pdfAttachment;
} catch {
}
let renderedBib = "";
try {
renderedBib = await client.getFormattedBibliography(itemKey, settings.defaultStyle);
} catch {
}
const tagList = mapItemTags(data, settings.syncTagPrefix, options.collectionName);
let existingNote = null;
if (app.findNote) {
try {
existingNote = await app.findNote({ name: title });
} catch {
}
}
let noteHandle;
let created = false;
let updated = false;
if (existingNote && (existingNote.uuid || existingNote.name)) {
noteHandle = { uuid: existingNote.uuid };
updated = true;
} else {
noteHandle = await app.createNote(title, tagList);
created = true;
}
let pdfMediaUrl = "";
if (settings.autoAttachPDF && pdfAttachment && app.attachNoteMedia) {
const attKey = pdfAttachment.key || pdfAttachment.data?.key;
if (attKey) {
try {
const mediaUrl = await downloadAndAttachPDF(app, noteHandle, client, attKey);
if (mediaUrl) pdfMediaUrl = mediaUrl;
} catch {
}
}
}
const markdown = formatLiteratureNote(item, {
childNotes,
childAttachments,
renderedBib,
pdfMediaUrl,
collectionName: options.collectionName
});
if (app.replaceNoteContent) {
await app.replaceNoteContent(noteHandle, markdown);
}
if (app.addNoteTag) {
for (const tag of tagList) {
try {
await app.addNoteTag(noteHandle, tag);
} catch {
}
}
}
return { noteHandle, title, created, updated };
}
async function handleImportItemAsNote(app) {
const settings = await ensureCredentials(app);
if (!settings) return null;
const queryResult = await app.prompt("Import Zotero Reference to Note", {
inputs: [
{
label: "Search library (Title, Author, Year, or Keyword)",
placeholder: "e.g. attention is all you need, vaswani, 2017",
type: "string"
}
]
});
if (!queryResult) return null;
const query = Array.isArray(queryResult) ? queryResult[0] : queryResult;
if (!query || !query.trim()) {
await app.alert("Please enter a valid search query.");
return null;
}
const client = new ZoteroClient({
userId: settings.userId,
apiKey: settings.apiKey,
libraryType: settings.libraryType
});
let items = [];
try {
items = await client.searchItems({ query: query.trim(), limit: 15 });
} catch (err) {
await app.alert(`Search Error:
 
${err.message}`);
return null;
}
if (!items || items.length === 0) {
await app.alert(`No references found matching "${query.trim()}".`);
return null;
}
let selectedItem = items[0];
if (items.length > 1) {
const itemOptions = items.map((it, idx) => {
const data = it.data || it;
const { shortCitationAuthor } = formatCreators(data);
const year = extractYear(data.date);
const title = data.title ? data.title.slice(0, 60) : "Untitled";
return {
label: `[${shortCitationAuthor}, ${year}] ${title}`,
value: idx
};
});
const selectResult = await app.prompt("Select Reference to Import", {
inputs: [
{
label: `Found ${items.length} references:`,
type: "select",
value: 0,
options: itemOptions
}
]
});
if (selectResult === null || selectResult === void 0) return null;
const selectedIdx = Array.isArray(selectResult) ? selectResult[0] : selectResult;
selectedItem = items[selectedIdx] || items[0];
}
try {
const result = await syncItemToNote(app, client, selectedItem, settings);
await app.alert(
`Literature Note ${result.created ? "Created" : "Updated"} Successfully!
 
Note: ${result.title}`
);
return result.noteHandle.uuid || null;
} catch (err) {
await app.alert(`Failed to import reference:
 
${err.message}`);
return null;
}
}
async function handleSyncCollection(app) {
const settings = await ensureCredentials(app);
if (!settings) return null;
const client = new ZoteroClient({
userId: settings.userId,
apiKey: settings.apiKey,
libraryType: settings.libraryType
});
let collections = [];
try {
collections = await client.getCollections();
} catch (err) {
await app.alert(`Failed to fetch collections:
 
${err.message}`);
return null;
}
if (!collections || collections.length === 0) {
await app.alert("No collections found in your Zotero library.");
return null;
}
const collectionOptions = collections.map((c) => ({
label: `${c.data?.name || "Untitled Collection"} (${c.meta?.numItems || 0} items)`,
value: c.key
}));
const selectResult = await app.prompt("Sync Zotero Collection", {
inputs: [
{
label: "Select Collection to Synchronize",
type: "select",
value: collections[0].key,
options: collectionOptions
},
{
label: "Max items to import",
type: "select",
value: "25",
options: [
{ label: "10 items", value: "10" },
{ label: "25 items", value: "25" },
{ label: "50 items", value: "50" },
{ label: "100 items", value: "100" }
]
}
]
});
if (!selectResult) return null;
const [selectedKey, maxItemsStr] = selectResult;
const maxItems = parseInt(maxItemsStr || "25", 10);
const selectedCollection = collections.find((c) => c.key === selectedKey);
const collectionName = selectedCollection?.data?.name || "Collection";
let items = [];
try {
items = await client.getCollectionItems(selectedKey, { limit: maxItems });
} catch (err) {
await app.alert(`Failed to fetch collection items:
 
${err.message}`);
return null;
}
if (!items || items.length === 0) {
await app.alert(`No top-level items found in collection "${collectionName}".`);
return null;
}
let createdCount = 0;
let updatedCount = 0;
let failCount = 0;
for (const item of items) {
try {
const res = await syncItemToNote(app, client, item, settings, { collectionName });
if (res.created) createdCount++;
else if (res.updated) updatedCount++;
} catch {
failCount++;
}
}
await app.alert(
`Zotero Collection Sync Completed!
 
Collection: ${collectionName}
Total Items Processed: ${items.length}
Created Notes: ${createdCount}
Updated Notes: ${updatedCount}
` + (failCount > 0 ? `Errors/Skipped: ${failCount}
` : "") + `
Notes are tagged under: ${settings.syncTagPrefix}/${collectionName.toLowerCase().replace(/\s+/g, "-")}`
);
return { createdCount, updatedCount, failCount, total: items.length };
}
 
// anp-25-zotero/lib/search-embed.js
function renderReferenceExplorerHtml(app) {
return `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zotero Reference Explorer</title>
<style>
:root {
--bg: #1e1e24;
--card-bg: #282832;
--card-border: #3d3d4e;
--text: #e2e2e8;
--text-muted: #9a9ab0;
--accent: #5c7cfa;
--accent-hover: #4c6ef5;
--success: #51cf66;
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
body {
margin: 0;
padding: 16px;
font-family: var(--font);
background: var(--bg);
color: var(--text);
box-sizing: border-box;
}
.header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
}
.header h2 {
margin: 0;
font-size: 16px;
font-weight: 600;
letter-spacing: -0.2px;
}
.search-box {
display: flex;
gap: 8px;
margin-bottom: 16px;
}
input[type="text"] {
flex: 1;
padding: 8px 12px;
border-radius: 6px;
border: 1px solid var(--card-border);
background: var(--card-bg);
color: var(--text);
font-size: 13px;
outline: none;
transition: border-color 0.2s;
}
input[type="text"]:focus {
border-color: var(--accent);
}
button {
padding: 8px 14px;
border-radius: 6px;
border: none;
background: var(--accent);
color: #fff;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s;
}
button:hover {
background: var(--accent-hover);
}
.btn-secondary {
background: var(--card-bg);
border: 1px solid var(--card-border);
color: var(--text);
font-size: 12px;
padding: 5px 9px;
}
.btn-secondary:hover {
background: #343442;
}
.results {
display: flex;
flex-direction: column;
gap: 10px;
max-height: 480px;
overflow-y: auto;
}
.card {
background: var(--card-bg);
border: 1px solid var(--card-border);
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 6px;
}
.card-title {
font-size: 14px;
font-weight: 600;
color: #fff;
}
.card-meta {
font-size: 12px;
color: var(--text-muted);
}
.card-actions {
display: flex;
gap: 6px;
margin-top: 6px;
flex-wrap: wrap;
}
.status-msg {
font-size: 13px;
color: var(--text-muted);
text-align: center;
padding: 24px;
}
</style>
</head>
<body>
<div class="header">
<span>\u{1F4D6}</span>
<h2>Zotero Reference Explorer</h2>
</div>
 
<div class="search-box">
<input type="text" id="queryInput" placeholder="Search references (author, title, year)..." />
<button id="searchBtn">Search</button>
</div>
 
<div id="resultsContainer" class="results">
<div class="status-msg">Enter a search term above to explore your Zotero library.</div>
</div>
 
<script>
const queryInput = document.getElementById("queryInput");
const searchBtn = document.getElementById("searchBtn");
const resultsContainer = document.getElementById("resultsContainer");
 
async function doSearch() {
const q = queryInput.value.trim();
if (!q) return;
 
resultsContainer.innerHTML = '<div class="status-msg">Searching Zotero...</div>';
 
try {
const response = await window.callAmplenotePlugin({ action: "search", query: q });
renderResults(response || []);
} catch (err) {
resultsContainer.innerHTML = '<div class="status-msg" style="color:#ff6b6b">Error: ' + err.message + '</div>';
}
}
 
function renderResults(items) {
if (!items || items.length === 0) {
resultsContainer.innerHTML = '<div class="status-msg">No matching references found.</div>';
return;
}
 
resultsContainer.innerHTML = "";
items.forEach(item => {
const d = item.data || item;
const card = document.createElement("div");
card.className = "card";
 
const title = document.createElement("div");
title.className = "card-title";
title.textContent = d.title || "Untitled";
 
const meta = document.createElement("div");
meta.className = "card-meta";
const author = (d.creators && d.creators[0]) ? (d.creators[0].lastName || d.creators[0].name || "") : "Unknown";
const year = d.date ? d.date.slice(0, 4) : "n.d.";
meta.textContent = author + " (" + year + ") \xB7 " + (d.publicationTitle || d.itemType || "");
 
const actions = document.createElement("div");
actions.className = "card-actions";
 
const btnCite = document.createElement("button");
btnCite.className = "btn-secondary";
btnCite.textContent = "\u{1F4DD} Insert Cite";
btnCite.onclick = () => window.callAmplenotePlugin({ action: "insert_cite", item: item });
 
const btnBib = document.createElement("button");
btnBib.className = "btn-secondary";
btnBib.textContent = "\u{1F4DA} Insert Bib";
btnBib.onclick = () => window.callAmplenotePlugin({ action: "insert_bib", item: item });
 
const btnNote = document.createElement("button");
btnNote.className = "btn-secondary";
btnNote.textContent = "\u{1F4E5} Create Note";
btnNote.onclick = () => window.callAmplenotePlugin({ action: "create_note", item: item });
 
actions.appendChild(btnCite);
actions.appendChild(btnBib);
actions.appendChild(btnNote);
 
card.appendChild(title);
card.appendChild(meta);
card.appendChild(actions);
resultsContainer.appendChild(card);
});
}
 
searchBtn.addEventListener("click", doSearch);
queryInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") doSearch();
});
</script>
</body>
</html>`;
}
async function handleEmbedCall(app, payload = {}) {
const settings = await ensureCredentials(app);
if (!settings) {
throw new Error("Zotero setup required. Please configure your API key.");
}
const client = new ZoteroClient({
userId: settings.userId,
apiKey: settings.apiKey,
libraryType: settings.libraryType
});
const { action, query, item } = payload;
switch (action) {
case "search": {
if (!query) return [];
const items = await client.searchItems({ query, limit: 20 });
return items;
}
case "insert_cite": {
if (!item) return;
const citation = formatInTextCitation(item);
if (app.context && app.context.replaceSelection) {
await app.context.replaceSelection(citation);
} else if (app.insertNoteContent) {
await app.insertNoteContent({ uuid: app.context?.noteUUID }, citation);
}
return { success: true };
}
case "insert_bib": {
if (!item) return;
let bibText = "";
try {
const rawBib = await client.getFormattedBibliography(item.key || item.data?.key, settings.defaultStyle);
bibText = htmlBibToMarkdown(rawBib) || formatFallbackBibliography(item);
} catch {
bibText = formatFallbackBibliography(item);
}
if (app.context && app.context.replaceSelection) {
await app.context.replaceSelection(bibText);
}
return { success: true };
}
case "create_note": {
if (!item) return;
const res = await syncItemToNote(app, client, item, settings);
await app.alert(`Literature Note ready!
 
${res.title}`);
return { success: true, noteUUID: res.noteHandle?.uuid };
}
default:
return null;
}
}
 
// anp-25-zotero/zotero.js
var plugin = {
constants: DEFAULT_SETTINGS,
insertText: {
"Insert Citation": async (app) => {
return await handleInsertCitation(app, "intext");
},
"Insert Bibliography": async (app) => {
return await handleInsertCitation(app, "bib");
},
"Insert Note Link": async (app) => {
return await handleInsertCitation(app, "notelink");
},
"Insert BibTeX": async (app) => {
return await handleInsertCitation(app, "bibtex");
}
},
appOption: {
"Configure Zotero Settings": async (app) => {
return await promptConfigureSettings(app, false);
},
"Sync Zotero Collection": async (app) => {
return await handleSyncCollection(app);
},
"Import Zotero Reference to Note": async (app) => {
return await handleImportItemAsNote(app);
},
"Open Zotero Explorer": async (app) => {
if (app.openSidebarEmbed) {
return await app.openSidebarEmbed();
}
return await handleInsertCitation(app);
}
},
renderEmbed(app, ...args) {
return renderReferenceExplorerHtml(app);
},
async onEmbedCall(app, payload) {
return await handleEmbedCall(app, payload);
},
// Library utilities exposed for testing and programmatic invocation
ZoteroClient,
getPluginSettings,
promptConfigureSettings,
ensureCredentials,
formatInTextCitation,
formatNoteLinkCitation,
formatBibTeX,
formatFallbackBibliography,
formatLiteratureNote,
mapItemTags,
buildZoteroUri,
categorizeChildItems,
syncItemToNote
};
var zotero_default = plugin;
 
 
return zotero_default;
})()