From 430f58d3c434edd507262a9f2e7872fe9a40899d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Fri, 5 May 2017 17:51:10 +0300 Subject: [PATCH 01/18] Moves utilities code away from Zotero.Integration --- chrome/content/zotero/bibliography.js | 3 +- .../content/zotero/integration/quickFormat.js | 2 +- chrome/content/zotero/xpcom/integration.js | 385 +----------------- .../content/zotero/xpcom/server_connector.js | 2 +- .../zotero/xpcom/utilities_internal.js | 376 +++++++++++++++++ 5 files changed, 384 insertions(+), 384 deletions(-) diff --git a/chrome/content/zotero/bibliography.js b/chrome/content/zotero/bibliography.js index a673c3172..3e83cf4ca 100644 --- a/chrome/content/zotero/bibliography.js +++ b/chrome/content/zotero/bibliography.js @@ -283,8 +283,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/quickFormat.js b/chrome/content/zotero/integration/quickFormat.js index 5aa1d4c5f..28e3e3f00 100644 --- a/chrome/content/zotero/integration/quickFormat.js +++ b/chrome/content/zotero/integration/quickFormat.js @@ -1320,7 +1320,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/integration.js b/chrome/content/zotero/xpcom/integration.js index bcdea3b39..526d39a6d 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -59,19 +59,7 @@ Zotero.Integration = new function() { 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 = {}; @@ -178,7 +166,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); @@ -258,7 +246,7 @@ Zotero.Integration = new function() { var document; if(inProgress) { - Zotero.Integration.activate(); + Zotero.Utilities.Internal.activate(); if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) { Zotero.Integration.currentWindow.focus(); } @@ -303,7 +291,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); @@ -343,369 +331,6 @@ Zotero.Integration = new function() { }; }; - /** - * 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 Date: Fri, 5 May 2017 17:57:47 +0300 Subject: [PATCH 02/18] Remove integration plugin version checks --- chrome/content/zotero/xpcom/integration.js | 74 +++++----------------- 1 file changed, 15 insertions(+), 59 deletions(-) diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index 526d39a6d..8d2335b2f 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -37,10 +37,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; @@ -58,8 +54,6 @@ 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"]; - this.currentWindow = false; this.sessions = {}; @@ -114,8 +108,6 @@ Zotero.Integration = new function() { } catch(e) { Zotero.logError(e); } - - Zotero.Promise.delay(1000).then(_checkPluginVersions); } /** @@ -182,42 +174,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 { @@ -242,10 +198,10 @@ Zotero.Integration = new function() { this.execCommand = new function() { var inProgress; - return function execCommand(agent, command, docId) { + return Zotero.Promise.coroutine(function* execCommand(agent, command, docId) { var document; - if(inProgress) { + if (inProgress) { Zotero.Utilities.Internal.activate(); if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) { Zotero.Integration.currentWindow.focus(); @@ -255,15 +211,15 @@ Zotero.Integration = new function() { } inProgress = true; - // Check integration component versions - return _checkPluginVersions().then(function() { - 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) { + 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()); + try { + yield Zotero.Promise.resolve((new Zotero.Integration.Document(application, document))[command]()); + } + catch (e) { if(!(e instanceof Zotero.Exception.UserCancelled)) { try { var displayError = null; @@ -301,8 +257,8 @@ Zotero.Integration = new function() { Zotero.logError(e); } } - }) - .finally(function() { + } + finally { if(document) { try { document.cleanup(); @@ -327,8 +283,8 @@ Zotero.Integration = new function() { } inProgress = Zotero.Integration.currentWindow = false; - }); - }; + } + }); }; /** From 41c93ab0349147545b4106cdae889495b1139611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Mon, 15 May 2017 18:19:46 +0300 Subject: [PATCH 03/18] Rename Integration.Document to Integration.Interface --- chrome/content/zotero/xpcom/integration.js | 24 +++++++++++----------- test/tests/integrationTest.js | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index 8d2335b2f..3a1a4a283 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -217,7 +217,7 @@ Zotero.Integration = new function() { // (depending on what is possible) document = (application.getDocument && docId ? application.getDocument(docId) : application.getActiveDocument()); try { - yield Zotero.Promise.resolve((new Zotero.Integration.Document(application, document))[command]()); + yield Zotero.Promise.resolve((new Zotero.Integration.Interface(application, document))[command]()); } catch (e) { if(!(e instanceof Zotero.Exception.UserCancelled)) { @@ -508,7 +508,7 @@ const BIBLIOGRAPHY_PLACEHOLDER = "{Bibliography}"; * All methods for interacting with a document * @constructor */ -Zotero.Integration.Document = function(app, doc) { +Zotero.Integration.Interface = function(app, doc) { this._app = app; this._doc = doc; } @@ -517,7 +517,7 @@ Zotero.Integration.Document = function(app, doc) { * @param data {Zotero.Integration.DocumentData} Document data for new session * @return {Zotero.Integration.Session} */ -Zotero.Integration.Document.prototype._createNewSession = function _createNewSession(data) { +Zotero.Integration.Interface.prototype._createNewSession = function _createNewSession(data) { data.sessionID = Zotero.randomString(); var session = Zotero.Integration.sessions[data.sessionID] = new Zotero.Integration.Session(this._doc); return session; @@ -533,7 +533,7 @@ Zotero.Integration.Document.prototype._createNewSession = function _createNewSes * dontRunSetDocPrefs is true and no session was found, or rejected with * Zotero.Exception.UserCancelled if the document preferences window was cancelled. */ -Zotero.Integration.Document.prototype._getSession = Zotero.Promise.coroutine(function *(require, dontRunSetDocPrefs) { +Zotero.Integration.Interface.prototype._getSession = Zotero.Promise.coroutine(function *(require, dontRunSetDocPrefs) { var dataString = this._doc.getDocumentData(), data, me = this; @@ -673,7 +673,7 @@ Zotero.Integration.Document.prototype._getSession = Zotero.Promise.coroutine(fun * Adds a citation to the current document. * @return {Promise} */ -Zotero.Integration.Document.prototype.addCitation = function() { +Zotero.Integration.Interface.prototype.addCitation = function() { var me = this; return this._getSession(false, false).then(function() { return (new Zotero.Integration.Fields(me._session, me._doc)).addEditCitation(null); @@ -684,7 +684,7 @@ Zotero.Integration.Document.prototype.addCitation = function() { * Edits the citation at the cursor position. * @return {Promise} */ -Zotero.Integration.Document.prototype.editCitation = function() { +Zotero.Integration.Interface.prototype.editCitation = function() { var me = this; return this._getSession(true, false).then(function() { var field = me._doc.cursorInField(me._session.data.prefs['fieldType']); @@ -701,7 +701,7 @@ Zotero.Integration.Document.prototype.editCitation = function() { * Edits the citation at the cursor position if one exists, or else adds a new one. * @return {Promise} */ -Zotero.Integration.Document.prototype.addEditCitation = function() { +Zotero.Integration.Interface.prototype.addEditCitation = function() { var me = this; return this._getSession(false, false).then(function() { var field = me._doc.cursorInField(me._session.data.prefs['fieldType']); @@ -713,7 +713,7 @@ Zotero.Integration.Document.prototype.addEditCitation = function() { * Adds a bibliography to the current document. * @return {Promise} */ -Zotero.Integration.Document.prototype.addBibliography = function() { +Zotero.Integration.Interface.prototype.addBibliography = function() { var me = this; return this._getSession(true, false).then(function() { // Make sure we can have a bibliography @@ -736,7 +736,7 @@ Zotero.Integration.Document.prototype.addBibliography = function() { * Edits bibliography metadata. * @return {Promise} */ -Zotero.Integration.Document.prototype.editBibliography = function() { +Zotero.Integration.Interface.prototype.editBibliography = function() { // Make sure we have a bibliography var me = this, fieldGetter; return this._getSession(true, false).then(function() { @@ -766,7 +766,7 @@ Zotero.Integration.Document.prototype.editBibliography = function() { } -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); @@ -803,7 +803,7 @@ Zotero.Integration.Document.prototype.addEditBibliography = Zotero.Promise.corou * Updates the citation data for all citations and bibliography entries. * @return {Promise} */ -Zotero.Integration.Document.prototype.refresh = function() { +Zotero.Integration.Interface.prototype.refresh = function() { var me = this; return this._getSession(true, false).then(function() { // Send request, forcing update of citations and bibliography @@ -818,7 +818,7 @@ Zotero.Integration.Document.prototype.refresh = function() { * Deletes field codes. * @return {Promise} */ -Zotero.Integration.Document.prototype.removeCodes = function() { +Zotero.Integration.Interface.prototype.removeCodes = function() { var me = this; return this._getSession(true, false).then(function() { var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc); diff --git a/test/tests/integrationTest.js b/test/tests/integrationTest.js index 106bbb63c..bdf5029c2 100644 --- a/test/tests/integrationTest.js +++ b/test/tests/integrationTest.js @@ -322,7 +322,7 @@ describe("Zotero.Integration", function () { return Zotero.Promise.resolve(); }); - addEditCitationSpy = sinon.spy(Zotero.Integration.Document.prototype, 'addEditCitation'); + addEditCitationSpy = sinon.spy(Zotero.Integration.Interface.prototype, 'addEditCitation'); }); after(function() { From 52fd0d992dee8e4a4f3b8e786abbbf708987897c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Wed, 17 May 2017 17:15:01 +0300 Subject: [PATCH 04/18] Add a wrapper class for citation and bibliography fields --- chrome/content/zotero/xpcom/integration.js | 484 ++++++++++++--------- 1 file changed, 276 insertions(+), 208 deletions(-) diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index 3a1a4a283..ab028ab2d 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -744,10 +744,9 @@ Zotero.Integration.Interface.prototype.editBibliography = function() { 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) { + for (let i = fields.length-1; i >= 0; i--) { + let field = Zotero.Integration.Field.loadExisting(fields[i]); + if (field.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { haveBibliography = true; break; } @@ -779,10 +778,9 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro var fields = yield fieldGetter.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) { + for (let i = fields.length-1; i >= 0; i--) { + let field = Zotero.Integration.Field.loadExisting(fields[i]); + if (field.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { haveBibliography = true; break; } @@ -792,7 +790,7 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro yield fieldGetter.updateSession(); yield session.editBibliography(this._doc); } else { - var field = yield fieldGetter.addField(); + var field = new Zotero.Integration.BibliographyField(yield fieldGetter.addField()); field.setCode("BIBL"); yield fieldGetter.updateSession(); } @@ -838,91 +836,84 @@ Zotero.Integration.Interface.prototype.removeCodes = function() { * Displays a dialog to set document preferences (style, footnotes/endnotes, etc.) * @return {Promise} */ -Zotero.Integration.Document.prototype.setDocPrefs = function() { - var me = this, - fieldGetter, +Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(function* () { + var 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()); - - // 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; + let haveSession = yield this._getSession(false, true); - // 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 this._text = this._text ? this._text : this.getText(), + set: (v) => {this._text = v; this.dirty = true} +}); + +Zotero.defineProperty(Zotero.Integration.Field.prototype, 'code', { + get: () => this._code = this._code ? this._code : this.getCode(), + set: (v) => {this._code = v; this.dirty = true;} +}); + +Zotero.Integration.Field.prototype = { + writeToDoc: function(doc) { + this.dirty = false; + + let text = this._text; + let isRich = false; + // If RTF wrap with RTF tags + if (text.indexOf("\\") !== -1) { + text = "{\\rtf "+text+"}"; + isRich = true; + } + this._field.setText(text, isRich); + + // Boo. Inconsistent. + if (this.type == INTEGRATION_TYPE_ITEM) { + this._field.setCode(`ITEM CSL_CITATION ${JSON.stringify(this._data)}`); + } else if (this.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { + this._field.setCode(`BIBL ${JSON.stringify(this._data)} CSL_BIBLIOGRAPHY`); + } + + // Retrigger retrieval from doc. + this._text = null; + this._code = null; + }, + + getCode: function() { + let code = this._field.getCode(); + let start = code.indexOf('{'); + if (start == -1) { + return '{}'; + } + return code.substr(start, start + code.indexOf('}')); + } +}; + + +Zotero.Integration.CitationField = function(field) { + Zotero.Integration.Field.call(this, field); + this.type = INTEGRATION_TYPE_ITEM; + + return new Proxy(this, Zotero.Integration.Field.proxyHandler); +}; +Zotero.extendClass(Zotero.Integration.Field, Zotero.Integration.CitationField); + +Zotero.Integration.BibliographyField = function(field) { + Zotero.Integration.Field.call(this, field); + this.type = INTEGRATION_TYPE_BIBLIOGRAPHY; + + return new Proxy(this, Zotero.Integration.Field.proxyHandler); +}; +Zotero.extendClass(Zotero.Integration.Field, Zotero.Integration.BibliographyField); From 41db61ecb9f17b92511806f223aee5d0e5c52ec5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Wed, 17 May 2017 17:12:05 +0300 Subject: [PATCH 05/18] Move citation unserialization Using new es6 class syntax because getters/setters don't retain `this` context with Zotero.extendClass and we're building with at least FX45 on every platform now where the syntax is supported --- chrome/content/zotero/xpcom/integration.js | 395 ++++++++++----------- 1 file changed, 194 insertions(+), 201 deletions(-) diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index ab028ab2d..4f0691670 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -878,7 +878,7 @@ Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(fu let field = Zotero.Integration.Field.loadExisting(fields[i]); if (convertItems && field.type === INTEGRATION_TYPE_ITEM) { - var citation = this._session.unserializeCitation(field.code); + var citation = field.unserialize(); if (!citation.properties.dontUpdate) { fieldsToConvert.push(field); fieldNoteTypes.push(this._session.data.prefs.noteType); @@ -1121,7 +1121,7 @@ Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(fu if (field.type === INTEGRATION_TYPE_ITEM) { var noteIndex = field.getNoteIndex(); try { - yield this._session.addCitation(i, noteIndex, field.code); + yield this._session.addCitation(i, noteIndex, field.unserialize()); } catch(e) { var removeCode = false; @@ -1347,7 +1347,7 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f } try { - citation = session.unserializeCitation(field.code); + citation = field.unserialize(); } catch(e) {} if (citation) { @@ -1869,17 +1869,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); @@ -2037,127 +2029,6 @@ Zotero.Integration.Session.prototype.lookupItems = Zotero.Promise.coroutine(func } }); -/** - * Unserializes a JSON citation into a citation object (sans items) - */ -Zotero.Integration.Session.prototype.unserializeCitation = function(code, index) { - var firstBracket = code.indexOf("{"); - if (firstBracket !== -1) { // JSON field - code = code.substr(firstBracket); - - // fix for corrupted fields - var lastBracket = code.lastIndexOf("}"); - if(lastBracket+1 != code.length) { - if(index) this.updateIndices[index] = true; - code = code.substr(0, lastBracket+1); - } - - // get JSON - try { - var citation = JSON.parse(code); - } catch(e) { - // fix for corrupted fields (corrupted by Word, somehow) - try { - var citation = JSON.parse(code.substr(0, code.length-1)); - } catch(e) { - // another fix for corrupted fields (corrupted by 2.1b1) - try { - var citation = JSON.parse(code.replace(/{{((?:\s*,?"unsorted":(?:true|false)|\s*,?"custom":"(?:(?:\\")?[^"]*\s*)*")*)}}/, "{$1}")); - } catch(e) { - throw new Zotero.Integration.CorruptFieldException(code, e); - } - } - } - - // fix for uppercase citation codes - if(citation.CITATIONITEMS) { - if(index) this.updateIndices[index] = true; - citation.citationItems = []; - for (var i=0; i this._text = this._text ? this._text : this.getText(), - set: (v) => {this._text = v; this.dirty = true} -}); - -Zotero.defineProperty(Zotero.Integration.Field.prototype, 'code', { - get: () => this._code = this._code ? this._code : this.getCode(), - set: (v) => {this._code = v; this.dirty = true;} -}); - -Zotero.Integration.Field.prototype = { - writeToDoc: function(doc) { - this.dirty = false; - - let text = this._text; - let isRich = false; - // If RTF wrap with RTF tags - if (text.indexOf("\\") !== -1) { - text = "{\\rtf "+text+"}"; - isRich = true; - } - this._field.setText(text, isRich); - - // Boo. Inconsistent. - if (this.type == INTEGRATION_TYPE_ITEM) { - this._field.setCode(`ITEM CSL_CITATION ${JSON.stringify(this._data)}`); - } else if (this.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { - this._field.setCode(`BIBL ${JSON.stringify(this._data)} CSL_BIBLIOGRAPHY`); - } - - // Retrigger retrieval from doc. - this._text = null; - this._code = null; - }, +Zotero.Integration.CitationField = class extends Zotero.Integration.Field { + constructor(field) { + super(field); + this.type = INTEGRATION_TYPE_ITEM; + } - getCode: function() { - let code = this._field.getCode(); - let start = code.indexOf('{'); - if (start == -1) { - return '{}'; + /** + * 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) + try { + return JSON.parse(code.replace(/{{((?:\s*,?"unsorted":(?:true|false)|\s*,?"custom":"(?:(?:\\")?[^"]*\s*)*")*)}}/, "{$1}")); + } catch(e) { + throw new Zotero.Integration.CorruptFieldException(code, e); + } + } } - return code.substr(start, start + code.indexOf('}')); + + function upgradeCruft(citation, code) { + // fix for uppercase citation codes + if(citation.CITATIONITEMS) { + citation.citationItems = []; + for (var i=0; i Date: Tue, 23 May 2017 14:31:31 +0300 Subject: [PATCH 06/18] Refactor CitationEditInterface This is the first really big change that detangles UI stuff from directly changing state. io.citation is no longer tied to the citation loaded in the session in any way and CitationEditInterface does not write anything to session or document. All writes are handled in Fields.addEditCitation() --- .../zotero/integration/addCitationDialog.js | 22 +- .../content/zotero/integration/quickFormat.js | 24 +- chrome/content/zotero/xpcom/integration.js | 279 ++++++------------ 3 files changed, 117 insertions(+), 208 deletions(-) diff --git a/chrome/content/zotero/integration/addCitationDialog.js b/chrome/content/zotero/integration/addCitationDialog.js index f8394d15b..3853767a1 100644 --- 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/quickFormat.js b/chrome/content/zotero/integration/quickFormat.js index 28e3e3f00..fd0f354bd 100644 --- a/chrome/content/zotero/integration/quickFormat.js +++ b/chrome/content/zotero/integration/quickFormat.js @@ -710,7 +710,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")}; @@ -733,11 +733,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) @@ -901,13 +901,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(); @@ -919,7 +919,7 @@ var Zotero_QuickFormat = new function () { _moveCursorToEnd(); } - } + }); /** * Shows the citation properties panel for a given bubble @@ -1058,7 +1058,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) { @@ -1070,7 +1070,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 /* ; */) { @@ -1149,7 +1149,7 @@ var Zotero_QuickFormat = new function () { } else { _resetSearchTimer(); } - } + }); /** * Adds a dummy element to make dragging work @@ -1177,7 +1177,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(); @@ -1195,9 +1195,9 @@ var Zotero_QuickFormat = new function () { keepSorted.removeAttribute("checked"); } - _previewAndSort(); + yield _previewAndSort(); _moveCursorToEnd(); - } + }); /** * Handle a click on a bubble diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index 4f0691670..e8eae0f39 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -1337,7 +1337,10 @@ 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, session = this._session; + var newField, citation; + + // TODO: refactor citation/field preparation + // Citation loading should be moved into Zotero.Integration.Citation // if there's already a citation, make sure we have item IDs in addition to keys if (field) { @@ -1352,7 +1355,7 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f if (citation) { try { - yield session.lookupItems(citation); + yield this._session.lookupItems(citation); } catch(e) { if(e instanceof Zotero.Integration.MissingItemException) { citation.citationItems = []; @@ -1391,7 +1394,41 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f citation = {"citationItems":[], "properties":{}}; } - var io = new Zotero.Integration.CitationEditInterface(citation, field, this, session); + // ------------------- + // Preparing stuff to pass into CitationEditInterface + var fieldIndexPromise = this.get().then(function(fields) { + for (var i=0, n=fields.length; i { return citationsByItemID[itemID] && citationsByItemID[itemID].length @@ -1616,7 +1541,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; @@ -1630,8 +1555,8 @@ Zotero.Integration.CitationEditInterface.prototype = { return indexB - indexA; }); - return Zotero.Cite.getItem(ids); - } + return Zotero.Cite.getItem(ids); + }), } /** @@ -2327,20 +2252,6 @@ Zotero.Integration.Session.prototype.getBibliographyData = function() { } } -/** - * 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; - } -} - /** * Edits integration bibliography */ From 54b4ec6f5cd94533c2b3531395f0c62d8b5d7620 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Tue, 23 May 2017 15:21:00 +0300 Subject: [PATCH 07/18] Fix bugs for style-switching and footnote citations --- chrome/content/zotero/xpcom/integration.js | 41 ++++++++++++---------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index e8eae0f39..8cf7d878f 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -50,6 +50,12 @@ 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 INTEGRATION_TYPE_REMOVE = 4; + + Zotero.Integration = new function() { Components.utils.import("resource://gre/modules/Services.jsm"); Components.utils.import("resource://gre/modules/AddonManager.jsm"); @@ -497,13 +503,6 @@ Zotero.Integration.CorruptBibliographyException.prototype = { } }; -const INTEGRATION_TYPE_ITEM = 1; -const INTEGRATION_TYPE_BIBLIOGRAPHY = 2; -const INTEGRATION_TYPE_TEMP = 3; - -// Placeholder for an empty bibliography -const BIBLIOGRAPHY_PLACEHOLDER = "{Bibliography}"; - /** * All methods for interacting with a document * @constructor @@ -724,7 +723,9 @@ Zotero.Integration.Interface.prototype.addBibliography = function() { var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); return fieldGetter.addField().then(function(field) { - field.setCode("BIBL"); + field.clearCode(); + field.type = INTEGRATION_TYPE_BIBLIOGRAPHY; + field.writeToDoc(); return fieldGetter.updateSession().then(function() { return fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); }); @@ -791,7 +792,9 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro yield session.editBibliography(this._doc); } else { var field = new Zotero.Integration.BibliographyField(yield fieldGetter.addField()); - field.setCode("BIBL"); + field.clearCode(); + field.type = INTEGRATION_TYPE_BIBLIOGRAPHY; + field.writeToDoc(); yield fieldGetter.updateSession(); } return fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); @@ -963,13 +966,13 @@ Zotero.Integration.Fields.prototype.addField = function(note) { } if (!field) { - field = this._doc.insertField(this._session.data.prefs['fieldType'], - (note ? this._session.data.prefs["noteType"] : 0)); + field = new Zotero.Integration.Field(this._doc.insertField(this._session.data.prefs['fieldType'], + (note ? this._session.data.prefs["noteType"] : 0))); } // If fields already retrieved, further this.get() calls will returned the cached version // So we append this field to that list if (this._fields) { - this._fields.push(field); + this._fields.push(field._field); } return Zotero.Promise.resolve(field); @@ -1390,7 +1393,8 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f } if (!citation) { - field.setCode("TEMP"); + field.clearCode(); + field.writeToDoc(); citation = {"citationItems":[], "properties":{}}; } @@ -2626,7 +2630,7 @@ Zotero.Integration.Field = class { }; writeToDoc(doc) { - let text = this._text; + let text = this.text; let isRich = false; // If RTF wrap with RTF tags if (text.indexOf("\\") !== -1) { @@ -2637,9 +2641,11 @@ Zotero.Integration.Field = class { // Boo. Inconsistent. if (this.type == INTEGRATION_TYPE_ITEM) { - this._field.setCode(`ITEM CSL_CITATION ${JSON.stringify(this.code)}`); + this._field.setCode(`ITEM CSL_CITATION ${this.code}`); } else if (this.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { this._field.setCode(`BIBL ${this.code} CSL_BIBLIOGRAPHY`); + } else { + this._field.setCode(`TEMP`); } this.dirty = false; @@ -2654,7 +2660,7 @@ Zotero.Integration.Field = class { if (start == -1) { return '{}'; } - return code.substr(start, start + code.indexOf('}')); + return code.substring(start, code.indexOf('}')+1); }; }; @@ -2682,7 +2688,7 @@ Zotero.Integration.Field.loadExisting = function(docField) { if (field) { let start = rawCode.indexOf('{'); if (start != -1) { - field._code = rawCode.substr(start, start + rawCode.lastIndexOf('}')); + field._code = rawCode.substring(start, rawCode.lastIndexOf('}')+1); } else { field._code = rawCode.substr(rawCode.indexOf(' ')+1); } @@ -2832,7 +2838,6 @@ Zotero.Integration.BibliographyField = class extends Zotero.Integration.Field { constructor(field) { super(field); this.type = INTEGRATION_TYPE_BIBLIOGRAPHY; - this.type = INTEGRATION_TYPE_ITEM; }; unserialize() { From a1acbd403837b03e3ea7f17700b99cbd7e659cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Wed, 24 May 2017 11:54:53 +0300 Subject: [PATCH 08/18] Make current session globally available Decoupling! Sessions can be instantiated without Zotero.Integration.Interface --- chrome/content/zotero/xpcom/integration.js | 313 ++++++++++----------- test/tests/integrationTest.js | 17 +- 2 files changed, 155 insertions(+), 175 deletions(-) diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index 8cf7d878f..65ef1ee04 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -205,7 +205,7 @@ Zotero.Integration = new function() { var inProgress; return Zotero.Promise.coroutine(function* execCommand(agent, command, docId) { - var document; + var document, session; if (inProgress) { Zotero.Utilities.Internal.activate(); @@ -221,9 +221,10 @@ Zotero.Integration = new function() { // 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()); + Zotero.Integration.currentDoc = document = (application.getDocument && docId ? application.getDocument(docId) : application.getActiveDocument()); + Zotero.Integration.currentSession = session = yield Zotero.Integration.getSession(application, document); try { - yield Zotero.Promise.resolve((new Zotero.Integration.Interface(application, document))[command]()); + yield (new Zotero.Integration.Interface(application, document, session))[command](); } catch (e) { if(!(e instanceof Zotero.Exception.UserCancelled)) { @@ -267,6 +268,7 @@ Zotero.Integration = new function() { finally { if(document) { try { + document.setDocumentData(session.data.serialize()); document.cleanup(); document.activate(); @@ -288,7 +290,10 @@ Zotero.Integration = new function() { }); } - inProgress = Zotero.Integration.currentWindow = false; + inProgress = + Zotero.Integration.currentDoc = + Zotero.Integration.currentWindow = + Zotero.Integration.currentSession = false; } }); }; @@ -343,6 +348,96 @@ Zotero.Integration = new function() { } throw err; } + + /** + * 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.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(); + } + + 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"); + } + + if (Zotero.Integration.sessions[data.sessionID]) { + // If communication occured with this document since restart + return Zotero.Integration.sessions[data.sessionID]; + } + } + session = new Zotero.Integration.Session(doc); + 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) { + 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; + } + } + yield session.setDocPrefs(app.primaryFieldType, app.secondaryFieldType); + } else { + throw e; + } + } + session.reload = true; + return session; + }); + } /** @@ -507,60 +602,39 @@ Zotero.Integration.CorruptBibliographyException.prototype = { * All methods for interacting with a document * @constructor */ -Zotero.Integration.Interface = 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.Interface.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 + * 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 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. + * @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.Interface.prototype._getSession = Zotero.Promise.coroutine(function *(require, dontRunSetDocPrefs) { - var dataString = this._doc.getDocumentData(), - data, - me = this; - - if(dataString) { - try { - data = new Zotero.Integration.DocumentData(dataString); - } catch(e) {}; - } - - // If no data or corrupted data, show doc prefs window again - if (!data || !data.prefs || !data.prefs.fieldType) { +Zotero.Integration.Interface.prototype._prepareData = Zotero.Promise.coroutine(function *(require, dontRunSetDocPrefs) { + var data = this._session.data; + // If no data, show doc prefs window + if (!data.prefs.fieldType) { var haveFields = false; data = new Zotero.Integration.DocumentData(); - if(require) { + 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; + var fields = this._doc.getFields(fieldType); + if (fields.hasMoreElements()) { + data.prefs.fieldType = fieldType; haveFields = true; break; } } // if no fields, throw an error - if(!haveFields) { + if (!haveFields) { return Zotero.Promise.reject(new Zotero.Exception.Alert( "integration.error.mustInsertCitation", [], "integration.error.title")); @@ -568,104 +642,15 @@ Zotero.Integration.Interface.prototype._getSession = Zotero.Promise.coroutine(fu 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; - }); - } 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); - } - } - + if(dontRunSetDocPrefs) return false; + + if (haveFields) { this._session.reload = true; } - return Zotero.Promise.resolve(this._session); + + yield this._session.setDocPrefs(this._app.primaryFieldType, this._app.secondaryFieldType); } + return true; }); /** @@ -674,7 +659,7 @@ Zotero.Integration.Interface.prototype._getSession = Zotero.Promise.coroutine(fu */ Zotero.Integration.Interface.prototype.addCitation = function() { var me = this; - return this._getSession(false, false).then(function() { + return this._prepareData(false, false).then(function() { return (new Zotero.Integration.Fields(me._session, me._doc)).addEditCitation(null); }); } @@ -685,7 +670,7 @@ Zotero.Integration.Interface.prototype.addCitation = function() { */ Zotero.Integration.Interface.prototype.editCitation = function() { var me = this; - return this._getSession(true, false).then(function() { + return this._prepareData(true, false).then(function() { var field = me._doc.cursorInField(me._session.data.prefs['fieldType']); if(!field) { throw new Zotero.Exception.Alert("integration.error.notInCitation", [], @@ -702,7 +687,7 @@ Zotero.Integration.Interface.prototype.editCitation = function() { */ Zotero.Integration.Interface.prototype.addEditCitation = function() { var me = this; - return this._getSession(false, false).then(function() { + return this._prepareData(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); }); @@ -714,7 +699,7 @@ Zotero.Integration.Interface.prototype.addEditCitation = function() { */ Zotero.Integration.Interface.prototype.addBibliography = function() { var me = this; - return this._getSession(true, false).then(function() { + return this._prepareData(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", [], @@ -740,7 +725,7 @@ Zotero.Integration.Interface.prototype.addBibliography = function() { Zotero.Integration.Interface.prototype.editBibliography = function() { // Make sure we have a bibliography var me = this, fieldGetter; - return this._getSession(true, false).then(function() { + return this._prepareData(true, false).then(function() { fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); return fieldGetter.get(); }).then(function(fields) { @@ -768,14 +753,14 @@ Zotero.Integration.Interface.prototype.editBibliography = 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._prepareData(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 fieldGetter = new Zotero.Integration.Fields(this._session, this._doc, Zotero.Integration.onFieldError); var fields = yield fieldGetter.get(); var haveBibliography = false; @@ -789,7 +774,7 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro if (haveBibliography) { yield fieldGetter.updateSession(); - yield session.editBibliography(this._doc); + yield this._session.editBibliography(this._doc); } else { var field = new Zotero.Integration.BibliographyField(yield fieldGetter.addField()); field.clearCode(); @@ -806,7 +791,7 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro */ Zotero.Integration.Interface.prototype.refresh = function() { var me = this; - return this._getSession(true, false).then(function() { + return this._prepareData(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() { @@ -821,7 +806,7 @@ Zotero.Integration.Interface.prototype.refresh = function() { */ Zotero.Integration.Interface.prototype.removeCodes = function() { var me = this; - return this._getSession(true, false).then(function() { + return this._prepareData(true, false).then(function() { var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc); return fieldGetter.get() }).then(function(fields) { @@ -842,10 +827,10 @@ Zotero.Integration.Interface.prototype.removeCodes = function() { Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(function* () { var fieldGetter, oldData; - let haveSession = yield this._getSession(false, true); + let haveSession = yield this._prepareData(false, true); fieldGetter = new Zotero.Integration.Fields(this._session, this._doc, Zotero.Integration.onFieldError); - var setDocPrefs = this._session.setDocPrefs.bind(this._session, this._doc, + var setDocPrefs = this._session.setDocPrefs.bind(this._session, this._app.primaryFieldType, this._app.secondaryFieldType); if(!haveSession) { @@ -862,9 +847,6 @@ Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(fu }); } - // Write document data to document - this._doc.setDocumentData(this._session.data.serialize()); - // If oldData is null, then there was no document data, so we don't need to update // fields if (!oldData) return false; @@ -966,13 +948,13 @@ Zotero.Integration.Fields.prototype.addField = function(note) { } if (!field) { - field = new Zotero.Integration.Field(this._doc.insertField(this._session.data.prefs['fieldType'], - (note ? this._session.data.prefs["noteType"] : 0))); + field = this._doc.insertField(this._session.data.prefs['fieldType'], + (note ? this._session.data.prefs["noteType"] : 0)); } // If fields already retrieved, further this.get() calls will returned the cached version // So we append this field to that list if (this._fields) { - this._fields.push(field._field); + this._fields.push(field); } return Zotero.Promise.resolve(field); @@ -1299,7 +1281,6 @@ 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()); } } @@ -1402,7 +1383,7 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f // Preparing stuff to pass into CitationEditInterface var fieldIndexPromise = this.get().then(function(fields) { for (var i=0, n=fields.length; i Date: Thu, 25 May 2017 10:48:43 +0300 Subject: [PATCH 09/18] Add Zotero.Integration.Citation - Moves a bunch of citation related processing from Integration.Session - Replaces missing item handling with a function instead of exception - Solves some really confusing flow issues in _processFields --- chrome/content/zotero/xpcom/cite.js | 2 +- chrome/content/zotero/xpcom/integration.js | 732 +++++++++------------ test/tests/integrationTest.js | 17 +- 3 files changed, 329 insertions(+), 422 deletions(-) diff --git a/chrome/content/zotero/xpcom/cite.js b/chrome/content/zotero/xpcom/cite.js index b4ded1b30..1c575d510 100644 --- a/chrome/content/zotero/xpcom/cite.js +++ b/chrome/content/zotero/xpcom/cite.js @@ -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 index 65ef1ee04..217be0a1e 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -305,8 +305,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 @@ -442,66 +442,13 @@ Zotero.Integration = new function() { /** * 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 = function() {}; 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"); - } - - // 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; - } - 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); - }); - } - } -} + "toString":function() { return this.message } +}; Zotero.Integration.CorruptFieldException = function(code, cause) { this.code = code; @@ -697,26 +644,23 @@ Zotero.Integration.Interface.prototype.addEditCitation = function() { * Adds a bibliography to the current document. * @return {Promise} */ -Zotero.Integration.Interface.prototype.addBibliography = function() { +Zotero.Integration.Interface.prototype.addBibliography = Zotero.Promise.coroutine(function* () { var me = this; - return this._prepareData(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.clearCode(); - field.type = INTEGRATION_TYPE_BIBLIOGRAPHY; - field.writeToDoc(); - return fieldGetter.updateSession().then(function() { - return fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); - }); - }); - }); -} + yield this._prepareData(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"); + } + + var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); + let field = new Zotero.Integration.BibliographyField(yield fieldGetter.addField()); + field.clearCode(); + field.type = INTEGRATION_TYPE_BIBLIOGRAPHY; + field.writeToDoc(); + yield fieldGetter.updateSession(); + yield fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); +}) /** * Edits bibliography metadata. @@ -774,11 +718,10 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro if (haveBibliography) { yield fieldGetter.updateSession(); - yield this._session.editBibliography(this._doc); + yield this._session.editBibliography(); } else { var field = new Zotero.Integration.BibliographyField(yield fieldGetter.addField()); field.clearCode(); - field.type = INTEGRATION_TYPE_BIBLIOGRAPHY; field.writeToDoc(); yield fieldGetter.updateSession(); } @@ -921,7 +864,6 @@ Zotero.Integration.Fields = function(session, doc, fieldErrorHandler) { this._doc = doc; this._deferreds = null; - this._removeCodeKeys = {}; this._removeCodeFields = {}; this._bibliographyFields = []; this._bibliographyData = ""; @@ -1054,7 +996,6 @@ Zotero.Integration.Fields.prototype.updateSession = Zotero.Promise.coroutine(fun yield this.get(); this._session.resetRequest(this._doc); - this._removeCodeKeys = {}; this._removeCodeFields = {}; this._bibliographyFields = []; this._bibliographyData = ""; @@ -1104,31 +1045,28 @@ Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(fu for(var n = this._fields.length; i {return {id: i.id} }); - var field = doc.insertField("Field", 0); - field.setCode('TEMP'); + var field = new Zotero.Integration.CitationField(Zotero.Integration.currentDoc.insertField("Field", 0)); + field.clearCode(); + field.writeToDoc(); + var citation = new Zotero.Integration.Citation(field); var integrationDoc = addEditCitationSpy.lastCall.thisValue; var fieldGetter = new Zotero.Integration.Fields(integrationDoc._session, integrationDoc._doc, () => 0); var io = new Zotero.Integration.CitationEditInterface( - { citationItems, properties: {} }, + citation, field, fieldGetter, integrationDoc._session @@ -313,10 +315,11 @@ 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 = ioResult(dialogName); } Object.assign(io, ioResult); return Zotero.Promise.resolve(); @@ -460,7 +463,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')); }); }); }); From 5805c7e562c820cdbe37221bb178ced99be22a8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Tue, 30 May 2017 15:55:57 +0300 Subject: [PATCH 10/18] Integration refactor megacommit - Removed obsolete logic for citation.properties.deleted, which is no longer set anywhere - Introduced a bibliography class - BibliographyEditInterface no longer edits state - Fields._processFields() now has linear flow because: - Exception handling for missing items and corrupt fields reworked to be handled in relevant Field classes, so that the flow remains linear - Document modifying functions (i.e. Fields.updateDocument()) now only called in Zotero.Integration.Interface functions instead of all over the place - document.setDocPrefs() now called after every execCommand() since the cost is trivial, but that simplifies a bunch of logic - Misc code cleanup TODO at some point in the future: - Move Integration.(init/delete)Pipe out - Decouple references and clarify functions in Integration.Fields and Integration.Session --- .../integration/editBibliographyDialog.js | 12 +- chrome/content/zotero/xpcom/cite.js | 4 +- chrome/content/zotero/xpcom/integration.js | 1498 +++++++---------- 3 files changed, 649 insertions(+), 865 deletions(-) diff --git a/chrome/content/zotero/integration/editBibliographyDialog.js b/chrome/content/zotero/integration/editBibliographyDialog.js index f5dfdd7ad..7c8138350 100644 --- 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/xpcom/cite.js b/chrome/content/zotero/xpcom/cite.js index 1c575d510..afcc210cc 100644 --- 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; } diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index 217be0a1e..93cdade4d 100644 --- 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 @@ -53,7 +50,6 @@ const NOTE_ENDNOTE = 2; const INTEGRATION_TYPE_ITEM = 1; const INTEGRATION_TYPE_BIBLIOGRAPHY = 2; const INTEGRATION_TYPE_TEMP = 3; -const INTEGRATION_TYPE_REMOVE = 4; Zotero.Integration = new function() { @@ -223,6 +219,9 @@ Zotero.Integration = new function() { // (depending on what is possible) 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; try { yield (new Zotero.Integration.Interface(application, document, session))[command](); } @@ -266,7 +265,7 @@ Zotero.Integration = new function() { } } finally { - if(document) { + if (document) { try { document.setDocumentData(session.data.serialize()); document.cleanup(); @@ -338,17 +337,6 @@ Zotero.Integration = new function() { return deferred.promise; }; - /** - * Default callback for field-related errors. All functions that do not define their - * own handlers for field-related errors should use this one. - */ - this.onFieldError = function onFieldError(err) { - if(err.attemptToResolve) { - return err.attemptToResolve(); - } - throw err; - } - /** * Gets a session for a given doc. * Either loads a cached session if doc communicated since restart or creates a new one @@ -395,7 +383,7 @@ Zotero.Integration = new function() { return Zotero.Integration.sessions[data.sessionID]; } } - session = new Zotero.Integration.Session(doc); + session = new Zotero.Integration.Session(doc, app); try { yield session.setData(data); } catch(e) { @@ -429,7 +417,7 @@ Zotero.Integration = new function() { return session; } } - yield session.setDocPrefs(app.primaryFieldType, app.secondaryFieldType); + yield session.setDocPrefs(); } else { throw e; } @@ -443,107 +431,16 @@ Zotero.Integration = new function() { /** * An exception thrown when a document contains an item that no longer exists in the current document. */ -Zotero.Integration.MissingItemException = function() {}; +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 } + "message":`An item in this document is missing from your Zotero library.}`, + "toString":function() { return this.message + `\n ${JSON.stringify(this.item)}` } }; -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"); - } - - 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(); - } - Zotero.Integration.currentWindow = oldWindow; - fieldGetter.progressCallback = oldProgressCallback; - return fieldGetter.updateSession(); - }); - } - } -}; - -/** - * An exception to encapsulate the case where bibliography data is invalid. - * @class - */ -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.NO_ACTION = 0; +Zotero.Integration.UPDATE = 1; +Zotero.Integration.DELETE = 2; /** * All methods for interacting with a document @@ -555,90 +452,43 @@ Zotero.Integration.Interface = function(app, doc, session) { this._session = session; } -/** - * 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.Interface.prototype._prepareData = Zotero.Promise.coroutine(function *(require, dontRunSetDocPrefs) { - var data = this._session.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._app.primaryFieldType, this._app.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(dontRunSetDocPrefs) return false; - - if (haveFields) { - this._session.reload = true; - } - - yield this._session.setDocPrefs(this._app.primaryFieldType, this._app.secondaryFieldType); - } - return true; -}); - /** * Adds a citation to the current document. * @return {Promise} */ -Zotero.Integration.Interface.prototype.addCitation = function() { - var me = this; - return this._prepareData(false, false).then(function() { - return (new Zotero.Integration.Fields(me._session, me._doc)).addEditCitation(null); - }); -} +Zotero.Integration.Interface.prototype.addCitation = Zotero.Promise.coroutine(function* () { + yield this._session.init(false, false); + + let [idx, field, citation] = yield this._session.fields.addEditCitation(null); + yield this._session.addCitation(idx, field.noteIndex, citation); + return this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, false, false); +}); /** * Edits the citation at the cursor position. * @return {Promise} */ -Zotero.Integration.Interface.prototype.editCitation = function() { - var me = this; - return this._prepareData(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.Interface.prototype.addEditCitation = function() { - var me = this; - return this._prepareData(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.noteIndex, citation); + return this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, false, false); +}); /** * Adds a bibliography to the current document. @@ -646,86 +496,91 @@ Zotero.Integration.Interface.prototype.addEditCitation = function() { */ Zotero.Integration.Interface.prototype.addBibliography = Zotero.Promise.coroutine(function* () { var me = this; - yield this._prepareData(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"); } - var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); - let field = new Zotero.Integration.BibliographyField(yield fieldGetter.addField()); + let field = new Zotero.Integration.BibliographyField(yield this._session.fields.addField()); field.clearCode(); - field.type = INTEGRATION_TYPE_BIBLIOGRAPHY; field.writeToDoc(); - yield fieldGetter.updateSession(); - yield fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); + if(this._session.data.prefs.delayCitationUpdates) { + // Refreshes citeproc state before proceeding + this._session.reload = true; + } + yield this._session.fields.updateSession(FORCE_CITATIONS_FALSE); + yield this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, true, false); }) /** * Edits bibliography metadata. * @return {Promise} */ -Zotero.Integration.Interface.prototype.editBibliography = function() { +Zotero.Integration.Interface.prototype.editBibliography = Zotero.Promise.coroutine(function*() { // Make sure we have a bibliography - var me = this, fieldGetter; - return this._prepareData(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 (let i = fields.length-1; i >= 0; i--) { - let field = Zotero.Integration.Field.loadExisting(fields[i]); - if (field.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { - haveBibliography = true; - break; - } + 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; } - - if(!haveBibliography) { - throw new Zotero.Exception.Alert("integration.error.mustInsertBibliography", - [], "integration.error.title"); - } - 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); + if(this._session.data.prefs.delayCitationUpdates) { + // Refreshes citeproc state before proceeding + this._session.reload = true; + } + yield this._session.fields.updateSession(FORCE_CITATIONS_FALSE); + yield this._session.editBibliography(bibliography); + yield this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, true, false); +}); Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coroutine(function *() { // Check if we have a bibliography - yield this._prepareData(true, false); + yield this._session.init(true, false); if (!this._session.data.style.hasBibliography) { throw new Zotero.Exception.Alert("integration.error.noBibliography", [], "integration.error.title"); } - var fieldGetter = new Zotero.Integration.Fields(this._session, this._doc, Zotero.Integration.onFieldError); - var fields = yield fieldGetter.get(); + var fields = yield this._session.fields.get(); - var haveBibliography = false; + 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) { - haveBibliography = true; + bibliographyField = field; break; } } - if (haveBibliography) { - yield fieldGetter.updateSession(); - yield this._session.editBibliography(); - } else { - var field = new Zotero.Integration.BibliographyField(yield fieldGetter.addField()); - field.clearCode(); - field.writeToDoc(); - yield fieldGetter.updateSession(); + if(!bibliographyField) { + bibliographyField = new Zotero.Integration.BibliographyField(yield this._session.fields.addField()); + bibliographyField.clearCode(); + bibliographyField.writeToDoc(); } - return fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); + + let bibliography = new Zotero.Integration.Bibliography(bibliographyField); + if(this._session.data.prefs.delayCitationUpdates) { + // Refreshes citeproc state before proceeding + this._session.reload = true; + } + yield this._session.fields.updateSession(FORCE_CITATIONS_FALSE); + yield this._session.editBibliography(bibliography); + yield this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, true, false); }); /** @@ -734,11 +589,10 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro */ Zotero.Integration.Interface.prototype.refresh = function() { var me = this; - return this._prepareData(true, false).then(function() { + return this._session.init(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); + return me._session.fields.updateSession(FORCE_CITATIONS_REGENERATE).then(function() { + return me._session.fields.updateDocument(FORCE_CITATIONS_REGENERATE, true, false); }); }); } @@ -747,43 +601,35 @@ Zotero.Integration.Interface.prototype.refresh = function() { * Deletes field codes. * @return {Promise} */ -Zotero.Integration.Interface.prototype.removeCodes = function() { +Zotero.Integration.Interface.prototype.removeCodes = Zotero.Promise.coroutine(function* () { var me = this; - return this._prepareData(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.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(function* () { - var fieldGetter, - oldData; - let haveSession = yield this._prepareData(false, true); + var oldData; + let haveSession = yield this._session.init(false, true); - fieldGetter = new Zotero.Integration.Fields(this._session, this._doc, Zotero.Integration.onFieldError); - var setDocPrefs = this._session.setDocPrefs.bind(this._session, - this._app.primaryFieldType, this._app.secondaryFieldType); - if(!haveSession) { // This is a brand new document; don't try to get fields - oldData = yield setDocPrefs(); + oldData = yield this._session.setDocPrefs(); } else { // Can get fields while dialog is open oldData = yield Zotero.Promise.all([ - fieldGetter.get(), - setDocPrefs() + this._session.fields.get(), + this._session.setDocPrefs() ]).spread(function (fields, setDocPrefs) { // Only return value from setDocPrefs return setDocPrefs; @@ -795,7 +641,7 @@ Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(fu if (!oldData) return false; // Perform noteType or fieldType conversion - let fields = yield fieldGetter.get(); + let fields = yield this._session.fields.get(); var convertBibliographies = oldData.prefs.fieldType != this._session.data.prefs.fieldType; var convertItems = convertBibliographies @@ -808,12 +654,12 @@ Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(fu if (convertItems && field.type === INTEGRATION_TYPE_ITEM) { var citation = field.unserialize(); if (!citation.properties.dontUpdate) { - fieldsToConvert.push(field); + fieldsToConvert.push(fields[i]); fieldNoteTypes.push(this._session.data.prefs.noteType); } } else if(convertBibliographies && type === INTEGRATION_TYPE_BIBLIOGRAPHY) { - fieldsToConvert.push(field); + fieldsToConvert.push(fields[i]); fieldNoteTypes.push(0); } } @@ -826,10 +672,10 @@ Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(fu } // Refresh contents - fieldGetter = new Zotero.Integration.Fields(this._session, this._doc, Zotero.Integration.onFieldError); - fieldGetter.ignoreEmptyBibliography = false; - return fieldGetter.updateSession().then(fieldGetter.updateDocument.bind( - fieldGetter, FORCE_CITATIONS_RESET_TEXT, true, true)); + 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); }); /** @@ -849,24 +695,17 @@ Zotero.Integration.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._removeCodeFields = {}; this._bibliographyFields = []; - this._bibliographyData = ""; } /** @@ -906,102 +745,95 @@ Zotero.Integration.Fields.prototype.addField = function(note) { * 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 {}); - this._session.updateIndices = {}; - this._session.updateItemIDs = {}; - this._session.citationText = {}; - this._session.bibliographyHasChanged = false; + this._session.restoreProcessorState(); delete this._session.reload; } }); @@ -1039,45 +851,40 @@ 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 {}); @@ -1127,28 +932,24 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, } var citation = this._session.citationsByIndex[i]; - var field = this._fields[i]; - - // If there is no citation, we're deleting it, or we shouldn't update it, ignore - // it - if(!citation || citation.properties.delete) continue; + let citationField = citation._field; if(!citation.properties.dontUpdate) { var formattedCitation = citation.properties.custom - ? citation.properties.custom : this._session.citationText[i]; + ? citation.properties.custom : citation.text; 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.text; + var plainCitation = citationField.text; 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(); + citationField.select(); var result = this._doc.displayAlert( Zotero.getString("integration.citationChanged")+"\n\n"+Zotero.getString("integration.citationChanged.description"), DIALOG_ICON_CAUTION, DIALOG_BUTTONS_YES_NO); @@ -1159,41 +960,49 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, } if(!citation.properties.dontUpdate) { - field.text = formattedCitation; + // setText and getText here bypass the setter/getter abstraction + var isRich = formattedCitation.includes('\\'); + if (isRich) { + formattedCitation = '{\\rtf' + formattedCitation + '}'; + } + citationField.setText(formattedCitation, isRich); + citationField.text = formattedCitation; citation.properties.formattedCitation = formattedCitation; - citation.properties.plainCitation = field.text; + citation.properties.plainCitation = citationField.getText(); + + // But we still need writeToDoc to trigger RTF write for LO (see comment in writeToDoc()) + citationField.text = formattedCitation; } } } var serializedCitation = citation.serialize(); if (serializedCitation != citation.properties.field) { - field.code = serializedCitation; + citationField.code = serializedCitation; } - field.writeToDoc(); + citationField.writeToDoc(); 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.code = bibliographyData; + if (forceBibliography || this._session.bibliographyDataHasChanged) { + let code = this._session.bibliography.serialize(); + for (let field of this._bibliographyFields) { + field.code = 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 @@ -1210,7 +1019,7 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, } // set bibliography text - for (let field of bibliographyFields) { + for (let field of this._bibliographyFields) { if(this.progressCallback) { try { this.progressCallback(75+(nUpdated/nFieldUpdates)*25); @@ -1231,14 +1040,8 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, } // 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(); } } @@ -1259,7 +1062,6 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f newField = true; field = new Zotero.Integration.CitationField(yield this.addField(true)); field.clearCode(); - field.writeToDoc(); } var citation = new Zotero.Integration.Citation(field); @@ -1270,24 +1072,29 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f var fieldIndexPromise = this.get().then(function(fields) { for (var i=0, n=fields.length; 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.uriMap.getURIsForItemID(id), this.customBibliographyText[id]]); - - - if(bibliographyData.uncited || bibliographyData.custom) { - return JSON.stringify(bibliographyData); - } else { - return ""; // nothing - } + this.style.rebuildProcessorState(citations, 'rtf', uncited); } /** * Edits integration bibliography + * @param {Zotero.Integration.Bibliography} bibliography */ -Zotero.Integration.Session.prototype.editBibliography = function() { - 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(); + + var bibliographyEditor = new Zotero.Integration.BibliographyEditInterface(bibliography, this.citationsByItemID, this.style); + + yield Zotero.Integration.displayDialog('chrome://zotero/content/integration/editBibliographyDialog.xul', 'resizable', bibliographyEditor); + if (bibliographyEditor.cancelled) throw new Zotero.Exception.UserCancelled("bibliography editing"); this.bibliographyDataHasChanged = this.bibliographyHasChanged = true; - - return Zotero.Integration.displayDialog('chrome://zotero/content/integration/editBibliographyDialog.xul', 'resizable', io); -} + this.bibliography = bibliographyEditor.bibliography; +}); /** * @class Interface for bibliography editor to alter document bibliography * @constructor * Creates a new bibliography editor interface - * @param session {Zotero.Integration.Session} + * @param bibliography {Zotero.Integration.Bibliography} */ -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]; - } - } - +Zotero.Integration.BibliographyEditInterface = function(bibliography, citationsByItemID, citeproc) { + this.bibliography = bibliography; + this.citeproc = citeproc; + this.wrappedJSObject = this; + this._citationsByItemID = citationsByItemID; this._update(); } -/** - * Updates stored 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.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(); } /** @@ -2280,10 +1874,12 @@ Zotero.Integration.Field = class { } get text() {return this._text = this._text ? this._text : this.getText()} - set text(v) {return this._text = v; this.dirty = true} + set text(v) {this._text = v; this.dirty = true} get code() {return this._code = this._code ? this._code : this.getCode()} - set code(v) {return this._code = v; this.dirty = true} + set code(v) {this._code = v; this.dirty = true} + + get noteIndex() {return this._noteIndex = this._noteIndex ? this._noteIndex : this.getNoteIndex()} clearCode() { this.code = '{}'; @@ -2299,21 +1895,24 @@ Zotero.Integration.Field = class { } else { this._field.setCode(`TEMP`); } - this.dirty = false; // NB: Setting code in LO removes rtf formatting, so the order here is important let text = this.text; let isRich = false; // If RTF wrap with RTF tags if (text.includes("\\")) { - text = "{\\rtf "+text+"}"; + if (text.substr(0,5) != "{\\rtf") { + text = "{\\rtf "+text+"}"; + } isRich = true; } this._field.setText(text, isRich); + this.dirty = false; // Retrigger retrieval from doc. this._text = null; this._code = null; + this._noteIndex = null; }; getCode() { @@ -2349,6 +1948,10 @@ Zotero.Integration.Field.loadExisting = function(docField) { 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) { @@ -2356,12 +1959,7 @@ Zotero.Integration.Field.loadExisting = function(docField) { } else { field._code = rawCode.substr(rawCode.indexOf(' ')+1); } - } - - if (rawCode.substr(0, 4) === "TEMP") { - field = new Zotero.Integration.Field(docField); - field._code = rawCode.substr(5); - } + }; return field; }; @@ -2387,11 +1985,7 @@ Zotero.Integration.CitationField = class extends Zotero.Integration.Field { return JSON.parse(code); } catch(e) { // fix for corrupted fields (corrupted by 2.1b1) - try { - return JSON.parse(code.replace(/{{((?:\s*,?"unsorted":(?:true|false)|\s*,?"custom":"(?:(?:\\")?[^"]*\s*)*")*)}}/, "{$1}")); - } catch(e) { - throw new Zotero.Integration.CorruptFieldException(code, e); - } + return JSON.parse(code.replace(/{{((?:\s*,?"unsorted":(?:true|false)|\s*,?"custom":"(?:(?:\\")?[^"]*\s*)*")*)}}/, "{$1}")); } } @@ -2484,17 +2078,49 @@ Zotero.Integration.CitationField = class extends Zotero.Integration.Field { } - if (this.code[0] == '{') { // JSON field - return upgradeCruft(unserialize(this.code), this.code); - } else { // ye olde style field - return unserializePreZotero1_0(this.code); + try { + if (this.code[0] == '{') { // JSON field + return upgradeCruft(unserialize(this.code), this.code); + } else { // ye olde style field + return unserializePreZotero1_0(this.code); + } + } catch (e) { + return this.resolveCorrupt(); } - }; + } clearCode() { this.code = JSON.stringify({citationItems: [], properties: {}}); this.writeToDoc(); } + + resolveCorrupt() { + return Zotero.Promise.coroutine(function* () { + Zotero.debug(`Integration: handling corrupt citation field ${this.code}`); + var msg = Zotero.getString("integration.corruptField")+'\n\n'+ + Zotero.getString('integration.corruptField.description'); + this.select(); + Zotero.Integration.currentDoc.activate(); + var result = Zotero.Integration.currentDoc.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); + } }; @@ -2508,28 +2134,44 @@ Zotero.Integration.BibliographyField = class extends Zotero.Integration.Field { try { return JSON.parse(this.code); } catch(e) { - throw new Zotero.Integration.CorruptFieldException(this.code, e); + return this.resolveCorrupt(); } } + + resolveCorrupt() { + return Zotero.Promise.coroutine(function* () { + Zotero.debug(`Integration: handling corrupt bibliography field ${this.code}`); + var msg = Zotero.getString("integration.corruptBibliography")+'\n\n'+ + Zotero.getString('integration.corruptBibliography.description'); + var result = Zotero.Integration.currentDoc.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.properties.noteIndex = citationField.noteIndex; this._field = citationField; } /** - * Load item data for current item + * Load item data for current item * @param {Boolean} [promptToReselect=true] - will throw a MissingItemException if false * @returns {Promise{Number}} - * - Zotero.Integration.Citation.NO_ACTION - * - Zotero.Integration.Citation.UPDATE - * - Zotero.Integration.Citation.DELETE + * - Zotero.Integration.NO_ACTION + * - Zotero.Integration.UPDATE + * - Zotero.Integration.REMOVE_CODE */ loadItemData() { return Zotero.Promise.coroutine(function *(promptToReselect=true){ @@ -2542,9 +2184,9 @@ Zotero.Integration.Citation = class { // get Zotero item var zoteroItem = false; if (citationItem.uris) { - let itemNeedUpdate; - [zoteroItem, itemNeedUpdate] = yield Zotero.Integration.currentSession.uriMap.getZoteroItemForURIs(citationItem.uris); - needUpdate = needUpdate || itemNeedUpdate; + 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 @@ -2576,6 +2218,7 @@ Zotero.Integration.Citation = class { 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); @@ -2597,10 +2240,10 @@ Zotero.Integration.Citation = class { } else if (promptToReselect) { zoteroItem = yield this.handleMissingItem(i); if (zoteroItem) needUpdate = true; - else return Zotero.Integration.Citation.DELETE; + else return Zotero.Integration.REMOVE_CODE; } else { // throw a MissingItemException - throw (new Zotero.Integration.MissingItemException(this, i)); + throw (new Zotero.Integration.MissingItemException(this, this.citationItems[i])); } } @@ -2621,7 +2264,7 @@ Zotero.Integration.Citation = class { if (items.length) { yield Zotero.Items.loadDataTypes(items); } - return needUpdate ? Zotero.Integration.Citation.UPDATE : Zotero.Integration.Citation.NO_ACTION; + return needUpdate ? Zotero.Integration.UPDATE : Zotero.Integration.NO_ACTION; }).apply(this, arguments); } @@ -2685,12 +2328,8 @@ Zotero.Integration.Citation = class { }).apply(this, arguments); } - - /** - * Serializes the citation into CSL code representation - * @returns {string} - */ - serialize() { + + toJSON() { const saveProperties = ["custom", "unsorted", "formattedCitation", "plainCitation", "dontUpdate"]; const saveCitationItemKeys = ["locator", "label", "suppress-author", "author-only", "prefix", "suffix"]; @@ -2713,7 +2352,7 @@ Zotero.Integration.Citation = class { var slashIndex; if (typeof citationItem.id === "string" && (slashIndex = citationItem.id.indexOf("/")) !== -1) { // this is an embedded item - serializeCitationItem.id = citationItem.itemData.id; + serializeCitationItem.id = citationItem.id; serializeCitationItem.uris = citationItem.uris; // XXX For compatibility with older versions of Zotero; to be removed at a later date @@ -2739,9 +2378,154 @@ Zotero.Integration.Citation = class { } citation.schema = "https://github.com/citation-style-language/schema/raw/master/csl-citation.json"; - return JSON.stringify(citation); + return citation; + } + + /** + * Serializes the citation into CSL code representation + * @returns {string} + */ + serialize() { + return JSON.stringify(this.toJSON()); } }; -Zotero.Integration.Citation.NO_ACTION = 0; -Zotero.Integration.Citation.UPDATE = 1; -Zotero.Integration.Citation.DELETE = 2; + +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); + } +} \ No newline at end of file From 6d05c3472bd8e2dd6509795b81a964ca0aba3fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Thu, 1 Jun 2017 13:03:51 +0300 Subject: [PATCH 11/18] Add more integration tests --- chrome/content/zotero/xpcom/integration.js | 36 +++---- test/tests/integrationTest.js | 106 ++++++++++++++++----- 2 files changed, 101 insertions(+), 41 deletions(-) diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index 93cdade4d..9e7b4905a 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -567,7 +567,8 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro } } - if(!bibliographyField) { + var newBibliography = !bibliographyField; + if (!bibliographyField) { bibliographyField = new Zotero.Integration.BibliographyField(yield this._session.fields.addField()); bibliographyField.clearCode(); bibliographyField.writeToDoc(); @@ -579,7 +580,7 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro this._session.reload = true; } yield this._session.fields.updateSession(FORCE_CITATIONS_FALSE); - yield this._session.editBibliography(bibliography); + if (!newBibliography) yield this._session.editBibliography(bibliography); yield this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, true, false); }); @@ -1134,13 +1135,13 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f let fieldIndex = yield fieldIndexPromise; this._session.updateIndices[fieldIndex] = true; + // Make sure session updated + yield citationsByItemIDPromise; if (newField) { - // On next updateSession call this field will be properly recognised as citation + // For further processing this field will be properly recognised as citation // instead of skipped as a temp field. field.writeToDoc(); } - // Make sure session updated - yield citationsByItemIDPromise; return [fieldIndex, field, io.citation]; }); @@ -1887,7 +1888,7 @@ Zotero.Integration.Field = class { writeToDoc() { if (!this.dirty) return; - // Boo. Inconsistent. + // Boo. Inconsistent order. if (this.type == INTEGRATION_TYPE_ITEM) { this._field.setCode(`ITEM CSL_CITATION ${this.code}`); } else if (this.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { @@ -1897,16 +1898,18 @@ Zotero.Integration.Field = class { } // NB: Setting code in LO removes rtf formatting, so the order here is important - let text = this.text; - let isRich = false; - // If RTF wrap with RTF tags - if (text.includes("\\")) { - if (text.substr(0,5) != "{\\rtf") { - text = "{\\rtf "+text+"}"; + let text = this._text; + if (text) { + let isRich = false; + // If RTF wrap with RTF tags + if (text.includes("\\")) { + if (text.substr(0,5) != "{\\rtf") { + text = "{\\rtf "+text+"}"; + } + isRich = true; } - isRich = true; + this._field.setText(text, isRich); } - this._field.setText(text, isRich); this.dirty = false; // Retrigger retrieval from doc. @@ -2048,7 +2051,6 @@ Zotero.Integration.CitationField = class extends Zotero.Integration.Field { citation.properties.custom = citation.custom; delete citation.custom; } - if(!citation.citationID) citation.citationID = Zotero.randomString(); citation.properties.field = code; return citation; @@ -2355,7 +2357,7 @@ Zotero.Integration.Citation = class { serializeCitationItem.id = citationItem.id; serializeCitationItem.uris = citationItem.uris; - // XXX For compatibility with older versions of Zotero; to be removed at a later date + // 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 @@ -2364,7 +2366,7 @@ Zotero.Integration.Citation = class { serializeCitationItem.id = citationItem.id; serializeCitationItem.uris = Zotero.Integration.currentSession.uriMap.getURIsForItemID(citationItem.id); - // XXX For compatibility with older versions of Zotero; to be removed at a later date + // 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); diff --git a/test/tests/integrationTest.js b/test/tests/integrationTest.js index c20086a72..4ef509f47 100644 --- 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,23 +277,13 @@ describe("Zotero.Integration", function () { function setAddEditItems(items) { if (items.length == undefined) items = [items]; - dialogResults.quickFormat = function(dialogName) { - var citationItems = items.map((i) => {return {id: i.id} }); - var field = new Zotero.Integration.CitationField(Zotero.Integration.currentDoc.insertField("Field", 0)); - field.clearCode(); - field.writeToDoc(); - var citation = new Zotero.Integration.Citation(field); - var integrationDoc = addEditCitationSpy.lastCall.thisValue; - var fieldGetter = new Zotero.Integration.Fields(integrationDoc._session, integrationDoc._doc, () => 0); - var io = new Zotero.Integration.CitationEditInterface( - citation, - 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* () { @@ -319,9 +313,10 @@ describe("Zotero.Integration", function () { Zotero.debug(`Display dialog: ${dialogName}`, 2); var ioResult = dialogResults[dialogName.substring(dialogName.lastIndexOf('/')+1, dialogName.length-4)]; if (typeof ioResult == 'function') { - ioResult = ioResult(dialogName); + ioResult(dialogName, io); + } else { + Object.assign(io, ioResult); } - Object.assign(io, ioResult); return Zotero.Promise.resolve(); }); @@ -334,8 +329,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(); @@ -437,6 +432,68 @@ 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('#addEditBibliography', function() { var docID = this.fullTitle(); beforeEach(function* () { @@ -445,12 +502,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; } From 2827f70daaf571765a37a5f4a1ddda8f13a1b76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Tue, 6 Jun 2017 12:07:12 +0300 Subject: [PATCH 12/18] Option to delay updating citation in document. The checkbox in doc prefs is hidden until an update takes 5s or longer after which the user is prompted to enable delaying. --- chrome/content/zotero/bibliography.js | 21 ++ .../integration/integrationDocPrefs.xul | 8 +- chrome/content/zotero/xpcom/integration.js | 213 ++++++++++++------ chrome/locale/en-US/zotero/zotero.dtd | 4 + chrome/locale/en-US/zotero/zotero.properties | 1 + test/tests/integrationTest.js | 131 ++++++++++- 6 files changed, 308 insertions(+), 70 deletions(-) diff --git a/chrome/content/zotero/bibliography.js b/chrome/content/zotero/bibliography.js index 3e83cf4ca..7702208d8 100644 --- 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 diff --git a/chrome/content/zotero/integration/integrationDocPrefs.xul b/chrome/content/zotero/integration/integrationDocPrefs.xul index 50f90205e..0d6a561f3 100644 --- a/chrome/content/zotero/integration/integrationDocPrefs.xul +++ b/chrome/content/zotero/integration/integrationDocPrefs.xul @@ -31,10 +31,11 @@ &zotero.integration.prefs.automaticJournalAbbeviations.caption; + + + + &zotero.integration.prefs.delayCitationUpdates.description; + \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index 9e7b4905a..d386fa2dc 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -51,6 +51,8 @@ const INTEGRATION_TYPE_ITEM = 1; const INTEGRATION_TYPE_BIBLIOGRAPHY = 2; const INTEGRATION_TYPE_TEMP = 3; +const DELAY_CITATIONS_PROMPT_TIMEOUT = 5/*seconds*/; + Zotero.Integration = new function() { Components.utils.import("resource://gre/modules/Services.jsm"); @@ -212,18 +214,25 @@ Zotero.Integration = new function() { return; } inProgress = true; - - var application = Zotero.Integration.getApplication(agent, command, docId); - + 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) - 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; 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); + + 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; yield (new Zotero.Integration.Interface(application, document, session))[command](); + document.setDocumentData(session.data.serialize()); } catch (e) { if(!(e instanceof Zotero.Exception.UserCancelled)) { @@ -262,12 +271,16 @@ 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 { + var diff = ((new Date()).getTime() - startTime)/1000; + Zotero.debug(`Integration: ${agent}-${command}${docId ? `:'${docId}'` : ''} complete in ${diff}s`) if (document) { try { - document.setDocumentData(session.data.serialize()); document.cleanup(); document.activate(); @@ -377,19 +390,20 @@ Zotero.Integration = new function() { throw new Zotero.Exception.Alert("integration.error.fieldTypeMismatch", [], "integration.error.title"); } - - if (Zotero.Integration.sessions[data.sessionID]) { - // If communication occured with this document since restart - return Zotero.Integration.sessions[data.sessionID]; - } + + session = Zotero.Integration.sessions[data.sessionID]; + } + if (!session) { + session = new Zotero.Integration.Session(doc, app); + session.reload = true; } - session = new Zotero.Integration.Session(doc, app); 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 || @@ -422,7 +436,6 @@ Zotero.Integration = new function() { throw e; } } - session.reload = true; return session; }); @@ -461,7 +474,12 @@ Zotero.Integration.Interface.prototype.addCitation = Zotero.Promise.coroutine(fu let [idx, field, citation] = yield this._session.fields.addEditCitation(null); yield this._session.addCitation(idx, field.noteIndex, citation); - return this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, false, false); + + if (this._session.data.prefs.delayCitationUpdates) { + return this._session.writeDelayedCitation(idx, field, citation); + } else { + return this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, false, false); + } }); /** @@ -487,7 +505,11 @@ Zotero.Integration.Interface.prototype.addEditCitation = Zotero.Promise.coroutin let [idx, field, citation] = yield this._session.fields.addEditCitation(docField); yield this._session.addCitation(idx, field.noteIndex, citation); - return this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, false, false); + if (this._session.data.prefs.delayCitationUpdates) { + return this._session.writeDelayedCitation(idx, field, citation); + } else { + return this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, false, false); + } }); /** @@ -588,14 +610,12 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro * Updates the citation data for all citations and bibliography entries. * @return {Promise} */ -Zotero.Integration.Interface.prototype.refresh = function() { - var me = this; - return this._session.init(true, false).then(function() { - // Send request, forcing update of citations and bibliography - return me._session.fields.updateSession(FORCE_CITATIONS_REGENERATE).then(function() { - return me._session.fields.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); } /** @@ -898,11 +918,26 @@ Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(fu */ Zotero.Integration.Fields.prototype.updateDocument = Zotero.Promise.coroutine(function* (forceCitations, forceBibliography, ignoreCitationChanges) { - // Iterate through citations, yielding for UI updates - yield Zotero.Promise.each(this._session._updateCitations(), () => {}); + var startTime = (new Date()).getTime(); - yield Zotero.Promise.each( - this._updateDocument(forceCitations, forceBibliography, ignoreCitationChanges), () => {}); + yield this._session._updateCitations() + yield this._updateDocument(forceCitations, forceBibliography, ignoreCitationChanges) + + var diff = ((new Date()).getTime() - startTime)/1000; + 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._doc.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); + } + } }); /** @@ -912,7 +947,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; @@ -929,34 +964,37 @@ 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]; let citationField = citation._field; - if(!citation.properties.dontUpdate) { - var formattedCitation = citation.properties.custom + if (!citation.properties.dontUpdate) { + var formattedCitation = citation.properties.formattedCitation && citation.properties.custom ? citation.properties.custom : citation.text; + var plainCitation = citation.properties.plainCitation && citationField.text; - if(forceCitations === FORCE_CITATIONS_RESET_TEXT - || citation.properties.formattedCitation !== formattedCitation) { - // Check if citation has been manually modified - if(!ignoreCitationChanges && citation.properties.plainCitation) { - var plainCitation = citationField.text; - 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._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._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; } } @@ -972,8 +1010,10 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, citation.properties.formattedCitation = formattedCitation; citation.properties.plainCitation = citationField.getText(); - // But we still need writeToDoc to trigger RTF write for LO (see comment in writeToDoc()) - citationField.text = formattedCitation; + if (isRich) { + // But we still need writeToDoc to trigger RTF write for LO (see comment in writeToDoc()) + citationField.text = formattedCitation; + } } } } @@ -1027,8 +1067,9 @@ 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(); if (bibliographyText) { field.text = bibliographyText; @@ -1081,14 +1122,17 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f } }); - var citationsByItemIDPromise = fieldIndexPromise.then(function() { - return this.updateSession(FORCE_CITATIONS_FALSE); - }.bind(this)).then(function() { - return this._session.citationsByItemID; - }.bind(this)); + 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)); + } - // Required for preview fn - citation.properties.noteIndex = field.noteIndex; var previewFn = Zotero.Promise.coroutine(function* (citation) { let idx = yield fieldIndexPromise; yield citationsByItemIDPromise; @@ -1352,7 +1396,7 @@ 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* () { +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; @@ -1362,6 +1406,9 @@ Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(func 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.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); } @@ -1380,8 +1427,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 && ( @@ -1396,9 +1445,11 @@ 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.regenAll = true; + this.reload = true; } return oldData || null; @@ -1450,7 +1501,6 @@ Zotero.Integration.Session.prototype.addCitation = Zotero.Promise.coroutine(func 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); @@ -1468,7 +1518,7 @@ Zotero.Integration.Session.prototype.getCiteprocLists = function() { /** * Updates the list of citations to be serialized to the document */ -Zotero.Integration.Session.prototype._updateCitations = function* () { +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"); @@ -1478,6 +1528,8 @@ Zotero.Integration.Session.prototype._updateCitations = function* () { for (let indexList of [this.newIndices, this.updateIndices]) { 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]; @@ -1503,9 +1555,7 @@ Zotero.Integration.Session.prototype._updateCitations = function* () { this.citationsByIndex[idx].text = text; } - // Yield for UI updates delete this.newIndices[index]; - yield; } } } @@ -1531,6 +1581,32 @@ Zotero.Integration.Session.prototype.restoreProcessorState = function() { this.style.rebuildProcessorState(citations, 'rtf', uncited); } + +Zotero.Integration.Session.prototype.writeDelayedCitation = Zotero.Promise.coroutine(function* (idx, field, citation) { + try { + var text = citation.properties.custom || this.style.previewCitationCluster(citation, [], [], "rtf"); + } catch(e) { + throw e; + } + text = `\\uldash{${text}}`; + + // Make sure we'll prompt for manually edited citations + if(!citation.properties.dontUpdate) { + // setText and getText here bypass the setter/getter abstraction + text = '{\\rtf' + text + '}'; + field.setText(text, true); + field.text = text; + + citation.properties.formattedCitation = text; + citation.properties.plainCitation = field.getText(); + } + + field.code = citation.serialize(); + field.text = text; + field.writeToDoc(); +}); + + /** * Edits integration bibliography * @param {Zotero.Integration.Bibliography} bibliography @@ -1681,7 +1757,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 += ``; } diff --git a/chrome/locale/en-US/zotero/zotero.dtd b/chrome/locale/en-US/zotero/zotero.dtd index c0c68c64e..b75631bb5 100644 --- a/chrome/locale/en-US/zotero/zotero.dtd +++ b/chrome/locale/en-US/zotero/zotero.dtd @@ -231,6 +231,10 @@ + + + + diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index 680e10f89..fe621bbb9 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -870,6 +870,7 @@ 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. 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 index 4ef509f47..ffe221a2a 100644 --- a/test/tests/integrationTest.js +++ b/test/tests/integrationTest.js @@ -292,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]); @@ -492,6 +495,132 @@ describe("Zotero.Integration", function () { 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() { From f8f403eca49346e447328c5e1d635024107a82ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Tue, 29 Aug 2017 16:01:04 +0300 Subject: [PATCH 13/18] Display a refresh notice instead of bibliography when citing delayed --- chrome/content/zotero/xpcom/integration.js | 12 ++++++++++++ chrome/locale/en-US/zotero/zotero.properties | 1 + 2 files changed, 13 insertions(+) diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index d386fa2dc..2d82a8d04 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -1604,6 +1604,18 @@ Zotero.Integration.Session.prototype.writeDelayedCitation = Zotero.Promise.corou field.code = citation.serialize(); field.text = text; field.writeToDoc(); + + // 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; + } + } + }); diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties index fe621bbb9..e9e3ac3ad 100644 --- a/chrome/locale/en-US/zotero/zotero.properties +++ b/chrome/locale/en-US/zotero/zotero.properties @@ -871,6 +871,7 @@ integration.citationChanged = You have modified this citation since Zotero ge 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" From 2ad0dc00daa945f22041612639abfcb001b96408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Mon, 16 Oct 2017 14:05:23 +0300 Subject: [PATCH 14/18] Add session.getItems in preparation for document collections --- chrome/content/zotero/xpcom/cite.js | 2 +- chrome/content/zotero/xpcom/integration.js | 29 +++++++++++++--------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/chrome/content/zotero/xpcom/cite.js b/chrome/content/zotero/xpcom/cite.js index afcc210cc..0745c4375 100644 --- a/chrome/content/zotero/xpcom/cite.js +++ b/chrome/content/zotero/xpcom/cite.js @@ -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) { diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index 2d82a8d04..fa68d77cd 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -231,6 +231,8 @@ Zotero.Integration = new function() { // 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()); } @@ -304,8 +306,7 @@ Zotero.Integration = new function() { inProgress = Zotero.Integration.currentDoc = - Zotero.Integration.currentWindow = - Zotero.Integration.currentSession = false; + Zotero.Integration.currentWindow = false; } }); }; @@ -906,6 +907,8 @@ Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(fu 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'); }); /** @@ -1263,7 +1266,7 @@ Zotero.Integration.CitationEditInterface.prototype = { return indexB - indexA; }); - return Zotero.Cite.getItem(ids); + return Zotero.Cite.getItem(ids); }), } @@ -1271,10 +1274,8 @@ Zotero.Integration.CitationEditInterface.prototype = { * Keeps track of all session-specific variables */ Zotero.Integration.Session = function(doc, app) { - // holds items not in document that should be in bibliography this.embeddedItems = {}; - this.embeddedZoteroItems = {}; - this.embeddedZoteroItemsByURI = {}; + this.embeddedItemsByURI = {}; this.resetRequest(doc); this.primaryFieldType = app.primaryFieldType; this.secondaryFieldType = app.secondaryFieldType; @@ -1619,6 +1620,11 @@ Zotero.Integration.Session.prototype.writeDelayedCitation = Zotero.Promise.corou }); +Zotero.Integration.Session.prototype.getItems = function() { + return Zotero.Cite.getItem(Object.keys(this.citationsByItemID)); +} + + /** * Edits integration bibliography * @param {Zotero.Integration.Bibliography} bibliography @@ -1903,8 +1909,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 @@ -2316,17 +2322,16 @@ Zotero.Integration.Citation = class { // assign a random string as an item ID var anonymousID = Zotero.randomString(); var globalID = itemData.id = citationItem.id = Zotero.Integration.currentSession.data.sessionID+"/"+anonymousID; - Zotero.Integration.currentSession.embeddedItems[anonymousID] = itemData; // assign a Zotero item - var surrogateItem = Zotero.Integration.currentSession.embeddedZoteroItems[anonymousID] = new 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 Date: Wed, 6 Dec 2017 11:41:39 +0200 Subject: [PATCH 15/18] Simplify field handling Don't use field.writeToDoc(), because it prevents from optimizing libreoffice text writes --- chrome/content/zotero/xpcom/integration.js | 156 +++++++++------------ 1 file changed, 66 insertions(+), 90 deletions(-) diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index fa68d77cd..a72397bda 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -474,7 +474,7 @@ Zotero.Integration.Interface.prototype.addCitation = Zotero.Promise.coroutine(fu yield this._session.init(false, false); let [idx, field, citation] = yield this._session.fields.addEditCitation(null); - yield this._session.addCitation(idx, field.noteIndex, citation); + yield this._session.addCitation(idx, field.getNoteIndex(), citation); if (this._session.data.prefs.delayCitationUpdates) { return this._session.writeDelayedCitation(idx, field, citation); @@ -505,7 +505,7 @@ Zotero.Integration.Interface.prototype.addEditCitation = Zotero.Promise.coroutin 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.noteIndex, citation); + yield this._session.addCitation(idx, field.getNoteIndex(), citation); if (this._session.data.prefs.delayCitationUpdates) { return this._session.writeDelayedCitation(idx, field, citation); } else { @@ -528,7 +528,6 @@ Zotero.Integration.Interface.prototype.addBibliography = Zotero.Promise.coroutin let field = new Zotero.Integration.BibliographyField(yield this._session.fields.addField()); field.clearCode(); - field.writeToDoc(); if(this._session.data.prefs.delayCitationUpdates) { // Refreshes citeproc state before proceeding this._session.reload = true; @@ -594,7 +593,6 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro if (!bibliographyField) { bibliographyField = new Zotero.Integration.BibliographyField(yield this._session.fields.addField()); bibliographyField.clearCode(); - bibliographyField.writeToDoc(); } let bibliography = new Zotero.Integration.Bibliography(bibliographyField); @@ -881,7 +879,7 @@ Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(fu 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.noteIndex, + var noteIndex = field.getNoteIndex(), citation = new Zotero.Integration.Citation(field); let action = yield citation.loadItemData(); @@ -896,7 +894,7 @@ Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(fu yield this._session.addCitation(i, noteIndex, citation); } else if (field.type === INTEGRATION_TYPE_BIBLIOGRAPHY) { - if (this.ignoreEmptyBibliography && field.text.trim() === "") { + if (this.ignoreEmptyBibliography && field.getText().trim() === "") { this._removeCodeFields[i] = true; } else { this._bibliographyFields.push(field); @@ -974,10 +972,11 @@ Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitati var citation = this._session.citationsByIndex[i]; let citationField = citation._field; + var isRich = false; if (!citation.properties.dontUpdate) { var formattedCitation = citation.properties.formattedCitation && citation.properties.custom ? citation.properties.custom : citation.text; - var plainCitation = citation.properties.plainCitation && citationField.text; + var plainCitation = citation.properties.plainCitation && citationField.getText(); // If we're not specifically *not* trying to regen text if (forceCitations != FORCE_CITATIONS_FALSE @@ -1002,30 +1001,25 @@ Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitati } if(!citation.properties.dontUpdate) { - // setText and getText here bypass the setter/getter abstraction - var isRich = formattedCitation.includes('\\'); - if (isRich) { - formattedCitation = '{\\rtf' + formattedCitation + '}'; - } - citationField.setText(formattedCitation, isRich); - citationField.text = formattedCitation; + isRich = citationField.setText(formattedCitation); citation.properties.formattedCitation = formattedCitation; citation.properties.plainCitation = citationField.getText(); - - if (isRich) { - // But we still need writeToDoc to trigger RTF write for LO (see comment in writeToDoc()) - citationField.text = formattedCitation; - } } } } var serializedCitation = citation.serialize(); if (serializedCitation != citation.properties.field) { - citationField.code = serializedCitation; + 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. + citationField.setText(formattedCitation, isRich); + } } - citationField.writeToDoc(); nUpdated++; } @@ -1038,7 +1032,7 @@ Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitati if (forceBibliography || this._session.bibliographyDataHasChanged) { let code = this._session.bibliography.serialize(); for (let field of this._bibliographyFields) { - field.code = code; + field.setCode(code); } } @@ -1075,11 +1069,10 @@ Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitati await Zotero.Promise.delay(); if (bibliographyText) { - field.text = bibliographyText; + field.setText(bibliographyText); } else { - field.text = "{Bibliography}"; + field.setText("{Bibliography}"); } - field.writeToDoc(); nUpdated += 5; } } @@ -1184,11 +1177,6 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f this._session.updateIndices[fieldIndex] = true; // Make sure session updated yield citationsByItemIDPromise; - if (newField) { - // For further processing this field will be properly recognised as citation - // instead of skipped as a temp field. - field.writeToDoc(); - } return [fieldIndex, field, io.citation]; }); @@ -1592,19 +1580,22 @@ Zotero.Integration.Session.prototype.writeDelayedCitation = Zotero.Promise.corou text = `\\uldash{${text}}`; // Make sure we'll prompt for manually edited citations + var isRich = false; if(!citation.properties.dontUpdate) { - // setText and getText here bypass the setter/getter abstraction - text = '{\\rtf' + text + '}'; - field.setText(text, true); - field.text = text; + isRich = field.setText(text); citation.properties.formattedCitation = text; citation.properties.plainCitation = field.getText(); } - field.code = citation.serialize(); - field.text = text; - field.writeToDoc(); + 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); + } // Update bibliography with a static string var fields = yield this.fields.get(); @@ -1964,55 +1955,21 @@ Zotero.Integration.Field = class { this[prop] = field[prop].bind ? field[prop].bind(field) : field[prop]; } } - this.dirty = false; this._field = field; this.type = INTEGRATION_TYPE_TEMP; } - get text() {return this._text = this._text ? this._text : this.getText()} - set text(v) {this._text = v; this.dirty = true} - - get code() {return this._code = this._code ? this._code : this.getCode()} - set code(v) {this._code = v; this.dirty = true} - - get noteIndex() {return this._noteIndex = this._noteIndex ? this._noteIndex : this.getNoteIndex()} - - clearCode() { - this.code = '{}'; - }; - - writeToDoc() { - if (!this.dirty) return; + setCode(code) { // Boo. Inconsistent order. if (this.type == INTEGRATION_TYPE_ITEM) { - this._field.setCode(`ITEM CSL_CITATION ${this.code}`); + this._field.setCode(`ITEM CSL_CITATION ${code}`); } else if (this.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { - this._field.setCode(`BIBL ${this.code} CSL_BIBLIOGRAPHY`); + this._field.setCode(`BIBL ${code} CSL_BIBLIOGRAPHY`); } else { this._field.setCode(`TEMP`); } - - // NB: Setting code in LO removes rtf formatting, so the order here is important - let text = this._text; - if (text) { - let 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); - } + } - this.dirty = false; - // Retrigger retrieval from doc. - this._text = null; - this._code = null; - this._noteIndex = null; - }; - getCode() { let code = this._field.getCode(); let start = code.indexOf('{'); @@ -2020,7 +1977,24 @@ Zotero.Integration.Field = class { 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; + } }; /** @@ -2176,24 +2150,24 @@ Zotero.Integration.CitationField = class extends Zotero.Integration.Field { try { - if (this.code[0] == '{') { // JSON field - return upgradeCruft(unserialize(this.code), this.code); + let code = this.getCode(); + if (code[0] == '{') { // JSON field + return upgradeCruft(unserialize(code), code); } else { // ye olde style field - return unserializePreZotero1_0(this.code); + return unserializePreZotero1_0(code); } } catch (e) { - return this.resolveCorrupt(); + return this.resolveCorrupt(code); } } clearCode() { - this.code = JSON.stringify({citationItems: [], properties: {}}); - this.writeToDoc(); + this.setCode(JSON.stringify({citationItems: [], properties: {}})); } - resolveCorrupt() { + resolveCorrupt(code) { return Zotero.Promise.coroutine(function* () { - Zotero.debug(`Integration: handling corrupt citation field ${this.code}`); + Zotero.debug(`Integration: handling corrupt citation field ${code}`); var msg = Zotero.getString("integration.corruptField")+'\n\n'+ Zotero.getString('integration.corruptField.description'); this.select(); @@ -2228,16 +2202,17 @@ Zotero.Integration.BibliographyField = class extends Zotero.Integration.Field { }; unserialize() { + var code = this.getCode(); try { - return JSON.parse(this.code); + return JSON.parse(code); } catch(e) { - return this.resolveCorrupt(); + return this.resolveCorrupt(code); } } resolveCorrupt() { return Zotero.Promise.coroutine(function* () { - Zotero.debug(`Integration: handling corrupt bibliography field ${this.code}`); + 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.currentDoc.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_OK_CANCEL); @@ -2257,7 +2232,7 @@ Zotero.Integration.Citation = class { this.citationID = data.citationID; this.citationItems = data.citationItems; this.properties = data.properties; - this.properties.noteIndex = citationField.noteIndex; + this.properties.noteIndex = citationField.getNoteIndex(); this._field = citationField; } @@ -2399,14 +2374,15 @@ Zotero.Integration.Citation = class { 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 - && this._field.text !== 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: " + this._field.text + + "Current: " + fieldText ); if (!Zotero.Integration.currentDoc.displayAlert(Zotero.getString("integration.citationChanged.edit"), DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL)) { From 4b78ebcd727aa82fd5fb45ffb5c84cb0cd90a58f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Wed, 6 Dec 2017 12:00:03 +0200 Subject: [PATCH 16/18] Pause the document update timer during warning dialogs --- chrome/content/zotero/xpcom/integration.js | 57 ++++++++++++++++++---- 1 file changed, 47 insertions(+), 10 deletions(-) diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index a72397bda..bbbe0f265 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -741,7 +741,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")); @@ -919,17 +919,19 @@ Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(fu */ Zotero.Integration.Fields.prototype.updateDocument = Zotero.Promise.coroutine(function* (forceCitations, forceBibliography, ignoreCitationChanges) { - var startTime = (new Date()).getTime(); + this._session.timer = new Zotero.Integration.Timer(); + this._session.timer.start(); yield this._session._updateCitations() yield this._updateDocument(forceCitations, forceBibliography, ignoreCitationChanges) - var diff = ((new Date()).getTime() - startTime)/1000; + 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._doc.displayAlert(Zotero.getString('integration.delayCitationUpdates.alert'), + 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; @@ -992,7 +994,7 @@ Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitati + "Current: " + plainCitation ); citationField.select(); - var result = this._doc.displayAlert( + 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) { @@ -1345,6 +1347,17 @@ Zotero.Integration.Session.prototype.init = Zotero.Promise.coroutine(function *( 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; +} + /** * Changes the Session style and data * @param data {Zotero.Integration.DocumentData} @@ -2172,7 +2185,7 @@ Zotero.Integration.CitationField = class extends Zotero.Integration.Field { Zotero.getString('integration.corruptField.description'); this.select(); Zotero.Integration.currentDoc.activate(); - var result = Zotero.Integration.currentDoc.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_YES_NO_CANCEL); + 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 @@ -2215,7 +2228,7 @@ Zotero.Integration.BibliographyField = class extends Zotero.Integration.Field { 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.currentDoc.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_OK_CANCEL); + 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 { @@ -2350,7 +2363,8 @@ Zotero.Integration.Citation = class { msg += '\n\n'+Zotero.getString('integration.missingItem.description'); this._field.select(); Zotero.Integration.currentDoc.activate(); - var result = Zotero.Integration.currentDoc.displayAlert(msg, 1, 3); + 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 @@ -2384,7 +2398,7 @@ Zotero.Integration.Citation = class { + "Original: " + this.properties.plainCitation + "\n" + "Current: " + fieldText ); - if (!Zotero.Integration.currentDoc.displayAlert(Zotero.getString("integration.citationChanged.edit"), + if (!Zotero.Integration.currentSession.displayAlert(Zotero.getString("integration.citationChanged.edit"), DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL)) { throw new Zotero.Exception.UserCancelled("editing citation"); } @@ -2600,4 +2614,27 @@ Zotero.Integration.Bibliography = class { return JSON.stringify(bibliography); } -} \ No newline at end of file +} + +// 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; + } + } +} From b985ef8a53e9d37b38ebb6c2628da4522ae66aff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Tue, 12 Dec 2017 14:21:42 +0200 Subject: [PATCH 17/18] Ensure delayed citation styling is removed upon proper update Ensure delayed citation styling is removed upon proper update --- chrome/content/zotero/xpcom/integration.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index bbbe0f265..5adb00602 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -52,6 +52,8 @@ 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() { @@ -1003,7 +1005,13 @@ Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitati } if(!citation.properties.dontUpdate) { - isRich = citationField.setText(formattedCitation); + // 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 = citationField.getText(); @@ -1590,7 +1598,7 @@ Zotero.Integration.Session.prototype.writeDelayedCitation = Zotero.Promise.corou } catch(e) { throw e; } - text = `\\uldash{${text}}`; + text = `${DELAYED_CITATION_STYLING}{${text}}`; // Make sure we'll prompt for manually edited citations var isRich = false; @@ -2223,7 +2231,7 @@ Zotero.Integration.BibliographyField = class extends Zotero.Integration.Field { } } - resolveCorrupt() { + resolveCorrupt(code) { return Zotero.Promise.coroutine(function* () { Zotero.debug(`Integration: handling corrupt bibliography field ${code}`); var msg = Zotero.getString("integration.corruptBibliography")+'\n\n'+ From 2b27e40308353371969336c8e9fd58bebdff943d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adomas=20Ven=C4=8Dkauskas?= Date: Tue, 16 Jan 2018 13:55:53 +0200 Subject: [PATCH 18/18] Refresh citation text upon citation insertion --- chrome/content/zotero/xpcom/integration.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js index 5adb00602..541a47516 100644 --- a/chrome/content/zotero/xpcom/integration.js +++ b/chrome/content/zotero/xpcom/integration.js @@ -530,12 +530,14 @@ Zotero.Integration.Interface.prototype.addBibliography = Zotero.Promise.coroutin 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(FORCE_CITATIONS_FALSE); - yield this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, true, false); + yield this._session.fields.updateSession(citationsMode); + yield this._session.fields.updateDocument(citationsMode, true, false); }) /** @@ -561,13 +563,15 @@ Zotero.Integration.Interface.prototype.editBibliography = Zotero.Promise.corouti [], "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(FORCE_CITATIONS_FALSE); + yield this._session.fields.updateSession(citationsMode); yield this._session.editBibliography(bibliography); - yield this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, true, false); + yield this._session.fields.updateDocument(citationsMode, true, false); }); @@ -598,13 +602,15 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro } 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(FORCE_CITATIONS_FALSE); + yield this._session.fields.updateSession(citationsMode); if (!newBibliography) yield this._session.editBibliography(bibliography); - yield this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, true, false); + yield this._session.fields.updateDocument(citationsMode, true, false); }); /**