Zotero Integration

setting

Zotero API key

setting

Zotero citation style

setting

Zotero citation format

name

Zotero Integration

setting

Zotero reference template

setting

Zotero highlight template

setting

Zotero sync filter

instructions

Bring your Zotero library into Amplenote: search it and drop citations into any note, and sync each item into its own note — reference, abstract, tags, your Zotero notes, and every highlight you made in Zotero's PDF reader.

Works with your online Zotero library (zotero.org), so Zotero desktop doesn't need to be open. Your library has to be syncing to zotero.org, which it does by default once you sign in to Zotero.

linkSet up (two minutes)

Create a Zotero API key at zotero.org/settings/keys → Create new private key.

Tick Allow library access and Allow notes access.

Leave Allow write access off. The plugin only reads from Zotero, and a read-only key is the safe choice: the key shows in plain text on the plugin's settings page.

To use a group library, also give the key read access to that group (or to all groups).

Run Zotero: Test connection (below for where to find it). The first time, it asks for your key: paste it in. The plugin checks it with Zotero, saves it, and answers with your Zotero username. If it says the key was rejected, check it was copied whole. (You can also paste the key into Settings → Plugins → Zotero Integration → Zotero API key yourself. Any action asks for it if it isn't there yet.)


Where the actions live. On desktop, type /zotero in any note: all seven actions are listed. On a phone, /zotero lists only Insert citation; for the other six — Test connection, Search citation, Sync now, Import item, Configure sync, Choose library — tap the search icon and type zotero.


linkCite as you write

Type /zotero where you want the citation and pick Zotero: Insert citation. Search by title, author or year, pick the reference from the list, and the citation replaces what you typed — right at the cursor.


It asks which format you want each time, unless you set one:

formatted — The in-text citation, in your citation style

bibliography — The full reference entry, in your citation style

pandoc — [@kahneman2011]

latex — \cite{kahneman2011}

biblatex — \autocite{kahneman2011}

bibtex — The full BibTeX entry, @book{kahneman2011, …}

The BibTeX entry comes from Zotero itself and uses the same cite key as the other formats, so [@kahneman2011] in your text matches the entry. With Search citation it's added as a code block.

To skip the question, put one of those words in the Zotero citation format setting. The citation style is Chicago unless you set Zotero citation style to another Zotero style id, such as apa, ieee or modern-language-association. Cite keys use the Citation Key field Better BibTeX fills in, if you use it; otherwise the plugin makes one from the first author and year.

Zotero: Search citation does the same search but adds the citation to the end of the note you have open, rather than at the cursor. Use Insert citation on a phone — see "On mobile".

linkSync your library into notes

Run Zotero: Sync now. Each Zotero item becomes its own Amplenote note, named after the item and tagged with its Zotero tags, laid out in four sections:

Reference — the formatted reference, abstract, a DOI (or source) link, and links to the item and its attachments on zotero.org.

Zotero Notes — the notes you wrote on the item in Zotero.

Highlights & Notes — every highlight, underline and note from Zotero's PDF reader, with its color, page, and comment.

My Notes — yours. Write anything here.


Run Sync now again whenever you want to catch up. After the first run it only fetches what changed in Zotero since last time, so it's quick. It's safe to stop and re-run.

What a re-sync changes, and what it leaves alone:

The Reference, Zotero Notes and Highlights & Notes sections are rewritten from Zotero, so edits you make inside those three are replaced next time the item changes in Zotero.

My Notes, and any section you add yourself, are never touched.

Tags are set once, when the note is first created. Tags you add or remove in Zotero later don't carry over.

Deleting an item in Zotero doesn't delete its note. The note just stops being updated.

Just one item? Run Zotero: Import item, search, and pick the reference. It gets the same note a sync would give it, without syncing anything else, and the plugin opens it. Run it again on the same item to update that note. From then on, Sync now keeps its highlights current too.

A note called Zotero Sync holds the plugin's bookkeeping. Leave it alone — editing it can make the next sync redo everything or skip items.

linkChoose what to sync

By default everything syncs. To sync only part of your library, run Zotero: Configure sync and tick the collections, tags and item types you want; leave everything unticked to go back to syncing the whole library. Changing the selection makes the next sync a full one, so newly included items come across.


Use Configure sync rather than typing into the Zotero sync filter setting — the setting holds the result, but the dialog writes it in the form the plugin reads.

linkGroup libraries

The plugin reads My Library unless you pick a group. Run Zotero: Choose library and pick one of the groups your key can see; search, sync and import then all use it. A public group you don't belong to won't be in that list: type its number (from its zotero.org/groups address) into the Zotero library setting instead. Switch back the same way. Notes from each library stay as they are when you switch — a sync only updates the library it's set to. The first sync after a switch is a full one, and a sync selection made for the other library is cleared, since its collections and tags belong to that library.

linkShape your notes with templates (optional)

Two optional settings change what goes inside a section. Leave them blank for the built-in layout. They don't change the section headings.

Zotero reference template — the Reference section.

Zotero highlight template — each highlight.

Write the template on one line, with \n wherever you want a line break — the setting field doesn't accept the Enter key. Any {{name}} from these lists is filled in:

Reference: {{title}} {{authors}} {{date}} {{bibliography}} {{citation}} {{abstract}} {{tags}} {{citeKey}} {{itemKey}} {{zoteroLink}} {{zoteroUrl}} {{attachmentLinks}} {{doi}} {{doiLink}} {{doiUrl}} {{publication}} {{volume}} {{issue}} {{pages}} {{publisher}} {{url}} {{itemType}}

Highlight: {{text}} {{comment}} {{color}} {{page}} {{type}} {{link}} {{linkUrl}}

Two rules make templates forgiving: a line whose fields are all empty is dropped, so **Abstract:** {{abstract}} disappears for an item with no abstract. A misspelled field is left as written, so a typo shows up in the note instead of silently vanishing.

To start from something, try:

**{{authors}}** ({{date}}) · `{{citeKey}}`\n{{bibliography}}\n**Abstract:** {{abstract}}\n**Tags:** {{tags}}\n{{zoteroLink}}
 
> {{text}} — p. {{page}} {{link}}\n**{{color}}** · {{comment}}
 

A template change shows on an item the next time sync rewrites it — when the item changes in Zotero, or on the full sync that follows a change to your sync selection. Sync doesn't rewrite items that haven't changed.

linkLinks back to Zotero

View in Zotero opens the item on zotero.org. Works everywhere, phone included.

Open in Zotero, next to each highlight, opens the PDF in Zotero desktop at that page. It needs Zotero installed on a computer. On a phone it does nothing — the Zotero Android app doesn't accept these links.

linkOn mobile

Everything works on a phone except two things:

Cite with /zotero (Insert citation). Search citation can't tell which note you have open when it's run from the phone's search, so it stops and tells you to use /zotero instead.

Open in Zotero links on highlights do nothing on a phone (above).

Sync now, Configure sync and Test connection work the same as on desktop. In the phone's plugin settings, the keyboard can cover the field you tapped; scroll the page up to see it.

On a phone, the citation results open as a list at the bottom of the screen:


Configure sync is one long list of checkboxes; scroll to the bottom for Submit:


Test connection, and a synced item note, on the phone:



linkPDFs

The plugin links to your PDFs; it doesn't copy them into Amplenote. Amplenote doesn't accept PDFs as plugin attachments, and Zotero's file storage can't be read from inside a plugin. Your highlights and notes come across in full regardless — they're stored as text, separately from the file.

linkIf something goes wrong

"Is … a valid Zotero style id" — the citation style setting isn't a style Zotero knows. Clear it to go back to Chicago.

An action is missing from the list — open the plugin's own note. A red "Plugin error" badge there means Amplenote couldn't load the plugin; reinstall it.

Source code on GitHub: https://github.com/tashreefshareef/amplenote-zotero-integration

description

Search your Zotero library, insert citations, and sync references, notes and highlights into Amplenote.

icon

menu_book

setting

Zotero library

entry: tashreefshareef/amplenote-zotero-integration/dist/plugin.js



linkCode block

(() => {
// Amplenote Zotero Integration - v0.1.0
// GENERATED FILE - do not edit. Edit src/ and run `npm run build`.
var __pluginModule=(()=>{var lt=Object.defineProperty;var ne=Object.getOwnPropertyDescriptor;var re=Object.getOwnPropertyNames;var oe=Object.prototype.hasOwnProperty;var ie=(t,e)=>{for(var n in e)lt(t,n,{get:e[n],enumerable:!0})},ae=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of re(e))!oe.call(t,r)&&r!==n&&lt(t,r,{get:()=>e[r],enumerable:!(o=ne(e,r))||o.enumerable});return t};var se=t=>ae(lt({},"__esModule",{value:!0}),t);var Ae={};ie(Ae,{default:()=>ke});var Et="https://api.zotero.org",St="3",W="Zotero API key",R="Zotero sync filter",x="chicago-note-bibliography",Q="Zotero library",D="Zotero citation style",xt="Zotero citation format",Nt="Zotero reference template",_t="Zotero highlight template",j="Zotero Sync",U="Zotero Sync State",tt="Reference",et="Zotero Notes",nt="Highlights & Notes",Ct="My Notes";function kt(t){return t.replace(/&#(\d+);/g,(e,n)=>String.fromCodePoint(Number(n))).replace(/&#x([0-9a-f]+);/gi,(e,n)=>String.fromCodePoint(parseInt(n,16))).replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&quot;/g,'"').replace(/&#0?39;/g,"'").replace(/&nbsp;/g," ")}function B(t){return t?kt(t.replace(/<[^>]*>/g,"")).replace(/\s+/g," ").trim():""}function At(t){if(!t)return"";let e=t.replace(/<br\s*\/?>/gi,`
`).replace(/<\/(p|div|li|h[1-6]|tr|blockquote)>/gi,`
`);return kt(e.replace(/<[^>]*>/g,"")).split(`
`).map(n=>n.replace(/[ \t]+/g," ").trim()).filter(Boolean).join(`
`)}function Ot(t,e=80){return!t||t.length<=e?t||"":`${t.slice(0,e-1).trimEnd()}\u2026`}function ut(t){return{key:t.key,version:t.version,title:t.data?.title||"(untitled)",abstract:t.data?.abstractNote||"",tags:(t.data?.tags||[]).map(e=>e.tag),citation:B(t.citation),bib:B(t.bib),url:t.links?.alternate?.href||null,citationKey:t.data?.citationKey||"",creators:t.data?.creators||[],date:t.data?.date||"",...ce(t.data||{})}}function ce(t){let e=t.DOI||((t.extra||"").match(/^DOI:\s*(\S+)/im)||[])[1]||"";return{itemType:t.itemType||"",doi:e,publication:t.publicationTitle||t.bookTitle||t.proceedingsTitle||t.websiteTitle||t.blogTitle||t.forumTitle||t.programTitle||"",volume:t.volume||"",issue:t.issue||"",pages:t.pages||"",publisher:t.publisher||t.repository||t.university||t.institution||"",sourceUrl:t.url||""}}var H=class extends Error{constructor(e,n){super(`Zotero API error: HTTP ${e}`),this.name="ZoteroApiError",this.status=e,this.body=n}},F=class extends Error{constructor(e,n){super(`Zotero API rate limited: HTTP ${e}, retry after ${n}s`),this.name="ZoteroRateLimitedError",this.status=e,this.retryAfterSeconds=n}};function S(t,{style:e}={}){return t instanceof F?`Zotero rate-limited this request \u2014 retry in ${t.retryAfterSeconds}s.`:t instanceof H?t.status===403?"Zotero rejected the API key (HTTP 403). Check it's current and still has library access.":e&&e!==x&&t.status>=500?`Zotero couldn't render the citation (HTTP ${t.status}). Is "${e}" a valid Zotero style id? That's what this status usually means.`:t.status>=500?`Zotero's server errored (HTTP ${t.status}). Worth retrying in a moment.`:`Zotero API error (HTTP ${t.status}).`:`Could not reach Zotero: ${t.message}`}var rt=t=>t===null||t===""?null:Number(t);function le(t){if(!t)return{};let e={};for(let n of t.split(",")){let o=n.match(/<([^>]+)>\s*;\s*rel="([^"]+)"/);o&&(e[o[2]]=o[1])}return e}var ue=t=>new Promise(e=>setTimeout(e,t*1e3));function ft({apiKey:t,userID:e,groupID:n,fetchImpl:o=typeof fetch=="function"?fetch:void 0,sleepImpl:r=ue}={}){if(!o)throw new Error("createZoteroClient: no fetch implementation available");async function i(u,{method:d="GET",params:g={},sinceVersion:T,ifModifiedSinceVersion:$}={}){let l=new URL(u,Et);for(let[k,Y]of Object.entries(g))Y!=null&&l.searchParams.set(k,String(Y));T!==void 0&&l.searchParams.set("since",String(T));let I={"Zotero-API-Version":St};t&&(I["Zotero-API-Key"]=t),$!==void 0&&(I["If-Unmodified-Since-Version"]=String($));let m=await o(l,{method:d,headers:I}),E=m.headers.get("Backoff"),O=m.headers.get("Retry-After"),G=rt(E);if(m.status===429||m.status===503)throw new F(m.status,rt(O)??G??1);if(!m.ok&&m.status!==304){let k;try{k=await m.text()}catch{k=null}throw new H(m.status,k)}return{status:m.status,notModified:m.status===304,data:m.status===304?null:await m.json(),lastModifiedVersion:rt(m.headers.get("Last-Modified-Version")),totalResults:rt(m.headers.get("Total-Results")),links:le(m.headers.get("Link")),backoffSeconds:G}}async function a(u,{params:d={},sinceVersion:g,pageSize:T=50}={}){let $=[],l=u,I={limit:T,...d},m=null;for(;l;){let E=await i(l,{params:I,sinceVersion:g});$.push(...E.data),m=E.lastModifiedVersion,E.backoffSeconds&&await r(E.backoffSeconds),l=E.links.next??null,I={},g=void 0}return{items:$,lastModifiedVersion:m}}async function s(){let u=await i("/keys/current");return c===null&&u.data?.userID!==void 0&&(c=u.data.userID),u.data}let c=e??null,f=null;async function h(){return c!==null?c:(f||(f=s().then(u=>(c=u.userID,f=null,c))),f)}async function p(){return n?`/groups/${n}`:`/users/${await h()}`}async function _(){let u=await h(),{items:d}=await a(`/users/${u}/groups`);return d.map(g=>({id:g.id,name:g.data?.name||`Group ${g.id}`}))}async function y({query:u,style:d=x,limit:g=25}={}){let T=await p();return(await i(`${T}/items/top`,{params:{q:u,qmode:"titleCreatorYear",itemType:"-attachment",include:"data,citation,bib",style:d,limit:g}})).data.map(l=>({key:l.key,title:l.data?.title||"(untitled)",citation:B(l.citation),bib:B(l.bib),citationKey:l.data?.citationKey||"",creators:l.data?.creators||[],date:l.data?.date||""}))}async function C({sinceVersion:u,style:d=x,pageSize:g=50}={}){let T=await p(),{items:$,lastModifiedVersion:l}=await a(`${T}/items/top`,{params:{itemType:"-attachment",include:"data,citation,bib",style:d},sinceVersion:u,pageSize:g});return{lastModifiedVersion:l,items:$.map(ut)}}async function z(){let u=await p(),{items:d}=await a(`${u}/collections`,{params:{include:"data"}});return d.map(g=>({key:g.key,name:g.data?.name||"(untitled collection)"}))}async function Z(){let u=await p(),{items:d}=await a(`${u}/tags`);return[...new Set(d.map(g=>g.tag).filter(Boolean))].sort()}async function q(){let{items:u}=await a("/itemTypes");return u.filter(d=>d.itemType!=="attachment"&&d.itemType!=="note").map(d=>({itemType:d.itemType,name:d.localized||d.itemType}))}async function J({sinceVersion:u,collectionKeys:d=[],tagNames:g=[],itemTypes:T=[],style:$=x,pageSize:l=50}={}){let I=await p(),m={itemType:"-attachment",include:"data,citation,bib",style:$},E=new Map,O=null,G=async(k,Y)=>{let ct=await a(k,{params:Y,sinceVersion:u,pageSize:l});ct.lastModifiedVersion!==null&&(O=Math.max(O??0,ct.lastModifiedVersion));for(let $t of ct.items)E.set($t.key,$t)};for(let k of d)await G(`${I}/collections/${k}/items/top`,m);return g.length&&await G(`${I}/items/top`,{...m,tag:g.join(" || ")}),T.length&&await G(`${I}/items/top`,{include:"data,citation,bib",style:$,itemType:T.join(" || ")}),{lastModifiedVersion:O,items:[...E.values()].map(ut)}}async function v(u){let d=await p(),{items:g}=await a(`${d}/items/${u}/children`,{params:{include:"data"}});return g}async function b(u){let d=await v(u),g=d.filter(l=>l.data?.itemType==="attachment"),T=[];for(let l of g){let I=await v(l.key);for(let m of I){if(m.data?.itemType!=="annotation")continue;let E=null;try{let O=JSON.parse(m.data.annotationPosition||"{}");Number.isInteger(O.pageIndex)&&(E=O.pageIndex)}catch{}T.push({key:m.key,attachmentKey:l.key,groupID:n??null,type:m.data.annotationType||"",text:m.data.annotationText||"",comment:m.data.annotationComment||"",color:(m.data.annotationColor||"").toLowerCase(),pageLabel:m.data.annotationPageLabel||"",pageIndex:E,sortIndex:m.data.annotationSortIndex||""})}}T.sort((l,I)=>l.sortIndex.localeCompare(I.sortIndex));let $=d.filter(l=>l.data?.itemType==="note").map(l=>({key:l.key,text:At(l.data.note||"")})).filter(l=>l.text);return{attachments:g.map(l=>({key:l.key,title:l.data?.title||"Attachment",url:l.links?.alternate?.href||null})),annotations:T,notes:$}}async function w(u){let d=await p();return{title:(await i(`${d}/items/${u}`,{params:{include:"data"}})).data?.data?.title||"(untitled)"}}async function A(u,{style:d=x}={}){let g=await p(),T=await i(`${g}/items/${u}`,{params:{include:"data,citation,bib",style:d}});return ut(T.data)}async function X(u){let d=await p();return((await i(`${d}/items/${u}`,{params:{include:"bibtex"}})).data?.bibtex||"").trim()}return{request:i,paginate:a,keysCurrent:s,searchItems:y,syncItems:C,getItemExtras:b,getItem:w,getSyncItem:A,getBibtex:X,listGroups:_,listCollections:z,listTags:Z,listItemTypes:q,syncFilteredItems:J}}function Lt(t){let e=(t||"").match(/\d+/);return e?{type:"group",id:Number(e[0])}:{type:"user"}}function Zt(t){return t?`Group ${t.id}: ${t.name}`:""}function P(t){return t.type==="group"?`group:${t.id}`:"user"}function ot(t,e){return t.type!=="group"?"My Library":e?`group "${e}"`:`group ${t.id}`}function fe(t){return Array.isArray(t)?t[0]:t}async function de(t){let e=await t.prompt("Connect Zotero. Create a key at zotero.org/settings/keys (Create new private key): tick Allow library access and Allow notes access, leave write access off. Paste it here.",{inputs:[{label:"Zotero API key",type:"secureText"}]});if(e===null)return null;let n=(fe(e)||"").trim();if(!n)return null;let o;try{o=await ft({apiKey:n}).keysCurrent()}catch(r){return await t.alert(S(r)),null}try{await t.setSetting(W,n)}catch(r){return await t.alert(`Connected as ${o.username}, but the key couldn't be saved (${r.message}). Paste it into the "${W}" setting so you aren't asked again.`),{apiKey:n,keyInfo:o}}return await t.alert(`Connected as ${o.username}. Your key is saved.`),{apiKey:n,keyInfo:o}}async function N(t){let e=(t.settings[W]||"").trim(),n=null;if(!e){let i=await de(t);if(!i)return null;({apiKey:e,keyInfo:n}=i)}let o=Lt(t.settings[Q]),r=ft({apiKey:e,userID:n?.userID,groupID:o.type==="group"?o.id:void 0});return{apiKey:e,keyInfo:n,library:o,client:r}}async function Rt(t){let e=await N(t);if(!e)return;let{client:n,library:o}=e;try{let{userID:r,username:i,access:a}=e.keyInfo||await n.keysCurrent(),s=a?.user?Object.entries(a.user).filter(([,f])=>f).map(([f])=>f).join(", ")||"none":"unknown",c="";if(o.type==="group"){let h=(await n.listGroups()).find(p=>p.id===o.id);c=h?` Library: ${ot(o,h.name)}.`:` Library: group ${o.id}, which isn't one of your key's groups \u2014 it works only if the group is public.`}await t.alert(`Connected as ${i} (userID ${r}). Access: ${s}.${c}`)}catch(r){r instanceof F?await t.alert(`Zotero rate-limited this request \u2014 retry in ${r.retryAfterSeconds}s.`):r instanceof H?await t.alert(`Zotero API key rejected (HTTP ${r.status}). Check the key is current.`):await t.alert(`Could not reach Zotero: ${r.message}`)}}var it=["formatted","bibliography","pandoc","latex","biblatex","bibtex"];function dt(t){let e=(t||"").trim().toLowerCase();return it.includes(e)?e:"formatted"}function ye(t){return(t||"").normalize("NFKD").replace(/[^\x00-\x7F]/g,"").replace(/[^a-z0-9]/gi,"").toLowerCase()}function M(t){if(t.citationKey)return t.citationKey;let e=(t.creators||[]).find(r=>r.lastName||r.name),n=ye(e?e.lastName||e.name:(t.title||"").split(/\s+/)[0]),o=((t.date||"").match(/\d{4}/)||[""])[0];return`${n}${o}`||t.key}function me(t,e){return(t||"").replace(/^(\s*@\w+\s*\{)[^,]*,/,`$1${M(e)},`)}function Pt(t,e){switch(dt(e)){case"bibtex":return me(t.bibtex,t);case"bibliography":return t.bib||t.citation;case"pandoc":return`[@${M(t)}]`;case"latex":return`\\cite{${M(t)}}`;case"biblatex":return`\\autocite{${M(t)}}`;default:return t.citation}}function yt(t){return Array.isArray(t)?t[0]:t}function ge(t,e){switch(t){case"bibliography":return"Bibliography entry";case"pandoc":return"Pandoc [@key]";case"latex":return"LaTeX \\cite{key}";case"biblatex":return"BibLaTeX \\autocite{key}";case"bibtex":return"BibTeX entry @article{key, ...}";default:return`Formatted citation (${e})`}}async function mt(t,e,n){let o=await t.prompt("Search Zotero",{inputs:[{label:"Search",type:"string"}]});if(o===null)return null;let r=(yt(o)||"").trim();if(!r)return null;let i;try{i=await e.searchItems({query:r,style:n})}catch(s){return await t.alert(S(s,{style:n})),null}if(!i.length)return await t.alert(`No Zotero items matched "${r}".`),null;let a=await t.prompt(`Results for "${r}"`,{inputs:[{label:"Reference",type:"select",options:i.map(s=>({label:Ot(s.citation||s.title),value:s.key}))}]});return a===null?null:i.find(s=>s.key===yt(a))||null}async function Gt(t){let e=await N(t);if(!e)return null;let{client:n}=e,o=(t.settings[D]||"").trim()||x,r=(t.settings[xt]||"").trim().toLowerCase(),i=it.includes(r)?r:null,a=await mt(t,n,o);if(!a)return null;let s=i;if(!s){let c=await t.prompt("Insert as",{inputs:[{label:"Format",type:"select",options:it.map(f=>({label:ge(f,o),value:f}))}]});if(c===null)return null;s=dt(yt(c))}if(s==="bibtex"){try{a.bibtex=await n.getBibtex(a.key)}catch(c){return await t.alert(S(c)),null}if(!a.bibtex)return await t.alert(`Zotero returned no BibTeX entry for "${a.title}".`),null}return{...a,format:s,text:Pt(a,s)}}async function Dt(t){let e=await Gt(t);return e?e.text:""}async function Ut(t){let e=t.context?.noteUUID;if(!e){await t.alert("No open note to add the citation to. Open a note and type /zotero to insert a citation at the cursor.");return}let n=await Gt(t);if(n){if(n.format==="bibtex"){await t.insertNoteContent({uuid:e},"```bibtex\n"+n.text+"\n```\n",{atEnd:!0}),await t.alert(`Appended the BibTeX entry for "${n.title}".`);return}await t.insertNoteContent({uuid:e},`${n.text}
`,{atEnd:!0}),await t.alert(`Appended: ${n.text}`)}}function V(t,e){return t.split(`
`).filter(n=>/^#{1,6}\s/.test(n)&&n.replace(/^#{1,6}\s+/,"").trim()===e).length}function Ht(t,e){let n=t.split(`
`),o=n.findIndex(i=>/^#{1,6}\s/.test(i)&&i.replace(/^#{1,6}\s+/,"").trim()===e);if(o===-1)return null;let r=n.length;for(let i=o+1;i<n.length;i++)if(/^#{1,6}\s/.test(n[i])){r=i;break}return n.slice(o+1,r).join(`
`)}async function L(t,e,n,o,{headingLevel:r="#",noteLabel:i=e}={}){let a=await t.getNoteContent({uuid:e});if(a==null)throw new Error(`${i} (uuid ${e}) could not be read \u2014 it may no longer exist.`);let s=V(a,n);if(s>1)throw new Error(`${i} has ${s} "${n}" sections \u2014 delete the extra one before syncing again.`);if(s===0){await t.insertNoteContent({uuid:e},`
${r} ${n}
${o}`,{atEnd:!0});return}await t.replaceNoteContent({uuid:e},o,{section:{heading:{text:n}}})}function he(t){let e=t.match(/```(?:json)?\n([\s\S]*?)\n```/);return e?e[1]:null}function pe(t){return`
Do not edit by hand \u2014 this section stores this plugin's sync bookkeeping as JSON.
 
\`\`\`json
${JSON.stringify({libraryVersion:t.libraryVersion,items:t.items,filterSignature:t.filterSignature,library:t.library},null,2)}
\`\`\`
`}async function at(t){let e=await t.findNote({name:j});if(!e)return{noteUUID:null,libraryVersion:null,items:{}};let n=await t.getNoteContent({uuid:e.uuid});if(n==null)return{noteUUID:e.uuid,libraryVersion:null,items:{}};let o=V(n,U);if(o>1)throw new Error(`The "${j}" note has ${o} "${U}" sections \u2014 delete the extra one before syncing again.`);let r=o===1?Ht(n,U):null,i=r?he(r):null,a=null;if(i)try{a=JSON.parse(i)}catch{a=null}return{noteUUID:e.uuid,libraryVersion:a?.libraryVersion??null,items:a?.items??{},filterSignature:a?.filterSignature,library:a?.library}}async function st(t,e){let n=pe(e);if(!e.noteUUID){let o=await t.createNote(j);await t.insertNoteContent({uuid:o},`# ${U}
${n}`,{atEnd:!0}),e.noteUUID=o;return}await L(t,e.noteUUID,U,n,{noteLabel:`The "${j}" note`})}function gt(t){return t.split(",").map(e=>e.trim()).filter(Boolean)}function Ft(t){let e=[],n=[],o=[];for(let r of(t||"").split(`
`)){let i=r.match(/^\s*Collections?:\s*(.+)$/i),a=r.match(/^\s*Tags?:\s*(.+)$/i),s=r.match(/^\s*Categor(?:y|ies):\s*(.+)$/i);i?e.push(...gt(i[1])):a?n.push(...gt(a[1])):s&&o.push(...gt(s[1]))}return{collections:e,tags:n,categories:o}}function Mt({collections:t=[],tags:e=[],categories:n=[]}){let o=[];return t.length&&o.push(`Collections: ${t.join(", ")}`),e.length&&o.push(`Tags: ${e.join(", ")}`),n.length&&o.push(`Categories: ${n.join(", ")}`),o.join(`
`)}function vt(t){return JSON.stringify({collections:[...t.collections].sort(),tags:[...t.tags].sort(),categories:[...t.categories||[]].sort()})}function be(t){let e=/^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(t.trim());if(!e)return null;let n=parseInt(e[1],16)/255,o=parseInt(e[2],16)/255,r=parseInt(e[3],16)/255,i=Math.max(n,o,r),a=Math.min(n,o,r),s=(i+a)/2;if(i===a)return{h:0,s:0,l:s*100};let c=i-a,f=s>.5?c/(2-i-a):c/(i+a),h;return i===n?h=(o-r)/c+(o<r?6:0):i===o?h=(r-n)/c+2:h=(n-o)/c+4,{h:h*60,s:f*100,l:s*100}}function ht(t){let e=t?be(t):null;if(!e)return"";let{h:n,s:o,l:r}=e;return r<12?"Black":r>98?"White":o<2?"Gray":n<15?"Red":n<45?"Orange":n<65?"Yellow":n<170?"Green":n<190?"Cyan":n<255?"Blue":n<280?"Purple":n<335?"Magenta":"Red"}var Yt=/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g;function Te(t){return(t||"").replace(/\\n/g,`
`)}function pt(t,e){let n=Te(t);return n.trim()?n.split(`
`).map(r=>{let i=[...r.matchAll(Yt)].map(s=>s[1]),a=i.filter(s=>Object.prototype.hasOwnProperty.call(e,s));return a.length&&a.length===i.length&&a.every(c=>!String(e[c]??"").trim())?null:r.replace(Yt,(s,c)=>Object.prototype.hasOwnProperty.call(e,c)?String(e[c]??""):s)}).filter(r=>r!==null).join(`
`).replace(/\n{3,}/g,`
 
`).trim()+`
`:""}function Bt(t){if(!t.attachmentKey)return"";let e=Number.isInteger(t.pageIndex)?`?page=${t.pageIndex+1}`:"";return`zotero://open-pdf/${t.groupID?`groups/${t.groupID}`:"library"}/items/${t.attachmentKey}${e}`}function Vt(t){let e=Bt(t);return e?`[Open in Zotero](${e})`:""}async function we(t,e,n,o){if(!e.collections.length&&!e.tags.length&&!e.categories.length)return t.syncItems({sinceVersion:n,style:o});let r=[];if(e.collections.length){let a=await t.listCollections(),s=e.collections.map(c=>c.toLowerCase());r=a.filter(c=>s.includes(c.name.toLowerCase())).map(c=>c.key)}let i=[];if(e.categories.length){let a=await t.listItemTypes(),s=e.categories.map(c=>c.toLowerCase());i=a.filter(c=>s.includes(c.name.toLowerCase())).map(c=>c.itemType)}return t.syncFilteredItems({sinceVersion:n,collectionKeys:r,tagNames:e.tags,itemTypes:i,style:o})}function Ie(t){let e=Vt(t);return{text:t.text||"",comment:t.comment||"",color:ht(t.color),page:t.pageLabel||"",type:t.type||"",link:e,linkUrl:Bt(t)}}function $e(t,e){if(e&&e.trim())return pt(e,Ie(t));let n=t.pageLabel?` (p. ${t.pageLabel})`:"",o=ht(t.color),r=o?`**(${o})** `:"",i=Vt(t),a=i?` \u2014 ${i}`:"";if(t.text){let s=`> ${r}${t.text}${n}${a}
`;return t.comment?`${s}
_Comment: ${t.comment}_
`:s}return t.comment?`**Note${n}:** ${r}${t.comment}${a}
`:`_${t.type||"annotation"}${n}_${a}
`}function bt(t,e){return t.length?t.map(n=>$e(n,e)).join(`
`):`_No highlights or notes yet._
`}function Ee(t,e){let n=e.attachments.filter(r=>r.url).map(r=>`[View "${r.title}" in Zotero](${r.url})`).join(`
`),o=(t.creators||[]).map(r=>[r.firstName,r.lastName].filter(Boolean).join(" ")||r.name||"").filter(Boolean).join(", ");return{title:t.title||"",bibliography:t.bib||"",citation:t.citation||"",abstract:t.abstract||"",authors:o,date:t.date||"",tags:(t.tags||[]).join(", "),itemKey:t.key||"",citeKey:M(t),zoteroUrl:t.url||"",zoteroLink:t.url?`[View in Zotero](${t.url})`:"",attachmentLinks:n,itemType:t.itemType||"",doi:t.doi||"",doiUrl:Kt(t),doiLink:zt({doi:t.doi}),publication:t.publication||"",volume:t.volume||"",issue:t.issue||"",pages:t.pages||"",publisher:t.publisher||"",url:t.sourceUrl||""}}var Kt=t=>t.doi?`https://doi.org/${t.doi}`:"";function zt(t){return t.doi?`[DOI: ${t.doi}](${Kt(t)})`:t.sourceUrl?`[Source](${t.sourceUrl})`:""}function qt(t,e,n){if(n&&n.trim())return pt(n,Ee(t,e));let o=[t.bib||t.citation||t.title];t.abstract&&o.push("",t.abstract);let r=zt(t);r&&o.push("",r),t.url&&o.push("",`[View in Zotero](${t.url})`);for(let i of e.attachments)i.url&&o.push("",`[View "${i.title}" in Zotero](${i.url})`);return o.join(`
`)+`
`}function Tt(t){return t.length?t.map(e=>e.text).join(`
 
`)+`
`:`_No Zotero notes._
`}var Se=`_Anything you write in this section is yours \u2014 sync never touches it._
`;function jt(t,e,n={}){return[`## ${tt}`,"",qt(t,e,n.reference).trim(),"",`## ${et}`,"",Tt(e.notes).trim(),"",`## ${nt}`,"",bt(e.annotations,n.highlight).trim(),"",`## ${Ct}`,"",Se.trim(),""].join(`
`)}var xe={attachments:[],annotations:[],notes:[]};async function Ne(t,e){try{return await t.getItemExtras(e)}catch{return xe}}async function Jt(t,e,n){if(await t.findNote({uuid:e.noteUUID}))return e.noteUUID;if(!n)return null;let o=await t.findNote({name:n});return o?(e.noteUUID=o.uuid,o.uuid):null}async function Xt(t,e){let n=await t.getNoteContent({uuid:e});return!!n&&V(n,tt)>0}var K=t=>({headingLevel:"##",noteLabel:t});function wt(t){return{reference:t.settings[Nt]||"",highlight:t.settings[_t]||""}}async function It(t,e,n,o,r,i="user"){let a=o.items[n.key],s=await Ne(e,n.key);if(a){let f=await Jt(t,a,n.title);if(!f)throw new Error(`note ${a.noteUUID} no longer exists, and no note named "${n.title}" was found to recover it`);if(await Xt(t,f)){let h=`"${n.title}"`;await L(t,f,tt,qt(n,s,r.reference),K(h)),await L(t,f,et,Tt(s.notes),K(h)),await L(t,f,nt,bt(s.annotations,r.highlight),K(h))}else await t.replaceNoteContent({uuid:f},jt(n,s,r));return a.title=n.title,{status:"updated",uuid:f}}let c=await t.createNote(n.title,n.tags);return await t.insertNoteContent({uuid:c},jt(n,s,r),{atEnd:!0}),o.items[n.key]={noteUUID:c,title:n.title},i!=="user"&&(o.items[n.key].library=i),{status:"created",uuid:c}}async function Wt(t){let e=await N(t);if(!e)return;let{client:n,library:o}=e,r=P(o),i;try{i=await at(t)}catch(b){await t.alert(b.message);return}let a=Ft(t.settings[R]),s=vt(a),c=i.filterSignature!==void 0&&i.filterSignature!==s,f=(i.library??"user")!==r,h=c||f?void 0:i.libraryVersion??void 0,p=(t.settings[D]||"").trim()||x,_=wt(t),y;try{y=await we(n,a,h,p)}catch(b){await t.alert(S(b,{style:p}));return}let C=0,z=0,Z=[],q=new Set;for(let b of y.items){q.add(b.key);try{let{status:w}=await It(t,n,b,i,_,r);w==="created"?C++:z++}catch(w){Z.push(`${b.title} (${w.message})`)}}let J=0;for(let[b,w]of Object.entries(i.items))if(!q.has(b)&&(w.library??"user")===r)try{if(!w.title)try{w.title=(await n.getItem(b)).title}catch{}let A=await Jt(t,w,w.title);if(!A){let d=w.title?`, and no note named "${w.title}" was found to recover it`:"";throw new Error(`note ${w.noteUUID} no longer exists${d}`)}let X=await n.getItemExtras(b),u=`The note for Zotero item ${b}`;await Xt(t,A)&&await L(t,A,et,Tt(X.notes),K(u)),await L(t,A,nt,bt(X.annotations,_.highlight),K(u)),J++}catch(A){Z.push(`highlights for ${b} (${A.message})`)}y.lastModifiedVersion!==null&&(i.libraryVersion=y.lastModifiedVersion),i.filterSignature=s,i.library=r;try{await st(t,i)}catch(b){await t.alert(b.message);return}let v=`Zotero sync complete: ${C} new, ${z} updated, ${J} highlights refreshed.`;await t.alert(Z.length?`${v} ${Z.length} failed: ${Z.join("; ")}`:v)}async function Qt(t){let e=await N(t);if(!e)return;let{client:n}=e,o,r,i;try{[o,r,i]=await Promise.all([n.listCollections(),n.listTags(),n.listItemTypes()])}catch(y){await t.alert(S(y));return}if(!o.length&&!r.length&&!i.length){await t.alert("No collections, tags, or categories found in your Zotero library \u2014 nothing to filter by.");return}let a=[...o.map(y=>({label:`Collection: ${y.name}`,type:"checkbox"})),...r.map(y=>({label:`Tag: ${y}`,type:"checkbox"})),...i.map(y=>({label:`Category: ${y.name}`,type:"checkbox"}))],s=await t.prompt("Choose what to sync \u2014 leave everything unchecked to sync your whole library",{inputs:a});if(s===null)return;let c=Array.isArray(s)?s:[s],f=o.filter((y,C)=>c[C]).map(y=>y.name),h=r.filter((y,C)=>c[o.length+C]).map(y=>y),p=i.filter((y,C)=>c[o.length+r.length+C]).map(y=>y.name),_=Mt({collections:f,tags:h,categories:p});try{await t.setSetting(R,_)}catch(y){await t.alert(`Could not save the setting: ${y.message}`);return}await t.alert(_?`Sync will now be limited to:
${_}`:"No filter set \u2014 sync will pull your whole library.")}async function te(t){let e=await N(t);if(!e)return;let{client:n,library:o}=e,r=(t.settings[D]||"").trim()||x,i=await mt(t,n,r);if(!i)return;let a;try{a=await at(t)}catch(f){await t.alert(f.message);return}let s;try{s=await n.getSyncItem(i.key,{style:r})}catch(f){await t.alert(S(f,{style:r}));return}let c;try{c=await It(t,n,s,a,wt(t),P(o)),await st(t,a)}catch(f){await t.alert(`Couldn't import "${s.title}": ${f.message}`);return}await t.alert(c.status==="created"?`Imported "${s.title}" into a new note.`:`Updated the note for "${s.title}".`),await t.navigate(`https://www.amplenote.com/notes/${c.uuid}`)}var _e="user";async function ee(t){let e=await N(t);if(!e)return;let{client:n,library:o}=e,r;try{r=await n.listGroups()}catch(y){await t.alert(S(y));return}if(!r.length){await t.alert("Your key can only see My Library. To use a group library, edit the key at zotero.org/settings/keys and give it read access to that group (or to all groups).");return}let i=await t.prompt("Which Zotero library should the plugin use?",{inputs:[{label:"Library",type:"select",options:[{label:"My Library",value:_e},...r.map(y=>({label:`Group: ${y.name}`,value:String(y.id)}))]}]});if(i===null)return;let a=String(Array.isArray(i)?i[0]:i),s=r.find(y=>String(y.id)===a)||null,c=s?{type:"group",id:s.id}:{type:"user"},f=P(c)!==P(o),h=!!(t.settings[R]||"").trim();try{await t.setSetting(Q,Zt(s)),f&&h&&await t.setSetting(R,"")}catch(y){await t.alert(`Could not save the setting: ${y.message}`);return}let p=ot(c,s?.name);if(!f){await t.alert(`Already using ${p}.`);return}let _=h?" Your sync filter was for the other library, so it's cleared \u2014 run Configure sync to set one here.":"";await t.alert(`Now using ${p}. The next Sync now is a full one.${_}`)}var Ce={name:"Zotero Integration",appOption:{"Zotero: Test connection":Rt,"Zotero: Search citation":Ut,"Zotero: Sync now":Wt,"Zotero: Import item":te,"Zotero: Configure sync":Qt,"Zotero: Choose library":ee},insertText:{"Zotero: Insert citation":Dt}},ke=Ce;return se(Ae);})();
 
var plugin = __pluginModule.default;
return plugin;
})()