www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

commit 0cd50b5560d5ef1e82ae7eb313f8de03d78f71df
parent fec3fa2a7273e5cf5d6fd6bb2514887530883ffc
Author: Dan Stillman <dan@danstillman.com>
Date:   Tue, 16 Jan 2018 09:11:21 -0500

Merge pull request #1242 from adomasven/feature/delay-updating-citatations

Refactor integration and delay citation updates
Diffstat:
Mchrome/content/zotero/bibliography.js | 24++++++++++++++++++++++--
Mchrome/content/zotero/integration/addCitationDialog.js | 22++++++++++------------
Mchrome/content/zotero/integration/editBibliographyDialog.js | 12++++++------
Mchrome/content/zotero/integration/integrationDocPrefs.xul | 8+++++++-
Mchrome/content/zotero/integration/quickFormat.js | 26+++++++++++++-------------
Mchrome/content/zotero/xpcom/cite.js | 8++++----
Mchrome/content/zotero/xpcom/integration.js | 3654+++++++++++++++++++++++++++++++++----------------------------------------------
Mchrome/content/zotero/xpcom/server_connector.js | 2+-
Mchrome/content/zotero/xpcom/utilities_internal.js | 376+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/locale/en-US/zotero/zotero.dtd | 4++++
Mchrome/locale/en-US/zotero/zotero.properties | 2++
Mtest/tests/integrationTest.js | 259++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
12 files changed, 2171 insertions(+), 2226 deletions(-)

diff --git a/chrome/content/zotero/bibliography.js b/chrome/content/zotero/bibliography.js @@ -167,10 +167,23 @@ var Zotero_File_Interface_Bibliography = new function() { document.getElementById("automaticJournalAbbreviations-checkbox").checked = true; } } + if (document.getElementById("delayCitationUpdates-checkbox")) { + if (_io.delayCitationUpdates) { + document.getElementById("delayCitationUpdates-checkbox").checked = true; + } + } // set style to false, in case this is cancelled _io.style = false; }); + + this.openHelpLink = function() { + var url = "https://www.zotero.org/support/word_processor_plugin_usage"; + var win = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator) + .getMostRecentWindow("navigator:browser"); + Zotero.launchURL(url); + }; /* * Called when locale is changed @@ -211,6 +224,13 @@ var Zotero_File_Interface_Bibliography = new function() { document.getElementById("automaticJournalAbbreviations-vbox").hidden = !selectedStyleObj.usesAbbreviation; } + // Hide the delayCitationUpdates checkbox before the prompt is shown + document.getElementById("delayCitationUpdates-vbox").hidden = _io.dontAskDelayCitationUpdates == undefined; + // Highlight delay citations checkbox after displaying the alert + // NOTE: Currently unused + if (_io.highlightDelayCitations) { + document.getElementById("delayCitationUpdates-vbox").style.border = "1px dashed #e52e2e" + } // // For bibliography.xul @@ -266,6 +286,7 @@ var Zotero_File_Interface_Bibliography = new function() { } _io.useEndnotes = document.getElementById("displayAs").selectedIndex; _io.fieldType = (document.getElementById("formatUsing").selectedIndex == 0 ? _io.primaryFieldType : _io.secondaryFieldType); + _io.delayCitationUpdates = document.getElementById("delayCitationUpdates-checkbox").checked; } // remember style and locale if user selected these explicitly @@ -283,8 +304,7 @@ var Zotero_File_Interface_Bibliography = new function() { document.documentElement.getButton('cancel').click(); var win = Zotero.Utilities.Internal.openPreferences('zotero-prefpane-cite', { tab: 'styles-tab' }); if (isDocPrefs) { - // TODO: Move activate() code elsewhere - Zotero.Integration.activate(win); + Zotero.Utilities.Internal.activate(win); } }; } diff --git a/chrome/content/zotero/integration/addCitationDialog.js b/chrome/content/zotero/integration/addCitationDialog.js @@ -59,10 +59,8 @@ var Zotero_Citation_Dialog = new function () { this.listItemSelected = listItemSelected; this.up = up; this.down = down; - this.add = add; this.remove = remove; this.setSortToggle = setSortToggle; - this.citationSortUnsort = citationSortUnsort; this.confirmRegenerate = confirmRegenerate; this.accept = accept; this.cancel = cancel; @@ -373,13 +371,13 @@ var Zotero_Citation_Dialog = new function () { /* * Adds an item to the multipleSources list */ - function add(first_item) { + this.add = Zotero.Promise.coroutine(function* (first_item) { var pos, len; var item = itemsView.getSelectedItems()[0]; // treeview from xpcom/itemTreeView.js if (!item) { - sortCitation(); + yield sortCitation(); _updateAccept(); _updatePreview(); return; @@ -412,11 +410,11 @@ var Zotero_Citation_Dialog = new function () { _citationList.ensureElementIsVisible(selectionNode); // allow user to press OK - selectionNode = sortCitation(selectionNode); + selectionNode = yield sortCitation(selectionNode); _citationList.selectItem(selectionNode); _updateAccept(); _updatePreview(); - } + }); /* * Deletes a citation from the multipleSources list @@ -446,11 +444,11 @@ var Zotero_Citation_Dialog = new function () { /* * Sorts preview citations, if preview is open. */ - function citationSortUnsort() { + this.citationSortUnsort = Zotero.Promise.coroutine(function* () { setSortToggle(); - sortCitation(); + yield sortCitation(); _updatePreview(); - } + }); /* * Sets the current sort toggle state persistently on the citation. @@ -468,7 +466,7 @@ var Zotero_Citation_Dialog = new function () { /* * Sorts the list of citations */ - function sortCitation(scrollToItem) { + var sortCitation = Zotero.Promise.coroutine(function* (scrollToItem) { if(!_sortCheckbox) return scrollToItem; if(!_sortCheckbox.checked) { io.citation.properties.unsorted = true; @@ -485,7 +483,7 @@ var Zotero_Citation_Dialog = new function () { // run preview function to re-sort, if it hasn't already been // run - io.sort(); + yield io.sort(); // add items back to list scrollToItem = null; @@ -502,7 +500,7 @@ var Zotero_Citation_Dialog = new function () { if(scrollToItem) _citationList.ensureElementIsVisible(scrollToItem); return scrollToItem; - } + }); /* * Ask whether to modifiy the preview diff --git a/chrome/content/zotero/integration/editBibliographyDialog.js b/chrome/content/zotero/integration/editBibliographyDialog.js @@ -74,8 +74,8 @@ var Zotero_Bibliography_Dialog = new function () { if(selectedItemIDs.length) { for (let itemID of selectedItemIDs) { var itemIndexToSelect = false; - for(var i in bibEditInterface.bibliography[0].entry_ids) { - if(bibEditInterface.bibliography[0].entry_ids[i].indexOf(itemID) !== -1) { + for(var i in bibEditInterface.bib[0].entry_ids) { + if(bibEditInterface.bib[0].entry_ids[i].indexOf(itemID) !== -1) { itemIndexToSelect = i; continue; } @@ -254,7 +254,7 @@ var Zotero_Bibliography_Dialog = new function () { */ function _getSelectedListItemIDs() { return Array.from(_itemList.selectedItems) - .map(item => bibEditInterface.bibliography[0].entry_ids[item.value][0]); + .map(item => bibEditInterface.bib[0].entry_ids[item.value][0]); } /** @@ -287,8 +287,8 @@ var Zotero_Bibliography_Dialog = new function () { editor.readonly = index === undefined; if(index !== undefined) { - var itemID = bibEditInterface.bibliography[0].entry_ids[index]; - editor.value = bibEditInterface.bibliography[1][index]; + var itemID = bibEditInterface.bib[0].entry_ids[index]; + editor.value = bibEditInterface.bib[1][index]; _lastSelectedIndex = index; _lastSelectedItemID = itemID; _lastSelectedValue = editor.value; @@ -304,7 +304,7 @@ var Zotero_Bibliography_Dialog = new function () { * loads items from itemSet */ function _loadItems() { - var itemIDs = bibEditInterface.bibliography[0].entry_ids; + var itemIDs = bibEditInterface.bib[0].entry_ids; var items = itemIDs.map(itemID => Zotero.Cite.getItem(itemID[0])); // delete all existing items from list diff --git a/chrome/content/zotero/integration/integrationDocPrefs.xul b/chrome/content/zotero/integration/integrationDocPrefs.xul @@ -31,10 +31,11 @@ <dialog id="zotero-doc-prefs-dialog" orient="vertical" - buttons="accept,cancel" + buttons="accept,cancel,help" title="&zotero.integration.docPrefs.title;" onload="Zotero_File_Interface_Bibliography.init();" ondialogaccept="Zotero_File_Interface_Bibliography.acceptSelection();" + ondialoghelp="Zotero_File_Interface_Bibliography.openHelpLink();" onclose="document.documentElement.cancelDialog(); event.preventDefault(); event.stopPropagation();" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" persist="screenX screenY" @@ -84,5 +85,10 @@ <checkbox id="automaticJournalAbbreviations-checkbox" label="&zotero.integration.prefs.automaticJournalAbbeviations.label;"/> <description class="radioDescription">&zotero.integration.prefs.automaticJournalAbbeviations.caption;</description> </vbox> + + <vbox id="delayCitationUpdates-vbox"> + <checkbox id="delayCitationUpdates-checkbox" label="&zotero.integration.prefs.delayCitationUpdates.label;" tooltiptext="&zotero.integration.prefs.delayCitationUpdates.tooltip;"/> + <description class="radioDescription">&zotero.integration.prefs.delayCitationUpdates.description;</description> + </vbox> </vbox> </dialog> \ No newline at end of file diff --git a/chrome/content/zotero/integration/quickFormat.js b/chrome/content/zotero/integration/quickFormat.js @@ -711,7 +711,7 @@ var Zotero_QuickFormat = new function () { /** * Converts the selected item to a bubble */ - function _bubbleizeSelected() { + var _bubbleizeSelected = Zotero.Promise.coroutine(function* () { if(!referenceBox.hasChildNodes() || !referenceBox.selectedItem) return false; var citationItem = {"id":referenceBox.selectedItem.getAttribute("zotero-item")}; @@ -734,11 +734,11 @@ var Zotero_QuickFormat = new function () { node.nodeValue = ""; var bubble = _insertBubble(citationItem, node); _clearEntryList(); - _previewAndSort(); + yield _previewAndSort(); _refocusQfe(); return true; - } + }); /** * Ignores clicks (for use on separators in the rich list box) @@ -902,13 +902,13 @@ var Zotero_QuickFormat = new function () { /** * Generates the preview and sorts citations */ - function _previewAndSort() { + var _previewAndSort = Zotero.Promise.coroutine(function* () { var shouldKeepSorted = keepSorted.hasAttribute("checked"), editorShowing = showEditor.hasAttribute("checked"); if(!shouldKeepSorted && !editorShowing) return; _updateCitationObject(); - io.sort(); + yield io.sort(); if(shouldKeepSorted) { // means we need to resort citations _clearCitation(); @@ -920,7 +920,7 @@ var Zotero_QuickFormat = new function () { _moveCursorToEnd(); } - } + }); /** * Shows the citation properties panel for a given bubble @@ -1071,7 +1071,7 @@ var Zotero_QuickFormat = new function () { /** * Handle return or escape */ - function _onQuickSearchKeyPress(event) { + var _onQuickSearchKeyPress = Zotero.Promise.coroutine(function* (event) { // Prevent hang if another key is pressed after Enter // https://forums.zotero.org/discussion/59157/ if (accepted) { @@ -1083,7 +1083,7 @@ var Zotero_QuickFormat = new function () { var keyCode = event.keyCode; if (keyCode === event.DOM_VK_RETURN) { event.preventDefault(); - if(!_bubbleizeSelected() && !_getEditorContent()) { + if(!(yield _bubbleizeSelected()) && !_getEditorContent()) { _accept(); } } else if(keyCode === event.DOM_VK_TAB || event.charCode === 59 /* ; */) { @@ -1162,7 +1162,7 @@ var Zotero_QuickFormat = new function () { } else { _resetSearchTimer(); } - } + }); /** * Adds a dummy element to make dragging work @@ -1190,7 +1190,7 @@ var Zotero_QuickFormat = new function () { /** * Replaces the dummy element with a node to make dropping work */ - function _onBubbleDrop(event) { + var _onBubbleDrop = Zotero.Promise.coroutine(function* (event) { event.preventDefault(); event.stopPropagation(); @@ -1208,9 +1208,9 @@ var Zotero_QuickFormat = new function () { keepSorted.removeAttribute("checked"); } - _previewAndSort(); + yield _previewAndSort(); _moveCursorToEnd(); - } + }); /** * Handle a click on a bubble @@ -1333,7 +1333,7 @@ var Zotero_QuickFormat = new function () { pane.selectItem(id); // Pull window to foreground - Zotero.Integration.activate(pane.document.defaultView); + Zotero.Utilities.Internal.activate(pane.document.defaultView); } /** diff --git a/chrome/content/zotero/xpcom/cite.js b/chrome/content/zotero/xpcom/cite.js @@ -16,13 +16,13 @@ Zotero.Cite = { * Remove specified item IDs in-place from a citeproc-js bibliography object returned * by makeBibliography() * @param {bib} citeproc-js bibliography object - * @param {Array} itemsToRemove Array of items to remove + * @param {Set} itemsToRemove Set of items to remove */ "removeFromBibliography":function(bib, itemsToRemove) { var removeItems = []; for(let i in bib[0].entry_ids) { for(let j in bib[0].entry_ids[i]) { - if(itemsToRemove[bib[0].entry_ids[i][j]]) { + if(itemsToRemove.has(`${bib[0].entry_ids[i][j]}`)) { removeItems.push(i); break; } @@ -303,7 +303,7 @@ Zotero.Cite = { session = Zotero.Integration.sessions[sessionID], item; if(session) { - item = session.embeddedZoteroItems[id.substr(slashIndex+1)]; + item = session.embeddedItems[id.substr(slashIndex+1)]; } if(!item) { @@ -526,7 +526,7 @@ Zotero.Cite.System.prototype = { } if(!zoteroItem) { - throw "Zotero.Cite.System.retrieveItem called on non-item "+item; + throw new Error("Zotero.Cite.System.retrieveItem called on non-item "+item); } var cslItem = Zotero.Utilities.itemToCSLJSON(zoteroItem); diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js @@ -24,9 +24,6 @@ ***** END LICENSE BLOCK ***** */ -const RESELECT_KEY_URI = 1; -const RESELECT_KEY_ITEM_KEY = 2; -const RESELECT_KEY_ITEM_ID = 3; const DATA_VERSION = 3; // Specifies that citations should only be updated if changed @@ -37,10 +34,6 @@ const FORCE_CITATIONS_REGENERATE = 1; // Specifies that citations should be reset regardless of whether formattedText has changed const FORCE_CITATIONS_RESET_TEXT = 2; -// this is used only for update checking -const INTEGRATION_PLUGINS = ["zoteroMacWordIntegration@zotero.org", - "zoteroOpenOfficeIntegration@zotero.org", "zoteroWinWordIntegration@zotero.org"]; - // These must match the constants in corresponding word plugins const DIALOG_ICON_STOP = 0; const DIALOG_ICON_WARNING = 1; @@ -54,24 +47,19 @@ const DIALOG_BUTTONS_YES_NO_CANCEL = 3; const NOTE_FOOTNOTE = 1; const NOTE_ENDNOTE = 2; +const INTEGRATION_TYPE_ITEM = 1; +const INTEGRATION_TYPE_BIBLIOGRAPHY = 2; +const INTEGRATION_TYPE_TEMP = 3; + +const DELAY_CITATIONS_PROMPT_TIMEOUT = 5/*seconds*/; +const DELAYED_CITATION_STYLING = "\\uldash"; +const DELAYED_CITATION_STYLING_CLEAR = "\\ulclear"; + + Zotero.Integration = new function() { Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/AddonManager.jsm"); - const INTEGRATION_MIN_VERSIONS = ["3.1.7.SOURCE", "3.5b2.SOURCE", "3.1.3.SOURCE"]; - - var _tmpFile = null; - var _osascriptFile; - - // these need to be global because of GC - var _updateTimer; - - // For Carbon and X11 - var _carbon, ProcessSerialNumber, SetFrontProcessWithOptions; - var _x11, _x11Display, _x11RootWindow, XClientMessageEvent, XFetchName, XFree, XQueryTree, - XOpenDisplay, XCloseDisplay, XFlush, XDefaultRootWindow, XInternAtom, XSendEvent, - XMapRaised, XGetWindowProperty, X11Atom, X11Bool, X11Display, X11Window, X11Status; - this.currentWindow = false; this.sessions = {}; @@ -126,8 +114,6 @@ Zotero.Integration = new function() { } catch(e) { Zotero.logError(e); } - - Zotero.Promise.delay(1000).then(_checkPluginVersions); } /** @@ -178,7 +164,7 @@ Zotero.Integration = new function() { var deletePipe = promptService.confirm(null, Zotero.getString("integration.error.title"), Zotero.getString("integration.error.deletePipe")); if(!deletePipe) return false; let escapedFifoFile = pipe.path.replace("'", "'\\''"); - _executeAppleScript("do shell script \"rmdir '"+escapedFifoFile+"'; rm -f '"+escapedFifoFile+"'\" with administrator privileges", true); + Zotero.Utilities.Internal.executeAppleScript("do shell script \"rmdir '"+escapedFifoFile+"'; rm -f '"+escapedFifoFile+"'\" with administrator privileges", true); if(pipe.exists()) return false; } catch(e) { Zotero.logError(e); @@ -194,42 +180,6 @@ Zotero.Integration = new function() { } }); - /** - * Checks to see that plugin versions are up to date. - * @return {Promise} Promise that is resolved with true if versions are up to date - * or with false if they are not. - */ - var _checkPluginVersions = new function () { - var integrationVersionsOK; - - return function _checkPluginVersions() { - if(integrationVersionsOK) { - if(integrationVersionsOK === true) { - return Zotero.Promise.resolve(integrationVersionsOK); - } else { - return Zotero.Promise.reject(integrationVersionsOK); - } - } - - var deferred = Zotero.Promise.defer(); - AddonManager.getAddonsByIDs(INTEGRATION_PLUGINS, function(addons) { - for(var i in addons) { - var addon = addons[i]; - if(!addon || addon.userDisabled) continue; - - if(Services.vc.compare(INTEGRATION_MIN_VERSIONS[i], addon.version) > 0) { - deferred.reject(integrationVersionsOK = new Zotero.Exception.Alert( - "integration.error.incompatibleVersion2", - [Zotero.version, addon.name, INTEGRATION_MIN_VERSIONS[i]], - "integration.error.title")); - } - } - deferred.resolve(integrationVersionsOK = true); - }); - return deferred.promise; - }; - } - this.getApplication = function(agent, command, docId) { // Try to load the appropriate Zotero component; otherwise display an error try { @@ -254,11 +204,11 @@ Zotero.Integration = new function() { this.execCommand = new function() { var inProgress; - return function execCommand(agent, command, docId) { - var document; + return Zotero.Promise.coroutine(function* execCommand(agent, command, docId) { + var document, session; - if(inProgress) { - Zotero.Integration.activate(); + if (inProgress) { + Zotero.Utilities.Internal.activate(); if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) { Zotero.Integration.currentWindow.focus(); } @@ -266,16 +216,29 @@ Zotero.Integration = new function() { return; } inProgress = true; - - // Check integration component versions - return _checkPluginVersions().then(function() { + Zotero.debug(`Integration: ${agent}-${command}${docId ? `:'${docId}'` : ''} invoked`) + + var startTime = (new Date()).getTime(); + + // Try to execute the command; otherwise display an error in alert service or word processor + // (depending on what is possible) + try { + // Word for windows throws RPC_E_CANTCALLOUT_ININPUTSYNCCALL if we invoke an OLE call in the + // current event loop (which.. who would have guessed would be the case?) + yield Zotero.Promise.delay(); var application = Zotero.Integration.getApplication(agent, command, docId); - // Try to execute the command; otherwise display an error in alert service or word processor - // (depending on what is possible) - document = (application.getDocument && docId ? application.getDocument(docId) : application.getActiveDocument()); - return Zotero.Promise.resolve((new Zotero.Integration.Document(application, document))[command]()); - }).catch(function(e) { + Zotero.Integration.currentDoc = document = (application.getDocument && docId ? application.getDocument(docId) : application.getActiveDocument()); + Zotero.Integration.currentSession = session = yield Zotero.Integration.getSession(application, document); + // TODO: this is pretty awful + session.fields = new Zotero.Integration.Fields(session, document); + session._doc = document; + // TODO: figure this out + // Zotero.Notifier.trigger('delete', 'collection', 'document'); + yield (new Zotero.Integration.Interface(application, document, session))[command](); + document.setDocumentData(session.data.serialize()); + } + catch (e) { if(!(e instanceof Zotero.Exception.UserCancelled)) { try { var displayError = null; @@ -303,7 +266,7 @@ Zotero.Integration = new function() { } if(showErrorInFirefox) { - Zotero.Integration.activate(); + Zotero.Utilities.Internal.activate(); Components.classes["@mozilla.org/embedcomp/prompt-service;1"] .getService(Components.interfaces.nsIPromptService) .alert(null, Zotero.getString("integration.error.title"), displayError); @@ -312,10 +275,15 @@ Zotero.Integration = new function() { } finally { Zotero.logError(e); } + } else { + // If user cancels we should still write the currently assigned session ID + document.setDocumentData(session.data.serialize()); } - }) - .finally(function() { - if(document) { + } + finally { + var diff = ((new Date()).getTime() - startTime)/1000; + Zotero.debug(`Integration: ${agent}-${command}${docId ? `:'${docId}'` : ''} complete in ${diff}s`) + if (document) { try { document.cleanup(); document.activate(); @@ -338,373 +306,12 @@ Zotero.Integration = new function() { }); } - inProgress = Zotero.Integration.currentWindow = false; - }); - }; - }; - - /** - * Activates Firefox - */ - this.activate = function(win) { - if(Zotero.isMac) { - const BUNDLE_IDS = { - "Zotero":"org.zotero.zotero", - "Firefox":"org.mozilla.firefox", - "Aurora":"org.mozilla.aurora", - "Nightly":"org.mozilla.nightly" - }; - - if(win) { - Components.utils.import("resource://gre/modules/ctypes.jsm"); - win.focus(); - - if(!_carbon) { - _carbon = ctypes.open("/System/Library/Frameworks/Carbon.framework/Carbon"); - /* - * struct ProcessSerialNumber { - * unsigned long highLongOfPSN; - * unsigned long lowLongOfPSN; - * }; - */ - ProcessSerialNumber = new ctypes.StructType("ProcessSerialNumber", - [{"highLongOfPSN":ctypes.uint32_t}, {"lowLongOfPSN":ctypes.uint32_t}]); - - /* - * OSStatus SetFrontProcessWithOptions ( - * const ProcessSerialNumber *inProcess, - * OptionBits inOptions - * ); - */ - SetFrontProcessWithOptions = _carbon.declare("SetFrontProcessWithOptions", - ctypes.default_abi, ctypes.int32_t, ProcessSerialNumber.ptr, - ctypes.uint32_t); - } - - var psn = new ProcessSerialNumber(); - psn.highLongOfPSN = 0; - psn.lowLongOfPSN = 2 // kCurrentProcess - - win.addEventListener("load", function() { - var res = SetFrontProcessWithOptions( - psn.address(), - 1 // kSetFrontProcessFrontWindowOnly = (1 << 0) - ); - }, false); - } else { - _executeAppleScript('tell application id "'+BUNDLE_IDS[Zotero.appName]+'" to activate'); - } - } else if(!Zotero.isWin && win) { - Components.utils.import("resource://gre/modules/ctypes.jsm"); - - if(_x11 === false) return; - if(!_x11) { - try { - _x11 = ctypes.open("libX11.so.6"); - } catch(e) { - try { - var libName = ctypes.libraryName("X11"); - } catch(e) { - _x11 = false; - Zotero.debug("Integration: Could not get libX11 name; not activating"); - Zotero.logError(e); - return; - } - - try { - _x11 = ctypes.open(libName); - } catch(e) { - _x11 = false; - Zotero.debug("Integration: Could not open "+libName+"; not activating"); - Zotero.logError(e); - return; - } - } - - X11Atom = ctypes.unsigned_long; - X11Bool = ctypes.int; - X11Display = new ctypes.StructType("Display"); - X11Window = ctypes.unsigned_long; - X11Status = ctypes.int; - - /* - * typedef struct { - * int type; - * unsigned long serial; / * # of last request processed by server * / - * Bool send_event; / * true if this came from a SendEvent request * / - * Display *display; / * Display the event was read from * / - * Window window; - * Atom message_type; - * int format; - * union { - * char b[20]; - * short s[10]; - * long l[5]; - * } data; - * } XClientMessageEvent; - */ - XClientMessageEvent = new ctypes.StructType("XClientMessageEvent", - [ - {"type":ctypes.int}, - {"serial":ctypes.unsigned_long}, - {"send_event":X11Bool}, - {"display":X11Display.ptr}, - {"window":X11Window}, - {"message_type":X11Atom}, - {"format":ctypes.int}, - {"l0":ctypes.long}, - {"l1":ctypes.long}, - {"l2":ctypes.long}, - {"l3":ctypes.long}, - {"l4":ctypes.long} - ] - ); - - /* - * Status XFetchName( - * Display* display, - * Window w, - * char** window_name_return - * ); - */ - XFetchName = _x11.declare("XFetchName", ctypes.default_abi, X11Status, - X11Display.ptr, X11Window, ctypes.char.ptr.ptr); - - /* - * Status XQueryTree( - * Display* display, - * Window w, - * Window* root_return, - * Window* parent_return, - * Window** children_return, - * unsigned int* nchildren_return - * ); - */ - XQueryTree = _x11.declare("XQueryTree", ctypes.default_abi, X11Status, - X11Display.ptr, X11Window, X11Window.ptr, X11Window.ptr, X11Window.ptr.ptr, - ctypes.unsigned_int.ptr); - - /* - * int XFree( - * void* data - * ); - */ - XFree = _x11.declare("XFree", ctypes.default_abi, ctypes.int, ctypes.voidptr_t); - - /* - * Display *XOpenDisplay( - * _Xconst char* display_name - * ); - */ - XOpenDisplay = _x11.declare("XOpenDisplay", ctypes.default_abi, X11Display.ptr, - ctypes.char.ptr); - - /* - * int XCloseDisplay( - * Display* display - * ); - */ - XCloseDisplay = _x11.declare("XCloseDisplay", ctypes.default_abi, ctypes.int, - X11Display.ptr); - - /* - * int XFlush( - * Display* display - * ); - */ - XFlush = _x11.declare("XFlush", ctypes.default_abi, ctypes.int, X11Display.ptr); - - /* - * Window XDefaultRootWindow( - * Display* display - * ); - */ - XDefaultRootWindow = _x11.declare("XDefaultRootWindow", ctypes.default_abi, - X11Window, X11Display.ptr); - - /* - * Atom XInternAtom( - * Display* display, - * _Xconst char* atom_name, - * Bool only_if_exists - * ); - */ - XInternAtom = _x11.declare("XInternAtom", ctypes.default_abi, X11Atom, - X11Display.ptr, ctypes.char.ptr, X11Bool); - - /* - * Status XSendEvent( - * Display* display, - * Window w, - * Bool propagate, - * long event_mask, - * XEvent* event_send - * ); - */ - XSendEvent = _x11.declare("XSendEvent", ctypes.default_abi, X11Status, - X11Display.ptr, X11Window, X11Bool, ctypes.long, XClientMessageEvent.ptr); - - /* - * int XMapRaised( - * Display* display, - * Window w - * ); - */ - XMapRaised = _x11.declare("XMapRaised", ctypes.default_abi, ctypes.int, - X11Display.ptr, X11Window); - - /* - * extern int XGetWindowProperty( - * Display* display, - * Window w, - * Atom property, - * long long_offset, - * long long_length, - * Bool delete, - * Atom req_type, - * Atom* actual_type_return, - * int* actual_format_return, - * unsigned long* nitems_return, - * unsigned long* bytes_after_return, - * unsigned char** prop_return - * ); - */ - XGetWindowProperty = _x11.declare("XGetWindowProperty", ctypes.default_abi, - ctypes.int, X11Display.ptr, X11Window, X11Atom, ctypes.long, ctypes.long, - X11Bool, X11Atom, X11Atom.ptr, ctypes.int.ptr, ctypes.unsigned_long.ptr, - ctypes.unsigned_long.ptr, ctypes.char.ptr.ptr); - - - _x11Display = XOpenDisplay(null); - if(!_x11Display) { - Zotero.debug("Integration: Could not open display; not activating"); - _x11 = false; - return; - } - - Zotero.addShutdownListener(function() { - XCloseDisplay(_x11Display); - }); - - _x11RootWindow = XDefaultRootWindow(_x11Display); - if(!_x11RootWindow) { - Zotero.debug("Integration: Could not get root window; not activating"); - _x11 = false; - return; - } - } - - win.addEventListener("load", function() { - var intervalID; - intervalID = win.setInterval(function() { - _X11BringToForeground(win, intervalID); - }, 50); - }, false); - } - } - - /** - * Get a property from an X11 window - */ - function _X11GetProperty(win, propertyName, propertyType) { - Components.utils.import("resource://gre/modules/ctypes.jsm"); - - var returnType = new X11Atom(), - returnFormat = new ctypes.int(), - nItemsReturned = new ctypes.unsigned_long(), - nBytesAfterReturn = new ctypes.unsigned_long(), - data = new ctypes.char.ptr(); - if(!XGetWindowProperty(_x11Display, win, XInternAtom(_x11Display, propertyName, 0), 0, 1024, - 0, propertyType, returnType.address(), returnFormat.address(), - nItemsReturned.address(), nBytesAfterReturn.address(), data.address())) { - var nElements = ctypes.cast(nItemsReturned, ctypes.unsigned_int).value; - if(nElements) return [data, nElements]; - } - return null; - } - - /** - * Bring a window to the foreground by interfacing directly with X11 - */ - function _X11BringToForeground(win, intervalID) { - var windowTitle = win.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIBaseWindow).title; - - var x11Window = _X11FindWindow(_x11RootWindow, windowTitle); - if(!x11Window) return; - win.clearInterval(intervalID); - - var event = new XClientMessageEvent(); - event.type = 33; /* ClientMessage*/ - event.serial = 0; - event.send_event = 1; - event.message_type = XInternAtom(_x11Display, "_NET_ACTIVE_WINDOW", 0); - event.display = _x11Display; - event.window = x11Window; - event.format = 32; - event.l0 = 2; - var mask = 1<<20 /* SubstructureRedirectMask */ | 1<<19 /* SubstructureNotifyMask */; - - if(XSendEvent(_x11Display, _x11RootWindow, 0, mask, event.address())) { - XMapRaised(_x11Display, x11Window); - XFlush(_x11Display); - Zotero.debug("Integration: Activated successfully"); - } else { - Zotero.debug("Integration: An error occurred activating the window"); - } - } - - /** - * Find an X11 window given a name - */ - function _X11FindWindow(w, searchName) { - Components.utils.import("resource://gre/modules/ctypes.jsm"); - - var res = _X11GetProperty(w, "_NET_CLIENT_LIST", 33 /** XA_WINDOW **/) - || _X11GetProperty(w, "_WIN_CLIENT_LIST", 6 /** XA_CARDINAL **/); - if(!res) return false; - - var nClients = res[1], - clientList = ctypes.cast(res[0], X11Window.array(nClients).ptr).contents, - foundName = new ctypes.char.ptr(); - for(var i=0; i<nClients; i++) { - if(XFetchName(_x11Display, clientList.addressOfElement(i).contents, - foundName.address())) { - var foundNameString = undefined; - try { - foundNameString = foundName.readString(); - } catch(e) {} - XFree(foundName); - if(foundNameString === searchName) return clientList.addressOfElement(i).contents; + inProgress = + Zotero.Integration.currentDoc = + Zotero.Integration.currentWindow = false; } - } - XFree(res[0]); - - return foundWindow; - } - - /** - * Runs an AppleScript on OS X - * - * @param script {String} - * @param block {Boolean} Whether the script should block until the process is finished. - */ - function _executeAppleScript(script, block) { - if(_osascriptFile === undefined) { - _osascriptFile = Components.classes["@mozilla.org/file/local;1"]. - createInstance(Components.interfaces.nsILocalFile); - _osascriptFile.initWithPath("/usr/bin/osascript"); - if(!_osascriptFile.exists()) _osascriptFile = false; - } - if(_osascriptFile) { - var proc = Components.classes["@mozilla.org/process/util;1"]. - createInstance(Components.interfaces.nsIProcess); - proc.init(_osascriptFile); - try { - proc.run(!!block, ['-e', script], 2); - } catch(e) {} - } - } + }); + }; /** * Displays a dialog in a modal-like fashion without hanging the thread @@ -713,8 +320,8 @@ Zotero.Integration = new function() { * @param {String} [io] Data to pass to the window * @return {Promise} Promise resolved when the window is closed */ - this.displayDialog = function displayDialog(doc, url, options, io) { - doc.cleanup(); + this.displayDialog = function displayDialog(url, options, io) { + Zotero.Integration.currentDoc.cleanup(); var allOptions = 'chrome,centerscreen'; // without this, Firefox gets raised with our windows under Compiz @@ -725,7 +332,7 @@ Zotero.Integration = new function() { .getService(Components.interfaces.nsIWindowWatcher) .openWindow(null, url, '', allOptions, (io ? io : null)); Zotero.Integration.currentWindow = window; - Zotero.Integration.activate(window); + Zotero.Utilities.Internal.activate(window); var deferred = Zotero.Promise.defer(); var listener = function() { @@ -747,601 +354,368 @@ Zotero.Integration = new function() { }; /** - * Default callback for field-related errors. All functions that do not define their - * own handlers for field-related errors should use this one. + * Gets a session for a given doc. + * Either loads a cached session if doc communicated since restart or creates a new one + * @return {Zotero.Integration.Session} Promise */ - this.onFieldError = function onFieldError(err) { - if(err.attemptToResolve) { - return err.attemptToResolve(); - } - throw err; - } -} - -/** - * An exception thrown when a document contains an item that no longer exists in the current document. - * - * @param reselectKeys {Array} Keys representing the missing item - * @param reselectKeyType {Integer} The type of the keys (see RESELECT_KEY_* constants) - * @param citationIndex {Integer} The index of the missing item within the citation cluster - * @param citationLength {Integer} The number of items cited in this citation cluster - */ -Zotero.Integration.MissingItemException = function(reselectKeys, reselectKeyType, citationIndex, citationLength) { - this.reselectKeys = reselectKeys; - this.reselectKeyType = reselectKeyType; - this.citationIndex = citationIndex; - this.citationLength = citationLength; -} -Zotero.Integration.MissingItemException.prototype = { - "name":"MissingItemException", - "message":"An item in this document is missing from your Zotero library.", - "toString":function() { return this.message }, - "setContext":function(fieldGetter, fieldIndex) { - this.fieldGetter = fieldGetter; - this.fieldIndex = fieldIndex; - }, - - "attemptToResolve":function() { - Zotero.logError(this); - if(!this.fieldGetter) { - throw new Error("Could not resolve "+this.name+": setContext not called"); + this.getSession = Zotero.Promise.coroutine(function *(app, doc) { + var dataString = doc.getDocumentData(), + data, session; + + try { + data = new Zotero.Integration.DocumentData(dataString); + } catch(e) { + data = new Zotero.Integration.DocumentData(); } - // Ask user what to do with this item - if(this.citationLength == 1) { - var msg = Zotero.getString("integration.missingItem.single"); - } else { - var msg = Zotero.getString("integration.missingItem.multiple", (this.citationIndex+1).toString()); - } - msg += '\n\n'+Zotero.getString('integration.missingItem.description'); - this.fieldGetter._fields[this.fieldIndex].select(); - this.fieldGetter._doc.activate(); - var result = this.fieldGetter._doc.displayAlert(msg, 1, 3); - if(result == 0) { // Cancel - return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("document update")); - } else if(result == 1) { // No - for (let reselectKey of this.reselectKeys) { - this.fieldGetter._removeCodeKeys[reselectKey] = true; + if (data.prefs.fieldType) { + if (data.dataVersion < DATA_VERSION) { + if (data.dataVersion == 1 + && data.prefs.fieldType == "Field" + && this._app.primaryFieldType == "ReferenceMark") { + // Converted OOo docs use ReferenceMarks, not fields + data.prefs.fieldType = "ReferenceMark"; + } + + var warning = doc.displayAlert(Zotero.getString("integration.upgradeWarning", [Zotero.clientName, '5.0']), + DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL); + if (!warning) { + throw new Zotero.Exception.UserCancelled("document upgrade"); + } + // Don't throw for version 4(JSON) during the transition from 4.0 to 5.0 + } else if ((data.dataVersion > DATA_VERSION) && data.dataVersion != 4) { + throw new Zotero.Exception.Alert("integration.error.newerDocumentVersion", + [data.zoteroVersion, Zotero.version], "integration.error.title"); + } + + if (data.prefs.fieldType !== app.primaryFieldType + && data.prefs.fieldType !== app.secondaryFieldType) { + throw new Zotero.Exception.Alert("integration.error.fieldTypeMismatch", + [], "integration.error.title"); } - this.fieldGetter._removeCodeFields[this.fieldIndex] = true; - return this.fieldGetter._processFields(this.fieldIndex+1); - } else { // Yes - // Display reselect item dialog - var fieldGetter = this.fieldGetter, - fieldIndex = this.fieldIndex, - oldCurrentWindow = Zotero.Integration.currentWindow; - return fieldGetter._session.reselectItem(fieldGetter._doc, this) - .then(function() { - // Now try again - Zotero.Integration.currentWindow = oldCurrentWindow; - fieldGetter._doc.activate(); - return fieldGetter._processFields(fieldIndex); - }); - } - } -} -Zotero.Integration.CorruptFieldException = function(code, cause) { - this.code = code; - this.cause = cause; -}; -Zotero.Integration.CorruptFieldException.prototype = { - "name":"CorruptFieldException", - "message":"A field code in this document is corrupted.", - "toString":function() { return this.cause.toString()+"\n\n"+this.code.toSource(); }, - "setContext":function(fieldGetter, fieldIndex, field) { - this.fieldGetter = fieldGetter; - this.fieldIndex = fieldIndex; - }, - - /** - * Tries to resolve the CorruptFieldException - * @return {Promise} A promise that is either resolved with true or rejected with - * Zotero.Exception.UserCancelled - */ - "attemptToResolve":function() { - Zotero.logError(this.cause); - if(!this.fieldGetter) { - throw new Error("Could not resolve "+this.name+": setContext not called"); + session = Zotero.Integration.sessions[data.sessionID]; } - - var msg = Zotero.getString("integration.corruptField")+'\n\n'+ - Zotero.getString('integration.corruptField.description'), - field = this.fieldGetter._fields[this.fieldIndex]; - field.select(); - this.fieldGetter._doc.activate(); - var result = this.fieldGetter._doc.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_YES_NO_CANCEL); - if(result == 0) { - return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("document update")); - } else if(result == 1) { // No - this.fieldGetter._removeCodeFields[this.fieldIndex] = true; - return this.fieldGetter._processFields(this.fieldIndex+1); - } else { - // Display reselect edit citation dialog - var fieldGetter = this.fieldGetter, - oldWindow = Zotero.Integration.currentWindow, - oldProgressCallback = this.progressCallback; - return fieldGetter.addEditCitation(field).then(function() { - if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) { - Zotero.Integration.currentWindow.close(); + if (!session) { + session = new Zotero.Integration.Session(doc, app); + session.reload = true; + } + try { + yield session.setData(data); + } catch(e) { + // make sure style is defined + if (e instanceof Zotero.Exception.Alert && e.name === "integration.error.invalidStyle") { + if (data.style.styleID) { + session.reload = true; + let trustedSource = /^https?:\/\/(www\.)?(zotero\.org|citationstyles\.org)/.test(data.style.styleID); + let errorString = Zotero.getString("integration.error.styleMissing", data.style.styleID); + if (trustedSource || + doc.displayAlert(errorString, DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO)) { + + let installed = false; + try { + yield Zotero.Styles.install( + {url: data.style.styleID}, data.style.styleID, true + ); + installed = true; + } + catch (e) { + me._doc.displayAlert( + Zotero.getString( + 'integration.error.styleNotFound', data.style.styleID + ), + DIALOG_ICON_WARNING, + DIALOG_BUTTONS_OK + ); + } + if (installed) { + yield session.setData(data, true); + } + return session; + } } - Zotero.Integration.currentWindow = oldWindow; - fieldGetter.progressCallback = oldProgressCallback; - return fieldGetter.updateSession(); - }); + yield session.setDocPrefs(); + } else { + throw e; + } } - } -}; + return session; + }); + +} /** - * An exception to encapsulate the case where bibliography data is invalid. - * @class + * An exception thrown when a document contains an item that no longer exists in the current document. */ -Zotero.Integration.CorruptBibliographyException = function(code, cause) { - this.code = code; - this.cause = cause; -} -Zotero.Integration.CorruptBibliographyException.prototype = { - "name":"CorruptBibliographyException", - "message":"A bibliography in this document is corrupted.", - "toString":function() { return this.cause.toString()+"\n\n"+this.code }, - - "setContext":function(fieldGetter) { - this.fieldGetter = fieldGetter; - }, - - /** - * Tries to resolve the CorruptBibliographyException - * @return {Promise} A promise that is either resolved with true or rejected with - * Zotero.Exception.UserCancelled - */ - "attemptToResolve":function() { - Zotero.debug("Attempting to resolve") - Zotero.logError(this.cause); - if(!this.fieldGetter) { - throw new Error("Could not resolve "+this.name+": setContext not called"); - } - - var msg = Zotero.getString("integration.corruptBibliography")+'\n\n'+ - Zotero.getString('integration.corruptBibliography.description'); - var result = this.fieldGetter._doc.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_OK_CANCEL); - if(result == 0) { - return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("clearing corrupted bibliography")); - } else { - this.fieldGetter._bibliographyData = ""; - this.fieldGetter._session.bibliographyHasChanged = true; - this.fieldGetter._session.bibliographyDataHasChanged = true; - return Zotero.Promise.resolve(true); - } - } +Zotero.Integration.MissingItemException = function(item) {this.item = item;}; +Zotero.Integration.MissingItemException.prototype = { + "name":"MissingItemException", + "message":`An item in this document is missing from your Zotero library.}`, + "toString":function() { return this.message + `\n ${JSON.stringify(this.item)}` } }; -const INTEGRATION_TYPE_ITEM = 1; -const INTEGRATION_TYPE_BIBLIOGRAPHY = 2; -const INTEGRATION_TYPE_TEMP = 3; - -// Placeholder for an empty bibliography -const BIBLIOGRAPHY_PLACEHOLDER = "{Bibliography}"; +Zotero.Integration.NO_ACTION = 0; +Zotero.Integration.UPDATE = 1; +Zotero.Integration.DELETE = 2; /** * All methods for interacting with a document * @constructor */ -Zotero.Integration.Document = function(app, doc) { +Zotero.Integration.Interface = function(app, doc, session) { this._app = app; this._doc = doc; + this._session = session; } -/** - * Creates a new session - * @param data {Zotero.Integration.DocumentData} Document data for new session - * @return {Zotero.Integration.Session} - */ -Zotero.Integration.Document.prototype._createNewSession = function _createNewSession(data) { - data.sessionID = Zotero.randomString(); - var session = Zotero.Integration.sessions[data.sessionID] = new Zotero.Integration.Session(this._doc); - return session; -}; /** - * Gets preferences for a document - * @param require {Boolean} Whether an error should be thrown if no preferences or fields - * exist (otherwise, the set doc prefs dialog is shown) - * @param dontRunSetDocPrefs {Boolean} Whether to show the Set Document Preferences - * window if no preferences exist - * @return {Promise} Promise resolved with true if a session was found or false if - * dontRunSetDocPrefs is true and no session was found, or rejected with - * Zotero.Exception.UserCancelled if the document preferences window was cancelled. + * Adds a citation to the current document. + * @return {Promise} */ -Zotero.Integration.Document.prototype._getSession = Zotero.Promise.coroutine(function *(require, dontRunSetDocPrefs) { - var dataString = this._doc.getDocumentData(), - data, - me = this; +Zotero.Integration.Interface.prototype.addCitation = Zotero.Promise.coroutine(function* () { + yield this._session.init(false, false); - if(dataString) { - try { - data = new Zotero.Integration.DocumentData(dataString); - } catch(e) {}; - } + let [idx, field, citation] = yield this._session.fields.addEditCitation(null); + yield this._session.addCitation(idx, field.getNoteIndex(), citation); - // If no data or corrupted data, show doc prefs window again - if (!data || !data.prefs || !data.prefs.fieldType) { - var haveFields = false; - data = new Zotero.Integration.DocumentData(); - - if(require) { - // check to see if fields already exist - for (let fieldType of [this._app.primaryFieldType, this._app.secondaryFieldType]) { - var fields = this._doc.getFields(this._app.primaryFieldType); - if(fields.hasMoreElements()) { - data.prefs.fieldType = this._app.primaryFieldType; - haveFields = true; - break; - } - } - - // if no fields, throw an error - if(!haveFields) { - return Zotero.Promise.reject(new Zotero.Exception.Alert( - "integration.error.mustInsertCitation", - [], "integration.error.title")); - } else { - Zotero.debug("Integration: No document preferences found, but found "+data.prefs.fieldType+" fields"); - } - } - - // Set doc prefs if no data string yet - this._session = this._createNewSession(data); - yield this._session.setData(data); - if(dontRunSetDocPrefs) return Zotero.Promise.resolve(false); - - return this._session.setDocPrefs(this._doc, this._app.primaryFieldType, - this._app.secondaryFieldType).then(function(status) { - // save doc prefs in doc - me._doc.setDocumentData(me._session.data.serialize()); - - if(haveFields) { - me._session.reload = true; - } - - return me._session; - }); + if (this._session.data.prefs.delayCitationUpdates) { + return this._session.writeDelayedCitation(idx, field, citation); } else { - if(data.dataVersion < DATA_VERSION) { - if(data.dataVersion == 1 - && data.prefs.fieldType == "Field" - && this._app.primaryFieldType == "ReferenceMark") { - // Converted OOo docs use ReferenceMarks, not fields - data.prefs.fieldType = "ReferenceMark"; - } - - var warning = this._doc.displayAlert(Zotero.getString("integration.upgradeWarning", [Zotero.clientName, '5.0']), - DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL); - if(!warning) { - return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("document upgrade")); - } - // Don't throw for version 4(JSON) during the transition from 4.0 to 5.0 - } else if((data.dataVersion > DATA_VERSION) && data.dataVersion != 4) { - return Zotero.Promise.reject(new Zotero.Exception.Alert("integration.error.newerDocumentVersion", - [data.zoteroVersion, Zotero.version], "integration.error.title")); - } - - if(data.prefs.fieldType !== this._app.primaryFieldType - && data.prefs.fieldType !== this._app.secondaryFieldType) { - return Zotero.Promise.reject(new Zotero.Exception.Alert("integration.error.fieldTypeMismatch", - [], "integration.error.title")); - } - - if (Zotero.Integration.sessions[data.sessionID]) { - // If communication occured with this document since restart - this._session = Zotero.Integration.sessions[data.sessionID]; - } else { - // Document has zotero data, but has not communicated since Zotero restart - this._session = this._createNewSession(data); - try { - yield this._session.setData(data); - // this._createNewSession() updates sessionID, which we need to store back into the doc - this._doc.setDocumentData(me._session.data.serialize()) - } catch(e) { - // make sure style is defined - if(e instanceof Zotero.Exception.Alert && e.name === "integration.error.invalidStyle") { - if (data.style.styleID) { - let displayError = Zotero.getString("integration.error.styleMissing", data.style.styleID); - if (/^https?:\/\/(www\.)?(zotero\.org|citationstyles\.org)/.test(data.style.styleID) || - me._doc.displayAlert(displayError, DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO)) { - - let installed = false; - try { - yield Zotero.Styles.install( - {url: data.style.styleID}, data.style.styleID, true - ); - installed = true; - } - catch (e) { - me._doc.displayAlert( - Zotero.getString( - 'integration.error.styleNotFound', data.style.styleID - ), - DIALOG_ICON_WARNING, - DIALOG_BUTTONS_OK - ); - } - if (installed) { - yield this._session.setData(data, true); - return Zotero.Promise.resolve(this._session); - } - } - } - return this._session.setDocPrefs(this._doc, this._app.primaryFieldType, - this._app.secondaryFieldType).then(function(status) { - me._doc.setDocumentData(me._session.data.serialize()); - me._session.reload = true; - return me._session; - }); - } else { - return Zotero.Promise.reject(e); - } - } - - this._session.reload = true; - } - return Zotero.Promise.resolve(this._session); + return this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, false, false); } }); /** - * Adds a citation to the current document. - * @return {Promise} - */ -Zotero.Integration.Document.prototype.addCitation = function() { - var me = this; - return this._getSession(false, false).then(function() { - return (new Zotero.Integration.Fields(me._session, me._doc)).addEditCitation(null); - }); -} - -/** * Edits the citation at the cursor position. * @return {Promise} */ -Zotero.Integration.Document.prototype.editCitation = function() { - var me = this; - return this._getSession(true, false).then(function() { - var field = me._doc.cursorInField(me._session.data.prefs['fieldType']); - if(!field) { - throw new Zotero.Exception.Alert("integration.error.notInCitation", [], - "integration.error.title"); - } - - return (new Zotero.Integration.Fields(me._session, me._doc)).addEditCitation(field); - }); -} +Zotero.Integration.Interface.prototype.editCitation = Zotero.Promise.coroutine(function* () { + var docField = this._doc.cursorInField(this._session.data.prefs['fieldType']); + if(!docField) { + throw new Zotero.Exception.Alert("integration.error.notInCitation", [], + "integration.error.title"); + } + return this.addEditCitation(docField); +}); /** * Edits the citation at the cursor position if one exists, or else adds a new one. * @return {Promise} */ -Zotero.Integration.Document.prototype.addEditCitation = function() { - var me = this; - return this._getSession(false, false).then(function() { - var field = me._doc.cursorInField(me._session.data.prefs['fieldType']); - return (new Zotero.Integration.Fields(me._session, me._doc)).addEditCitation(field); - }); -} +Zotero.Integration.Interface.prototype.addEditCitation = Zotero.Promise.coroutine(function* (docField) { + yield this._session.init(false, false); + docField = docField || this._doc.cursorInField(this._session.data.prefs['fieldType']); + + let [idx, field, citation] = yield this._session.fields.addEditCitation(docField); + yield this._session.addCitation(idx, field.getNoteIndex(), citation); + if (this._session.data.prefs.delayCitationUpdates) { + return this._session.writeDelayedCitation(idx, field, citation); + } else { + return this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, false, false); + } +}); /** * Adds a bibliography to the current document. * @return {Promise} */ -Zotero.Integration.Document.prototype.addBibliography = function() { +Zotero.Integration.Interface.prototype.addBibliography = Zotero.Promise.coroutine(function* () { var me = this; - return this._getSession(true, false).then(function() { - // Make sure we can have a bibliography - if(!me._session.data.style.hasBibliography) { - throw new Zotero.Exception.Alert("integration.error.noBibliography", [], - "integration.error.title"); - } - - var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); - return fieldGetter.addField().then(function(field) { - field.setCode("BIBL"); - return fieldGetter.updateSession().then(function() { - return fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); - }); - }); - }); -} + yield this._session.init(true, false); + // Make sure we can have a bibliography + if(!me._session.data.style.hasBibliography) { + throw new Zotero.Exception.Alert("integration.error.noBibliography", [], + "integration.error.title"); + } + + let field = new Zotero.Integration.BibliographyField(yield this._session.fields.addField()); + field.clearCode(); + var citationsMode = FORCE_CITATIONS_FALSE; + if(this._session.data.prefs.delayCitationUpdates) { + // Refreshes citeproc state before proceeding + this._session.reload = true; + citationsMode = FORCE_CITATIONS_REGENERATE; + } + yield this._session.fields.updateSession(citationsMode); + yield this._session.fields.updateDocument(citationsMode, true, false); +}) /** * Edits bibliography metadata. * @return {Promise} */ -Zotero.Integration.Document.prototype.editBibliography = function() { +Zotero.Integration.Interface.prototype.editBibliography = Zotero.Promise.coroutine(function*() { // Make sure we have a bibliography - var me = this, fieldGetter; - return this._getSession(true, false).then(function() { - fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); - return fieldGetter.get(); - }).then(function(fields) { - var haveBibliography = false; - for(var i=fields.length-1; i>=0; i--) { - var code = fields[i].getCode(); - var [type, content] = fieldGetter.getCodeTypeAndContent(code); - if(type == INTEGRATION_TYPE_BIBLIOGRAPHY) { - haveBibliography = true; - break; - } - } - - if(!haveBibliography) { - throw new Zotero.Exception.Alert("integration.error.mustInsertBibliography", - [], "integration.error.title"); + yield this._session.init(true, false); + var fields = yield this._session.fields.get(); + + var bibliographyField; + for (let i = fields.length-1; i >= 0; i--) { + let field = Zotero.Integration.Field.loadExisting(fields[i]); + if (field.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { + bibliographyField = field; + break; } - return fieldGetter.updateSession(); - }).then(function() { - return me._session.editBibliography(me._doc); - }).then(function() { - return fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); - }); -} + } + + if(!bibliographyField) { + throw new Zotero.Exception.Alert("integration.error.mustInsertBibliography", + [], "integration.error.title"); + } + let bibliography = new Zotero.Integration.Bibliography(bibliographyField); + var citationsMode = FORCE_CITATIONS_FALSE; + if(this._session.data.prefs.delayCitationUpdates) { + // Refreshes citeproc state before proceeding + this._session.reload = true; + citationsMode = FORCE_CITATIONS_REGENERATE; + } + yield this._session.fields.updateSession(citationsMode); + yield this._session.editBibliography(bibliography); + yield this._session.fields.updateDocument(citationsMode, true, false); +}); -Zotero.Integration.Document.prototype.addEditBibliography = Zotero.Promise.coroutine(function *() { +Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coroutine(function *() { // Check if we have a bibliography - var session = yield this._getSession(true, false); + yield this._session.init(true, false); - if (!session.data.style.hasBibliography) { + if (!this._session.data.style.hasBibliography) { throw new Zotero.Exception.Alert("integration.error.noBibliography", [], "integration.error.title"); } - var fieldGetter = new Zotero.Integration.Fields(session, this._doc, Zotero.Integration.onFieldError); - var fields = yield fieldGetter.get(); + var fields = yield this._session.fields.get(); - var haveBibliography = false; - for (var i=fields.length-1; i>=0; i--) { - var code = fields[i].getCode(); - var [type, content] = fieldGetter.getCodeTypeAndContent(code); - if (type == INTEGRATION_TYPE_BIBLIOGRAPHY) { - haveBibliography = true; + var bibliographyField; + for (let i = fields.length-1; i >= 0; i--) { + let field = Zotero.Integration.Field.loadExisting(fields[i]); + if (field.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { + bibliographyField = field; break; } } - if (haveBibliography) { - yield fieldGetter.updateSession(); - yield session.editBibliography(this._doc); - } else { - var field = yield fieldGetter.addField(); - field.setCode("BIBL"); - yield fieldGetter.updateSession(); + var newBibliography = !bibliographyField; + if (!bibliographyField) { + bibliographyField = new Zotero.Integration.BibliographyField(yield this._session.fields.addField()); + bibliographyField.clearCode(); } - return fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); + + let bibliography = new Zotero.Integration.Bibliography(bibliographyField); + var citationsMode = FORCE_CITATIONS_FALSE; + if(this._session.data.prefs.delayCitationUpdates) { + // Refreshes citeproc state before proceeding + this._session.reload = true; + citationsMode = FORCE_CITATIONS_REGENERATE; + } + yield this._session.fields.updateSession(citationsMode); + if (!newBibliography) yield this._session.editBibliography(bibliography); + yield this._session.fields.updateDocument(citationsMode, true, false); }); /** * Updates the citation data for all citations and bibliography entries. * @return {Promise} */ -Zotero.Integration.Document.prototype.refresh = function() { - var me = this; - return this._getSession(true, false).then(function() { - // Send request, forcing update of citations and bibliography - var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); - return fieldGetter.updateSession().then(function() { - return fieldGetter.updateDocument(FORCE_CITATIONS_REGENERATE, true, false); - }); - }); +Zotero.Integration.Interface.prototype.refresh = async function() { + await this._session.init(true, false) + + this._session.reload = this._session.data.prefs.delayCitationUpdates; + await this._session.fields.updateSession(FORCE_CITATIONS_REGENERATE) + await this._session.fields.updateDocument(FORCE_CITATIONS_REGENERATE, true, false); } /** * Deletes field codes. * @return {Promise} */ -Zotero.Integration.Document.prototype.removeCodes = function() { +Zotero.Integration.Interface.prototype.removeCodes = Zotero.Promise.coroutine(function* () { var me = this; - return this._getSession(true, false).then(function() { - var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc); - return fieldGetter.get() - }).then(function(fields) { - var result = me._doc.displayAlert(Zotero.getString("integration.removeCodesWarning"), - DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL); - if(result) { - for(var i=fields.length-1; i>=0; i--) { - fields[i].removeCode(); - } + yield this._session.init(true, false) + let fields = yield this._session.fields.get() + var result = me._doc.displayAlert(Zotero.getString("integration.removeCodesWarning"), + DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL); + if (result) { + for(var i=fields.length-1; i>=0; i--) { + fields[i].removeCode(); } - }); -} + } +}) /** * Displays a dialog to set document preferences (style, footnotes/endnotes, etc.) * @return {Promise} */ -Zotero.Integration.Document.prototype.setDocPrefs = function() { - var me = this, - fieldGetter, - oldData; - return this._getSession(false, true).then(function(haveSession) { - fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); - var setDocPrefs = me._session.setDocPrefs.bind(me._session, me._doc, - me._app.primaryFieldType, me._app.secondaryFieldType); - if(!haveSession) { - // This is a brand new document; don't try to get fields - return setDocPrefs(); - } else { - // Can get fields while dialog is open - return Zotero.Promise.all([ - fieldGetter.get(), - setDocPrefs() - ]).spread(function (fields, setDocPrefs) { - // Only return value from setDocPrefs - return setDocPrefs; - }); - } - }).then(function(aOldData) { // After setDocPrefs call - oldData = aOldData; - - // Write document data to document - me._doc.setDocumentData(me._session.data.serialize()); +Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(function* () { + var oldData; + let haveSession = yield this._session.init(false, true); + + if(!haveSession) { + // This is a brand new document; don't try to get fields + oldData = yield this._session.setDocPrefs(); + } else { + // Can get fields while dialog is open + oldData = yield Zotero.Promise.all([ + this._session.fields.get(), + this._session.setDocPrefs() + ]).spread(function (fields, setDocPrefs) { + // Only return value from setDocPrefs + return setDocPrefs; + }); + } + + // If oldData is null, then there was no document data, so we don't need to update + // fields + if (!oldData) return false; + + // Perform noteType or fieldType conversion + let fields = yield this._session.fields.get(); + + var convertBibliographies = oldData.prefs.fieldType != this._session.data.prefs.fieldType; + var convertItems = convertBibliographies + || oldData.prefs.noteType != this._session.data.prefs.noteType; + var fieldsToConvert = new Array(); + var fieldNoteTypes = new Array(); + for (var i=0, n=fields.length; i<n; i++) { + let field = Zotero.Integration.Field.loadExisting(fields[i]); - // If oldData is null, then there was no document data, so we don't need to update - // fields - if(!oldData) return false; - return fieldGetter.get(); - }).then(function(fields) { - if(!fields || !fields.length) return; - - // If there are fields, we will have to convert some things; get a list of what - // we need to deal with - var convertBibliographies = oldData === true - || oldData.prefs.fieldType != me._session.data.prefs.fieldType; - var convertItems = convertBibliographies - || oldData.prefs.noteType != me._session.data.prefs.noteType; - var fieldsToConvert = new Array(); - var fieldNoteTypes = new Array(); - for(var i=0, n=fields.length; i<n; i++) { - var field = fields[i], - fieldCode = field.getCode(), - [type, content] = fieldGetter.getCodeTypeAndContent(fieldCode); - - if(convertItems && type === INTEGRATION_TYPE_ITEM) { - var citation = me._session.unserializeCitation(fieldCode); - if(!citation.properties.dontUpdate) { - fieldsToConvert.push(field); - fieldNoteTypes.push(me._session.data.prefs.noteType); - } - } else if(convertBibliographies - && type === INTEGRATION_TYPE_BIBLIOGRAPHY) { - fieldsToConvert.push(field); - fieldNoteTypes.push(0); + if (convertItems && field.type === INTEGRATION_TYPE_ITEM) { + var citation = field.unserialize(); + if (!citation.properties.dontUpdate) { + fieldsToConvert.push(fields[i]); + fieldNoteTypes.push(this._session.data.prefs.noteType); } + } else if(convertBibliographies + && type === INTEGRATION_TYPE_BIBLIOGRAPHY) { + fieldsToConvert.push(fields[i]); + fieldNoteTypes.push(0); } - - if(fieldsToConvert.length) { - // Pass to conversion function - me._doc.convert(new Zotero.Integration.Document.JSEnumerator(fieldsToConvert), - me._session.data.prefs.fieldType, fieldNoteTypes, - fieldNoteTypes.length); - } - - // Refresh contents - fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); - fieldGetter.ignoreEmptyBibliography = false; - return fieldGetter.updateSession().then(fieldGetter.updateDocument.bind( - fieldGetter, FORCE_CITATIONS_RESET_TEXT, true, true)); - }); -} + } + + if(fieldsToConvert.length) { + // Pass to conversion function + this._doc.convert(new Zotero.Integration.JSEnumerator(fieldsToConvert), + this._session.data.prefs.fieldType, fieldNoteTypes, + fieldNoteTypes.length); + } + + // Refresh contents + this._session.fields = new Zotero.Integration.Fields(this._session, this._doc); + this._session.fields.ignoreEmptyBibliography = false; + yield this._session.fields.updateSession(FORCE_CITATIONS_RESET_TEXT); + return this._session.fields.updateDocument(FORCE_CITATIONS_RESET_TEXT, true, true); +}); /** * An exceedingly simple nsISimpleEnumerator implementation */ -Zotero.Integration.Document.JSEnumerator = function(objArray) { +Zotero.Integration.JSEnumerator = function(objArray) { this.objArray = objArray; } -Zotero.Integration.Document.JSEnumerator.prototype.hasMoreElements = function() { +Zotero.Integration.JSEnumerator.prototype.hasMoreElements = function() { return this.objArray.length; } -Zotero.Integration.Document.JSEnumerator.prototype.getNext = function() { +Zotero.Integration.JSEnumerator.prototype.getNext = function() { return this.objArray.shift(); } @@ -1349,25 +723,17 @@ Zotero.Integration.Document.JSEnumerator.prototype.getNext = function() { * Methods for retrieving fields from a document * @constructor */ -Zotero.Integration.Fields = function(session, doc, fieldErrorHandler) { +Zotero.Integration.Fields = function(session, doc) { this.ignoreEmptyBibliography = true; // Callback called while retrieving fields with the percentage complete. this.progressCallback = null; - // Promise injected into the middle of the promise chain while retrieving fields, to check for - // recoverable errors. If the fieldErrorHandler is fulfilled, then the rest of the promise - // chain continues. If the fieldErrorHandler is rejected, then the promise chain is rejected. - this.fieldErrorHandler = fieldErrorHandler; - this._session = session; this._doc = doc; - this._deferreds = null; - this._removeCodeKeys = {}; this._removeCodeFields = {}; this._bibliographyFields = []; - this._bibliographyData = ""; } /** @@ -1383,7 +749,7 @@ Zotero.Integration.Fields.prototype.addField = function(note) { var field = this._doc.cursorInField(this._session.data.prefs['fieldType']); if (field) { - if (!this._doc.displayAlert(Zotero.getString("integration.replace"), + if (!this._session.displayAlert(Zotero.getString("integration.replace"), DIALOG_ICON_STOP, DIALOG_BUTTONS_OK_CANCEL)) { return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("inserting citation")); @@ -1404,127 +770,98 @@ Zotero.Integration.Fields.prototype.addField = function(note) { } /** - * Gets the type and content of a field object - */ -Zotero.Integration.Fields.prototype.getCodeTypeAndContent = function(rawCode) { - for (let code of ["ITEM", "CITATION"]) { - if(rawCode.substr(0, code.length) === code) { - return [INTEGRATION_TYPE_ITEM, rawCode.substr(code.length+1)]; - } - } - - if(rawCode.substr(0, 4) === "BIBL") { - return [INTEGRATION_TYPE_BIBLIOGRAPHY, rawCode.substr(5)]; - } - - if(rawCode.substr(0, 4) === "TEMP") { - return [INTEGRATION_TYPE_TEMP, rawCode.substr(5)]; - } - - return [null, rawCode]; -} - -/** * Gets all fields for a document * @return {Promise} Promise resolved with field list. */ -Zotero.Integration.Fields.prototype.get = function get() { - // If we already have fields, just return them - if(this._fields) { - return Zotero.Promise.resolve(this._fields); - } - - // Create a new promise and add it to promise list - var deferred = Zotero.Promise.defer(); - - // If already getting fields, just return the promise - if(this._deferreds) { - this._deferreds.push(deferred); - return deferred.promise; - } else { - this._deferreds = [deferred]; - } - - // Otherwise, start getting fields - var getFieldsTime = (new Date()).getTime(), - me = this; - this._doc.getFieldsAsync(this._session.data.prefs['fieldType'], - {"observe":function(subject, topic, data) { - if(topic === "fields-available") { - if(me.progressCallback) { - try { - me.progressCallback(75); - } catch(e) { - Zotero.logError(e); - }; - } - - try { - // Add fields to fields array - var fieldsEnumerator = subject.QueryInterface(Components.interfaces.nsISimpleEnumerator); - var fields = me._fields = []; - while(fieldsEnumerator.hasMoreElements()) { - let field = fieldsEnumerator.getNext(); +Zotero.Integration.Fields.prototype.get = new function() { + var deferred; + return function() { + // If we already have fields, just return them + if(this._fields) { + return Zotero.Promise.resolve(this._fields); + } + + if (deferred) { + return deferred.promise; + } + deferred = Zotero.Promise.defer(); + var promise = deferred.promise; + + // Otherwise, start getting fields + var getFieldsTime = (new Date()).getTime(), + me = this; + this._doc.getFieldsAsync(this._session.data.prefs['fieldType'], + {"observe":function(subject, topic, data) { + if(topic === "fields-available") { + if(me.progressCallback) { try { - fields.push(field.QueryInterface(Components.interfaces.zoteroIntegrationField)); - } catch (e) { - fields.push(field); - } + me.progressCallback(75); + } catch(e) { + Zotero.logError(e); + }; } - if(Zotero.Debug.enabled) { - var endTime = (new Date()).getTime(); - Zotero.debug("Integration: Retrieved "+fields.length+" fields in "+ - (endTime-getFieldsTime)/1000+"; "+ - 1000/((endTime-getFieldsTime)/fields.length)+" fields/second"); - } - } catch(e) { - // Reject promises - for(var i=0, n=me._deferreds.length; i<n; i++) { - me._deferreds[i].reject(e); - } - me._deferreds = []; - return; - } - - // Resolve promises - for(var i=0, n=me._deferreds.length; i<n; i++) { - me._deferreds[i].resolve(fields); - } - me._deferreds = []; - } else if(topic === "fields-progress") { - if(me.progressCallback) { try { - me.progressCallback((data ? parseInt(data, 10)*(3/4) : null)); + // Add fields to fields array + var fieldsEnumerator = subject.QueryInterface(Components.interfaces.nsISimpleEnumerator); + var fields = me._fields = []; + while(fieldsEnumerator.hasMoreElements()) { + let field = fieldsEnumerator.getNext(); + try { + fields.push(field.QueryInterface(Components.interfaces.zoteroIntegrationField)); + } catch (e) { + fields.push(field); + } + } + + if(Zotero.Debug.enabled) { + var endTime = (new Date()).getTime(); + Zotero.debug("Integration: Retrieved "+fields.length+" fields in "+ + (endTime-getFieldsTime)/1000+"; "+ + 1000/((endTime-getFieldsTime)/fields.length)+" fields/second"); + } } catch(e) { - Zotero.logError(e); - }; - } - } else if(topic === "fields-error") { - for(var i=0, n=me._deferreds.length; i<n; i++) { - me._deferreds[i].reject(data); + deferred.reject(e); + deferred = null; + return; + } + + deferred.resolve(fields); + deferred = null; + } else if(topic === "fields-progress") { + if(me.progressCallback) { + try { + me.progressCallback((data ? parseInt(data, 10)*(3/4) : null)); + } catch(e) { + Zotero.logError(e); + }; + } + } else if(topic === "fields-error") { + deferred.reject(e); + deferred = null; } - me._deferreds = []; - } - }, QueryInterface:XPCOMUtils.generateQI([Components.interfaces.nsIObserver, Components.interfaces.nsISupports])}); - return deferred.promise; + }, QueryInterface:XPCOMUtils.generateQI([Components.interfaces.nsIObserver, Components.interfaces.nsISupports])}); + return promise; + } } /** * Updates Zotero.Integration.Session attached to Zotero.Integration.Fields in line with document */ -Zotero.Integration.Fields.prototype.updateSession = Zotero.Promise.coroutine(function* () { +Zotero.Integration.Fields.prototype.updateSession = Zotero.Promise.coroutine(function* (forceCitations) { var collectFieldsTime; yield this.get(); this._session.resetRequest(this._doc); - this._removeCodeKeys = {}; this._removeCodeFields = {}; this._bibliographyFields = []; - this._bibliographyData = ""; collectFieldsTime = (new Date()).getTime(); + if (forceCitations) { + this._session.regenAll = true; + } yield this._processFields(); + this._session.regenAll = false; var endTime = (new Date()).getTime(); if(Zotero.Debug.enabled) { @@ -1533,28 +870,8 @@ Zotero.Integration.Fields.prototype.updateSession = Zotero.Promise.coroutine(fun + 1000/ ((endTime - collectFieldsTime) / this._fields.length) + " fields/second"); } - // Load uncited items from bibliography - if (this._bibliographyData && !this._session.bibliographyData) { - try { - yield this._session.loadBibliographyData(this._bibliographyData); - } catch(e) { - var exception = new Zotero.Integration.CorruptBibliographyException(this, e); - exception.setContext(this); - throw exception; - } - } - - // if we are reloading this session, assume no item IDs to be updated except for - // edited items if (this._session.reload) { - //this._session.restoreProcessorState(); TODO doesn't appear to be working properly - this._session.updateUpdateIndices(); - // Iterate through citations, yielding for UI updates - yield Zotero.Promise.each(this._session._updateCitations(), () => {}); - this._session.updateIndices = {}; - this._session.updateItemIDs = {}; - this._session.citationText = {}; - this._session.bibliographyHasChanged = false; + this._session.restoreProcessorState(); delete this._session.reload; } }); @@ -1562,59 +879,42 @@ Zotero.Integration.Fields.prototype.updateSession = Zotero.Promise.coroutine(fun /** * Keep processing fields until all have been processed */ -Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(function* (i) { - if(!i) i = 0; +Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(function* () { + if (!this._fields) { + throw new Error("_processFields called without fetching fields first"); + } - for(var n = this._fields.length; i<n; i++) { - var field = this._fields[i]; - - try { - var fieldCode = field.getCode(); - } catch(e) { - var corruptFieldException = new Zotero.Integration.CorruptFieldException( - "Field code not retrievable", e); - corruptFieldException.setContext(this, i); - throw corruptFieldException; - } - - var [type, content] = this.getCodeTypeAndContent(fieldCode); - if(type === INTEGRATION_TYPE_ITEM) { - var noteIndex = field.getNoteIndex(); - try { - yield this._session.addCitation(i, noteIndex, content); - } catch(e) { - var removeCode = false; - - if(e instanceof Zotero.Integration.CorruptFieldException) { - e.setContext(this, i) - } else if(e instanceof Zotero.Integration.MissingItemException) { - // Check if we've already decided to remove this field code - for (let reselectKey of e.reselectKeys) { - if(this._removeCodeKeys[reselectKey]) { - this._removeCodeFields[i] = true; - removeCode = true; - break; - } - } - if(!removeCode) e.setContext(this, i); - } - - if(!removeCode) { - if(this.fieldErrorHandler) return this.fieldErrorHandler(e); - throw e; - } + for (var i = 0; i < this._fields.length; i++) { + let field = Zotero.Integration.Field.loadExisting(this._fields[i]); + if (field.type === INTEGRATION_TYPE_ITEM) { + var noteIndex = field.getNoteIndex(), + citation = new Zotero.Integration.Citation(field); + + let action = yield citation.loadItemData(); + + if (action == Zotero.Integration.REMOVE_CODE) { + this._removeCodeFields[i] = true; + // Mark for removal and continue + continue; + } else if (action == Zotero.Integration.UPDATE) { + this._session.updateIndices[index] = true; } - } else if(type === INTEGRATION_TYPE_BIBLIOGRAPHY) { - if(this.ignoreEmptyBibliography && field.getText().trim() === "") { + + yield this._session.addCitation(i, noteIndex, citation); + } else if (field.type === INTEGRATION_TYPE_BIBLIOGRAPHY) { + if (this.ignoreEmptyBibliography && field.getText().trim() === "") { this._removeCodeFields[i] = true; } else { this._bibliographyFields.push(field); - if(!this._session.bibliographyData && !this._bibliographyData) { - this._bibliographyData = content; - } } } } + if (this._bibliographyFields.length) { + this._session.bibliography = new Zotero.Integration.Bibliography(this._bibliographyFields[0]); + yield this._session.bibliography.loadItemData(); + } + // TODO: figure this out + // Zotero.Notifier.trigger('add', 'collection', 'document'); }); /** @@ -1627,13 +927,28 @@ Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(fu */ Zotero.Integration.Fields.prototype.updateDocument = Zotero.Promise.coroutine(function* (forceCitations, forceBibliography, ignoreCitationChanges) { - // Update citations - this._session.updateUpdateIndices(forceCitations); - // Iterate through citations, yielding for UI updates - yield Zotero.Promise.each(this._session._updateCitations(), () => {}); + this._session.timer = new Zotero.Integration.Timer(); + this._session.timer.start(); - yield Zotero.Promise.each( - this._updateDocument(forceCitations, forceBibliography, ignoreCitationChanges), () => {}); + yield this._session._updateCitations() + yield this._updateDocument(forceCitations, forceBibliography, ignoreCitationChanges) + + var diff = this._session.timer.stop(); + this._session.timer = null; + Zotero.debug(`Integration: updateDocument complete in ${diff}s`) + // If the update takes longer than 5s suggest delaying citation updates + if (diff > DELAY_CITATIONS_PROMPT_TIMEOUT && !this._session.data.prefs.dontAskDelayCitationUpdates && !this._session.data.prefs.delayCitationUpdates) { + this._doc.activate(); + var result = this._session.displayAlert(Zotero.getString('integration.delayCitationUpdates.alert'), + DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO_CANCEL); + if (result == 2) { + this._session.data.prefs.delayCitationUpdates = true; + } + if (result) { + this._session.data.prefs.dontAskDelayCitationUpdates = true; + // yield this._session.setDocPrefs(true); + } + } }); /** @@ -1643,7 +958,7 @@ Zotero.Integration.Fields.prototype.updateDocument = Zotero.Promise.coroutine(fu * @param {Boolean} [ignoreCitationChanges] Whether to ignore changes to citations that have been * modified since they were created, instead of showing a warning */ -Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, forceBibliography, +Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitations, forceBibliography, ignoreCitationChanges) { if(this.progressCallback) { var nFieldUpdates = Object.keys(this._session.updateIndices).length; @@ -1660,90 +975,88 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, } catch(e) { Zotero.logError(e); } - yield; } + // Jump to next event loop step for UI updates + await Zotero.Promise.delay(); var citation = this._session.citationsByIndex[i]; - var field = this._fields[i]; + let citationField = citation._field; - // If there is no citation, we're deleting it, or we shouldn't update it, ignore - // it - if(!citation || citation.properties.delete) continue; var isRich = false; - - if(!citation.properties.dontUpdate) { - var formattedCitation = citation.properties.custom - ? citation.properties.custom : this._session.citationText[i]; - - if(formattedCitation.indexOf("\\") !== -1) { - // need to set text as RTF - formattedCitation = "{\\rtf "+formattedCitation+"}" - isRich = true; - } + if (!citation.properties.dontUpdate) { + var formattedCitation = citation.properties.formattedCitation && citation.properties.custom + ? citation.properties.custom : citation.text; + var plainCitation = citation.properties.plainCitation && citationField.getText(); - if(forceCitations === FORCE_CITATIONS_RESET_TEXT - || citation.properties.formattedCitation !== formattedCitation) { - // Check if citation has been manually modified - if(!ignoreCitationChanges && citation.properties.plainCitation) { - var plainCitation = field.getText(); - if(plainCitation !== citation.properties.plainCitation) { - // Citation manually modified; ask user if they want to save changes - Zotero.debug("[_updateDocument] Attempting to update manually modified citation.\n" - + "Original: " + citation.properties.plainCitation + "\n" - + "Current: " + plainCitation - ); - field.select(); - var result = this._doc.displayAlert( - Zotero.getString("integration.citationChanged")+"\n\n"+Zotero.getString("integration.citationChanged.description"), - DIALOG_ICON_CAUTION, DIALOG_BUTTONS_YES_NO); - if(result) { - citation.properties.dontUpdate = true; - } + // If we're not specifically *not* trying to regen text + if (forceCitations != FORCE_CITATIONS_FALSE + // Or metadata has changed thus changing the formatted citation + || (citation.properties.formattedCitation !== formattedCitation) + // Or we shouldn't ignore citation changes and the citation text has changed + || (!ignoreCitationChanges && plainCitation !== citation.properties.plainCitation)) { + + if (plainCitation !== citation.properties.plainCitation) { + // Citation manually modified; ask user if they want to save changes + Zotero.debug("[_updateDocument] Attempting to update manually modified citation.\n" + + "Original: " + citation.properties.plainCitation + "\n" + + "Current: " + plainCitation + ); + citationField.select(); + var result = this._session.displayAlert( + Zotero.getString("integration.citationChanged")+"\n\n"+Zotero.getString("integration.citationChanged.description"), + DIALOG_ICON_CAUTION, DIALOG_BUTTONS_YES_NO); + if (result) { + citation.properties.dontUpdate = true; } } if(!citation.properties.dontUpdate) { - field.setText(formattedCitation, isRich); + // Word will preserve previous text styling, so we need to force remove it + // for citations that were inserted with delay styling + if (citation.properties.formattedCitation && citation.properties.formattedCitation.includes(DELAYED_CITATION_STYLING)) { + isRich = citationField.setText(`${DELAYED_CITATION_STYLING_CLEAR}{${formattedCitation}}`); + } else { + isRich = citationField.setText(formattedCitation); + } citation.properties.formattedCitation = formattedCitation; - citation.properties.plainCitation = field.getText(); + citation.properties.plainCitation = citationField.getText(); } } } - var fieldCode = this._session.getCitationField(citation); - if(fieldCode != citation.properties.field) { - field.setCode(`ITEM CSL_CITATION ${fieldCode}`); - if(this._session.data.prefs.fieldType === "ReferenceMark" + var serializedCitation = citation.serialize(); + if (serializedCitation != citation.properties.field) { + citationField.setCode(serializedCitation); + if (this._session.data.prefs.fieldType === "ReferenceMark" && this._session.data.prefs.noteType != 0 && isRich && !citation.properties.dontUpdate) { // For ReferenceMarks with formatting, we need to set the text again, because // setting the field code removes formatting from the mark. I don't like this. - field.setText(formattedCitation, isRich); + citationField.setText(formattedCitation, isRich); } } nUpdated++; } // update bibliographies - if(this._bibliographyFields.length // if bibliography exists + if (this._session.bibliography // if bibliography exists && (this._session.bibliographyHasChanged // and bibliography changed || forceBibliography)) { // or if we should generate regardless of // changes - var bibliographyFields = this._bibliographyFields; - if(forceBibliography || this._session.bibliographyDataHasChanged) { - var bibliographyData = this._session.getBibliographyData(); - for (let field of bibliographyFields) { - field.setCode(`BIBL ${bibliographyData} CSL_BIBLIOGRAPHY`); + if (forceBibliography || this._session.bibliographyDataHasChanged) { + let code = this._session.bibliography.serialize(); + for (let field of this._bibliographyFields) { + field.setCode(code); } } // get bibliography and format as RTF - var bib = this._session.getBibliography(); + var bib = this._session.bibliography.getCiteprocBibliography(this._session.style); var bibliographyText = ""; - if(bib) { + if (bib) { bibliographyText = bib[0].bibstart+bib[1].join("\\\r\n")+"\\\r\n"+bib[0].bibend; // if bibliography style not set, set it @@ -1756,39 +1069,33 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, // set bibliographyStyleHasBeenSet parameter to prevent further changes this._session.data.style.bibliographyStyleHasBeenSet = true; - this._doc.setDocumentData(this._session.data.serialize()); } } // set bibliography text - for (let field of bibliographyFields) { + for (let field of this._bibliographyFields) { if(this.progressCallback) { try { this.progressCallback(75+(nUpdated/nFieldUpdates)*25); } catch(e) { Zotero.logError(e); } - yield; } + // Jump to next event loop step for UI updates + await Zotero.Promise.delay(); - if(bibliographyText) { - field.setText(bibliographyText, true); + if (bibliographyText) { + field.setText(bibliographyText); } else { - field.setText("{Bibliography}", false); + field.setText("{Bibliography}"); } nUpdated += 5; } } // Do these operations in reverse in case plug-ins care about order - for(var i=this._session.citationsByIndex.length-1; i>=0; i--) { - if(this._session.citationsByIndex[i] && - this._session.citationsByIndex[i].properties.delete) { - this._fields[i].delete(); - } - } var removeCodeFields = Object.keys(this._removeCodeFields).sort(); - for(var i=(removeCodeFields.length-1); i>=0; i--) { + for (var i=(removeCodeFields.length-1); i>=0; i--) { this._fields[removeCodeFields[i]].removeCode(); } } @@ -1797,244 +1104,130 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, * Brings up the addCitationDialog, prepopulated if a citation is provided */ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(function* (field) { - var newField, citation, fieldIndex, session = this._session; + var newField; - // if there's already a citation, make sure we have item IDs in addition to keys - if(field) { - try { - var code = field.getCode(); - } catch(e) {} - - if(code) { - var [type, content] = this.getCodeTypeAndContent(code); - if(type != INTEGRATION_TYPE_ITEM) { - throw new Zotero.Exception.Alert("integration.error.notInCitation"); - } - - try { - citation = session.unserializeCitation(content); - } catch(e) {} - - if(citation) { - try { - yield session.lookupItems(citation); - } catch(e) { - if(e instanceof Zotero.Integration.MissingItemException) { - citation.citationItems = []; - } else { - throw e; - } - } - - if(citation.properties.dontUpdate - || (citation.properties.plainCitation - && field.getText() !== citation.properties.plainCitation)) { - this._doc.activate(); - Zotero.debug("[addEditCitation] Attempting to update manually modified citation.\n" - + "citation.properties.dontUpdate: " + citation.properties.dontUpdate + "\n" - + "Original: " + citation.properties.plainCitation + "\n" - + "Current: " + field.getText() - ); - if(!this._doc.displayAlert(Zotero.getString("integration.citationChanged.edit"), - DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL)) { - throw new Zotero.Exception.UserCancelled("editing citation"); - } - } - - // make sure it's going to get updated - delete citation.properties["formattedCitation"]; - delete citation.properties["plainCitation"]; - delete citation.properties["dontUpdate"]; - } + if (field) { + field = Zotero.Integration.Field.loadExisting(field); + + if (field.type != INTEGRATION_TYPE_ITEM) { + throw new Zotero.Exception.Alert("integration.error.notInCitation"); } } else { newField = true; - field = this.addField(true); + field = new Zotero.Integration.CitationField(yield this.addField(true)); + field.clearCode(); } - var me = this; - return Zotero.Promise.resolve(field).then(function(field) { - if(!citation) { - field.setCode("TEMP"); - citation = {"citationItems":[], "properties":{}}; + var citation = new Zotero.Integration.Citation(field); + yield citation.prepareForEditing(); + + // ------------------- + // Preparing stuff to pass into CitationEditInterface + var fieldIndexPromise = this.get().then(function(fields) { + for (var i=0, n=fields.length; i<n; i++) { + if (fields[i].equals(field._field)) { + // This is needed, because LibreOffice integration plugin caches the field code instead of asking + // the document every time when calling #getCode(). + fields[i] = field._field; + return i; + } } + }); + + var citationsByItemIDPromise; + if (this._session.data.prefs.delayCitationUpdates) { + citationsByItemIDPromise = Zotero.Promise.resolve(this._session.citationsByItemID); + } else { + citationsByItemIDPromise = fieldIndexPromise.then(function() { + return this.updateSession(FORCE_CITATIONS_FALSE); + }.bind(this)).then(function() { + return this._session.citationsByItemID; + }.bind(this)); + } + + var previewFn = Zotero.Promise.coroutine(function* (citation) { + let idx = yield fieldIndexPromise; + yield citationsByItemIDPromise; - var io = new Zotero.Integration.CitationEditInterface(citation, field, me, session); - - if(Zotero.Prefs.get("integration.useClassicAddCitationDialog")) { - Zotero.Integration.displayDialog(me._doc, - 'chrome://zotero/content/integration/addCitationDialog.xul', 'alwaysRaised,resizable', - io); - } else { - var mode = (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised') - ? 'popup' : 'alwaysRaised')+',resizable=false'; - Zotero.Integration.displayDialog(me._doc, - 'chrome://zotero/content/integration/quickFormat.xul', mode, io); + let citations = this._session.getCiteprocLists(); + let citationsPre = citations.slice(0, idx); + let citationsPost = citations.slice(idx+1); + try { + return this._session.style.previewCitationCluster(citation, citationsPre, citationsPost, "rtf"); + } catch(e) { + throw e; } + }.bind(this)); - if(newField) { - return io.promise.catch(function(e) { - // Try to delete new field on failure - try { - field.delete(); - } catch(e) {} - throw e; - }); - } else { - return io.promise; + var io = new Zotero.Integration.CitationEditInterface( + citation, + this._session.style.opt.sort_citations, fieldIndexPromise, citationsByItemIDPromise, + previewFn + ); + + if (Zotero.Prefs.get("integration.useClassicAddCitationDialog")) { + Zotero.Integration.displayDialog('chrome://zotero/content/integration/addCitationDialog.xul', + 'alwaysRaised,resizable', io); + } else { + var mode = (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised') + ? 'popup' : 'alwaysRaised')+',resizable=false'; + Zotero.Integration.displayDialog('chrome://zotero/content/integration/quickFormat.xul', + mode, io); + } + + // ------------------- + // io.promise resolves when the citation dialog is closed + this.progressCallback = yield io.promise; + + if (!io.citation.citationItems.length) { + // Try to delete new field on cancel + if (newField) { + try { + field.delete(); + } catch(e) {} } - }); + throw new Zotero.Exception.UserCancelled("inserting citation"); + } + + let fieldIndex = yield fieldIndexPromise; + this._session.updateIndices[fieldIndex] = true; + // Make sure session updated + yield citationsByItemIDPromise; + return [fieldIndex, field, io.citation]; }); /** * Citation editing functions and propertiesaccessible to quickFormat.js and addCitationDialog.js */ -Zotero.Integration.CitationEditInterface = function(citation, field, fieldGetter, session) { +Zotero.Integration.CitationEditInterface = function(citation, sortable, fieldIndexPromise, citationsByItemIDPromise, previewFn) { this.citation = citation; - this._field = field; - this._fieldGetter = fieldGetter; - this._session = session; + this.sortable = sortable; + this.previewFn = previewFn; + this._fieldIndexPromise = fieldIndexPromise; + this._citationsByItemIDPromise = citationsByItemIDPromise; - this._sessionUpdateResolveErrors = false; - this._sessionUpdateDeferreds = []; - - // Needed to make this work across boundaries + // Not available in quickFormat.js if this unspecified this.wrappedJSObject = this; - - // Determine whether citation is sortable in current style - this.sortable = session.style.opt.sort_citations; - - // Citeproc-js style object for use of third-party extension - this.style = session.style; - - // Start getting citation data + this._acceptDeferred = Zotero.Promise.defer(); - this._fieldIndexPromise = fieldGetter.get().then(function(fields) { - for(var i=0, n=fields.length; i<n; i++) { - if(fields[i].equals(field)) { - return i; - } - } - }); - - var me = this; - this.promise = this._fieldIndexPromise.then(function(fieldIndex) { - me._fieldIndex = fieldIndex; - return me._acceptDeferred.promise; - }).then(function(progressCallback) { - if(!me.citation.citationItems.length) { - throw new Zotero.Exception.UserCancelled("inserting citation"); - } - me._fieldGetter.progressCallback = progressCallback; - return me._updateSession(true); - }).then(Zotero.Promise.coroutine(function* () { - // Add new citation - yield me._session.addCitation(me._fieldIndex, me._field.getNoteIndex(), me.citation); - me._session.updateIndices[me._fieldIndex] = true; - - // Check if bibliography changed - if(!me._session.bibliographyHasChanged) { - var citationItems = me.citation.citationItems; - for(var i=0, n=citationItems.length; i<n; i++) { - if(me._session.citationsByItemID[citationItems[i].itemID] && - me._session.citationsByItemID[citationItems[i].itemID].length == 1) { - me._session.bibliographyHasChanged = true; - break; - } - } - } - - // Update document - return me._fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, false, false); - })); + this.promise = this._acceptDeferred.promise; } Zotero.Integration.CitationEditInterface.prototype = { /** - * Run a function when the session information has been updated - * @param {Boolean} [resolveErrors] Whether to attempt to resolve errors that occur - * while session information is being updated, e.g. by showing a dialog to the - * user. - * @return {Promise} A promise resolved when session information has been updated - */ - "_updateSession":function _updateSession(resolveErrors) { - var me = this; - if(this._sessionUpdatePromise && this._sessionUpdatePromise.isFulfilled()) { - // Session has already been updated. If we were deferring resolving an error, - // and we are supposed to resolve it now, then do that - if(this._sessionUpdateError) { - if(resolveErrors && this._sessionUpdateError.attemptToResolve) { - return this._sessionUpdateError.attemptToResolve().then(function() { - delete me._sessionUpdateError; - }); - } else { - return Zotero.Promise.reject(this._sessionUpdateError); - } - } else { - return Zotero.Promise.resolve(true); - } - } else { - var deferred = Zotero.Promise.defer(); - - this._sessionUpdateResolveErrors = this._sessionUpdateResolveErrors || resolveErrors; - this._sessionUpdateDeferreds.push(deferred); - - if(!this._sessionUpdatePromise) { - // Add deferred to queue - - var me = this; - this._fieldGetter.fieldErrorHandler = function(err) { - // If an error occurred, either try to resolve it or reject it - // depending on whether anyone has called _updateSession with - // resolveErrors set to true. This is necessary to prevent field code - // errors from appearing while the user interacts with the QuickFormat - // dialog, since some people find this very confusing. - if(me._sessionUpdateResolveErrors && err.attemptToResolve) { - return err.attemptToResolve(); - } else { - throw err; - } - }; - this._sessionUpdatePromise = this._fieldGetter.updateSession().then(function() { - // If no errors occurred, or errors were resolved, resolve promises - for(var i=0; i<me._sessionUpdateDeferreds.length; i++) { - me._sessionUpdateDeferreds[i].resolve(true); - } - }, function(err) { - // Error propagates if attemptToResolve failed or wasn't called to - // begin with - me._sessionUpdateError = err; - for(var i=0; i<me._sessionUpdateDeferreds.length; i++) { - me._sessionUpdateDeferreds[i].reject(err); - } - Zotero.logError(err); - }); - } - - return deferred.promise; - } - }, - - /** * Execute a callback with a preview of the given citation * @return {Promise} A promise resolved with the previewed citation string */ - "preview":function preview() { - var me = this; - return this._updateSession().then(function() { - me.citation.properties.zoteroIndex = me._fieldIndex; - me.citation.properties.noteIndex = me._field.getNoteIndex(); - return me._session.previewCitation(me.citation); - }); + preview: function() { + return this.previewFn(this.citation); }, /** - * Sort the citation + * Sort the citationItems within citation (depends on this.citation.properties.unsorted) + * @return {Promise} A promise resolved with the previewed citation string */ - "sort":function() { - // Unlike above, we can do the previewing here without waiting for all the fields - // to load, since they won't change the sorting (I don't think) - this._session.previewCitation(this.citation); + sort: function() { + return this.preview(); }, /** @@ -2042,8 +1235,8 @@ Zotero.Integration.CitationEditInterface.prototype = { * @param {Function} [progressCallback] A callback to be run when progress has changed. * Receives a number from 0 to 100 indicating current status. */ - "accept":function(progressCallback) { - if(!this._acceptDeferred.promise.isFulfilled()) { + accept: function(progressCallback) { + if (!this._acceptDeferred.promise.isFulfilled()) { this._acceptDeferred.resolve(progressCallback); } }, @@ -2052,30 +1245,8 @@ Zotero.Integration.CitationEditInterface.prototype = { * Get a list of items used in the current document * @return {Promise} A promise resolved by the items */ - "getItems":function() { - if(this._fieldIndexPromise.isFulfilled() - || Zotero.Utilities.isEmpty(this._session.citationsByItemID)) { - // Either we already have field data for this run or we have no item data at all. - // Update session before continuing. - var me = this; - return this._updateSession().then(function() { - return me._getItems(); - }, function() { - return []; - }); - } else { - // We have item data left over from a previous run with this document, so we don't need - // to wait. - return Zotero.Promise.resolve(this._getItems()); - } - }, - - /** - * Helper function for getItems. Does the same thing, but this can assume that the session data - * has already been updated if it should be. - */ - "_getItems":function() { - var citationsByItemID = this._session.citationsByItemID; + getItems: Zotero.Promise.coroutine(function* () { + var citationsByItemID = yield this._citationsByItemIDPromise; var ids = Object.keys(citationsByItemID).filter(itemID => { return citationsByItemID[itemID] && citationsByItemID[itemID].length @@ -2085,7 +1256,7 @@ Zotero.Integration.CitationEditInterface.prototype = { }); // Sort all previously cited items at top, and all items cited later at bottom - var fieldIndex = this._fieldIndex; + var fieldIndex = yield this._fieldIndexPromise; ids.sort(function(a, b) { var indexA = citationsByItemID[a][0].properties.zoteroIndex, indexB = citationsByItemID[b][0].properties.zoteroIndex; @@ -2100,22 +1271,21 @@ Zotero.Integration.CitationEditInterface.prototype = { }); return Zotero.Cite.getItem(ids); - } + }), } /** * Keeps track of all session-specific variables */ -Zotero.Integration.Session = function(doc) { - // holds items not in document that should be in bibliography - this.uncitedItems = {}; - this.omittedItems = {}; +Zotero.Integration.Session = function(doc, app) { this.embeddedItems = {}; - this.embeddedZoteroItems = {}; - this.embeddedZoteroItemsByURI = {}; - this.customBibliographyText = {}; - this.reselectedItems = {}; + this.embeddedItemsByURI = {}; this.resetRequest(doc); + this.primaryFieldType = app.primaryFieldType; + this.secondaryFieldType = app.secondaryFieldType; + + this.sessionID = Zotero.randomString(); + Zotero.Integration.sessions[this.sessionID] = this; } /** @@ -2124,22 +1294,82 @@ Zotero.Integration.Session = function(doc) { Zotero.Integration.Session.prototype.resetRequest = function(doc) { this.uriMap = new Zotero.Integration.URIMap(this); - this.regenerateAll = false; this.bibliographyHasChanged = false; this.bibliographyDataHasChanged = false; - this.updateItemIDs = {}; this.updateIndices = {}; this.newIndices = {}; - this.oldCitationIDs = this.citeprocCitationIDs; - this.citationsByItemID = {}; - this.citationsByIndex = []; + this.citationsByIndex = {}; this.documentCitationIDs = {}; - this.citeprocCitationIDs = {}; - this.citationText = {}; - this.doc = doc; + this._doc = doc; +} + +/** + * Prepares session data and displays docPrefs dialog if needed + * @param require {Boolean} Whether an error should be thrown if no preferences or fields + * exist (otherwise, the set doc prefs dialog is shown) + * @param dontRunSetDocPrefs {Boolean} Whether to show the Document Preferences window if no preferences exist + * @return {Promise{Boolean}} true if session ready to, false if preferences dialog needs to be displayed first + */ +Zotero.Integration.Session.prototype.init = Zotero.Promise.coroutine(function *(require, dontRunSetDocPrefs) { + var data = this.data; + // If no data, show doc prefs window + if (!data.prefs.fieldType) { + var haveFields = false; + data = new Zotero.Integration.DocumentData(); + + if (require) { + // check to see if fields already exist + for (let fieldType of [this.primaryFieldType, this.secondaryFieldType]) { + var fields = this._doc.getFields(fieldType); + if (fields.hasMoreElements()) { + data.prefs.fieldType = fieldType; + haveFields = true; + break; + } + } + + // if no fields, throw an error + if (!haveFields) { + return Zotero.Promise.reject(new Zotero.Exception.Alert( + "integration.error.mustInsertCitation", + [], "integration.error.title")); + } else { + Zotero.debug("Integration: No document preferences found, but found "+data.prefs.fieldType+" fields"); + } + } + + if (haveFields && !Object.keys(this.citationsByIndex).length) { + // We should load up the fields on the first interaction with the document, so + // that we can display a list of existing citations, etc. + yield this.fields.get(); + yield this.fields.updateSession(FORCE_CITATIONS_FALSE); + } + + if (dontRunSetDocPrefs) return false; + + yield this.setDocPrefs(); + } + if (!Object.keys(this.citationsByIndex).length) { + // We should load up the fields on the first interaction with the document, so + // that we can display a list of existing citations, etc. + yield this.fields.get(); + yield this.fields.updateSession(FORCE_CITATIONS_FALSE); + } + return true; +}); + +Zotero.Integration.Session.prototype.displayAlert = function() { + if (this.timer) { + this.timer.pause(); + } + var result = this._doc.displayAlert.apply(this._doc, arguments); + if (this.timer) { + this.timer.resume(); + } + return result; } /** @@ -2152,7 +1382,10 @@ Zotero.Integration.Session.prototype.resetRequest = function(doc) { Zotero.Integration.Session.prototype.setData = Zotero.Promise.coroutine(function *(data, resetStyle) { var oldStyle = (this.data && this.data.style ? this.data.style : false); this.data = data; + this.data.sessionID = this.sessionID; if (data.style.styleID && (!oldStyle || oldStyle.styleID != data.style.styleID || resetStyle)) { + // We're changing the citeproc instance, so we'll have to reinsert all citations into the registry + this.reload = true; this.styleID = data.style.styleID; try { yield Zotero.Styles.init(); @@ -2161,7 +1394,6 @@ Zotero.Integration.Session.prototype.setData = Zotero.Promise.coroutine(function this.style = getStyle.getCiteProc(data.style.locale, data.prefs.automaticJournalAbbreviations); this.style.setOutputFormat("rtf"); this.styleClass = getStyle.class; - this.dateModified = new Object(); } catch (e) { Zotero.logError(e); throw new Zotero.Exception.Alert("integration.error.invalidStyle"); @@ -2180,26 +1412,26 @@ Zotero.Integration.Session.prototype.setData = Zotero.Promise.coroutine(function * if there wasn't, or rejected with Zotero.Exception.UserCancelled if the dialog was * cancelled. */ -Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(function* (doc, primaryFieldType, secondaryFieldType) { - var io = new function() { - this.wrappedJSObject = this; - }; +Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(function* (highlightDelayCitations=false) { + var io = new function() { this.wrappedJSObject = this; }; + io.primaryFieldType = this.primaryFieldType; + io.secondaryFieldType = this.secondaryFieldType; if (this.data) { io.style = this.data.style.styleID; io.locale = this.data.style.locale; io.useEndnotes = this.data.prefs.noteType == 0 ? 0 : this.data.prefs.noteType-1; io.fieldType = this.data.prefs.fieldType; - io.primaryFieldType = primaryFieldType; - io.secondaryFieldType = secondaryFieldType; + io.delayCitationUpdates = this.data.prefs.delayCitationUpdates; + io.dontAskDelayCitationUpdates = this.data.prefs.dontAskDelayCitationUpdates; + io.highlightDelayCitations = highlightDelayCitations; io.automaticJournalAbbreviations = this.data.prefs.automaticJournalAbbreviations; io.requireStoreReferences = !Zotero.Utilities.isEmpty(this.embeddedItems); } // Make sure styles are initialized for new docs yield Zotero.Styles.init(); - yield Zotero.Integration.displayDialog(doc, - 'chrome://zotero/content/integration/integrationDocPrefs.xul', '', io); + yield Zotero.Integration.displayDialog('chrome://zotero/content/integration/integrationDocPrefs.xul', '', io); if (!io.style || !io.fieldType) { throw new Zotero.Exception.UserCancelled("document preferences window"); @@ -2211,8 +1443,10 @@ Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(func data.sessionID = oldData.sessionID; data.style.styleID = io.style; data.style.locale = io.locale; + data.prefs = oldData ? Object.assign({}, oldData.prefs) : {}; data.prefs.fieldType = io.fieldType; data.prefs.automaticJournalAbbreviations = io.automaticJournalAbbreviations; + data.prefs.delayCitationUpdates = io.delayCitationUpdates var forceStyleReset = oldData && ( @@ -2227,106 +1461,17 @@ Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(func if (!oldData || oldData.style.styleID != data.style.styleID || oldData.prefs.noteType != data.prefs.noteType || oldData.prefs.fieldType != data.prefs.fieldType + || (!data.prefs.delayCitationUpdates && oldData.prefs.delayCitationUpdates != data.prefs.delayCitationUpdates) || oldData.prefs.automaticJournalAbbreviations != data.prefs.automaticJournalAbbreviations) { // This will cause us to regenerate all citations - this.oldCitationIDs = {}; + this.regenAll = true; + this.reload = true; } return oldData || null; }) /** - * Reselects an item to replace a deleted item - * @param exception {Zotero.Integration.MissingItemException} - */ -Zotero.Integration.Session.prototype.reselectItem = function(doc, exception) { - var io = new function() { this.wrappedJSObject = this; }, - me = this; - io.addBorder = Zotero.isWin; - io.singleSelection = true; - - return Zotero.Integration.displayDialog(doc, 'chrome://zotero/content/selectItemsDialog.xul', - 'resizable', io).then(function() { - if(io.dataOut && io.dataOut.length) { - var itemID = io.dataOut[0]; - - // add reselected item IDs to hash, so they can be used - for (let reselectKey of exception.reselectKeys) { - me.reselectedItems[reselectKey] = itemID; - } - // add old URIs to map, so that they will be included - if(exception.reselectKeyType == RESELECT_KEY_URI) { - me.uriMap.add(itemID, exception.reselectKeys.concat(me.uriMap.getURIsForItemID(itemID))); - } - // flag for update - me.updateItemIDs[itemID] = true; - } - }); -} - -/** - * Generates a field from a citation object - */ -Zotero.Integration.Session.prototype.getCitationField = function(citation) { - const saveProperties = ["custom", "unsorted", "formattedCitation", "plainCitation", "dontUpdate"]; - const saveCitationItemKeys = ["locator", "label", "suppress-author", "author-only", "prefix", - "suffix"]; - - var type; - var field = []; - - field.push('"citationID":'+uneval(citation.citationID)); - - var properties = JSON.stringify(citation.properties, saveProperties); - if(properties != "{}") { - field.push('"properties":'+properties); - } - - var m = citation.citationItems.length; - var citationItems = new Array(m); - for(var j=0; j<m; j++) { - var citationItem = citation.citationItems[j], - serializeCitationItem = {}, - key, value; - - // add URI and itemData - var slashIndex; - if(typeof citationItem.id === "string" && (slashIndex = citationItem.id.indexOf("/")) !== -1) { - // this is an embedded item - serializeCitationItem.id = citationItem.itemData.id; - serializeCitationItem.uris = citationItem.uris; - - // XXX For compatibility with older versions of Zotero; to be removed at a later date - serializeCitationItem.uri = serializeCitationItem.uris; - - // always store itemData, since we have no way to get it back otherwise - serializeCitationItem.itemData = citationItem.itemData; - } else { - serializeCitationItem.id = citationItem.id; - serializeCitationItem.uris = this.uriMap.getURIsForItemID(citationItem.id); - - // XXX For compatibility with older versions of Zotero; to be removed at a later date - serializeCitationItem.uri = serializeCitationItem.uris; - - serializeCitationItem.itemData = this.style.sys.retrieveItem(citationItem.id); - } - - // copy saveCitationItemKeys - for(var i=0, n=saveCitationItemKeys.length; i<n; i++) { - if((value = citationItem[(key = saveCitationItemKeys[i])])) { - serializeCitationItem[key] = value; - } - } - - citationItems[j] = JSON.stringify(serializeCitationItem); - } - field.push('"citationItems":['+citationItems.join(",")+"]"); - field.push('"schema":"https://github.com/citation-style-language/schema/raw/master/csl-citation.json"'); - - return "{"+field.join(",")+"}"; -} - -/** * Adds a citation based on a serialized Word field */ Zotero.Integration._oldCitationLocatorMap = { @@ -2338,20 +1483,9 @@ Zotero.Integration._oldCitationLocatorMap = { /** * Adds a citation to the arrays representing the document */ -Zotero.Integration.Session.prototype.addCitation = Zotero.Promise.coroutine(function* (index, noteIndex, arg) { +Zotero.Integration.Session.prototype.addCitation = Zotero.Promise.coroutine(function* (index, noteIndex, citation) { var index = parseInt(index, 10); - - if(typeof(arg) == "string") { // text field - if(arg == "!" || arg == "X") return; - - var citation = this.unserializeCitation(arg, index); - } else { // a citation already - var citation = arg; - } - - // get items - yield this.lookupItems(citation, index); - + citation.properties.added = true; citation.properties.zoteroIndex = index; citation.properties.noteIndex = noteIndex; @@ -2378,707 +1512,252 @@ Zotero.Integration.Session.prototype.addCitation = Zotero.Promise.coroutine(func // We need a new ID if there's another citation with the same citation ID in this document var needNewID = !citation.citationID || this.documentCitationIDs[citation.citationID]; - if(needNewID || !this.oldCitationIDs[citation.citationID]) { + if(needNewID || this.regenAll) { if(needNewID) { Zotero.debug("Integration: "+citation.citationID+" ("+index+") needs new citationID"); citation.citationID = Zotero.randomString(); } - this.newIndices[index] = true; this.updateIndices[index] = true; } Zotero.debug("Integration: Adding citationID "+citation.citationID); this.documentCitationIDs[citation.citationID] = citation.citationID; }); -/** - * Looks up item IDs to correspond with keys or generates embedded items for given citation object. - * Throws a MissingItemException if item was not found. - */ -Zotero.Integration.Session.prototype.lookupItems = Zotero.Promise.coroutine(function* (citation, index) { - let items = []; - - for(var i=0, n=citation.citationItems.length; i<n; i++) { - var citationItem = citation.citationItems[i]; - - // get Zotero item - var zoteroItem = false, - needUpdate; - if(citationItem.uris) { - [zoteroItem, needUpdate] = yield this.uriMap.getZoteroItemForURIs(citationItem.uris); - if(needUpdate && index) this.updateIndices[index] = true; - - // Unfortunately, people do weird things with their documents. One weird thing people - // apparently like to do (http://forums.zotero.org/discussion/22262/) is to copy and - // paste citations from other documents created with earlier versions of Zotero into - // their documents and then not refresh the document. Usually, this isn't a problem. If - // document is edited by the same user, it will work without incident. If the first - // citation of a given item doesn't contain itemData, the user will get a - // MissingItemException. However, it may also happen that the first citation contains - // itemData, but later citations don't, because the user inserted the item properly and - // then copied and pasted the same citation from another document. We check for that - // possibility here. - if(zoteroItem.cslItemData && !citationItem.itemData) { - citationItem.itemData = zoteroItem.cslItemData; - this.updateIndices[index] = true; - } - } else { - if(citationItem.key && citationItem.libraryID) { - // DEBUG: why no library id? - zoteroItem = Zotero.Items.getByLibraryAndKey(citationItem.libraryID, citationItem.key); - } else if(citationItem.itemID) { - zoteroItem = Zotero.Items.get(citationItem.itemID); - } else if(citationItem.id) { - zoteroItem = Zotero.Items.get(citationItem.id); - } - if(zoteroItem && index) this.updateIndices[index] = true; - } - - // if no item, check if it was already reselected and otherwise handle as a missing item - if(!zoteroItem) { - if(citationItem.uris) { - var reselectKeys = citationItem.uris; - var reselectKeyType = RESELECT_KEY_URI; - } else if(citationItem.key) { - var reselectKeys = [citationItem.key]; - var reselectKeyType = RESELECT_KEY_ITEM_KEY; - } else if(citationItem.id) { - var reselectKeys = [citationItem.id]; - var reselectKeyType = RESELECT_KEY_ITEM_ID; - } else { - var reselectKeys = [citationItem.itemID]; - var reselectKeyType = RESELECT_KEY_ITEM_ID; - } - - // look to see if item has already been reselected - for (let reselectKey of reselectKeys) { - if(this.reselectedItems[reselectKey]) { - zoteroItem = Zotero.Items.get(this.reselectedItems[reselectKey]); - citationItem.id = zoteroItem.id; - if(index) this.updateIndices[index] = true; - break; - } - } - - if(!zoteroItem) { - if(citationItem.itemData) { - // add new embedded item - var itemData = Zotero.Utilities.deepCopy(citationItem.itemData); - - // assign a random string as an item ID - var anonymousID = Zotero.randomString(); - var globalID = itemData.id = citationItem.id = this.data.sessionID+"/"+anonymousID; - this.embeddedItems[anonymousID] = itemData; - - // assign a Zotero item - var surrogateItem = this.embeddedZoteroItems[anonymousID] = new Zotero.Item(); - Zotero.Utilities.itemFromCSLJSON(surrogateItem, itemData); - surrogateItem.cslItemID = globalID; - surrogateItem.cslURIs = citationItem.uris; - surrogateItem.cslItemData = itemData; - - for(var j=0, m=citationItem.uris.length; j<m; j++) { - this.embeddedZoteroItemsByURI[citationItem.uris[j]] = surrogateItem; - } - } else { - // if not already reselected, throw a MissingItemException - throw(new Zotero.Integration.MissingItemException( - reselectKeys, reselectKeyType, i, citation.citationItems.length)); - } - } - } - - if(zoteroItem) { - if (zoteroItem.cslItemID) { - citationItem.id = zoteroItem.cslItemID; - } - else { - citationItem.id = zoteroItem.id; - items.push(zoteroItem); - } - } - } - - // Items may be in libraries that haven't been loaded, and retrieveItem() is synchronous, so load - // all data (as required by toJSON(), which is used by itemToExportFormat(), which is used by - // itemToCSLJSON()) now - if (items.length) { - yield Zotero.Items.loadDataTypes(items); - } -}); - -/** - * Unserializes a JSON citation into a citation object (sans items) - */ -Zotero.Integration.Session.prototype.unserializeCitation = function(arg, index) { - var firstBracket = arg.indexOf("{"); - if(firstBracket !== -1) { // JSON field - arg = arg.substr(firstBracket); - - // fix for corrupted fields - var lastBracket = arg.lastIndexOf("}"); - if(lastBracket+1 != arg.length) { - if(index) this.updateIndices[index] = true; - arg = arg.substr(0, lastBracket+1); - } - - // get JSON - try { - var citation = JSON.parse(arg); - } catch(e) { - // fix for corrupted fields (corrupted by Word, somehow) - try { - var citation = JSON.parse(arg.substr(0, arg.length-1)); - } catch(e) { - // another fix for corrupted fields (corrupted by 2.1b1) - try { - var citation = JSON.parse(arg.replace(/{{((?:\s*,?"unsorted":(?:true|false)|\s*,?"custom":"(?:(?:\\")?[^"]*\s*)*")*)}}/, "{$1}")); - } catch(e) { - throw new Zotero.Integration.CorruptFieldException(arg, e); - } - } - } - - // fix for uppercase citation codes - if(citation.CITATIONITEMS) { - if(index) this.updateIndices[index] = true; - citation.citationItems = []; - for (var i=0; i<citation.CITATIONITEMS.length; i++) { - for (var j in citation.CITATIONITEMS[i]) { - switch (j) { - case 'ITEMID': - var field = 'itemID'; - break; - - // 'position', 'custom' - default: - var field = j.toLowerCase(); - } - if (!citation.citationItems[i]) { - citation.citationItems[i] = {}; - } - citation.citationItems[i][field] = citation.CITATIONITEMS[i][j]; - } - } - } - - if(!citation.properties) citation.properties = {}; - - for (let citationItem of citation.citationItems) { - // for upgrade from Zotero 2.0 or earlier - if(citationItem.locatorType) { - citationItem.label = citationItem.locatorType; - delete citationItem.locatorType; - } else if(citationItem.suppressAuthor) { - citationItem["suppress-author"] = citationItem["suppressAuthor"]; - delete citationItem.suppressAuthor; - } - - // fix for improper upgrade from Zotero 2.1 in <2.1.5 - if(parseInt(citationItem.label) == citationItem.label) { - const locatorTypeTerms = ["page", "book", "chapter", "column", "figure", "folio", - "issue", "line", "note", "opus", "paragraph", "part", "section", "sub verbo", - "volume", "verse"]; - citationItem.label = locatorTypeTerms[parseInt(citationItem.label)]; - } - - // for update from Zotero 2.1 or earlier - if(citationItem.uri) { - citationItem.uris = citationItem.uri; - delete citationItem.uri; - } - } - - // for upgrade from Zotero 2.0 or earlier - if(citation.sort) { - citation.properties.unsorted = !citation.sort; - delete citation.sort; - } - if(citation.custom) { - citation.properties.custom = citation.custom; - delete citation.custom; - } - if(!citation.citationID) citation.citationID = Zotero.randomString(); - - citation.properties.field = arg; - } else { // ye olde style field - var underscoreIndex = arg.indexOf("_"); - var itemIDs = arg.substr(0, underscoreIndex).split("|"); - - var lastIndex = arg.lastIndexOf("_"); - if(lastIndex != underscoreIndex+1) { - var locatorString = arg.substr(underscoreIndex+1, lastIndex-underscoreIndex-1); - var locators = locatorString.split("|"); - } - - var citationItems = new Array(); - for(var i=0; i<itemIDs.length; i++) { - var citationItem = {id:itemIDs[i]}; - if(locators) { - citationItem.locator = locators[i].substr(1); - citationItem.label = Zotero.Integration._oldCitationLocatorMap[locators[i][0]]; - } - citationItems.push(citationItem); - } - - var citation = {"citationItems":citationItems, properties:{}}; - if(index) this.updateIndices[index] = true; - } - - return citation; -} - -/** - * Gets integration bibliography - */ -Zotero.Integration.Session.prototype.getBibliography = function() { - this.updateUncitedItems(); - - if(Zotero.Utilities.isEmpty(this.citationsByItemID)) return false; - - // generate bibliography - var bib = this.style.makeBibliography(); - - if(bib) { - // omit items - Zotero.Cite.removeFromBibliography(bib, this.omittedItems); - - // replace items with their custom counterpars - for(var i in bib[0].entry_ids) { - if(this.customBibliographyText[bib[0].entry_ids[i]]) { - bib[1][i] = this.customBibliographyText[bib[0].entry_ids[i]]; - } - } - } - - return bib; -} - -/** - * Calls CSL.Engine.updateUncitedItems() to reconcile list of uncited items - */ -Zotero.Integration.Session.prototype.updateUncitedItems = function() { - // There appears to be a bug somewhere here. - if(Zotero.Debug.enabled) Zotero.debug("Integration: style.updateUncitedItems("+this.uncitedItems.toSource()+")"); - this.style.updateUncitedItems(Object.keys(this.uncitedItems).map(i => parseInt(i))); -} - -/** - * Refreshes updateIndices variable to include fields for modified items - */ -Zotero.Integration.Session.prototype.updateUpdateIndices = function(regenerateAll) { - if(regenerateAll || this.regenerateAll) { - // update all indices - for(var i in this.citationsByIndex) { - this.newIndices[i] = true; - this.updateIndices[i] = true; - } - } else { - // update only item IDs - for(var i in this.updateItemIDs) { - if(this.citationsByItemID[i] && this.citationsByItemID[i].length) { - for(var j=0; j<this.citationsByItemID[i].length; j++) { - this.updateIndices[this.citationsByItemID[i][j].properties.zoteroIndex] = true; - } - } - } - } -} - -/** - * Returns citations before and after a given index - */ -Zotero.Integration.Session.prototype._getPrePost = function(index) { - var citationIndices = []; - var citationsPre = []; - for(var i=0; i<index; i++) { - if(this.citationsByIndex[i] && !this.newIndices[i] && !this.citationsByIndex[i].properties.delete) { - citationsPre.push([this.citationsByIndex[i].citationID, this.citationsByIndex[i].properties.noteIndex]); - citationIndices.push(i); - } - } - citationIndices.push(index); - var citationsPost = []; - for(var i=index+1; i<this.citationsByIndex.length; i++) { - if(this.citationsByIndex[i] && !this.newIndices[i] && !this.citationsByIndex[i].properties.delete) { - citationsPost.push([this.citationsByIndex[i].citationID, this.citationsByIndex[i].properties.noteIndex]); - citationIndices.push(i); - } - } - return [citationsPre, citationsPost, citationIndices]; -} - -/** - * Returns a formatted citation - */ -Zotero.Integration.Session.prototype.formatCitation = function(index, citation) { - if(!this.citationText[index]) { - var citationsPre, citationsPost, citationIndices; - [citationsPre, citationsPost, citationIndices] = this._getPrePost(index); - if(Zotero.Debug.enabled) { - Zotero.debug("Integration: style.processCitationCluster("+citation.toSource()+", "+citationsPre.toSource()+", "+citationsPost.toSource()); - } - var newCitations = this.style.processCitationCluster(citation, citationsPre, citationsPost); - for (let newCitation of newCitations[1]) { - this.citationText[citationIndices[newCitation[0]]] = newCitation[1]; - this.updateIndices[citationIndices[newCitation[0]]] = true; - } - return newCitations.bibchange; +Zotero.Integration.Session.prototype.getCiteprocLists = function() { + var citations = []; + for(let i in this.citationsByIndex) { + citations.push([this.citationsByIndex[i].citationID, this.citationsByIndex[i].properties.noteIndex]); } + return citations; } /** * Updates the list of citations to be serialized to the document */ -Zotero.Integration.Session.prototype._updateCitations = function* () { - /*var allUpdatesForced = false; - var forcedUpdates = {}; - if(force) { - allUpdatesForced = true; - // make sure at least one citation gets updated - updateLoop: for (let indexList of [this.newIndices, this.updateIndices]) { - for(var i in indexList) { - if(!this.citationsByIndex[i].properties.delete) { - allUpdatesForced = false; - break updateLoop; - } - } - } - - if(allUpdatesForced) { - for(i in this.citationsByIndex) { - if(this.citationsByIndex[i] && !this.citationsByIndex[i].properties.delete) { - forcedUpdates[i] = true; - break; - } - } - } - }*/ - - if(Zotero.Debug.enabled) { - Zotero.debug("Integration: Indices of new citations"); - Zotero.debug(Object.keys(this.newIndices)); - Zotero.debug("Integration: Indices of updated citations"); - Zotero.debug(Object.keys(this.updateIndices)); - } +Zotero.Integration.Session.prototype._updateCitations = async function () { + Zotero.debug("Integration: Indices of new citations"); + Zotero.debug(Object.keys(this.newIndices)); + Zotero.debug("Integration: Indices of updated citations"); + Zotero.debug(Object.keys(this.updateIndices)); + var citations = this.getCiteprocLists(); for (let indexList of [this.newIndices, this.updateIndices]) { - for(var index in indexList) { + for (let index in indexList) { + // Jump to next event loop step for UI updates + await Zotero.Promise.delay(); index = parseInt(index); var citation = this.citationsByIndex[index]; - if(!citation || citation.properties.delete) continue; - if(this.formatCitation(index, citation)) { - this.bibliographyHasChanged = true; + if (!citation) continue; + citation = citation.toJSON(); + + let citationsPre = citations.slice(0, index); + var citationsPost; + if (index in this.newIndices) { + citationsPost = []; + } else { + citationsPost = citations.slice(index+1); } - this.citeprocCitationIDs[citation.citationID] = true; + + Zotero.debug("Integration: style.processCitationCluster("+citation.toSource()+", "+citationsPre.toSource()+", "+citationsPost.toSource()); + let [info, newCitations] = this.style.processCitationCluster(citation, citationsPre, citationsPost); + + this.bibliographyHasChanged |= info.bibchange; + + for (let citationInfo of newCitations) { + let idx = citationInfo[0], text = citationInfo[1]; + this.updateIndices[idx] = true; + this.citationsByIndex[idx].text = text; + } + delete this.newIndices[index]; - yield; } } - - /*if(allUpdatesForced) { - this.newIndices = {}; - this.updateIndices = {}; - }*/ } /** * Restores processor state from document, without requesting citation updates */ Zotero.Integration.Session.prototype.restoreProcessorState = function() { + if (this.fields._bibliographyFields.length && !this.bibliography) { + throw new Error ("Attempting to restore processor state without loading bibliography"); + } + let uncited = []; + if (this.bibliography) { + uncited = Array.from(this.bibliography.uncitedItemIDs.values()); + } + var citations = []; for(var i in this.citationsByIndex) { - if(this.citationsByIndex[i] && !this.newIndices[i] && !this.citationsByIndex[i].properties.delete) { + if(this.citationsByIndex[i] && !this.newIndices[i]) { citations.push(this.citationsByIndex[i]); } } - this.style.restoreProcessorState(citations); + this.style.rebuildProcessorState(citations, 'rtf', uncited); } -/** - * Loads document data from a JSON object - */ -Zotero.Integration.Session.prototype.loadBibliographyData = Zotero.Promise.coroutine(function* (json) { - var openBraceIndex = json.indexOf("{"); - if(openBraceIndex == -1) return; - + +Zotero.Integration.Session.prototype.writeDelayedCitation = Zotero.Promise.coroutine(function* (idx, field, citation) { try { - var documentData = JSON.parse(json.substring(openBraceIndex, json.lastIndexOf("}")+1)); + var text = citation.properties.custom || this.style.previewCitationCluster(citation, [], [], "rtf"); } catch(e) { - try { - var documentData = JSON.parse(json.substr(0, json.length-1)); - } catch(e) { - throw new Zotero.Integration.CorruptFieldException(json); - } + throw e; } + text = `${DELAYED_CITATION_STYLING}{${text}}`; - var needUpdate; - - // set uncited - if(documentData.uncited) { - if(documentData.uncited[0]) { - // new style array of arrays with URIs - let zoteroItem, needUpdate; - for (let uris of documentData.uncited) { - [zoteroItem, needUpdate] = yield this.uriMap.getZoteroItemForURIs(uris); - var id = zoteroItem.cslItemID ? zoteroItem.cslItemID : zoteroItem.id; - if(zoteroItem && !this.citationsByItemID[id]) { - this.uncitedItems[id] = true; - } else { - needUpdate = true; - } - this.bibliographyDataHasChanged |= needUpdate; - } - } else { - for(var itemID in documentData.uncited) { - // if not yet in item set, add to item set - // DEBUG: why no libraryID? - var zoteroItem = Zotero.Items.getByLibraryAndKey(0, itemID); - if(!zoteroItem) zoteroItem = Zotero.Items.get(itemID); - if(zoteroItem) this.uncitedItems[zoteroItem.id] = true; - } - this.bibliographyDataHasChanged = true; - } + // Make sure we'll prompt for manually edited citations + var isRich = false; + if(!citation.properties.dontUpdate) { + isRich = field.setText(text); - this.updateUncitedItems(); - } - - // set custom bibliography entries - if(documentData.custom) { - if(documentData.custom[0]) { - // new style array of arrays with URIs - var zoteroItem, needUpdate; - for (let custom of documentData.custom) { - [zoteroItem, needUpdate] = yield this.uriMap.getZoteroItemForURIs(custom[0]); - if(!zoteroItem) continue; - if(needUpdate) this.bibliographyDataHasChanged = true; - - var id = zoteroItem.cslItemID ? zoteroItem.cslItemID : zoteroItem.id; - if(this.citationsByItemID[id] || this.uncitedItems[id]) { - this.customBibliographyText[id] = custom[1]; - } - } - } else { - // old style hash - for(var itemID in documentData.custom) { - // DEBUG: why no libraryID? - var zoteroItem = Zotero.Items.getByLibraryAndKey(0, itemID); - if(!zoteroItem) zoteroItem = Zotero.Items.get(itemID); - if(!zoteroItem) continue; - - if(this.citationsByItemID[zoteroItem.id] || this.uncitedItems[zoteroItem.id]) { - this.customBibliographyText[zoteroItem.id] = documentData.custom[itemID]; - } - } - this.bibliographyDataHasChanged = true; - } + citation.properties.formattedCitation = text; + citation.properties.plainCitation = field.getText(); } - // set entries to be omitted from bibliography - if(documentData.omitted) { - let zoteroItem, needUpdate; - for (let uris of documentData.omitted) { - [zoteroItem, update] = yield this.uriMap.getZoteroItemForURIs(uris); - var id = zoteroItem.cslItemID ? zoteroItem.cslItemID : zoteroItem.id; - if(zoteroItem && this.citationsByItemID[id]) { - this.omittedItems[id] = true; - } else { - needUpdate = true; - } - this.bibliographyDataHasChanged |= needUpdate; - } + field.setCode(citation.serialize()); + if (this.data.prefs.fieldType === "ReferenceMark" + && this.data.prefs.noteType != 0 && isRich + && !citation.properties.dontUpdate) { + // For ReferenceMarks with formatting, we need to set the text again, because + // setting the field code removes formatting from the mark. I don't like this. + field.setText(text, isRich); } - this.bibliographyData = json; -}); - -/** - * Saves document data from a JSON object - */ -Zotero.Integration.Session.prototype.getBibliographyData = function() { - var bibliographyData = {}; - - // add uncited if there is anything - for(var item in this.uncitedItems) { - if(item) { - if(!bibliographyData.uncited) bibliographyData.uncited = []; - bibliographyData.uncited.push(this.uriMap.getURIsForItemID(item)); - } - } - for(var item in this.omittedItems) { - if(item) { - if(!bibliographyData.omitted) bibliographyData.omitted = []; - bibliographyData.omitted.push(this.uriMap.getURIsForItemID(item)); + // Update bibliography with a static string + var fields = yield this.fields.get(); + var bibliographyField; + for (let i = fields.length-1; i >= 0; i--) { + let field = Zotero.Integration.Field.loadExisting(fields[i]); + if (field.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { + field.setText(Zotero.getString('integration.delayCitationUpdates.bibliography'), false) + break; } } - // look for custom bibliography entries - bibliographyData.custom = Object.keys(this.customBibliographyText) - .map(id => [this.uriMap.getURIsForItemID(id), this.customBibliographyText[id]]); - - - if(bibliographyData.uncited || bibliographyData.custom) { - return JSON.stringify(bibliographyData); - } else { - return ""; // nothing - } -} +}); -/** - * Returns a preview, given a citation object (whose citationItems lack item - * and position) - */ -Zotero.Integration.Session.prototype.previewCitation = function(citation) { - var citationsPre, citationsPost, citationIndices; - [citationsPre, citationsPost, citationIndices] = this._getPrePost(citation.properties.zoteroIndex); - try { - return this.style.previewCitationCluster(citation, citationsPre, citationsPost, "rtf"); - } catch(e) { - throw e; - } + +Zotero.Integration.Session.prototype.getItems = function() { + return Zotero.Cite.getItem(Object.keys(this.citationsByItemID)); } + /** * Edits integration bibliography + * @param {Zotero.Integration.Bibliography} bibliography */ -Zotero.Integration.Session.prototype.editBibliography = function(doc) { - var bibliographyEditor = new Zotero.Integration.Session.BibliographyEditInterface(this); - var io = new function() { this.wrappedJSObject = bibliographyEditor; } +Zotero.Integration.Session.prototype.editBibliography = Zotero.Promise.coroutine(function *(bibliography) { + if (!Object.keys(this.citationsByIndex).length) { + throw new Error('Integration.Session.editBibliography: called without loaded citations'); + } + yield bibliography.loadItemData(); - this.bibliographyDataHasChanged = this.bibliographyHasChanged = true; + var bibliographyEditor = new Zotero.Integration.BibliographyEditInterface(bibliography, this.citationsByItemID, this.style); - return Zotero.Integration.displayDialog(doc, - 'chrome://zotero/content/integration/editBibliographyDialog.xul', 'resizable', io); -} - -/** - * @class Interface for bibliography editor to alter document bibliography - * @constructor - * Creates a new bibliography editor interface - * @param session {Zotero.Integration.Session} - */ -Zotero.Integration.Session.BibliographyEditInterface = function(session) { - this.session = session; - - this._changed = { - "customBibliographyText":{}, - "uncitedItems":{}, - "omittedItems":{} - } - for(var list in this._changed) { - for(var key in this.session[list]) { - this._changed[list][key] = this.session[list][key]; - } - } + yield Zotero.Integration.displayDialog('chrome://zotero/content/integration/editBibliographyDialog.xul', 'resizable', bibliographyEditor); + if (bibliographyEditor.cancelled) throw new Zotero.Exception.UserCancelled("bibliography editing"); - this._update(); -} + this.bibliographyDataHasChanged = this.bibliographyHasChanged = true; + this.bibliography = bibliographyEditor.bibliography; +}); /** - * Updates stored bibliography + * @class Interface for bibliography editor to alter document bibliography + * @constructor + * Creates a new bibliography editor interface + * @param bibliography {Zotero.Integration.Bibliography} */ -Zotero.Integration.Session.BibliographyEditInterface.prototype._update = function() { - this.session.updateUncitedItems(); - this.session.style.setOutputFormat("rtf"); - this.bibliography = this.session.style.makeBibliography(); - Zotero.Cite.removeFromBibliography(this.bibliography, this.session.omittedItems); - - for(var i in this.bibliography[0].entry_ids) { - if(this.bibliography[0].entry_ids[i].length != 1) continue; - var itemID = this.bibliography[0].entry_ids[i][0]; - if(this.session.customBibliographyText[itemID]) { - this.bibliography[1][i] = this.session.customBibliographyText[itemID]; - } - } +Zotero.Integration.BibliographyEditInterface = function(bibliography, citationsByItemID, citeproc) { + this.bibliography = bibliography; + this.citeproc = citeproc; + this.wrappedJSObject = this; + this._citationsByItemID = citationsByItemID; + this._update(); } +Zotero.Integration.BibliographyEditInterface.prototype._update = Zotero.Promise.coroutine(function* () { + this.bib = this.bibliography.getCiteprocBibliography(this.citeproc); +}); + /** * Reverts the text of an individual bibliography entry */ -Zotero.Integration.Session.BibliographyEditInterface.prototype.revert = function(itemID) { - delete this.session.customBibliographyText[itemID]; - this._update(); +Zotero.Integration.BibliographyEditInterface.prototype.revert = function(itemID) { + delete this.bibliography.customEntryText[itemID]; + return this._update(); } /** * Reverts bibliography to condition in which no edits have been made */ -Zotero.Integration.Session.BibliographyEditInterface.prototype.revertAll = function() { - for(var list in this._changed) { - this.session[list] = {}; - } - this._update(); -} +Zotero.Integration.BibliographyEditInterface.prototype.revertAll = Zotero.Promise.coroutine(function* () { + this.bibliography.customEntryText = {}; + this.bibliography.uncitedItemIDs.clear(); + this.bibliography.omittedItemIDs.clear(); + return this._update(); +}); /** * Reverts bibliography to condition before BibliographyEditInterface was opened - * Does not run _update automatically, since this will usually only happen with a cancel request */ -Zotero.Integration.Session.BibliographyEditInterface.prototype.cancel = function() { - for(var list in this._changed) { - this.session[list] = this._changed[list]; - } - this.session.updateUncitedItems(); -} +Zotero.Integration.BibliographyEditInterface.prototype.cancel = function() { + this.cancelled = true; +}; /** * Checks whether a given reference is cited within the main document text */ -Zotero.Integration.Session.BibliographyEditInterface.prototype.isCited = function(item) { - if(this.session.citationsByItemID[item]) return true; +Zotero.Integration.BibliographyEditInterface.prototype.isCited = function(item) { + return this._citationsByItemID[item]; } /** * Checks whether an item ID is cited in the bibliography being edited */ -Zotero.Integration.Session.BibliographyEditInterface.prototype.isEdited = function(itemID) { - if(this.session.customBibliographyText[itemID]) return true; - return false; +Zotero.Integration.BibliographyEditInterface.prototype.isEdited = function(itemID) { + return itemID in this.bibliography.customEntryText; } /** * Checks whether any citations in the bibliography have been edited */ -Zotero.Integration.Session.BibliographyEditInterface.prototype.isAnyEdited = function() { - for(var list in this._changed) { - for(var a in this.session[list]) { - return true; - } - } - return false; +Zotero.Integration.BibliographyEditInterface.prototype.isAnyEdited = function() { + return Object.keys(this.bibliography.customEntryText).length || + this.bibliography.uncitedItemIDs.size || + this.bibliography.omittedItemIDs.size; } /** * Adds an item to the bibliography */ -Zotero.Integration.Session.BibliographyEditInterface.prototype.add = function(itemID) { - if(this.session.omittedItems[itemID]) { - delete this.session.omittedItems[itemID]; +Zotero.Integration.BibliographyEditInterface.prototype.add = function(itemID) { + if (itemID in this.bibliography.omittedItemIDs) { + this.bibliography.omittedItemIDs.delete(`${itemID}`); } else { - this.session.uncitedItems[itemID] = true; + this.bibliography.uncitedItemIDs.add(`${itemID}`); } - this._update(); + return this._update(); } /** * Removes an item from the bibliography being edited */ -Zotero.Integration.Session.BibliographyEditInterface.prototype.remove = function(itemID) { - if(this.session.uncitedItems[itemID]) { - delete this.session.uncitedItems[itemID]; +Zotero.Integration.BibliographyEditInterface.prototype.remove = function(itemID) { + if (itemID in this.bibliography.uncitedItemIDs) { + this.bibliography.uncitedItemIDs.delete(`${itemID}`); } else { - this.session.omittedItems[itemID] = true; + this.bibliography.omittedItemIDs.add(`${itemID}`); } - this._update(); + return this._update(); } /** * Sets custom bibliography text for a given item */ -Zotero.Integration.Session.BibliographyEditInterface.prototype.setCustomText = function(itemID, text) { - this.session.customBibliographyText[itemID] = text; - this._update(); +Zotero.Integration.BibliographyEditInterface.prototype.setCustomText = function(itemID, text) { + this.bibliography.customEntryText[itemID] = text; + return this._update(); } /** @@ -3088,7 +1767,7 @@ Zotero.Integration.DocumentData = function(string) { this.style = {}; this.prefs = {}; this.sessionID = null; - if(string) { + if (string) { this.unserialize(string); } } @@ -3114,7 +1793,8 @@ Zotero.Integration.DocumentData.prototype.serialize = function() { } // Otherwise default to XML for now var prefs = ""; - for(var pref in this.prefs) { + for (var pref in this.prefs) { + if (!this.prefs[pref]) continue; prefs += `<pref name="${Zotero.Utilities.htmlSpecialChars(pref)}" `+ `value="${Zotero.Utilities.htmlSpecialChars(this.prefs[pref].toString())}"/>`; } @@ -3247,8 +1927,8 @@ Zotero.Integration.URIMap.prototype.getZoteroItemForURIs = Zotero.Promise.corout var uri = uris[i]; // First try embedded URI - if(this.session.embeddedZoteroItemsByURI[uri]) { - embeddedItem = this.session.embeddedZoteroItemsByURI[uri]; + if(this.session.embeddedItemsByURI[uri]) { + embeddedItem = this.session.embeddedItemsByURI[uri]; } // Next try getting URI directly @@ -3290,3 +1970,685 @@ Zotero.Integration.URIMap.prototype.getZoteroItemForURIs = Zotero.Promise.corout return [zoteroItem, needUpdate]; }); + +Zotero.Integration.Field = class { + constructor(field) { + if (field instanceof Zotero.Integration.Field) { + throw new Error("Trying to instantiate Integration.Field with Integration.Field, not doc field"); + } + // This is not the best solution in terms of performance + for (let prop in field) { + if (!(prop in this)) { + this[prop] = field[prop].bind ? field[prop].bind(field) : field[prop]; + } + } + this._field = field; + this.type = INTEGRATION_TYPE_TEMP; + } + + setCode(code) { + // Boo. Inconsistent order. + if (this.type == INTEGRATION_TYPE_ITEM) { + this._field.setCode(`ITEM CSL_CITATION ${code}`); + } else if (this.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { + this._field.setCode(`BIBL ${code} CSL_BIBLIOGRAPHY`); + } else { + this._field.setCode(`TEMP`); + } + } + + getCode() { + let code = this._field.getCode(); + let start = code.indexOf('{'); + if (start == -1) { + return '{}'; + } + return code.substring(start, code.lastIndexOf('}')+1); + } + + clearCode() { + this.setCode('{}'); + } + + setText(text) { + var isRich = false; + // If RTF wrap with RTF tags + if (text.includes("\\")) { + if (text.substr(0,5) != "{\\rtf") { + text = "{\\rtf "+text+"}"; + } + isRich = true; + } + this._field.setText(text, isRich); + return isRich; + } +}; + +/** + * Load existing field in document and return correct instance of field type + * @param docField + * @param rawCode + * @param idx + * @returns {Zotero.Integration.Field|Zotero.Integration.CitationField|Zotero.Integration.BibliographyField} + */ +Zotero.Integration.Field.loadExisting = function(docField) { + var field; + // Already loaded + if (docField instanceof Zotero.Integration.Field) return docField; + let rawCode = docField.getCode(); + + // ITEM/CITATION CSL_ITEM {json: 'data'} + for (let type of ["ITEM", "CITATION"]) { + if (rawCode.substr(0, type.length) === type) { + field = new Zotero.Integration.CitationField(docField); + } + } + // BIBL {json: 'data'} CSL_BIBLIOGRAPHY + if (rawCode.substr(0, 4) === "BIBL") { + field = new Zotero.Integration.BibliographyField(docField); + } + + if (!field) { + field = new Zotero.Integration.Field(docField); + } + if (field) { + let start = rawCode.indexOf('{'); + if (start != -1) { + field._code = rawCode.substring(start, rawCode.lastIndexOf('}')+1); + } else { + field._code = rawCode.substr(rawCode.indexOf(' ')+1); + } + }; + + return field; +}; + +Zotero.Integration.CitationField = class extends Zotero.Integration.Field { + constructor(field) { + super(field); + this.type = INTEGRATION_TYPE_ITEM; + } + + /** + * Don't be fooled, this should be as simple as JSON.parse(). + * The schema for the code is defined @ https://raw.githubusercontent.com/citation-style-language/schema/master/csl-citation.json + * + * However, over the years and different versions of Zotero there's been changes to the schema, + * incorrect serialization, etc. Therefore this function is cruft-full and we can't get rid of it. + * + * @returns {{citationItems: Object[], properties: Object}} + */ + unserialize() { + function unserialize(code) { + try { + return JSON.parse(code); + } catch(e) { + // fix for corrupted fields (corrupted by 2.1b1) + return JSON.parse(code.replace(/{{((?:\s*,?"unsorted":(?:true|false)|\s*,?"custom":"(?:(?:\\")?[^"]*\s*)*")*)}}/, "{$1}")); + } + } + + function upgradeCruft(citation, code) { + // fix for uppercase citation codes + if(citation.CITATIONITEMS) { + citation.citationItems = []; + for (var i=0; i<citation.CITATIONITEMS.length; i++) { + for (var j in citation.CITATIONITEMS[i]) { + switch (j) { + case 'ITEMID': + var field = 'itemID'; + break; + + // 'position', 'custom' + default: + var field = j.toLowerCase(); + } + if (!citation.citationItems[i]) { + citation.citationItems[i] = {}; + } + citation.citationItems[i][field] = citation.CITATIONITEMS[i][j]; + } + } + } + + if(!citation.properties) citation.properties = {}; + + for (let citationItem of citation.citationItems) { + // for upgrade from Zotero 2.0 or earlier + if(citationItem.locatorType) { + citationItem.label = citationItem.locatorType; + delete citationItem.locatorType; + } else if(citationItem.suppressAuthor) { + citationItem["suppress-author"] = citationItem["suppressAuthor"]; + delete citationItem.suppressAuthor; + } + + // fix for improper upgrade from Zotero 2.1 in <2.1.5 + if(parseInt(citationItem.label) == citationItem.label) { + const locatorTypeTerms = ["page", "book", "chapter", "column", "figure", "folio", + "issue", "line", "note", "opus", "paragraph", "part", "section", "sub verbo", + "volume", "verse"]; + citationItem.label = locatorTypeTerms[parseInt(citationItem.label)]; + } + + // for update from Zotero 2.1 or earlier + if(citationItem.uri) { + citationItem.uris = citationItem.uri; + delete citationItem.uri; + } + } + + // for upgrade from Zotero 2.0 or earlier + if(citation.sort) { + citation.properties.unsorted = !citation.sort; + delete citation.sort; + } + if(citation.custom) { + citation.properties.custom = citation.custom; + delete citation.custom; + } + + citation.properties.field = code; + return citation; + } + + function unserializePreZotero1_0(code) { + var underscoreIndex = code.indexOf("_"); + var itemIDs = code.substr(0, underscoreIndex).split("|"); + + var lastIndex = code.lastIndexOf("_"); + if(lastIndex != underscoreIndex+1) { + var locatorString = code.substr(underscoreIndex+1, lastIndex-underscoreIndex-1); + var locators = locatorString.split("|"); + } + + var citationItems = new Array(); + for(var i=0; i<itemIDs.length; i++) { + var citationItem = {id:itemIDs[i]}; + if(locators) { + citationItem.locator = locators[i].substr(1); + citationItem.label = Zotero.Integration._oldCitationLocatorMap[locators[i][0]]; + } + citationItems.push(citationItem); + } + + return {"citationItems":citationItems, properties:{}}; + } + + + try { + let code = this.getCode(); + if (code[0] == '{') { // JSON field + return upgradeCruft(unserialize(code), code); + } else { // ye olde style field + return unserializePreZotero1_0(code); + } + } catch (e) { + return this.resolveCorrupt(code); + } + } + + clearCode() { + this.setCode(JSON.stringify({citationItems: [], properties: {}})); + } + + resolveCorrupt(code) { + return Zotero.Promise.coroutine(function* () { + Zotero.debug(`Integration: handling corrupt citation field ${code}`); + var msg = Zotero.getString("integration.corruptField")+'\n\n'+ + Zotero.getString('integration.corruptField.description'); + this.select(); + Zotero.Integration.currentDoc.activate(); + var result = Zotero.Integration.currentSession.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_YES_NO_CANCEL); + if (result == 0) { // Cancel + return new Zotero.Exception.UserCancelled("corrupt citation resolution"); + } else if (result == 1) { // No + return false; + } else { // Yes + var fieldGetter = Zotero.Integration.currentSession.fields, + oldWindow = Zotero.Integration.currentWindow, + oldProgressCallback = this.progressCallback; + // Display reselect edit citation dialog + let [idx, field, citation] = yield fieldGetter.addEditCitation(this); + if (Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) { + Zotero.Integration.currentWindow.close(); + } + Zotero.Integration.currentWindow = oldWindow; + fieldGetter.progressCallback = oldProgressCallback; + return citation; + } + }).apply(this, arguments); + } +}; + + +Zotero.Integration.BibliographyField = class extends Zotero.Integration.Field { + constructor(field) { + super(field); + this.type = INTEGRATION_TYPE_BIBLIOGRAPHY; + }; + + unserialize() { + var code = this.getCode(); + try { + return JSON.parse(code); + } catch(e) { + return this.resolveCorrupt(code); + } + } + + resolveCorrupt(code) { + return Zotero.Promise.coroutine(function* () { + Zotero.debug(`Integration: handling corrupt bibliography field ${code}`); + var msg = Zotero.getString("integration.corruptBibliography")+'\n\n'+ + Zotero.getString('integration.corruptBibliography.description'); + var result = Zotero.Integration.currentSession.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_OK_CANCEL); + if (result == 0) { + throw new Zotero.Exception.UserCancelled("corrupt bibliography resolution"); + } else { + this.clearCode(); + return unserialize(); + } + }).apply(this, arguments); + } +}; + +Zotero.Integration.Citation = class { + constructor(citationField) { + let data = citationField.unserialize(); + this.citationID = data.citationID; + this.citationItems = data.citationItems; + this.properties = data.properties; + this.properties.noteIndex = citationField.getNoteIndex(); + + this._field = citationField; + } + + /** + * Load item data for current item + * @param {Boolean} [promptToReselect=true] - will throw a MissingItemException if false + * @returns {Promise{Number}} + * - Zotero.Integration.NO_ACTION + * - Zotero.Integration.UPDATE + * - Zotero.Integration.REMOVE_CODE + */ + loadItemData() { + return Zotero.Promise.coroutine(function *(promptToReselect=true){ + let items = []; + var needUpdate = false; + + for (var i=0, n=this.citationItems.length; i<n; i++) { + var citationItem = this.citationItems[i]; + + // get Zotero item + var zoteroItem = false; + if (citationItem.uris) { + let itemNeedsUpdate; + [zoteroItem, itemNeedsUpdate] = yield Zotero.Integration.currentSession.uriMap.getZoteroItemForURIs(citationItem.uris); + needUpdate = needUpdate || itemNeedsUpdate; + + // Unfortunately, people do weird things with their documents. One weird thing people + // apparently like to do (http://forums.zotero.org/discussion/22262/) is to copy and + // paste citations from other documents created with earlier versions of Zotero into + // their documents and then not refresh the document. Usually, this isn't a problem. If + // document is edited by the same user, it will work without incident. If the first + // citation of a given item doesn't contain itemData, the user will get a + // MissingItemException. However, it may also happen that the first citation contains + // itemData, but later citations don't, because the user inserted the item properly and + // then copied and pasted the same citation from another document. We check for that + // possibility here. + if (zoteroItem.cslItemData && !citationItem.itemData) { + citationItem.itemData = zoteroItem.cslItemData; + needUpdate = true; + } + } else { + if (citationItem.key && citationItem.libraryID) { + // DEBUG: why no library id? + zoteroItem = Zotero.Items.getByLibraryAndKey(citationItem.libraryID, citationItem.key); + } else if (citationItem.itemID) { + zoteroItem = Zotero.Items.get(citationItem.itemID); + } else if (citationItem.id) { + zoteroItem = Zotero.Items.get(citationItem.id); + } + if (zoteroItem) needUpdate = true; + } + + // Item no longer in library + if (!zoteroItem) { + // Use embedded item + if (citationItem.itemData) { + Zotero.debug(`Item ${JSON.stringify(citationItem.uris)} not in library. Using embedded data`); + // add new embedded item + var itemData = Zotero.Utilities.deepCopy(citationItem.itemData); + + // assign a random string as an item ID + var anonymousID = Zotero.randomString(); + var globalID = itemData.id = citationItem.id = Zotero.Integration.currentSession.data.sessionID+"/"+anonymousID; + + // assign a Zotero item + var surrogateItem = Zotero.Integration.currentSession.embeddedItems[anonymousID] = new Zotero.Item(); + Zotero.Utilities.itemFromCSLJSON(surrogateItem, itemData); + surrogateItem.cslItemID = globalID; + surrogateItem.cslURIs = citationItem.uris; + surrogateItem.cslItemData = itemData; + + for(var j=0, m=citationItem.uris.length; j<m; j++) { + Zotero.Integration.currentSession.embeddedItemsByURI[citationItem.uris[j]] = surrogateItem; + } + } else if (promptToReselect) { + zoteroItem = yield this.handleMissingItem(i); + if (zoteroItem) needUpdate = true; + else return Zotero.Integration.REMOVE_CODE; + } else { + // throw a MissingItemException + throw (new Zotero.Integration.MissingItemException(this, this.citationItems[i])); + } + } + + if (zoteroItem) { + if (zoteroItem.cslItemID) { + citationItem.id = zoteroItem.cslItemID; + } + else { + citationItem.id = zoteroItem.id; + items.push(zoteroItem); + } + } + } + + // Items may be in libraries that haven't been loaded, and retrieveItem() is synchronous, so load + // all data (as required by toJSON(), which is used by itemToExportFormat(), which is used by + // itemToCSLJSON()) now + if (items.length) { + yield Zotero.Items.loadDataTypes(items); + } + return needUpdate ? Zotero.Integration.UPDATE : Zotero.Integration.NO_ACTION; + }).apply(this, arguments); + } + + handleMissingItem() { + return Zotero.Promise.coroutine(function* (idx) { + // Ask user what to do with this item + if (this.citationItems.length == 1) { + var msg = Zotero.getString("integration.missingItem.single"); + } else { + var msg = Zotero.getString("integration.missingItem.multiple", (idx).toString()); + } + msg += '\n\n'+Zotero.getString('integration.missingItem.description'); + this._field.select(); + Zotero.Integration.currentDoc.activate(); + var result = Zotero.Integration.currentSession.displayAlert(msg, + DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO_CANCEL); + if (result == 0) { // Cancel + throw new Zotero.Exception.UserCancelled("document update"); + } else if(result == 1) { // No + return false; + } + + // Yes - prompt to reselect + var io = new function() { this.wrappedJSObject = this; }; + + io.addBorder = Zotero.isWin; + io.singleSelection = true; + + yield Zotero.Integration.displayDialog('chrome://zotero/content/selectItemsDialog.xul', 'resizable', io); + + if (io.dataOut && io.dataOut.length) { + return Zotero.Items.get(io.dataOut[0]); + } + }).apply(this, arguments); + } + + prepareForEditing() { + return Zotero.Promise.coroutine(function *(){ + // Check for modified field text or dontUpdate flag + var fieldText = this._field.getText(); + if (this.properties.dontUpdate + || (this.properties.plainCitation + && fieldText !== this.properties.plainCitation)) { + Zotero.Integration.currentDoc.activate(); + Zotero.debug("[addEditCitation] Attempting to update manually modified citation.\n" + + "citaion.properties.dontUpdate: " + this.properties.dontUpdate + "\n" + + "Original: " + this.properties.plainCitation + "\n" + + "Current: " + fieldText + ); + if (!Zotero.Integration.currentSession.displayAlert(Zotero.getString("integration.citationChanged.edit"), + DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL)) { + throw new Zotero.Exception.UserCancelled("editing citation"); + } + } + + // make sure it's going to get updated + delete this.properties["formattedCitation"]; + delete this.properties["plainCitation"]; + delete this.properties["dontUpdate"]; + + // Load items to be displayed in edit dialog + yield this.loadItemData(); + + }).apply(this, arguments); + } + + toJSON() { + const saveProperties = ["custom", "unsorted", "formattedCitation", "plainCitation", "dontUpdate"]; + const saveCitationItemKeys = ["locator", "label", "suppress-author", "author-only", "prefix", + "suffix"]; + + var citation = {}; + + citation.citationID = this.citationID; + + citation.properties = {}; + for (let key of saveProperties) { + if (key in this.properties) citation.properties[key] = this.properties[key]; + } + + citation.citationItems = new Array(this.citationItems.length); + for (let i=0; i < this.citationItems.length; i++) { + var citationItem = this.citationItems[i], + serializeCitationItem = {}; + + // add URI and itemData + var slashIndex; + if (typeof citationItem.id === "string" && (slashIndex = citationItem.id.indexOf("/")) !== -1) { + // this is an embedded item + serializeCitationItem.id = citationItem.id; + serializeCitationItem.uris = citationItem.uris; + + // XXX For compatibility with Zotero 2.0; to be removed at a later date + serializeCitationItem.uri = serializeCitationItem.uris; + + // always store itemData, since we have no way to get it back otherwise + serializeCitationItem.itemData = citationItem.itemData; + } else { + serializeCitationItem.id = citationItem.id; + serializeCitationItem.uris = Zotero.Integration.currentSession.uriMap.getURIsForItemID(citationItem.id); + + // XXX For compatibility with Zotero 2.0; to be removed at a later date + serializeCitationItem.uri = serializeCitationItem.uris; + + serializeCitationItem.itemData = Zotero.Integration.currentSession.style.sys.retrieveItem(citationItem.id); + } + + for (let key of saveCitationItemKeys) { + if (key in citationItem) serializeCitationItem[key] = citationItem[key]; + } + + citation.citationItems[i] = serializeCitationItem; + } + citation.schema = "https://github.com/citation-style-language/schema/raw/master/csl-citation.json"; + + return citation; + } + + /** + * Serializes the citation into CSL code representation + * @returns {string} + */ + serialize() { + return JSON.stringify(this.toJSON()); + } +}; + +Zotero.Integration.Bibliography = class { + constructor(bibliographyField) { + this._field = bibliographyField; + this.data = bibliographyField.unserialize(); + + this.uncitedItemIDs = new Set(); + this.omittedItemIDs = new Set(); + this.customEntryText = {}; + this.dataLoaded = false; + } + + loadItemData() { + return Zotero.Promise.coroutine(function* () { + // set uncited + var needUpdate = false; + if (this.data.uncited) { + if (this.data.uncited[0]) { + // new style array of arrays with URIs + let zoteroItem, itemNeedsUpdate; + for (let uris of this.data.uncited) { + [zoteroItem, itemNeedsUpdate] = yield Zotero.Integration.currentSession.uriMap.getZoteroItemForURIs(uris); + var id = zoteroItem.cslItemID ? zoteroItem.cslItemID : zoteroItem.id; + if(zoteroItem && !Zotero.Integration.currentSession.citationsByItemID[id]) { + this.uncitedItemIDs.add(`${id}`); + } else { + needUpdate = true; + } + needUpdate |= itemNeedsUpdate; + } + } else { + for(var itemID in this.data.uncited) { + // if not yet in item set, add to item set + // DEBUG: why no libraryID? + var zoteroItem = Zotero.Items.getByLibraryAndKey(0, itemID); + if (!zoteroItem) zoteroItem = Zotero.Items.get(itemID); + if (zoteroItem) this.uncitedItemIDs.add(`${id}`); + } + needUpdate = true; + } + } + + // set custom bibliography entries + if(this.data.custom) { + if(this.data.custom[0]) { + // new style array of arrays with URIs + var zoteroItem, itemNeedsUpdate; + for (let custom of this.data.custom) { + [zoteroItem, itemNeedsUpdate] = yield Zotero.Integration.currentSession.uriMap.getZoteroItemForURIs(custom[0]); + if (!zoteroItem) continue; + if (needUpdate) needUpdate = true; + + var id = zoteroItem.cslItemID ? zoteroItem.cslItemID : zoteroItem.id; + if (Zotero.Integration.currentSession.citationsByItemID[id] || id in this.uncitedItemIDs) { + this.customEntryText[id] = custom[1]; + } + } + } else { + // old style hash + for(var itemID in this.data.custom) { + var zoteroItem = Zotero.Items.getByLibraryAndKey(0, itemID); + if (!zoteroItem) zoteroItem = Zotero.Items.get(itemID); + if (!zoteroItem) continue; + + if(Zotero.Integration.currentSession.citationsByItemID[zoteroItem.id] || zoteroItem.id in this.uncitedItemIDs) { + this.customEntryText[zoteroItem.id] = this.data.custom[itemID]; + } + } + needUpdate = true; + } + } + + // set entries to be omitted from bibliography + if (this.data.omitted) { + let zoteroItem, itemNeedsUpdate; + for (let uris of this.data.omitted) { + [zoteroItem, itemNeedsUpdate] = yield Zotero.Integration.currentSession.uriMap.getZoteroItemForURIs(uris); + var id = zoteroItem.cslItemID ? zoteroItem.cslItemID : zoteroItem.id; + if (zoteroItem && Zotero.Integration.currentSession.citationsByItemID[id]) { + this.omittedItemIDs.add(`${id}`); + } else { + needUpdate = true; + } + needUpdate |= itemNeedsUpdate; + } + } + this.dataLoaded = true; + return needUpdate; + }).apply(this, arguments); + } + + getCiteprocBibliography(citeproc) { + if (Zotero.Utilities.isEmpty(Zotero.Integration.currentSession.citationsByItemID)) { + throw new Error("Attempting to generate bibliography without having updated processor items"); + }; + if (!this.dataLoaded) { + throw new Error("Attempting to generate bibliography without having loaded item data"); + } + + Zotero.debug(`Integration: style.updateUncitedItems ${Array.from(this.uncitedItemIDs.values()).toSource()}`); + citeproc.updateUncitedItems(Array.from(this.uncitedItemIDs.values())); + citeproc.setOutputFormat("rtf"); + let bibliography = citeproc.makeBibliography(); + Zotero.Cite.removeFromBibliography(bibliography, this.omittedItemIDs); + + for (let i in bibliography[0].entry_ids) { + if (bibliography[0].entry_ids[i].length != 1) continue; + let itemID = bibliography[0].entry_ids[i][0]; + if (itemID in this.customEntryText) { + bibliography[1][i] = this.customEntryText[itemID]; + } + } + return bibliography; + } + + serialize() { + if (!this.dataLoaded) { + throw new Error("Attempting to generate bibliography without having loaded item data"); + } + var bibliography = { + uncited: [], + omitted: [], + custom: [] + }; + + // add uncited if there is anything + for (let itemID of this.uncitedItemIDs.values()) { + bibliography.uncited.push(Zotero.Integration.currentSession.uriMap.getURIsForItemID(itemID)); + } + for (let itemID of this.omittedItemIDs.values()) { + bibliography.omitted.push(Zotero.Integration.currentSession.uriMap.getURIsForItemID(itemID)); + } + + bibliography.custom = Object.keys(this.customEntryText) + .map(id => [Zotero.Integration.currentSession.uriMap.getURIsForItemID(id), this.customEntryText[id]]); + + + return JSON.stringify(bibliography); + } +} + +// perhaps not the best place for a timer +Zotero.Integration.Timer = class { + start() { + this.startTime = (new Date()).getTime(); + } + + stop() { + this.resume(); + return ((new Date()).getTime() - this.startTime)/1000; + } + + pause() { + this.pauseTime = (new Date()).getTime(); + } + + resume() { + if (this.pauseTime) { + this.startTime += (this.pauseTime - this.startTime); + this.pauseTime = null; + } + } +} diff --git a/chrome/content/zotero/xpcom/server_connector.js b/chrome/content/zotero/xpcom/server_connector.js @@ -884,7 +884,7 @@ Zotero.Server.Connector.IncompatibleVersion.prototype = { sendResponseCallback(404); if(Zotero.Server.Connector.IncompatibleVersion._errorShown) return; - Zotero.Integration.activate(); + Zotero.Utilities.Internal.activate(); var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]. createInstance(Components.interfaces.nsIPromptService); ps.alert(null, diff --git a/chrome/content/zotero/xpcom/utilities_internal.js b/chrome/content/zotero/xpcom/utilities_internal.js @@ -1267,6 +1267,382 @@ Zotero.Utilities.Internal = { } /** + * Runs an AppleScript on OS X + * + * @param script {String} + * @param block {Boolean} Whether the script should block until the process is finished. + */ +Zotero.Utilities.Internal.executeAppleScript = new function() { + var _osascriptFile; + + return function(script, block) { + if(_osascriptFile === undefined) { + _osascriptFile = Components.classes["@mozilla.org/file/local;1"]. + createInstance(Components.interfaces.nsILocalFile); + _osascriptFile.initWithPath("/usr/bin/osascript"); + if(!_osascriptFile.exists()) _osascriptFile = false; + } + if(_osascriptFile) { + var proc = Components.classes["@mozilla.org/process/util;1"]. + createInstance(Components.interfaces.nsIProcess); + proc.init(_osascriptFile); + try { + proc.run(!!block, ['-e', script], 2); + } catch(e) {} + } + } +} + + +/** + * Activates Firefox + */ +Zotero.Utilities.Internal.activate = new function() { + // For Carbon and X11 + var _carbon, ProcessSerialNumber, SetFrontProcessWithOptions; + var _x11, _x11Display, _x11RootWindow, XClientMessageEvent, XFetchName, XFree, XQueryTree, + XOpenDisplay, XCloseDisplay, XFlush, XDefaultRootWindow, XInternAtom, XSendEvent, + XMapRaised, XGetWindowProperty, X11Atom, X11Bool, X11Display, X11Window, X11Status; + + /** + * Bring a window to the foreground by interfacing directly with X11 + */ + function _X11BringToForeground(win, intervalID) { + var windowTitle = win.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation).QueryInterface(Ci.nsIBaseWindow).title; + + var x11Window = _X11FindWindow(_x11RootWindow, windowTitle); + if(!x11Window) return; + win.clearInterval(intervalID); + + var event = new XClientMessageEvent(); + event.type = 33; /* ClientMessage*/ + event.serial = 0; + event.send_event = 1; + event.message_type = XInternAtom(_x11Display, "_NET_ACTIVE_WINDOW", 0); + event.display = _x11Display; + event.window = x11Window; + event.format = 32; + event.l0 = 2; + var mask = 1<<20 /* SubstructureRedirectMask */ | 1<<19 /* SubstructureNotifyMask */; + + if(XSendEvent(_x11Display, _x11RootWindow, 0, mask, event.address())) { + XMapRaised(_x11Display, x11Window); + XFlush(_x11Display); + Zotero.debug("Integration: Activated successfully"); + } else { + Zotero.debug("Integration: An error occurred activating the window"); + } + } + + /** + * Find an X11 window given a name + */ + function _X11FindWindow(w, searchName) { + Components.utils.import("resource://gre/modules/ctypes.jsm"); + + var res = _X11GetProperty(w, "_NET_CLIENT_LIST", 33 /** XA_WINDOW **/) + || _X11GetProperty(w, "_WIN_CLIENT_LIST", 6 /** XA_CARDINAL **/); + if(!res) return false; + + var nClients = res[1], + clientList = ctypes.cast(res[0], X11Window.array(nClients).ptr).contents, + foundName = new ctypes.char.ptr(); + for(var i=0; i<nClients; i++) { + if(XFetchName(_x11Display, clientList.addressOfElement(i).contents, + foundName.address())) { + var foundNameString = undefined; + try { + foundNameString = foundName.readString(); + } catch(e) {} + XFree(foundName); + if(foundNameString === searchName) return clientList.addressOfElement(i).contents; + } + } + XFree(res[0]); + + return foundWindow; + } + + /** + * Get a property from an X11 window + */ + function _X11GetProperty(win, propertyName, propertyType) { + Components.utils.import("resource://gre/modules/ctypes.jsm"); + + var returnType = new X11Atom(), + returnFormat = new ctypes.int(), + nItemsReturned = new ctypes.unsigned_long(), + nBytesAfterReturn = new ctypes.unsigned_long(), + data = new ctypes.char.ptr(); + if(!XGetWindowProperty(_x11Display, win, XInternAtom(_x11Display, propertyName, 0), 0, 1024, + 0, propertyType, returnType.address(), returnFormat.address(), + nItemsReturned.address(), nBytesAfterReturn.address(), data.address())) { + var nElements = ctypes.cast(nItemsReturned, ctypes.unsigned_int).value; + if(nElements) return [data, nElements]; + } + return null; + } + + return function(win) { + if (Zotero.isMac) { + const BUNDLE_IDS = { + "Zotero":"org.zotero.zotero", + "Firefox":"org.mozilla.firefox", + "Aurora":"org.mozilla.aurora", + "Nightly":"org.mozilla.nightly" + }; + + if (win) { + Components.utils.import("resource://gre/modules/ctypes.jsm"); + win.focus(); + + if(!_carbon) { + _carbon = ctypes.open("/System/Library/Frameworks/Carbon.framework/Carbon"); + /* + * struct ProcessSerialNumber { + * unsigned long highLongOfPSN; + * unsigned long lowLongOfPSN; + * }; + */ + ProcessSerialNumber = new ctypes.StructType("ProcessSerialNumber", + [{"highLongOfPSN":ctypes.uint32_t}, {"lowLongOfPSN":ctypes.uint32_t}]); + + /* + * OSStatus SetFrontProcessWithOptions ( + * const ProcessSerialNumber *inProcess, + * OptionBits inOptions + * ); + */ + SetFrontProcessWithOptions = _carbon.declare("SetFrontProcessWithOptions", + ctypes.default_abi, ctypes.int32_t, ProcessSerialNumber.ptr, + ctypes.uint32_t); + } + + var psn = new ProcessSerialNumber(); + psn.highLongOfPSN = 0; + psn.lowLongOfPSN = 2 // kCurrentProcess + + win.addEventListener("load", function() { + var res = SetFrontProcessWithOptions( + psn.address(), + 1 // kSetFrontProcessFrontWindowOnly = (1 << 0) + ); + }, false); + } else { + Zotero.Utilities.Internal.executeAppleScript('tell application id "'+BUNDLE_IDS[Zotero.appName]+'" to activate'); + } + } else if(!Zotero.isWin && win) { + Components.utils.import("resource://gre/modules/ctypes.jsm"); + + if(_x11 === false) return; + if(!_x11) { + try { + _x11 = ctypes.open("libX11.so.6"); + } catch(e) { + try { + var libName = ctypes.libraryName("X11"); + } catch(e) { + _x11 = false; + Zotero.debug("Integration: Could not get libX11 name; not activating"); + Zotero.logError(e); + return; + } + + try { + _x11 = ctypes.open(libName); + } catch(e) { + _x11 = false; + Zotero.debug("Integration: Could not open "+libName+"; not activating"); + Zotero.logError(e); + return; + } + } + + X11Atom = ctypes.unsigned_long; + X11Bool = ctypes.int; + X11Display = new ctypes.StructType("Display"); + X11Window = ctypes.unsigned_long; + X11Status = ctypes.int; + + /* + * typedef struct { + * int type; + * unsigned long serial; / * # of last request processed by server * / + * Bool send_event; / * true if this came from a SendEvent request * / + * Display *display; / * Display the event was read from * / + * Window window; + * Atom message_type; + * int format; + * union { + * char b[20]; + * short s[10]; + * long l[5]; + * } data; + * } XClientMessageEvent; + */ + XClientMessageEvent = new ctypes.StructType("XClientMessageEvent", + [ + {"type":ctypes.int}, + {"serial":ctypes.unsigned_long}, + {"send_event":X11Bool}, + {"display":X11Display.ptr}, + {"window":X11Window}, + {"message_type":X11Atom}, + {"format":ctypes.int}, + {"l0":ctypes.long}, + {"l1":ctypes.long}, + {"l2":ctypes.long}, + {"l3":ctypes.long}, + {"l4":ctypes.long} + ] + ); + + /* + * Status XFetchName( + * Display* display, + * Window w, + * char** window_name_return + * ); + */ + XFetchName = _x11.declare("XFetchName", ctypes.default_abi, X11Status, + X11Display.ptr, X11Window, ctypes.char.ptr.ptr); + + /* + * Status XQueryTree( + * Display* display, + * Window w, + * Window* root_return, + * Window* parent_return, + * Window** children_return, + * unsigned int* nchildren_return + * ); + */ + XQueryTree = _x11.declare("XQueryTree", ctypes.default_abi, X11Status, + X11Display.ptr, X11Window, X11Window.ptr, X11Window.ptr, X11Window.ptr.ptr, + ctypes.unsigned_int.ptr); + + /* + * int XFree( + * void* data + * ); + */ + XFree = _x11.declare("XFree", ctypes.default_abi, ctypes.int, ctypes.voidptr_t); + + /* + * Display *XOpenDisplay( + * _Xconst char* display_name + * ); + */ + XOpenDisplay = _x11.declare("XOpenDisplay", ctypes.default_abi, X11Display.ptr, + ctypes.char.ptr); + + /* + * int XCloseDisplay( + * Display* display + * ); + */ + XCloseDisplay = _x11.declare("XCloseDisplay", ctypes.default_abi, ctypes.int, + X11Display.ptr); + + /* + * int XFlush( + * Display* display + * ); + */ + XFlush = _x11.declare("XFlush", ctypes.default_abi, ctypes.int, X11Display.ptr); + + /* + * Window XDefaultRootWindow( + * Display* display + * ); + */ + XDefaultRootWindow = _x11.declare("XDefaultRootWindow", ctypes.default_abi, + X11Window, X11Display.ptr); + + /* + * Atom XInternAtom( + * Display* display, + * _Xconst char* atom_name, + * Bool only_if_exists + * ); + */ + XInternAtom = _x11.declare("XInternAtom", ctypes.default_abi, X11Atom, + X11Display.ptr, ctypes.char.ptr, X11Bool); + + /* + * Status XSendEvent( + * Display* display, + * Window w, + * Bool propagate, + * long event_mask, + * XEvent* event_send + * ); + */ + XSendEvent = _x11.declare("XSendEvent", ctypes.default_abi, X11Status, + X11Display.ptr, X11Window, X11Bool, ctypes.long, XClientMessageEvent.ptr); + + /* + * int XMapRaised( + * Display* display, + * Window w + * ); + */ + XMapRaised = _x11.declare("XMapRaised", ctypes.default_abi, ctypes.int, + X11Display.ptr, X11Window); + + /* + * extern int XGetWindowProperty( + * Display* display, + * Window w, + * Atom property, + * long long_offset, + * long long_length, + * Bool delete, + * Atom req_type, + * Atom* actual_type_return, + * int* actual_format_return, + * unsigned long* nitems_return, + * unsigned long* bytes_after_return, + * unsigned char** prop_return + * ); + */ + XGetWindowProperty = _x11.declare("XGetWindowProperty", ctypes.default_abi, + ctypes.int, X11Display.ptr, X11Window, X11Atom, ctypes.long, ctypes.long, + X11Bool, X11Atom, X11Atom.ptr, ctypes.int.ptr, ctypes.unsigned_long.ptr, + ctypes.unsigned_long.ptr, ctypes.char.ptr.ptr); + + + _x11Display = XOpenDisplay(null); + if(!_x11Display) { + Zotero.debug("Integration: Could not open display; not activating"); + _x11 = false; + return; + } + + Zotero.addShutdownListener(function() { + XCloseDisplay(_x11Display); + }); + + _x11RootWindow = XDefaultRootWindow(_x11Display); + if(!_x11RootWindow) { + Zotero.debug("Integration: Could not get root window; not activating"); + _x11 = false; + return; + } + } + + win.addEventListener("load", function() { + var intervalID; + intervalID = win.setInterval(function() { + _X11BringToForeground(win, intervalID); + }, 50); + }, false); + } + } +}; + +/** * Base64 encode / decode * From http://www.webtoolkit.info/ */ diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd @@ -231,6 +231,10 @@ <!ENTITY zotero.integration.prefs.bookmarks.label "Bookmarks"> <!ENTITY zotero.integration.prefs.bookmarks.caption "Bookmarks can be shared between Word and LibreOffice, but may cause errors if accidentally modified and cannot be inserted into footnotes."> +<!ENTITY zotero.integration.prefs.delayCitationUpdates.label "Delay citation updates until manual refresh"> +<!ENTITY zotero.integration.prefs.delayCitationUpdates.tooltip "Citations with pending updates are visually highlighted"> +<!ENTITY zotero.integration.prefs.delayCitationUpdates.description "Delaying updates can speed up citation insertion in large documents."> + <!ENTITY zotero.integration.prefs.automaticJournalAbbeviations.label "Use MEDLINE journal abbreviations"> <!ENTITY zotero.integration.prefs.automaticJournalAbbeviations.caption "The “Journal Abbr” field will be ignored."> diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties @@ -870,6 +870,8 @@ integration.corruptBibliography.description = All items cited in the text will a integration.citationChanged = You have modified this citation since Zotero generated it. Do you want to keep your modifications and prevent future updates? integration.citationChanged.description = Clicking "Yes" will prevent Zotero from updating this citation if you add additional citations, switch styles, or modify the item to which it refers. Clicking "No" will erase your changes. integration.citationChanged.edit = You have modified this citation since Zotero generated it. Editing will clear your modifications. Do you want to continue? +integration.delayCitationUpdates.alert = Updating citations in this document is taking a long time. Would you like to delay citation updates until manual refresh?\n\nYou can change this setting later in the document preferences. +integration.delayCitationUpdates.bibliography = Delayed citing mode is enabled. To see the bibliography click Refresh in Zotero plugin. styles.install.title = Install Style styles.install.unexpectedError = An unexpected error occurred while installing "%1$S" diff --git a/test/tests/integrationTest.js b/test/tests/integrationTest.js @@ -2,6 +2,9 @@ describe("Zotero.Integration", function () { Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); + const INTEGRATION_TYPE_ITEM = 1; + const INTEGRATION_TYPE_BIBLIOGRAPHY = 2; + const INTEGRATION_TYPE_TEMP = 3; /** * To be used as a reference for Zotero-Word Integration plugins */ @@ -235,6 +238,7 @@ describe("Zotero.Integration", function () { if (typeof docID === "undefined") { throw new Error(`docID cannot be undefined`) } + Zotero.debug(`execCommand '${command}': ${docID}`, 2); return Zotero.Integration.execCommand("dummy", command, docID); } @@ -273,21 +277,13 @@ describe("Zotero.Integration", function () { function setAddEditItems(items) { if (items.length == undefined) items = [items]; - dialogResults.quickFormat = function(doc, dialogName) { - var citationItems = items.map((i) => {return {id: i.id} }); - var field = doc.insertField("Field", 0); - field.setCode('TEMP'); - var integrationDoc = addEditCitationSpy.lastCall.thisValue; - var fieldGetter = new Zotero.Integration.Fields(integrationDoc._session, integrationDoc._doc, () => 0); - var io = new Zotero.Integration.CitationEditInterface( - { citationItems, properties: {} }, - field, - fieldGetter, - integrationDoc._session - ); - io._acceptDeferred.resolve(); - return io; - } + dialogResults.quickFormat = function(dialogName, io) { + io.citation.citationItems = items.map(function(item) { + item = Zotero.Cite.getItem(item.id); + return {id: item.id, uris: item.cslURIs, itemData: item.cslItemData}; + }); + io._acceptDeferred.resolve(() => {}); + }; } before(function* () { @@ -296,7 +292,10 @@ describe("Zotero.Integration", function () { testItems = []; for (let i = 0; i < 5; i++) { - testItems.push(yield createDataObject('item', {libraryID: Zotero.Libraries.userLibraryID})); + let testItem = yield createDataObject('item', {libraryID: Zotero.Libraries.userLibraryID}); + testItem.setField('title', `title${1}`); + testItem.setCreator(0, {creatorType: 'author', name: `Author No${i}`}); + testItems.push(testItem); } setAddEditItems(testItems[0]); @@ -313,16 +312,18 @@ describe("Zotero.Integration", function () { // possible bug that reset() erases callsFake. // @NOTE: https://github.com/sinonjs/sinon/issues/1341 // displayDialogStub.callsFake(function(doc, dialogName, prefs, io) { - function(doc, dialogName, prefs, io) { + function(dialogName, prefs, io) { + Zotero.debug(`Display dialog: ${dialogName}`, 2); var ioResult = dialogResults[dialogName.substring(dialogName.lastIndexOf('/')+1, dialogName.length-4)]; if (typeof ioResult == 'function') { - ioResult = ioResult(doc, dialogName); + ioResult(dialogName, io); + } else { + Object.assign(io, ioResult); } - Object.assign(io, ioResult); return Zotero.Promise.resolve(); }); - addEditCitationSpy = sinon.spy(Zotero.Integration.Document.prototype, 'addEditCitation'); + addEditCitationSpy = sinon.spy(Zotero.Integration.Interface.prototype, 'addEditCitation'); }); after(function() { @@ -331,8 +332,8 @@ describe("Zotero.Integration", function () { addEditCitationSpy.restore(); }); - describe('Document', function() { - describe('#addEditCitation', function() { + describe('Interface', function() { + describe('#execCommand', function() { var setDocumentDataSpy; var docID = this.fullTitle(); @@ -348,18 +349,7 @@ describe("Zotero.Integration", function () { setDocumentDataSpy.restore(); }); - it('should call doc.setDocumentData on a fresh document', function* () { - yield execCommand('addEditCitation', docID); - assert.isTrue(setDocumentDataSpy.calledOnce); - }); - - it('should not call doc.setDocumentData on subsequent invocations', function* () { - yield execCommand('addEditCitation', docID); - assert.isFalse(setDocumentDataSpy.called); - }); - - it('should call doc.setDocumentData when document communicates for first time since restart to write new sessionID', function* () { - Zotero.Integration.sessions = {}; + it('should call doc.setDocumentData once', function* () { yield execCommand('addEditCitation', docID); assert.isTrue(setDocumentDataSpy.calledOnce); }); @@ -390,8 +380,7 @@ describe("Zotero.Integration", function () { }); describe('when the style is not from a trusted source', function() { - it('should download the style and not call doc.setDocumentData if user clicks YES', function* () { - setDocumentDataSpy.reset(); + it('should download the style and if user clicks YES', function* () { var styleInstallStub = sinon.stub(Zotero.Styles, "install").resolves(); var style = Zotero.Styles.get(styleID); var styleGetCalledOnce = false; @@ -407,7 +396,6 @@ describe("Zotero.Integration", function () { assert.isTrue(displayAlertStub.calledOnce); assert.isFalse(displayDialogStub.calledWith(applications[docID].doc, 'chrome://zotero/content/integration/integrationDocPrefs.xul')); assert.isTrue(styleInstallStub.calledOnce); - assert.isFalse(setDocumentDataSpy.called); assert.isOk(Zotero.Styles.get(style.styleID)); styleInstallStub.restore(); styleGetStub.restore(); @@ -447,6 +435,194 @@ describe("Zotero.Integration", function () { }); }); + describe('#addEditCitation', function() { + var insertMultipleCitations = Zotero.Promise.coroutine(function *() { + var docID = this.test.fullTitle(); + if (!(docID in applications)) initDoc(docID); + var doc = applications[docID].doc; + + setAddEditItems(testItems[0]); + yield execCommand('addEditCitation', docID); + assert.equal(doc.fields.length, 1); + var citation = (new Zotero.Integration.CitationField(doc.fields[0])).unserialize(); + assert.equal(citation.citationItems.length, 1); + assert.equal(citation.citationItems[0].id, testItems[0].id); + + setAddEditItems(testItems.slice(1, 3)); + yield execCommand('addEditCitation', docID); + assert.equal(doc.fields.length, 2); + citation = (new Zotero.Integration.CitationField(doc.fields[1])).unserialize(); + assert.equal(citation.citationItems.length, 2); + for (let i = 1; i < 3; i++) { + assert.equal(citation.citationItems[i-1].id, testItems[i].id); + } + }); + it('should insert citation if not in field', insertMultipleCitations); + + it('should edit citation if in citation field', function* () { + yield insertMultipleCitations.call(this); + var docID = this.test.fullTitle(); + var doc = applications[docID].doc; + + sinon.stub(doc, 'cursorInField').returns(doc.fields[0]); + sinon.stub(doc, 'canInsertField').returns(false); + + setAddEditItems(testItems.slice(3, 5)); + yield execCommand('addEditCitation', docID); + assert.equal(doc.fields.length, 2); + var citation = (new Zotero.Integration.CitationField(doc.fields[0])).unserialize(); + assert.equal(citation.citationItems.length, 2); + assert.equal(citation.citationItems[0].id, testItems[3].id); + }); + + it('should update bibliography if present', function* () { + yield insertMultipleCitations.call(this); + var docID = this.test.fullTitle(); + var doc = applications[docID].doc; + + let getCiteprocBibliographySpy = + sinon.spy(Zotero.Integration.Bibliography.prototype, 'getCiteprocBibliography'); + + yield execCommand('addEditBibliography', docID); + assert.isTrue(getCiteprocBibliographySpy.calledOnce); + + assert.equal(getCiteprocBibliographySpy.lastCall.returnValue[0].entry_ids.length, 3); + getCiteprocBibliographySpy.reset(); + + setAddEditItems(testItems[3]); + yield execCommand('addEditCitation', docID); + assert.equal(getCiteprocBibliographySpy.lastCall.returnValue[0].entry_ids.length, 4); + + getCiteprocBibliographySpy.restore(); + }); + + describe('when original citation text has been modified', function() { + var displayAlertStub; + before(function* () { + displayAlertStub = sinon.stub(DocumentPluginDummy.Document.prototype, 'displayAlert').returns(0); + }); + beforeEach(function() { + displayAlertStub.reset(); + }); + after(function() { + displayAlertStub.restore(); + }); + it('should keep modification if "Cancel" selected in editCitation triggered alert', async function () { + await insertMultipleCitations.call(this); + var docID = this.test.fullTitle(); + var doc = applications[docID].doc; + + doc.fields[0].text = "modified"; + sinon.stub(doc, 'cursorInField').returns(doc.fields[0]); + sinon.stub(doc, 'canInsertField').returns(false); + + await execCommand('addEditCitation', docID); + assert.equal(doc.fields.length, 2); + assert.equal(doc.fields[0].text, "modified"); + }); + it('should display citation dialog if "OK" selected in editCitation triggered alert', async function () { + await insertMultipleCitations.call(this); + var docID = this.test.fullTitle(); + var doc = applications[docID].doc; + + let origText = doc.fields[0].text; + doc.fields[0].text = "modified"; + // Return OK + displayAlertStub.returns(1); + sinon.stub(doc, 'cursorInField').returns(doc.fields[0]); + sinon.stub(doc, 'canInsertField').returns(false); + setAddEditItems(testItems[0]); + + await execCommand('addEditCitation', docID); + assert.isTrue(displayAlertStub.called); + assert.equal(doc.fields.length, 2); + assert.equal(doc.fields[0].text, origText); + }); + it('should set dontUpdate: true if "yes" selected in refresh prompt', async function() { + await insertMultipleCitations.call(this); + var docID = this.test.fullTitle(); + var doc = applications[docID].doc; + + var citation = (new Zotero.Integration.CitationField(doc.fields[0])).unserialize(); + assert.isNotOk(citation.properties.dontUpdate); + doc.fields[0].text = "modified"; + // Return Yes + displayAlertStub.returns(1); + + await execCommand('refresh', docID); + assert.isTrue(displayAlertStub.called); + assert.equal(doc.fields.length, 2); + assert.equal(doc.fields[0].text, "modified"); + var citation = (new Zotero.Integration.CitationField(doc.fields[0])).unserialize(); + assert.isOk(citation.properties.dontUpdate); + }); + it('should reset citation text if "no" selected in refresh prompt', async function() { + await insertMultipleCitations.call(this); + var docID = this.test.fullTitle(); + var doc = applications[docID].doc; + + var citation = (new Zotero.Integration.CitationField(doc.fields[0])).unserialize(); + assert.isNotOk(citation.properties.dontUpdate); + let origText = doc.fields[0].text; + doc.fields[0].text = "modified"; + // Return No + displayAlertStub.returns(0); + + await execCommand('refresh', docID); + assert.isTrue(displayAlertStub.called); + assert.equal(doc.fields.length, 2); + assert.equal(doc.fields[0].text, origText); + var citation = (new Zotero.Integration.CitationField(doc.fields[0])).unserialize(); + assert.isNotOk(citation.properties.dontUpdate); + }); + }); + + describe('when delayCitationUpdates is set', function() { + it('should insert a citation with wave underlining', function* (){ + yield insertMultipleCitations.call(this); + var docID = this.test.fullTitle(); + var doc = applications[docID].doc; + var data = new Zotero.Integration.DocumentData(doc.data); + data.prefs.delayCitationUpdates = true; + doc.data = data.serialize(); + + var setTextSpy = sinon.spy(DocumentPluginDummy.Field.prototype, 'setText'); + setAddEditItems(testItems[3]); + yield execCommand('addEditCitation', docID); + assert.isTrue(setTextSpy.lastCall.args[0].includes('\\uldash')); + + setTextSpy.restore(); + }); + + it('should not write to any other fields besides the one being updated', function* () { + yield insertMultipleCitations.call(this); + var docID = this.test.fullTitle(); + var doc = applications[docID].doc; + var data = new Zotero.Integration.DocumentData(doc.data); + data.prefs.delayCitationUpdates = true; + doc.data = data.serialize(); + + var setTextSpy = sinon.spy(DocumentPluginDummy.Field.prototype, 'setText'); + var setCodeSpy = sinon.spy(DocumentPluginDummy.Field.prototype, 'setCode'); + + setAddEditItems(testItems[3]); + yield execCommand('addEditCitation', docID); + var field = setTextSpy.firstCall.thisValue; + + for (let i = 0; i < setTextSpy.callCount; i++) { + assert.isTrue(field.equals(setTextSpy.getCall(i).thisValue)); + } + + for (let i = 0; i < setCodeSpy.callCount; i++) { + assert.isTrue(field.equals(setCodeSpy.getCall(i).thisValue)); + } + + setTextSpy.restore(); + setCodeSpy.restore(); + }) + }); + }); + describe('#addEditBibliography', function() { var docID = this.fullTitle(); beforeEach(function* () { @@ -455,12 +631,13 @@ describe("Zotero.Integration", function () { }); it('should insert bibliography if no bibliography field present', function* () { + displayDialogStub.reset(); yield execCommand('addEditBibliography', docID); + assert.isFalse(displayDialogStub.called); var biblPresent = false; for (let i = applications[docID].doc.fields.length-1; i >= 0; i--) { - let field = applications[docID].doc.fields[i]; - Zotero.debug(field.getCode(), 1); - if (field.getCode().includes("CSL_BIBLIOGRAPHY")) { + let field = Zotero.Integration.Field.loadExisting(applications[docID].doc.fields[i]); + if (field.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { biblPresent = true; break; } @@ -473,7 +650,7 @@ describe("Zotero.Integration", function () { displayDialogStub.reset(); yield execCommand('addEditBibliography', docID); assert.isTrue(displayDialogStub.calledOnce); - assert.isTrue(displayDialogStub.lastCall.args[1].includes('editBibliographyDialog')); + assert.isTrue(displayDialogStub.lastCall.args[0].includes('editBibliographyDialog')); }); }); });