integration.js (101540B)
1 "use strict"; 2 /* 3 ***** BEGIN LICENSE BLOCK ***** 4 5 Copyright © 2009 Center for History and New Media 6 George Mason University, Fairfax, Virginia, USA 7 http://zotero.org 8 9 This file is part of Zotero. 10 11 Zotero is free software: you can redistribute it and/or modify 12 it under the terms of the GNU Affero General Public License as published by 13 the Free Software Foundation, either version 3 of the License, or 14 (at your option) any later version. 15 16 Zotero is distributed in the hope that it will be useful, 17 but WITHOUT ANY WARRANTY; without even the implied warranty of 18 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 GNU Affero General Public License for more details. 20 21 You should have received a copy of the GNU Affero General Public License 22 along with Zotero. If not, see <http://www.gnu.org/licenses/>. 23 24 ***** END LICENSE BLOCK ***** 25 */ 26 27 const DATA_VERSION = 3; 28 29 // Specifies that citations should only be updated if changed 30 const FORCE_CITATIONS_FALSE = 0; 31 // Specifies that citations should only be updated if formattedText has changed from what is encoded 32 // in the field code 33 const FORCE_CITATIONS_REGENERATE = 1; 34 // Specifies that citations should be reset regardless of whether formattedText has changed 35 const FORCE_CITATIONS_RESET_TEXT = 2; 36 37 // These must match the constants in corresponding word plugins 38 const DIALOG_ICON_STOP = 0; 39 const DIALOG_ICON_WARNING = 1; 40 const DIALOG_ICON_CAUTION = 2; 41 42 const DIALOG_BUTTONS_OK = 0; 43 const DIALOG_BUTTONS_OK_CANCEL = 1; 44 const DIALOG_BUTTONS_YES_NO = 2; 45 const DIALOG_BUTTONS_YES_NO_CANCEL = 3; 46 47 const NOTE_FOOTNOTE = 1; 48 const NOTE_ENDNOTE = 2; 49 50 const INTEGRATION_TYPE_ITEM = 1; 51 const INTEGRATION_TYPE_BIBLIOGRAPHY = 2; 52 const INTEGRATION_TYPE_TEMP = 3; 53 54 const DELAY_CITATIONS_PROMPT_TIMEOUT = 15/*seconds*/; 55 56 const DELAYED_CITATION_RTF_STYLING = "\\uldash"; 57 const DELAYED_CITATION_RTF_STYLING_CLEAR = "\\ulclear"; 58 59 const DELAYED_CITATION_HTML_STYLING = "<div class='delayed-zotero-citation-updates'>" 60 const DELAYED_CITATION_HTML_STYLING_END = "</div>" 61 62 63 Zotero.Integration = new function() { 64 Components.utils.import("resource://gre/modules/Services.jsm"); 65 Components.utils.import("resource://gre/modules/AddonManager.jsm"); 66 67 this.currentWindow = false; 68 this.sessions = {}; 69 70 /** 71 * Initializes the pipe used for integration on non-Windows platforms. 72 */ 73 this.init = function() { 74 // We only use an integration pipe on OS X. 75 // On Linux, we use the alternative communication method in the OOo plug-in 76 // On Windows, we use a command line handler for integration. See 77 // components/zotero-integration-service.js for this implementation. 78 if(!Zotero.isMac) return; 79 80 // Determine where to put the pipe 81 // on OS X, first try /Users/Shared for those who can't put pipes in their home 82 // directories 83 var pipe = null; 84 var sharedDir = Components.classes["@mozilla.org/file/local;1"]. 85 createInstance(Components.interfaces.nsILocalFile); 86 sharedDir.initWithPath("/Users/Shared"); 87 88 if(sharedDir.exists() && sharedDir.isDirectory()) { 89 var logname = Components.classes["@mozilla.org/process/environment;1"]. 90 getService(Components.interfaces.nsIEnvironment). 91 get("LOGNAME"); 92 var sharedPipe = sharedDir.clone(); 93 sharedPipe.append(".zoteroIntegrationPipe_"+logname); 94 95 if(sharedPipe.exists()) { 96 if(this.deletePipe(sharedPipe) && sharedDir.isWritable()) { 97 pipe = sharedPipe; 98 } 99 } else if(sharedDir.isWritable()) { 100 pipe = sharedPipe; 101 } 102 } 103 104 if(!pipe) { 105 // on other platforms, or as a fallback, use home directory 106 pipe = Components.classes["@mozilla.org/file/directory_service;1"]. 107 getService(Components.interfaces.nsIProperties). 108 get("Home", Components.interfaces.nsIFile); 109 pipe.append(".zoteroIntegrationPipe"); 110 111 // destroy old pipe, if one exists 112 if(!this.deletePipe(pipe)) return; 113 } 114 115 // try to initialize pipe 116 try { 117 this.initPipe(pipe); 118 } catch(e) { 119 Zotero.logError(e); 120 } 121 } 122 123 /** 124 * Begin listening for integration commands on the given pipe 125 * @param {String} pipe The path to the pipe 126 */ 127 this.initPipe = function(pipe) { 128 Zotero.IPC.Pipe.initPipeListener(pipe, function(string) { 129 if(string != "") { 130 // exec command if possible 131 var parts = string.match(/^([^ \n]*) ([^ \n]*)(?: ([^\n]*))?\n?$/); 132 if(parts) { 133 var agent = parts[1].toString(); 134 var cmd = parts[2].toString(); 135 var document = parts[3] ? parts[3].toString() : null; 136 Zotero.Integration.execCommand(agent, cmd, document); 137 } else { 138 Components.utils.reportError("Zotero: Invalid integration input received: "+string); 139 } 140 } 141 }); 142 } 143 144 /** 145 * Deletes a defunct pipe on OS X 146 */ 147 this.deletePipe = function(pipe) { 148 try { 149 if(pipe.exists()) { 150 Zotero.IPC.safePipeWrite(pipe, "Zotero shutdown\n"); 151 pipe.remove(false); 152 } 153 return true; 154 } catch (e) { 155 // if pipe can't be deleted, log an error 156 Zotero.debug("Error removing old integration pipe "+pipe.path, 1); 157 Zotero.logError(e); 158 Components.utils.reportError( 159 "Zotero word processor integration initialization failed. " 160 + "See http://forums.zotero.org/discussion/12054/#Item_10 " 161 + "for instructions on correcting this problem." 162 ); 163 164 // can attempt to delete on OS X 165 try { 166 var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 167 .getService(Components.interfaces.nsIPromptService); 168 var deletePipe = promptService.confirm(null, Zotero.getString("integration.error.title"), Zotero.getString("integration.error.deletePipe")); 169 if(!deletePipe) return false; 170 let escapedFifoFile = pipe.path.replace("'", "'\\''"); 171 Zotero.Utilities.Internal.executeAppleScript("do shell script \"rmdir '"+escapedFifoFile+"'; rm -f '"+escapedFifoFile+"'\" with administrator privileges", true); 172 if(pipe.exists()) return false; 173 } catch(e) { 174 Zotero.logError(e); 175 return false; 176 } 177 } 178 } 179 180 this.resetSessionStyles = Zotero.Promise.coroutine(function* (){ 181 for (let sessionID in Zotero.Integration.sessions) { 182 let session = Zotero.Integration.sessions[sessionID]; 183 yield session.setData(session.data, true); 184 } 185 }); 186 187 this.getApplication = function(agent, command, docId) { 188 if (agent == 'http') { 189 return new Zotero.HTTPIntegrationClient.Application(); 190 } 191 // Try to load the appropriate Zotero component; otherwise display an error 192 var component 193 try { 194 var componentClass = "@zotero.org/Zotero/integration/application?agent="+agent+";1"; 195 Zotero.debug("Integration: Instantiating "+componentClass+" for command "+command+(docId ? " with doc "+docId : "")); 196 try { 197 return Components.classes[componentClass] 198 .getService(Components.interfaces.zoteroIntegrationApplication); 199 } catch (e) { 200 return Components.classes[componentClass] 201 .getService(Components.interfaces.nsISupports).wrappedJSObject; 202 } 203 } catch(e) { 204 throw new Zotero.Exception.Alert("integration.error.notInstalled", 205 [], "integration.error.title"); 206 } 207 }, 208 209 /** 210 * Executes an integration command, first checking to make sure that versions are compatible 211 */ 212 this.execCommand = async function(agent, command, docId) { 213 var document, session; 214 215 if (Zotero.Integration.currentDoc) { 216 Zotero.Utilities.Internal.activate(); 217 if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) { 218 Zotero.Integration.currentWindow.focus(); 219 } 220 Zotero.debug("Integration: Request already in progress; not executing "+agent+" "+command); 221 return; 222 } 223 Zotero.Integration.currentDoc = true; 224 Zotero.debug(`Integration: ${agent}-${command}${docId ? `:'${docId}'` : ''} invoked`) 225 226 var startTime = (new Date()).getTime(); 227 228 // Try to execute the command; otherwise display an error in alert service or word processor 229 // (depending on what is possible) 230 try { 231 // Word for windows throws RPC_E_CANTCALLOUT_ININPUTSYNCCALL if we invoke an OLE call in the 232 // current event loop (which.. who would have guessed would be the case?) 233 await Zotero.Promise.delay(); 234 var application = Zotero.Integration.getApplication(agent, command, docId); 235 236 var documentPromise = (application.getDocument && docId ? application.getDocument(docId) : application.getActiveDocument()); 237 if (!documentPromise.then) { 238 Zotero.debug('Synchronous integration plugin functions are deprecated -- ' + 239 'update to asynchronous methods'); 240 application = Zotero.Integration.LegacyPluginWrapper(application); 241 documentPromise = new Zotero.Promise(resolve => 242 resolve(Zotero.Integration.LegacyPluginWrapper.wrapDocument(documentPromise))); 243 } 244 Zotero.Integration.currentDoc = document = await documentPromise; 245 246 Zotero.Integration.currentSession = session = await Zotero.Integration.getSession(application, document, agent); 247 // TODO: this is a pretty awful circular dependence 248 session.fields = new Zotero.Integration.Fields(session, document); 249 // TODO: figure this out 250 // Zotero.Notifier.trigger('delete', 'collection', 'document'); 251 await (new Zotero.Integration.Interface(application, document, session))[command](); 252 await document.setDocumentData(session.data.serialize()); 253 } 254 catch (e) { 255 if(!(e instanceof Zotero.Exception.UserCancelled)) { 256 try { 257 var displayError = null; 258 if(e instanceof Zotero.Exception.Alert) { 259 displayError = e.message; 260 } else { 261 if(e.toString().indexOf("ExceptionAlreadyDisplayed") === -1) { 262 displayError = Zotero.getString("integration.error.generic")+"\n\n"+(e.message || e.toString()); 263 } 264 if(e.stack) { 265 Zotero.debug(e.stack); 266 } 267 } 268 269 if(displayError) { 270 var showErrorInFirefox = !document; 271 272 if(document) { 273 try { 274 await document.activate(); 275 await document.displayAlert(displayError, DIALOG_ICON_STOP, DIALOG_BUTTONS_OK); 276 } catch(e) { 277 showErrorInFirefox = true; 278 } 279 } 280 281 if(showErrorInFirefox) { 282 Zotero.Utilities.Internal.activate(); 283 Components.classes["@mozilla.org/embedcomp/prompt-service;1"] 284 .getService(Components.interfaces.nsIPromptService) 285 .alert(null, Zotero.getString("integration.error.title"), displayError); 286 } 287 } 288 } finally { 289 Zotero.logError(e); 290 } 291 } else { 292 // If user cancels we should still write the currently assigned session ID 293 await document.setDocumentData(session.data.serialize()); 294 } 295 } 296 finally { 297 var diff = ((new Date()).getTime() - startTime)/1000; 298 Zotero.debug(`Integration: ${agent}-${command}${docId ? `:'${docId}'` : ''} complete in ${diff}s`) 299 if (document) { 300 try { 301 await document.cleanup(); 302 await document.activate(); 303 304 // Call complete function if one exists 305 if (document.wrappedJSObject && document.wrappedJSObject.complete) { 306 document.wrappedJSObject.complete(); 307 } else if (document.complete) { 308 await document.complete(); 309 } 310 } catch(e) { 311 Zotero.logError(e); 312 } 313 } 314 315 if(Zotero.Integration.currentWindow && !Zotero.Integration.currentWindow.closed) { 316 var oldWindow = Zotero.Integration.currentWindow; 317 Zotero.Promise.delay(100).then(function() { 318 oldWindow.close(); 319 }); 320 } 321 322 if (Zotero.Integration.currentSession && Zotero.Integration.currentSession.progressBar) { 323 Zotero.Promise.delay(5).then(function() { 324 Zotero.Integration.currentSession.progressBar.hide(); 325 }); 326 } 327 // This technically shouldn't be necessary since we call document.activate(), 328 // but http integration plugins may not have OS level access to windows to be 329 // able to activate themselves. E.g. Google Docs on Safari. 330 if (Zotero.isMac && agent == 'http') { 331 Zotero.Utilities.Internal.sendToBack(); 332 } 333 334 Zotero.Integration.currentDoc = Zotero.Integration.currentWindow = false; 335 } 336 }; 337 338 /** 339 * Displays a dialog in a modal-like fashion without hanging the thread 340 * @param {String} url The chrome:// URI of the window 341 * @param {String} [options] Options to pass to the window 342 * @param {String} [io] Data to pass to the window 343 * @return {Promise} Promise resolved when the window is closed 344 */ 345 this.displayDialog = async function displayDialog(url, options, io) { 346 Zotero.debug(`Integration: Displaying dialog ${url}`); 347 await Zotero.Integration.currentDoc.cleanup(); 348 Zotero.Integration.currentSession && await Zotero.Integration.currentSession.progressBar.hide(true); 349 350 var allOptions = 'chrome,centerscreen'; 351 // without this, Firefox gets raised with our windows under Compiz 352 if(Zotero.isLinux) allOptions += ',dialog=no'; 353 if(options) allOptions += ','+options; 354 355 var window = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] 356 .getService(Components.interfaces.nsIWindowWatcher) 357 .openWindow(null, url, '', allOptions, (io ? io : null)); 358 Zotero.Integration.currentWindow = window; 359 Zotero.Utilities.Internal.activate(window); 360 361 var deferred = Zotero.Promise.defer(); 362 var listener = function() { 363 if(window.location.toString() === "about:blank") return; 364 365 if(window.newWindow) { 366 window = window.newWindow; 367 Zotero.Integration.currentWindow = window; 368 window.addEventListener("unload", listener, false); 369 return; 370 } 371 372 Zotero.Integration.currentWindow = false; 373 deferred.resolve(); 374 } 375 window.addEventListener("unload", listener, false); 376 377 await deferred.promise; 378 // We do not want to redisplay the progress bar if this window close 379 // was the final close of the integration command 380 await Zotero.Promise.delay(10); 381 if (Zotero.Integration.currentDoc && Zotero.Integration.currentSession 382 && Zotero.Integration.currentSession.progressBar) { 383 Zotero.Integration.currentSession.progressBar.show(); 384 } 385 }; 386 387 /** 388 * Gets a session for a given doc. 389 * Either loads a cached session if doc communicated since restart or creates a new one 390 * @return {Zotero.Integration.Session} Promise 391 */ 392 this.getSession = async function (app, doc, agent) { 393 try { 394 var progressBar = new Zotero.Integration.Progress(4, Zotero.isMac && agent != 'http'); 395 progressBar.show(); 396 397 var dataString = await doc.getDocumentData(), 398 data, session; 399 400 try { 401 data = new Zotero.Integration.DocumentData(dataString); 402 } catch(e) { 403 data = new Zotero.Integration.DocumentData(); 404 } 405 406 if (data.prefs.fieldType) { 407 if (data.dataVersion < DATA_VERSION) { 408 if (data.dataVersion == 1 409 && data.prefs.fieldType == "Field" 410 && app.primaryFieldType == "ReferenceMark") { 411 // Converted OOo docs use ReferenceMarks, not fields 412 data.prefs.fieldType = "ReferenceMark"; 413 } 414 415 var warning = await doc.displayAlert(Zotero.getString("integration.upgradeWarning", [Zotero.clientName, '5.0']), 416 DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL); 417 if (!warning) { 418 throw new Zotero.Exception.UserCancelled("document upgrade"); 419 } 420 // Don't throw for version 4(JSON) during the transition from 4.0 to 5.0 421 } else if ((data.dataVersion > DATA_VERSION) && data.dataVersion != 4) { 422 throw new Zotero.Exception.Alert("integration.error.newerDocumentVersion", 423 [data.zoteroVersion, Zotero.version], "integration.error.title"); 424 } 425 426 if (data.prefs.fieldType !== app.primaryFieldType 427 && data.prefs.fieldType !== app.secondaryFieldType) { 428 throw new Zotero.Exception.Alert("integration.error.fieldTypeMismatch", 429 [], "integration.error.title"); 430 } 431 432 session = Zotero.Integration.sessions[data.sessionID]; 433 } 434 if (!session) { 435 session = new Zotero.Integration.Session(doc, app); 436 session.reload = true; 437 } 438 try { 439 await session.setData(data); 440 } catch(e) { 441 // make sure style is defined 442 if (e instanceof Zotero.Exception.Alert && e.name === "integration.error.invalidStyle") { 443 if (data.style.styleID) { 444 let trustedSource = /^https?:\/\/(www\.)?(zotero\.org|citationstyles\.org)/.test(data.style.styleID); 445 let errorString = Zotero.getString("integration.error.styleMissing", data.style.styleID); 446 if (trustedSource || 447 await doc.displayAlert(errorString, DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO)) { 448 449 let installed = false; 450 try { 451 await Zotero.Styles.install( 452 {url: data.style.styleID}, data.style.styleID, true 453 ); 454 installed = true; 455 } 456 catch (e) { 457 await doc.displayAlert( 458 Zotero.getString( 459 'integration.error.styleNotFound', data.style.styleID 460 ), 461 DIALOG_ICON_WARNING, 462 DIALOG_BUTTONS_OK 463 ); 464 } 465 if (installed) { 466 await session.setData(data, true); 467 } 468 } 469 } 470 await session.setDocPrefs(); 471 } else { 472 throw e; 473 } 474 } 475 } catch (e) { 476 progressBar.hide(true); 477 throw e; 478 } 479 session.agent = agent; 480 session._doc = doc; 481 if (session.progressBar) { 482 progressBar.reset(); 483 progressBar.segments = session.progressBar.segments; 484 } 485 session.progressBar = progressBar; 486 return session; 487 }; 488 489 } 490 491 /** 492 * An exception thrown when a document contains an item that no longer exists in the current document. 493 */ 494 Zotero.Integration.MissingItemException = function(item) {this.item = item;}; 495 Zotero.Integration.MissingItemException.prototype = { 496 "name":"MissingItemException", 497 "message":`An item in this document is missing from your Zotero library.}`, 498 "toString":function() { return this.message + `\n ${JSON.stringify(this.item)}` } 499 }; 500 501 Zotero.Integration.NO_ACTION = 0; 502 Zotero.Integration.UPDATE = 1; 503 Zotero.Integration.DELETE = 2; 504 Zotero.Integration.REMOVE_CODE = 3; 505 506 /** 507 * All methods for interacting with a document 508 * @constructor 509 */ 510 Zotero.Integration.Interface = function(app, doc, session) { 511 this._app = app; 512 this._doc = doc; 513 this._session = session; 514 } 515 516 /** 517 * Adds a citation to the current document. 518 * @return {Promise} 519 */ 520 Zotero.Integration.Interface.prototype.addCitation = Zotero.Promise.coroutine(function* () { 521 yield this._session.init(false, false); 522 523 let [idx, field, citation] = yield this._session.fields.addEditCitation(null); 524 yield this._session.addCitation(idx, yield field.getNoteIndex(), citation); 525 526 if (this._session.data.prefs.delayCitationUpdates) { 527 return this._session.writeDelayedCitation(idx, field, citation); 528 } else { 529 return this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, false, false); 530 } 531 }); 532 533 /** 534 * Edits the citation at the cursor position. 535 * @return {Promise} 536 */ 537 Zotero.Integration.Interface.prototype.editCitation = Zotero.Promise.coroutine(function* () { 538 yield this._session.init(true, false); 539 var docField = yield this._doc.cursorInField(this._session.data.prefs['fieldType']); 540 if(!docField) { 541 throw new Zotero.Exception.Alert("integration.error.notInCitation", [], 542 "integration.error.title"); 543 } 544 return this.addEditCitation(docField); 545 }); 546 547 /** 548 * Edits the citation at the cursor position if one exists, or else adds a new one. 549 * @return {Promise} 550 */ 551 Zotero.Integration.Interface.prototype.addEditCitation = async function (docField) { 552 await this._session.init(false, false); 553 docField = docField || await this._doc.cursorInField(this._session.data.prefs['fieldType']); 554 555 let [idx, field, citation] = await this._session.fields.addEditCitation(docField); 556 await this._session.addCitation(idx, await field.getNoteIndex(), citation); 557 if (this._session.data.prefs.delayCitationUpdates) { 558 return this._session.writeDelayedCitation(idx, field, citation); 559 } else { 560 return this._session.fields.updateDocument(FORCE_CITATIONS_FALSE, false, false); 561 } 562 }; 563 564 /** 565 * Adds a bibliography to the current document. 566 * @return {Promise} 567 */ 568 Zotero.Integration.Interface.prototype.addBibliography = Zotero.Promise.coroutine(function* () { 569 var me = this; 570 yield this._session.init(true, false); 571 // Make sure we can have a bibliography 572 if(!me._session.data.style.hasBibliography) { 573 throw new Zotero.Exception.Alert("integration.error.noBibliography", [], 574 "integration.error.title"); 575 } 576 577 let field = new Zotero.Integration.BibliographyField(yield this._session.fields.addField()); 578 var citationsMode = FORCE_CITATIONS_FALSE; 579 yield field.clearCode(); 580 if(this._session.data.prefs.delayCitationUpdates) { 581 // Refreshes citeproc state before proceeding 582 this._session.reload = true; 583 citationsMode = FORCE_CITATIONS_REGENERATE; 584 } 585 yield this._session.fields.updateSession(citationsMode); 586 yield this._session.fields.updateDocument(citationsMode, true, false); 587 }) 588 589 /** 590 * Edits bibliography metadata. 591 * @return {Promise} 592 */ 593 Zotero.Integration.Interface.prototype.editBibliography = Zotero.Promise.coroutine(function*() { 594 // Make sure we have a bibliography 595 yield this._session.init(true, false); 596 var fields = yield this._session.fields.get(); 597 598 var bibliographyField; 599 for (let i = fields.length-1; i >= 0; i--) { 600 let field = yield Zotero.Integration.Field.loadExisting(fields[i]); 601 if (field.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { 602 bibliographyField = field; 603 break; 604 } 605 } 606 607 if(!bibliographyField) { 608 throw new Zotero.Exception.Alert("integration.error.mustInsertBibliography", 609 [], "integration.error.title"); 610 } 611 let bibliography = new Zotero.Integration.Bibliography(bibliographyField, yield bibliographyField.unserialize()); 612 var citationsMode = FORCE_CITATIONS_FALSE; 613 if(this._session.data.prefs.delayCitationUpdates) { 614 // Refreshes citeproc state before proceeding 615 this._session.reload = true; 616 citationsMode = FORCE_CITATIONS_REGENERATE; 617 } 618 yield this._session.fields.updateSession(citationsMode); 619 yield this._session.editBibliography(bibliography); 620 yield this._session.fields.updateDocument(citationsMode, true, false); 621 }); 622 623 624 Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coroutine(function *() { 625 // Check if we have a bibliography 626 yield this._session.init(true, false); 627 628 if (!this._session.data.style.hasBibliography) { 629 throw new Zotero.Exception.Alert("integration.error.noBibliography", [], 630 "integration.error.title"); 631 } 632 633 var fields = yield this._session.fields.get(); 634 635 var bibliographyField; 636 for (let i = fields.length-1; i >= 0; i--) { 637 let field = yield Zotero.Integration.Field.loadExisting(fields[i]); 638 if (field.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { 639 bibliographyField = field; 640 break; 641 } 642 } 643 644 var newBibliography = !bibliographyField; 645 if (!bibliographyField) { 646 bibliographyField = new Zotero.Integration.BibliographyField(yield this._session.fields.addField()); 647 yield bibliographyField.clearCode(); 648 } 649 650 let bibliography = new Zotero.Integration.Bibliography(bibliographyField, yield bibliographyField.unserialize()); 651 var citationsMode = FORCE_CITATIONS_FALSE; 652 if(this._session.data.prefs.delayCitationUpdates) { 653 // Refreshes citeproc state before proceeding 654 this._session.reload = true; 655 citationsMode = FORCE_CITATIONS_REGENERATE; 656 } 657 yield this._session.fields.updateSession(citationsMode); 658 if (!newBibliography) yield this._session.editBibliography(bibliography); 659 yield this._session.fields.updateDocument(citationsMode, true, false); 660 }); 661 662 /** 663 * Updates the citation data for all citations and bibliography entries. 664 * @return {Promise} 665 */ 666 Zotero.Integration.Interface.prototype.refresh = async function() { 667 await this._session.init(true, false) 668 669 this._session.reload = this._session.reload || this._session.data.prefs.delayCitationUpdates; 670 await this._session.fields.updateSession(FORCE_CITATIONS_REGENERATE) 671 await this._session.fields.updateDocument(FORCE_CITATIONS_REGENERATE, true, false); 672 } 673 674 /** 675 * Deletes field codes. 676 * @return {Promise} 677 */ 678 Zotero.Integration.Interface.prototype.removeCodes = Zotero.Promise.coroutine(function* () { 679 var me = this; 680 yield this._session.init(true, false) 681 let fields = yield this._session.fields.get() 682 var result = yield me._doc.displayAlert(Zotero.getString("integration.removeCodesWarning"), 683 DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL); 684 if (result) { 685 for(var i=fields.length-1; i>=0; i--) { 686 yield fields[i].removeCode(); 687 } 688 } 689 }) 690 691 /** 692 * Displays a dialog to set document preferences (style, footnotes/endnotes, etc.) 693 * @return {Promise} 694 */ 695 Zotero.Integration.Interface.prototype.setDocPrefs = Zotero.Promise.coroutine(function* () { 696 var oldData; 697 let haveSession = yield this._session.init(false, true); 698 699 if(!haveSession) { 700 // This is a brand new document; don't try to get fields 701 oldData = yield this._session.setDocPrefs(); 702 } else { 703 // Can get fields while dialog is open 704 oldData = yield Zotero.Promise.all([ 705 this._session.fields.get(), 706 this._session.setDocPrefs() 707 ]).spread(function (fields, setDocPrefs) { 708 // Only return value from setDocPrefs 709 return setDocPrefs; 710 }); 711 } 712 713 // If oldData is null, then there was no document data, so we don't need to update 714 // fields 715 if (!oldData) return false; 716 717 // Perform noteType or fieldType conversion 718 let fields = yield this._session.fields.get(); 719 720 var convertBibliographies = oldData.prefs.fieldType != this._session.data.prefs.fieldType; 721 var convertItems = convertBibliographies 722 || oldData.prefs.noteType != this._session.data.prefs.noteType; 723 var fieldsToConvert = new Array(); 724 var fieldNoteTypes = new Array(); 725 for (var i=0, n=fields.length; i<n; i++) { 726 let field = yield Zotero.Integration.Field.loadExisting(fields[i]); 727 728 if (convertItems && field.type === INTEGRATION_TYPE_ITEM) { 729 var citation = yield field.unserialize(); 730 if (!citation.properties.dontUpdate) { 731 fieldsToConvert.push(fields[i]); 732 fieldNoteTypes.push(this._session.data.prefs.noteType); 733 } 734 } else if(convertBibliographies 735 && field.type === INTEGRATION_TYPE_BIBLIOGRAPHY) { 736 fieldsToConvert.push(fields[i]); 737 fieldNoteTypes.push(0); 738 } 739 } 740 741 if(fieldsToConvert.length) { 742 // Pass to conversion function 743 yield this._doc.convert(fieldsToConvert, 744 this._session.data.prefs.fieldType, fieldNoteTypes, 745 fieldNoteTypes.length); 746 } 747 748 // Refresh contents 749 this._session.fields = new Zotero.Integration.Fields(this._session, this._doc); 750 this._session.fields.ignoreEmptyBibliography = false; 751 752 if (this._session.data.prefs.delayCitationUpdates) return; 753 754 yield this._session.fields.updateSession(FORCE_CITATIONS_RESET_TEXT); 755 return this._session.fields.updateDocument(FORCE_CITATIONS_RESET_TEXT, true, true); 756 }); 757 758 /** 759 * An exceedingly simple nsISimpleEnumerator implementation 760 */ 761 Zotero.Integration.JSEnumerator = function(objArray) { 762 this.objArray = objArray; 763 } 764 Zotero.Integration.JSEnumerator.prototype.hasMoreElements = function() { 765 return this.objArray.length; 766 } 767 Zotero.Integration.JSEnumerator.prototype.getNext = function() { 768 return this.objArray.shift(); 769 } 770 771 /** 772 * Methods for retrieving fields from a document 773 * @constructor 774 */ 775 Zotero.Integration.Fields = function(session, doc) { 776 this.ignoreEmptyBibliography = true; 777 778 // Callback called while retrieving fields with the percentage complete. 779 this.progressCallback = null; 780 781 this._session = session; 782 this._doc = doc; 783 784 this._removeCodeFields = {}; 785 this._deleteFields = {}; 786 this._bibliographyFields = []; 787 } 788 789 /** 790 * Checks that it is appropriate to add fields to the current document at the current 791 * positon, then adds one. 792 */ 793 Zotero.Integration.Fields.prototype.addField = async function(note) { 794 // Get citation types if necessary 795 if (!await this._doc.canInsertField(this._session.data.prefs['fieldType'])) { 796 return Zotero.Promise.reject(new Zotero.Exception.Alert("integration.error.cannotInsertHere", 797 [], "integration.error.title")); 798 } 799 800 var field = await this._doc.cursorInField(this._session.data.prefs['fieldType']); 801 if (field) { 802 if (!await this._session.displayAlert(Zotero.getString("integration.replace"), 803 DIALOG_ICON_STOP, 804 DIALOG_BUTTONS_OK_CANCEL)) { 805 return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("inserting citation")); 806 } 807 } 808 809 if (!field) { 810 field = await this._doc.insertField(this._session.data.prefs['fieldType'], 811 (note ? this._session.data.prefs["noteType"] : 0)); 812 // Older doc plugins do not initialize the field code to anything meaningful 813 // so we ensure it here manually 814 field.setCode('TEMP'); 815 } 816 // If fields already retrieved, further this.get() calls will returned the cached version 817 // So we append this field to that list 818 if (this._fields) { 819 this._fields.push(field); 820 } 821 822 return Zotero.Promise.resolve(field); 823 } 824 825 /** 826 * Gets all fields for a document 827 * @return {Promise} Promise resolved with field list. 828 */ 829 Zotero.Integration.Fields.prototype.get = new function() { 830 var deferred; 831 return async function() { 832 // If we already have fields, just return them 833 if(this._fields != undefined) { 834 return this._fields; 835 } 836 837 if (deferred) { 838 return deferred.promise; 839 } 840 deferred = Zotero.Promise.defer(); 841 var promise = deferred.promise; 842 843 // Otherwise, start getting fields 844 var timer = new Zotero.Integration.Timer(); 845 timer.start(); 846 this._session.progressBar.start(); 847 try { 848 var fields = this._fields = Array.from(await this._doc.getFields(this._session.data.prefs['fieldType'])); 849 850 var retrieveTime = timer.stop(); 851 this._session.progressBar.finishSegment(); 852 Zotero.debug("Integration: Retrieved " + fields.length + " fields in " + 853 retrieveTime + "; " + fields.length/retrieveTime + " fields/second"); 854 deferred.resolve(fields); 855 } catch(e) { 856 deferred.reject(e); 857 } 858 859 deferred = null; 860 return promise; 861 } 862 } 863 864 /** 865 * Updates Zotero.Integration.Session attached to Zotero.Integration.Fields in line with document 866 */ 867 Zotero.Integration.Fields.prototype.updateSession = Zotero.Promise.coroutine(function* (forceCitations) { 868 yield this.get(); 869 this._session.resetRequest(this._doc); 870 871 this._removeCodeFields = {}; 872 this._deleteFields = {}; 873 this._bibliographyFields = []; 874 875 var timer = new Zotero.Integration.Timer(); 876 timer.start(); 877 this._session.progressBar.start(); 878 if (forceCitations) { 879 this._session.regenAll = true; 880 } 881 yield this._processFields(); 882 this._session.regenAll = false; 883 884 var updateTime = timer.stop(); 885 this._session.progressBar.finishSegment(); 886 Zotero.debug("Integration: Updated session data for " + this._fields.length + " fields in " 887 + updateTime + "; " + this._fields.length/updateTime + " fields/second"); 888 889 if (this._session.reload) { 890 this._session.restoreProcessorState(); 891 delete this._session.reload; 892 } 893 }); 894 895 /** 896 * Keep processing fields until all have been processed 897 */ 898 Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(function* () { 899 if (!this._fields) { 900 throw new Error("_processFields called without fetching fields first"); 901 } 902 903 for (var i = 0; i < this._fields.length; i++) { 904 let field = yield Zotero.Integration.Field.loadExisting(this._fields[i]); 905 if (field.type === INTEGRATION_TYPE_ITEM) { 906 var noteIndex = yield field.getNoteIndex(), 907 data = yield field.unserialize(), 908 citation = new Zotero.Integration.Citation(field, data, noteIndex); 909 910 yield this._session.addCitation(i, noteIndex, citation); 911 } else if (field.type === INTEGRATION_TYPE_BIBLIOGRAPHY) { 912 if (this.ignoreEmptyBibliography && (yield field.getText()).trim() === "") { 913 this._removeCodeFields[i] = true; 914 } else { 915 this._bibliographyFields.push(field); 916 } 917 } 918 } 919 if (this._bibliographyFields.length) { 920 var data = yield this._bibliographyFields[0].unserialize() 921 this._session.bibliography = new Zotero.Integration.Bibliography(this._bibliographyFields[0], data); 922 yield this._session.bibliography.loadItemData(); 923 } else { 924 delete this._session.bibliography; 925 } 926 // TODO: figure this out 927 // Zotero.Notifier.trigger('add', 'collection', 'document'); 928 }); 929 930 /** 931 * Updates bibliographies and fields within a document 932 * @param {Boolean} forceCitations Whether to regenerate all citations 933 * @param {Boolean} forceBibliography Whether to regenerate all bibliography entries 934 * @param {Boolean} [ignoreCitationChanges] Whether to ignore changes to citations that have been 935 * modified since they were created, instead of showing a warning 936 * @return {Promise} A promise resolved when the document is updated 937 */ 938 Zotero.Integration.Fields.prototype.updateDocument = Zotero.Promise.coroutine(function* (forceCitations, forceBibliography, 939 ignoreCitationChanges) { 940 this._session.timer = new Zotero.Integration.Timer(); 941 this._session.timer.start(); 942 943 this._session.progressBar.start(); 944 yield this._session._updateCitations() 945 this._session.progressBar.finishSegment(); 946 this._session.progressBar.start(); 947 yield this._updateDocument(forceCitations, forceBibliography, ignoreCitationChanges) 948 this._session.progressBar.finishSegment(); 949 950 var diff = this._session.timer.stop(); 951 this._session.timer = null; 952 Zotero.debug(`Integration: updateDocument complete in ${diff}s`) 953 // If the update takes longer than 5s suggest delaying citation updates 954 if (diff > DELAY_CITATIONS_PROMPT_TIMEOUT && !this._session.data.prefs.dontAskDelayCitationUpdates && !this._session.data.prefs.delayCitationUpdates) { 955 yield this._doc.activate(); 956 957 var interfaceType = 'tab'; 958 if (['MacWord2008', 'OpenOffice'].includes(this._session.agent)) { 959 interfaceType = 'toolbar'; 960 } 961 962 var result = yield this._session.displayAlert( 963 Zotero.getString('integration.delayCitationUpdates.alert.text1') 964 + "\n\n" 965 + Zotero.getString(`integration.delayCitationUpdates.alert.text2.${interfaceType}`) 966 + "\n\n" 967 + Zotero.getString('integration.delayCitationUpdates.alert.text3'), 968 DIALOG_ICON_WARNING, 969 DIALOG_BUTTONS_YES_NO_CANCEL 970 ); 971 if (result == 2) { 972 this._session.data.prefs.delayCitationUpdates = true; 973 } 974 if (result) { 975 this._session.data.prefs.dontAskDelayCitationUpdates = true; 976 // yield this._session.setDocPrefs(true); 977 } 978 } 979 }); 980 981 /** 982 * Helper function to update bibliographys and fields within a document 983 * @param {Boolean} forceCitations Whether to regenerate all citations 984 * @param {Boolean} forceBibliography Whether to regenerate all bibliography entries 985 * @param {Boolean} [ignoreCitationChanges] Whether to ignore changes to citations that have been 986 * modified since they were created, instead of showing a warning 987 */ 988 Zotero.Integration.Fields.prototype._updateDocument = async function(forceCitations, forceBibliography, 989 ignoreCitationChanges) { 990 if(this.progressCallback) { 991 var nFieldUpdates = Object.keys(this._session.updateIndices).length; 992 if(this._session.bibliographyHasChanged || forceBibliography) { 993 nFieldUpdates += this._bibliographyFields.length*5; 994 } 995 } 996 997 var nUpdated=0; 998 for(var i in this._session.updateIndices) { 999 if(this.progressCallback && nUpdated % 10 == 0) { 1000 try { 1001 this.progressCallback(75+(nUpdated/nFieldUpdates)*25); 1002 } catch(e) { 1003 Zotero.logError(e); 1004 } 1005 } 1006 // Jump to next event loop step for UI updates 1007 await Zotero.Promise.delay(); 1008 1009 var citation = this._session.citationsByIndex[i]; 1010 let citationField = citation._field; 1011 1012 var isRich = false; 1013 if (!citation.properties.dontUpdate) { 1014 var formattedCitation = citation.properties.custom 1015 ? citation.properties.custom : citation.text; 1016 var plainCitation = citation.properties.plainCitation && await citationField.getText(); 1017 var plaintextChanged = citation.properties.plainCitation 1018 && plainCitation !== citation.properties.plainCitation; 1019 1020 if (!ignoreCitationChanges && plaintextChanged) { 1021 // Citation manually modified; ask user if they want to save changes 1022 Zotero.debug("[_updateDocument] Attempting to update manually modified citation.\n" 1023 + "Original: " + citation.properties.plainCitation + "\n" 1024 + "Current: " + plainCitation 1025 ); 1026 await citationField.select(); 1027 var result = await this._session.displayAlert( 1028 Zotero.getString("integration.citationChanged")+"\n\n" 1029 + Zotero.getString("integration.citationChanged.description")+"\n\n" 1030 + Zotero.getString("integration.citationChanged.original", citation.properties.plainCitation)+"\n" 1031 + Zotero.getString("integration.citationChanged.modified", plainCitation)+"\n", 1032 DIALOG_ICON_CAUTION, DIALOG_BUTTONS_YES_NO); 1033 if (result) { 1034 citation.properties.dontUpdate = true; 1035 } 1036 } 1037 1038 // Update citation text: 1039 // If we're looking to reset the text even if it matches previous text 1040 if (forceCitations == FORCE_CITATIONS_RESET_TEXT 1041 // Or metadata has changed thus changing the formatted citation 1042 || (citation.properties.formattedCitation !== formattedCitation) 1043 // Or plaintext has changed and user does not want to keep the change 1044 || (plaintextChanged && !citation.properties.dontUpdate)) { 1045 1046 1047 // Word will preserve previous text styling, so we need to force remove it 1048 // for citations that were inserted with delay styling 1049 var wasDelayed = citation.properties.formattedCitation 1050 && citation.properties.formattedCitation.includes(DELAYED_CITATION_RTF_STYLING); 1051 if (this._session.outputFormat == 'rtf' && wasDelayed) { 1052 isRich = await citationField.setText(`${DELAYED_CITATION_RTF_STYLING_CLEAR}{${formattedCitation}}`); 1053 } else { 1054 isRich = await citationField.setText(formattedCitation); 1055 } 1056 1057 citation.properties.formattedCitation = formattedCitation; 1058 citation.properties.plainCitation = await citationField.getText(); 1059 } 1060 } 1061 1062 var serializedCitation = citation.serialize(); 1063 if (serializedCitation != citation.properties.field) { 1064 await citationField.setCode(serializedCitation); 1065 } 1066 nUpdated++; 1067 } 1068 1069 // update bibliographies 1070 if (this._session.bibliography // if bibliography exists 1071 && (this._session.bibliographyHasChanged // and bibliography changed 1072 || forceBibliography)) { // or if we should generate regardless of 1073 // changes 1074 1075 if (forceBibliography || this._session.bibliographyDataHasChanged) { 1076 let code = this._session.bibliography.serialize(); 1077 for (let field of this._bibliographyFields) { 1078 await field.setCode(code); 1079 } 1080 } 1081 1082 // get bibliography and format as RTF 1083 var bib = this._session.bibliography.getCiteprocBibliography(this._session.style); 1084 1085 var bibliographyText = ""; 1086 if (bib) { 1087 if (this._session.outputFormat == 'rtf') { 1088 bibliographyText = bib[0].bibstart+bib[1].join("\\\r\n")+"\\\r\n"+bib[0].bibend; 1089 } else { 1090 bibliographyText = bib[0].bibstart+bib[1].join("")+bib[0].bibend; 1091 } 1092 1093 // if bibliography style not set, set it 1094 if(!this._session.data.style.bibliographyStyleHasBeenSet) { 1095 var bibStyle = Zotero.Cite.getBibliographyFormatParameters(bib); 1096 1097 // set bibliography style 1098 await this._doc.setBibliographyStyle(bibStyle.firstLineIndent, bibStyle.indent, 1099 bibStyle.lineSpacing, bibStyle.entrySpacing, bibStyle.tabStops, bibStyle.tabStops.length); 1100 1101 // set bibliographyStyleHasBeenSet parameter to prevent further changes 1102 this._session.data.style.bibliographyStyleHasBeenSet = true; 1103 } 1104 } 1105 1106 // set bibliography text 1107 for (let field of this._bibliographyFields) { 1108 if(this.progressCallback) { 1109 try { 1110 this.progressCallback(75+(nUpdated/nFieldUpdates)*25); 1111 } catch(e) { 1112 Zotero.logError(e); 1113 } 1114 } 1115 // Jump to next event loop step for UI updates 1116 await Zotero.Promise.delay(); 1117 1118 if (bibliographyText) { 1119 await field.setText(bibliographyText); 1120 } else { 1121 await field.setText("{Bibliography}"); 1122 } 1123 nUpdated += 5; 1124 } 1125 } 1126 1127 // Do these operations in reverse in case plug-ins care about order 1128 var removeCodeFields = Object.keys(this._removeCodeFields).sort(); 1129 for (var i=(removeCodeFields.length-1); i>=0; i--) { 1130 await this._fields[removeCodeFields[i]].removeCode(); 1131 } 1132 1133 var deleteFields = Object.keys(this._deleteFields).sort(); 1134 for (var i=(deleteFields.length-1); i>=0; i--) { 1135 this._fields[deleteFields[i]].delete(); 1136 } 1137 } 1138 1139 /** 1140 * Brings up the addCitationDialog, prepopulated if a citation is provided 1141 */ 1142 Zotero.Integration.Fields.prototype.addEditCitation = async function (field) { 1143 var newField; 1144 var citation; 1145 1146 if (field) { 1147 field = await Zotero.Integration.Field.loadExisting(field); 1148 1149 if (field.type != INTEGRATION_TYPE_ITEM) { 1150 throw new Zotero.Exception.Alert("integration.error.notInCitation"); 1151 } 1152 citation = new Zotero.Integration.Citation(field, await field.unserialize(), await field.getNoteIndex()); 1153 } else { 1154 newField = true; 1155 field = new Zotero.Integration.CitationField(await this.addField(true)); 1156 citation = new Zotero.Integration.Citation(field); 1157 } 1158 1159 await citation.prepareForEditing(); 1160 1161 // ------------------- 1162 // Preparing stuff to pass into CitationEditInterface 1163 var fieldIndexPromise = this.get().then(async function(fields) { 1164 for (var i=0, n=fields.length; i<n; i++) { 1165 if (await fields[i].equals(field._field)) { 1166 // This is needed, because LibreOffice integration plugin caches the field code instead of asking 1167 // the document every time when calling #getCode(). 1168 field = new Zotero.Integration.CitationField(fields[i]); 1169 return i; 1170 } 1171 } 1172 }); 1173 1174 var citationsByItemIDPromise; 1175 if (this._session.data.prefs.delayCitationUpdates) { 1176 citationsByItemIDPromise = Zotero.Promise.resolve(this._session.citationsByItemID); 1177 } else { 1178 citationsByItemIDPromise = this.updateSession(FORCE_CITATIONS_FALSE).then(function() { 1179 return this._session.citationsByItemID; 1180 }.bind(this)); 1181 } 1182 1183 var previewFn = async function (citation) { 1184 let idx = await fieldIndexPromise; 1185 await citationsByItemIDPromise; 1186 var fields = await this.get(); 1187 1188 var [citations, fieldToCitationIdxMapping, citationToFieldIdxMapping] = this._session.getCiteprocLists(true); 1189 for (var prevIdx = idx-1; prevIdx >= 0; prevIdx--) { 1190 if (prevIdx in fieldToCitationIdxMapping) break; 1191 } 1192 for (var nextIdx = idx+1; nextIdx < fields.length; nextIdx++) { 1193 if (nextIdx in fieldToCitationIdxMapping) break; 1194 } 1195 let citationsPre = citations.slice(0, fieldToCitationIdxMapping[prevIdx]+1); 1196 let citationsPost = citations.slice(fieldToCitationIdxMapping[nextIdx]); 1197 let citationID = citation.citationID; 1198 try { 1199 var result = this._session.style.previewCitationCluster(citation, citationsPre, citationsPost, "rtf"); 1200 } catch(e) { 1201 throw e; 1202 } finally { 1203 // CSL.previewCitationCluster() sets citationID, which means that we do not mark it 1204 // as a new citation in Session.addCitation() if the ID is still present 1205 citation.citationID = citationID; 1206 } 1207 return result; 1208 }.bind(this); 1209 1210 var io = new Zotero.Integration.CitationEditInterface( 1211 citation, this._session.style.opt.sort_citations, 1212 fieldIndexPromise, citationsByItemIDPromise, previewFn 1213 ); 1214 1215 if (Zotero.Prefs.get("integration.useClassicAddCitationDialog")) { 1216 Zotero.Integration.displayDialog('chrome://zotero/content/integration/addCitationDialog.xul', 1217 'alwaysRaised,resizable', io); 1218 } else { 1219 var mode = (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised') 1220 ? 'popup' : 'alwaysRaised')+',resizable=false'; 1221 Zotero.Integration.displayDialog('chrome://zotero/content/integration/quickFormat.xul', 1222 mode, io); 1223 } 1224 1225 // ------------------- 1226 // io.promise resolves when the citation dialog is closed 1227 this.progressCallback = await io.promise; 1228 1229 if (!io.citation.citationItems.length) { 1230 // Try to delete new field on cancel 1231 if (newField) { 1232 try { 1233 await field.delete(); 1234 } catch(e) {} 1235 } 1236 throw new Zotero.Exception.UserCancelled("inserting citation"); 1237 } 1238 1239 var fieldIndex = await fieldIndexPromise; 1240 this._session.updateIndices[fieldIndex] = true; 1241 // Make sure session is updated 1242 await citationsByItemIDPromise; 1243 return [fieldIndex, field, io.citation]; 1244 }; 1245 1246 /** 1247 * Citation editing functions and propertiesaccessible to quickFormat.js and addCitationDialog.js 1248 */ 1249 Zotero.Integration.CitationEditInterface = function(citation, sortable, fieldIndexPromise, citationsByItemIDPromise, previewFn) { 1250 this.citation = citation; 1251 this.sortable = sortable; 1252 this.previewFn = previewFn; 1253 this._fieldIndexPromise = fieldIndexPromise; 1254 this._citationsByItemIDPromise = citationsByItemIDPromise; 1255 1256 // Not available in quickFormat.js if this unspecified 1257 this.wrappedJSObject = this; 1258 1259 this._acceptDeferred = Zotero.Promise.defer(); 1260 this.promise = this._acceptDeferred.promise; 1261 } 1262 1263 Zotero.Integration.CitationEditInterface.prototype = { 1264 /** 1265 * Execute a callback with a preview of the given citation 1266 * @return {Promise} A promise resolved with the previewed citation string 1267 */ 1268 preview: function() { 1269 return this.previewFn(this.citation); 1270 }, 1271 1272 /** 1273 * Sort the citationItems within citation (depends on this.citation.properties.unsorted) 1274 * @return {Promise} A promise resolved with the previewed citation string 1275 */ 1276 sort: function() { 1277 return this.preview(); 1278 }, 1279 1280 /** 1281 * Accept changes to the citation 1282 * @param {Function} [progressCallback] A callback to be run when progress has changed. 1283 * Receives a number from 0 to 100 indicating current status. 1284 */ 1285 accept: function(progressCallback) { 1286 if (!this._acceptDeferred.promise.isFulfilled()) { 1287 this._acceptDeferred.resolve(progressCallback); 1288 } 1289 }, 1290 1291 /** 1292 * Get a list of items used in the current document 1293 * @return {Promise} A promise resolved by the items 1294 */ 1295 getItems: async function () { 1296 var fieldIndex = await this._fieldIndexPromise; 1297 var citationsByItemID = await this._citationsByItemIDPromise; 1298 var ids = Object.keys(citationsByItemID).filter(itemID => { 1299 return citationsByItemID[itemID] 1300 && citationsByItemID[itemID].length 1301 // Exclude the present item 1302 && (citationsByItemID[itemID].length > 1 1303 || citationsByItemID[itemID][0].properties.zoteroIndex !== fieldIndex); 1304 }); 1305 1306 // Sort all previously cited items at top, and all items cited later at bottom 1307 ids.sort(function(a, b) { 1308 var indexA = citationsByItemID[a][0].properties.zoteroIndex, 1309 indexB = citationsByItemID[b][0].properties.zoteroIndex; 1310 1311 if (indexA >= fieldIndex){ 1312 if(indexB < fieldIndex) return 1; 1313 return indexA - indexB; 1314 } 1315 1316 if (indexB > fieldIndex) return -1; 1317 return indexB - indexA; 1318 }); 1319 1320 return Zotero.Cite.getItem(ids); 1321 }, 1322 } 1323 1324 /** 1325 * Keeps track of all session-specific variables 1326 */ 1327 Zotero.Integration.Session = function(doc, app) { 1328 this.embeddedItems = {}; 1329 this.embeddedZoteroItems = {}; 1330 this.embeddedItemsByURI = {}; 1331 this.citationsByIndex = {}; 1332 this.resetRequest(doc); 1333 this.primaryFieldType = app.primaryFieldType; 1334 this.secondaryFieldType = app.secondaryFieldType; 1335 this.outputFormat = app.outputFormat || 'rtf'; 1336 this._app = app; 1337 1338 this.sessionID = Zotero.randomString(); 1339 Zotero.Integration.sessions[this.sessionID] = this; 1340 } 1341 1342 /** 1343 * Resets per-request variables in the CitationSet 1344 */ 1345 Zotero.Integration.Session.prototype.resetRequest = function(doc) { 1346 this.uriMap = new Zotero.Integration.URIMap(this); 1347 1348 this.bibliographyHasChanged = false; 1349 this.bibliographyDataHasChanged = false; 1350 // After adding fields to the session 1351 // citations that are new to the document will be marked 1352 // as new, so that they are correctly loaded into and processed with citeproc 1353 this.newIndices = {}; 1354 // After the processing of new indices with citeproc, some 1355 // citations require additional work (because of disambiguation, numbering changes, etc) 1356 // and will be marked for an additional reprocessing with citeproc to retrieve updated text 1357 this.updateIndices = {}; 1358 1359 // When processing citations this list will be checked for citations that are new to the document 1360 // (i.e. copied from somewhere else) and marked as newIndices to be processed with citeproc if 1361 // not present 1362 this.oldCitations = new Set(); 1363 for (let i in this.citationsByIndex) { 1364 this.oldCitations.add(this.citationsByIndex[i].citationID); 1365 } 1366 this.citationsByItemID = {}; 1367 this.citationsByIndex = {}; 1368 this.documentCitationIDs = {}; 1369 1370 this._doc = doc; 1371 } 1372 1373 /** 1374 * Prepares session data and displays docPrefs dialog if needed 1375 * @param require {Boolean} Whether an error should be thrown if no preferences or fields 1376 * exist (otherwise, the set doc prefs dialog is shown) 1377 * @param dontRunSetDocPrefs {Boolean} Whether to show the Document Preferences window if no preferences exist 1378 * @return {Promise{Boolean}} true if session ready to, false if preferences dialog needs to be displayed first 1379 */ 1380 Zotero.Integration.Session.prototype.init = Zotero.Promise.coroutine(function *(require, dontRunSetDocPrefs) { 1381 var data = this.data; 1382 var haveFields = false; 1383 1384 // If prefs exist 1385 if (require && data.prefs.fieldType) { 1386 // check to see if fields already exist 1387 for (let fieldType of [this.primaryFieldType, this.secondaryFieldType]) { 1388 var fields = yield this._doc.getFields(fieldType); 1389 if (fields.length) { 1390 data.prefs.fieldType = fieldType; 1391 haveFields = true; 1392 break; 1393 } 1394 } 1395 } 1396 1397 if (require && (!haveFields || !data.prefs.fieldType)) { 1398 // If required but no fields and preferences exist throw an error 1399 return Zotero.Promise.reject(new Zotero.Exception.Alert( 1400 "integration.error.mustInsertCitation", 1401 [], "integration.error.title")); 1402 } else if (!data.prefs.fieldType) { 1403 Zotero.debug("Integration: No document preferences found, but found "+data.prefs.fieldType+" fields"); 1404 // Unless explicitly disabled 1405 if (dontRunSetDocPrefs) return false; 1406 1407 // Show the doc prefs dialogue 1408 yield this.setDocPrefs(); 1409 } 1410 1411 return true; 1412 }); 1413 1414 Zotero.Integration.Session.prototype.displayAlert = async function() { 1415 if (this.timer) { 1416 this.timer.pause(); 1417 } 1418 var result = await this._doc.displayAlert.apply(this._doc, arguments); 1419 if (this.timer) { 1420 this.timer.resume(); 1421 } 1422 return result; 1423 } 1424 1425 /** 1426 * Changes the Session style and data 1427 * @param data {Zotero.Integration.DocumentData} 1428 * @param resetStyle {Boolean} Whether to force the style to be reset 1429 * regardless of whether it has changed. This is desirable if the 1430 * automaticJournalAbbreviations or locale has changed. 1431 */ 1432 Zotero.Integration.Session.prototype.setData = async function (data, resetStyle) { 1433 var oldStyle = (this.data && this.data.style ? this.data.style : false); 1434 this.data = data; 1435 this.data.sessionID = this.sessionID; 1436 if (data.style.styleID && (!oldStyle || oldStyle.styleID != data.style.styleID || resetStyle)) { 1437 try { 1438 await Zotero.Styles.init(); 1439 var getStyle = Zotero.Styles.get(data.style.styleID); 1440 data.style.hasBibliography = getStyle.hasBibliography; 1441 this.style = getStyle.getCiteProc(data.style.locale, data.prefs.automaticJournalAbbreviations); 1442 this.style.setOutputFormat(this.outputFormat); 1443 this.styleClass = getStyle.class; 1444 // We're changing the citeproc instance, so we'll have to reinsert all citations into the registry 1445 this.reload = true; 1446 this.styleID = data.style.styleID; 1447 } catch (e) { 1448 Zotero.logError(e); 1449 throw new Zotero.Exception.Alert("integration.error.invalidStyle"); 1450 } 1451 1452 return true; 1453 } else if (oldStyle) { 1454 data.style = oldStyle; 1455 } 1456 return false; 1457 }; 1458 1459 /** 1460 * Displays a dialog to set document preferences 1461 * @return {Promise} A promise resolved with old document data, if there was any or null, 1462 * if there wasn't, or rejected with Zotero.Exception.UserCancelled if the dialog was 1463 * cancelled. 1464 */ 1465 Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(function* (highlightDelayCitations=false) { 1466 var io = new function() { this.wrappedJSObject = this; }; 1467 io.primaryFieldType = this.primaryFieldType; 1468 io.secondaryFieldType = this.secondaryFieldType; 1469 1470 if (this.data) { 1471 io.style = this.data.style.styleID; 1472 io.locale = this.data.style.locale; 1473 io.supportedNotes = this._app.supportedNotes; 1474 io.useEndnotes = this.data.prefs.noteType == 0 ? 0 : this.data.prefs.noteType-1; 1475 io.fieldType = this.data.prefs.fieldType; 1476 io.delayCitationUpdates = this.data.prefs.delayCitationUpdates; 1477 io.dontAskDelayCitationUpdates = this.data.prefs.dontAskDelayCitationUpdates; 1478 io.highlightDelayCitations = highlightDelayCitations; 1479 io.automaticJournalAbbreviations = this.data.prefs.automaticJournalAbbreviations; 1480 io.requireStoreReferences = !Zotero.Utilities.isEmpty(this.embeddedItems); 1481 } 1482 1483 // Make sure styles are initialized for new docs 1484 yield Zotero.Styles.init(); 1485 yield Zotero.Integration.displayDialog('chrome://zotero/content/integration/integrationDocPrefs.xul', '', io); 1486 1487 if (!io.style || !io.fieldType) { 1488 throw new Zotero.Exception.UserCancelled("document preferences window"); 1489 } 1490 1491 // set data 1492 var oldData = this.data; 1493 var data = new Zotero.Integration.DocumentData(); 1494 data.dataVersion = oldData.dataVersion; 1495 data.sessionID = oldData.sessionID; 1496 data.style.styleID = io.style; 1497 data.style.locale = io.locale; 1498 data.style.bibliographyStyleHasBeenSet = false; 1499 data.prefs = oldData ? Object.assign({}, oldData.prefs) : {}; 1500 data.prefs.fieldType = io.fieldType; 1501 data.prefs.automaticJournalAbbreviations = io.automaticJournalAbbreviations; 1502 data.prefs.delayCitationUpdates = io.delayCitationUpdates 1503 1504 var forceStyleReset = oldData 1505 && ( 1506 oldData.prefs.automaticJournalAbbreviations != data.prefs.automaticJournalAbbreviations 1507 || oldData.style.locale != io.locale 1508 ); 1509 yield this.setData(data, forceStyleReset); 1510 1511 // need to do this after setting the data so that we know if it's a note style 1512 this.data.prefs.noteType = this.style && this.styleClass == "note" ? io.useEndnotes+1 : 0; 1513 1514 if (!oldData || oldData.style.styleID != data.style.styleID 1515 || oldData.prefs.noteType != data.prefs.noteType 1516 || oldData.prefs.fieldType != data.prefs.fieldType 1517 || (!data.prefs.delayCitationUpdates && oldData.prefs.delayCitationUpdates != data.prefs.delayCitationUpdates) 1518 || oldData.prefs.automaticJournalAbbreviations != data.prefs.automaticJournalAbbreviations) { 1519 // This will cause us to regenerate all citations 1520 this.regenAll = true; 1521 this.reload = true; 1522 } 1523 1524 return oldData || null; 1525 }) 1526 1527 /** 1528 * Adds a citation based on a serialized Word field 1529 */ 1530 Zotero.Integration._oldCitationLocatorMap = { 1531 p:"page", 1532 g:"paragraph", 1533 l:"line" 1534 }; 1535 1536 /** 1537 * Adds a citation to the arrays representing the document 1538 */ 1539 Zotero.Integration.Session.prototype.addCitation = Zotero.Promise.coroutine(function* (index, noteIndex, citation) { 1540 var index = parseInt(index, 10); 1541 1542 var action = yield citation.loadItemData(); 1543 1544 if (action == Zotero.Integration.REMOVE_CODE) { 1545 // Mark for removal and return 1546 this.fields._removeCodeFields[index] = true; 1547 return; 1548 } else if (action == Zotero.Integration.DELETE) { 1549 // Mark for deletion and return 1550 this.fields._deleteFields[index] = true; 1551 return; 1552 } else if (action == Zotero.Integration.UPDATE) { 1553 this.updateIndices[index] = true; 1554 } 1555 // All new fields will initially be marked for deletion because they contain no 1556 // citationItems 1557 delete this.fields._deleteFields[index]; 1558 1559 citation.properties.added = true; 1560 citation.properties.zoteroIndex = index; 1561 citation.properties.noteIndex = noteIndex; 1562 this.citationsByIndex[index] = citation; 1563 1564 // add to citationsByItemID and citationsByIndex 1565 for(var i=0; i<citation.citationItems.length; i++) { 1566 var citationItem = citation.citationItems[i]; 1567 if(!this.citationsByItemID[citationItem.id]) { 1568 this.citationsByItemID[citationItem.id] = [citation]; 1569 this.bibliographyHasChanged = true; 1570 } else { 1571 var byItemID = this.citationsByItemID[citationItem.id]; 1572 if(byItemID[byItemID.length-1].properties.zoteroIndex < index) { 1573 // if index is greater than the last index, add to end 1574 byItemID.push(citation); 1575 } else { 1576 // otherwise, splice in at appropriate location 1577 for(var j=0; byItemID[j].properties.zoteroIndex < index && j<byItemID.length-1; j++) {} 1578 byItemID.splice(j++, 0, citation); 1579 } 1580 } 1581 } 1582 1583 // We need a new ID if there's another citation with the same citation ID in this document 1584 var duplicateIndex = this.documentCitationIDs[citation.citationID]; 1585 var needNewID = !citation.citationID || duplicateIndex != undefined; 1586 if(needNewID) { 1587 if (duplicateIndex != undefined) { 1588 // If this is a duplicate, we need to mark both citations as "new" 1589 // since we do not know which one was the "original" one 1590 // and either one may need to be updated 1591 this.newIndices[duplicateIndex] = true; 1592 } 1593 if(needNewID) { 1594 Zotero.debug("Integration: "+citation.citationID+" ("+index+") needs new citationID"); 1595 citation.citationID = Zotero.Utilities.randomString(); 1596 } 1597 this.newIndices[index] = true; 1598 } 1599 // Deal with citations that are copied into the document from somewhere else 1600 // and have not been added to the processor yet 1601 if (! this.oldCitations.has(citation.citationID) && !this.reload) { 1602 this.newIndices[index] = true; 1603 } 1604 if (this.regenAll && !this.newIndices[index]) { 1605 this.updateIndices[index] = true; 1606 } 1607 Zotero.debug("Integration: Adding citationID "+citation.citationID); 1608 this.documentCitationIDs[citation.citationID] = index; 1609 }); 1610 1611 Zotero.Integration.Session.prototype.getCiteprocLists = function(excludeNew) { 1612 var citations = []; 1613 var fieldToCitationIdxMapping = {}; 1614 var citationToFieldIdxMapping = {}; 1615 var i = 0; 1616 // This relies on the order of citationsByIndex keys being stable and sorted in ascending order 1617 // Which it seems to currently be true for every modern JS engine, so we're probably fine 1618 for(let idx in this.citationsByIndex) { 1619 if (excludeNew && this.newIndices[idx]) { 1620 i++; 1621 continue; 1622 } 1623 citations.push([this.citationsByIndex[idx].citationID, this.citationsByIndex[idx].properties.noteIndex]); 1624 fieldToCitationIdxMapping[i] = idx; 1625 citationToFieldIdxMapping[idx] = i++; 1626 } 1627 return [citations, fieldToCitationIdxMapping, citationToFieldIdxMapping]; 1628 } 1629 1630 /** 1631 * Updates the list of citations to be serialized to the document 1632 */ 1633 Zotero.Integration.Session.prototype._updateCitations = async function () { 1634 Zotero.debug("Integration: Indices of new citations"); 1635 Zotero.debug(Object.keys(this.newIndices)); 1636 Zotero.debug("Integration: Indices of updated citations"); 1637 Zotero.debug(Object.keys(this.updateIndices)); 1638 1639 var [citations, fieldToCitationIdxMapping, citationToFieldIdxMapping] = this.getCiteprocLists(); 1640 1641 for (let indexList of [this.newIndices, this.updateIndices]) { 1642 for (let index in indexList) { 1643 // Jump to next event loop step for UI updates 1644 await Zotero.Promise.delay(); 1645 index = parseInt(index); 1646 1647 var citation = this.citationsByIndex[index]; 1648 if (!citation) continue; 1649 citation = citation.toJSON(); 1650 1651 let citationsPre = citations.slice(0, citationToFieldIdxMapping[index]); 1652 var citationsPost; 1653 if (index in this.newIndices) { 1654 citationsPost = []; 1655 delete this.newIndices[index]; 1656 // If this item will need updating later citation processing will reset this index later in the loop 1657 delete this.updateIndices[index]; 1658 } else { 1659 citationsPost = citations.slice(citationToFieldIdxMapping[index]+1); 1660 } 1661 1662 Zotero.debug("Integration: style.processCitationCluster("+citation.toSource()+", "+citationsPre.toSource()+", "+citationsPost.toSource()); 1663 let [info, newCitations] = this.style.processCitationCluster(citation, citationsPre, citationsPost); 1664 1665 this.bibliographyHasChanged |= info.bibchange; 1666 1667 for (let citationInfo of newCitations) { 1668 let idx = fieldToCitationIdxMapping[citationInfo[0]], text = citationInfo[1]; 1669 this.updateIndices[idx] = true; 1670 this.citationsByIndex[idx].text = text; 1671 } 1672 1673 } 1674 } 1675 } 1676 1677 /** 1678 * Restores processor state from document, without requesting citation updates 1679 */ 1680 Zotero.Integration.Session.prototype.restoreProcessorState = function() { 1681 if (this.fields._bibliographyFields.length && !this.bibliography) { 1682 throw new Error ("Attempting to restore processor state without loading bibliography"); 1683 } 1684 let uncited = []; 1685 if (this.bibliography) { 1686 uncited = Array.from(this.bibliography.uncitedItemIDs.values()); 1687 } 1688 1689 var citations = []; 1690 for(var i in this.citationsByIndex) { 1691 if(this.citationsByIndex[i] && !this.newIndices[i]) { 1692 citations.push(this.citationsByIndex[i]); 1693 } 1694 } 1695 this.style.rebuildProcessorState(citations, this.outputFormat, uncited); 1696 } 1697 1698 1699 Zotero.Integration.Session.prototype.writeDelayedCitation = Zotero.Promise.coroutine(function* (idx, field, citation) { 1700 try { 1701 var text = citation.properties.custom || this.style.previewCitationCluster(citation, [], [], this.outputFormat); 1702 } catch(e) { 1703 throw e; 1704 } 1705 if (this.outputFormat == 'rtf') { 1706 text = `${DELAYED_CITATION_RTF_STYLING}{${text}}`; 1707 } else { 1708 text = `${DELAYED_CITATION_HTML_STYLING}${text}${DELAYED_CITATION_HTML_STYLING_END}`; 1709 } 1710 1711 // Make sure we'll prompt for manually edited citations 1712 var isRich = false; 1713 if(!citation.properties.dontUpdate) { 1714 isRich = yield field.setText(text); 1715 1716 citation.properties.formattedCitation = text; 1717 citation.properties.plainCitation = yield field._field.getText(); 1718 } 1719 1720 yield field.setCode(citation.serialize()); 1721 1722 // Update bibliography with a static string 1723 var fields = yield this.fields.get(); 1724 var bibliographyField; 1725 for (let i = fields.length-1; i >= 0; i--) { 1726 let field = yield Zotero.Integration.Field.loadExisting(fields[i]); 1727 if (field.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { 1728 var interfaceType = 'tab'; 1729 if (['MacWord2008', 'OpenOffice'].includes(this.agent)) { 1730 interfaceType = 'toolbar'; 1731 } 1732 1733 yield field.setText(Zotero.getString(`integration.delayCitationUpdates.bibliography.${interfaceType}`), false) 1734 break; 1735 } 1736 } 1737 1738 }); 1739 1740 1741 Zotero.Integration.Session.prototype.getItems = function() { 1742 return Zotero.Cite.getItem(Object.keys(this.citationsByItemID)); 1743 } 1744 1745 1746 /** 1747 * Edits integration bibliography 1748 * @param {Zotero.Integration.Bibliography} bibliography 1749 */ 1750 Zotero.Integration.Session.prototype.editBibliography = Zotero.Promise.coroutine(function *(bibliography) { 1751 if (!Object.keys(this.citationsByIndex).length) { 1752 throw new Error('Integration.Session.editBibliography: called without loaded citations'); 1753 } 1754 yield bibliography.loadItemData(); 1755 1756 var bibliographyEditor = new Zotero.Integration.BibliographyEditInterface(bibliography, this.citationsByItemID, this.style); 1757 1758 yield Zotero.Integration.displayDialog('chrome://zotero/content/integration/editBibliographyDialog.xul', 'resizable', bibliographyEditor); 1759 if (bibliographyEditor.cancelled) throw new Zotero.Exception.UserCancelled("bibliography editing"); 1760 1761 this.bibliographyDataHasChanged = this.bibliographyHasChanged = true; 1762 this.bibliography = bibliographyEditor.bibliography; 1763 }); 1764 1765 /** 1766 * @class Interface for bibliography editor to alter document bibliography 1767 * @constructor 1768 * Creates a new bibliography editor interface 1769 * @param bibliography {Zotero.Integration.Bibliography} 1770 */ 1771 Zotero.Integration.BibliographyEditInterface = function(bibliography, citationsByItemID, citeproc) { 1772 this.bibliography = bibliography; 1773 this.citeproc = citeproc; 1774 this.wrappedJSObject = this; 1775 this._citationsByItemID = citationsByItemID; 1776 this._update(); 1777 } 1778 1779 Zotero.Integration.BibliographyEditInterface.prototype._update = Zotero.Promise.coroutine(function* () { 1780 this.bib = this.bibliography.getCiteprocBibliography(this.citeproc); 1781 }); 1782 1783 /** 1784 * Reverts the text of an individual bibliography entry 1785 */ 1786 Zotero.Integration.BibliographyEditInterface.prototype.revert = function(itemID) { 1787 delete this.bibliography.customEntryText[itemID]; 1788 return this._update(); 1789 } 1790 1791 /** 1792 * Reverts bibliography to condition in which no edits have been made 1793 */ 1794 Zotero.Integration.BibliographyEditInterface.prototype.revertAll = Zotero.Promise.coroutine(function* () { 1795 this.bibliography.customEntryText = {}; 1796 this.bibliography.uncitedItemIDs.clear(); 1797 this.bibliography.omittedItemIDs.clear(); 1798 return this._update(); 1799 }); 1800 1801 /** 1802 * Reverts bibliography to condition before BibliographyEditInterface was opened 1803 */ 1804 Zotero.Integration.BibliographyEditInterface.prototype.cancel = function() { 1805 this.cancelled = true; 1806 }; 1807 1808 /** 1809 * Checks whether a given reference is cited within the main document text 1810 */ 1811 Zotero.Integration.BibliographyEditInterface.prototype.isCited = function(item) { 1812 return this._citationsByItemID[item]; 1813 } 1814 1815 /** 1816 * Checks whether an item ID is cited in the bibliography being edited 1817 */ 1818 Zotero.Integration.BibliographyEditInterface.prototype.isEdited = function(itemID) { 1819 return itemID in this.bibliography.customEntryText; 1820 } 1821 1822 /** 1823 * Checks whether any citations in the bibliography have been edited 1824 */ 1825 Zotero.Integration.BibliographyEditInterface.prototype.isAnyEdited = function() { 1826 return Object.keys(this.bibliography.customEntryText).length || 1827 this.bibliography.uncitedItemIDs.size || 1828 this.bibliography.omittedItemIDs.size; 1829 } 1830 1831 /** 1832 * Adds an item to the bibliography 1833 */ 1834 Zotero.Integration.BibliographyEditInterface.prototype.add = function(itemID) { 1835 if (itemID in this.bibliography.omittedItemIDs) { 1836 this.bibliography.omittedItemIDs.delete(`${itemID}`); 1837 } else { 1838 this.bibliography.uncitedItemIDs.add(`${itemID}`); 1839 } 1840 return this._update(); 1841 } 1842 1843 /** 1844 * Removes an item from the bibliography being edited 1845 */ 1846 Zotero.Integration.BibliographyEditInterface.prototype.remove = function(itemID) { 1847 if (itemID in this.bibliography.uncitedItemIDs) { 1848 this.bibliography.uncitedItemIDs.delete(`${itemID}`); 1849 } else { 1850 this.bibliography.omittedItemIDs.add(`${itemID}`); 1851 } 1852 return this._update(); 1853 } 1854 1855 /** 1856 * Sets custom bibliography text for a given item 1857 */ 1858 Zotero.Integration.BibliographyEditInterface.prototype.setCustomText = function(itemID, text) { 1859 this.bibliography.customEntryText[itemID] = text; 1860 return this._update(); 1861 } 1862 1863 /** 1864 * A class for parsing and passing around document-specific data 1865 */ 1866 Zotero.Integration.DocumentData = function(string) { 1867 this.style = {}; 1868 this.prefs = {}; 1869 this.sessionID = null; 1870 if (string) { 1871 this.unserialize(string); 1872 } 1873 } 1874 1875 /** 1876 * Serializes document-specific data as JSON 1877 */ 1878 Zotero.Integration.DocumentData.prototype.serialize = function() { 1879 // If we've retrieved data with version 4 (JSON), serialize back to JSON 1880 if (this.dataVersion == 4) { 1881 // Filter style properties 1882 let style = {}; 1883 for (let prop of ['styleID', 'locale', 'hasBibliography', 'bibliographyStyleHasBeenSet']) { 1884 style[prop] = this.style[prop]; 1885 } 1886 return JSON.stringify({ 1887 style, 1888 prefs: this.prefs, 1889 sessionID: this.sessionID, 1890 zoteroVersion: Zotero.version, 1891 dataVersion: 4 1892 }); 1893 } 1894 // Otherwise default to XML for now 1895 var prefs = ""; 1896 for (var pref in this.prefs) { 1897 if (!this.prefs[pref]) continue; 1898 prefs += `<pref name="${Zotero.Utilities.htmlSpecialChars(pref)}" `+ 1899 `value="${Zotero.Utilities.htmlSpecialChars(this.prefs[pref].toString())}"/>`; 1900 } 1901 1902 return '<data data-version="'+Zotero.Utilities.htmlSpecialChars(`${DATA_VERSION}`)+'" '+ 1903 'zotero-version="'+Zotero.Utilities.htmlSpecialChars(Zotero.version)+'">'+ 1904 '<session id="'+Zotero.Utilities.htmlSpecialChars(this.sessionID)+'"/>'+ 1905 '<style id="'+Zotero.Utilities.htmlSpecialChars(this.style.styleID)+'" '+ 1906 (this.style.locale ? 'locale="' + Zotero.Utilities.htmlSpecialChars(this.style.locale) + '" ': '') + 1907 'hasBibliography="'+(this.style.hasBibliography ? "1" : "0")+'" '+ 1908 'bibliographyStyleHasBeenSet="'+(this.style.bibliographyStyleHasBeenSet ? "1" : "0")+'"/>'+ 1909 (prefs ? '<prefs>'+prefs+'</prefs>' : '<prefs/>')+'</data>'; 1910 }; 1911 1912 /** 1913 * Unserializes document-specific XML 1914 */ 1915 Zotero.Integration.DocumentData.prototype.unserializeXML = function(xmlData) { 1916 var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] 1917 .createInstance(Components.interfaces.nsIDOMParser), 1918 doc = parser.parseFromString(xmlData, "application/xml"); 1919 1920 this.sessionID = Zotero.Utilities.xpathText(doc, '/data/session[1]/@id'); 1921 this.style = {"styleID":Zotero.Utilities.xpathText(doc, '/data/style[1]/@id'), 1922 "locale":Zotero.Utilities.xpathText(doc, '/data/style[1]/@locale'), 1923 "hasBibliography":(Zotero.Utilities.xpathText(doc, '/data/style[1]/@hasBibliography') == 1), 1924 "bibliographyStyleHasBeenSet":(Zotero.Utilities.xpathText(doc, '/data/style[1]/@bibliographyStyleHasBeenSet') == 1)}; 1925 this.prefs = {}; 1926 for (let pref of Zotero.Utilities.xpath(doc, '/data/prefs[1]/pref')) { 1927 var name = pref.getAttribute("name"); 1928 var value = pref.getAttribute("value"); 1929 if(value === "true") { 1930 value = true; 1931 } else if(value === "false") { 1932 value = false; 1933 } 1934 1935 this.prefs[name] = value; 1936 } 1937 1938 this.prefs.noteType = parseInt(this.prefs.noteType) || 0; 1939 if (this.prefs["automaticJournalAbbreviations"] === undefined) this.prefs["automaticJournalAbbreviations"] = false; 1940 this.zoteroVersion = doc.documentElement.getAttribute("zotero-version"); 1941 if (!this.zoteroVersion) this.zoteroVersion = "2.0"; 1942 this.dataVersion = doc.documentElement.getAttribute("data-version"); 1943 if (!this.dataVersion) this.dataVersion = 2; 1944 }; 1945 1946 /** 1947 * Unserializes document-specific data, either as XML or as the string form used previously 1948 */ 1949 Zotero.Integration.DocumentData.prototype.unserialize = function(input) { 1950 try { 1951 return Object.assign(this, JSON.parse(input)) 1952 } catch (e) { 1953 if (!(e instanceof SyntaxError)) { 1954 throw e; 1955 } 1956 } 1957 if (input[0] == "<") { 1958 this.unserializeXML(input); 1959 } else { 1960 const splitRe = /(^|[^:]):(?!:)/; 1961 1962 var splitOutput = input.split(splitRe); 1963 var prefParameters = []; 1964 for(var i=0; i<splitOutput.length; i+=2) { 1965 prefParameters.push((splitOutput[i]+(splitOutput[i+1] ? splitOutput[i+1] : "")).replace("::", ":", "g")); 1966 } 1967 1968 this.sessionID = prefParameters[0]; 1969 this.style = {"styleID":prefParameters[1], 1970 "hasBibliography":(prefParameters[3] == "1" || prefParameters[3] == "True"), 1971 "bibliographyStyleHasBeenSet":false}; 1972 this.prefs = {"fieldType":((prefParameters[5] == "1" || prefParameters[5] == "True") ? "Bookmark" : "Field")}; 1973 if(prefParameters[2] == "note") { 1974 if(prefParameters[4] == "1" || prefParameters[4] == "True") { 1975 this.prefs.noteType = NOTE_ENDNOTE; 1976 } else { 1977 this.prefs.noteType = NOTE_FOOTNOTE; 1978 } 1979 } else { 1980 this.prefs.noteType = 0; 1981 } 1982 1983 this.zoteroVersion = "2.0b6 or earlier"; 1984 this.dataVersion = 1; 1985 } 1986 } 1987 1988 /** 1989 * Handles mapping of item IDs to URIs 1990 */ 1991 Zotero.Integration.URIMap = function(session) { 1992 this.itemIDURIs = {}; 1993 this.session = session; 1994 } 1995 1996 /** 1997 * Adds a given mapping to the URI map 1998 */ 1999 Zotero.Integration.URIMap.prototype.add = function(id, uris) { 2000 this.itemIDURIs[id] = uris; 2001 } 2002 2003 /** 2004 * Gets URIs for a given item ID, and adds to map 2005 */ 2006 Zotero.Integration.URIMap.prototype.getURIsForItemID = function(id) { 2007 if(typeof id === "string" && id.indexOf("/") !== -1) { 2008 return Zotero.Cite.getItem(id).cslURIs; 2009 } 2010 2011 if(!this.itemIDURIs[id]) { 2012 this.itemIDURIs[id] = [Zotero.URI.getItemURI(Zotero.Items.get(id))]; 2013 } 2014 2015 return this.itemIDURIs[id]; 2016 } 2017 2018 /** 2019 * Gets Zotero item for a given set of URIs 2020 */ 2021 Zotero.Integration.URIMap.prototype.getZoteroItemForURIs = Zotero.Promise.coroutine(function* (uris) { 2022 var zoteroItem = false; 2023 var needUpdate = false; 2024 var embeddedItem = false;; 2025 2026 for(var i=0, n=uris.length; i<n; i++) { 2027 var uri = uris[i]; 2028 2029 // First try embedded URI 2030 if(this.session.embeddedItemsByURI[uri]) { 2031 embeddedItem = this.session.embeddedItemsByURI[uri]; 2032 } 2033 2034 // Next try getting URI directly 2035 try { 2036 zoteroItem = yield Zotero.URI.getURIItem(uri); 2037 if(zoteroItem) { 2038 // Ignore items in the trash 2039 if(zoteroItem.deleted) { 2040 zoteroItem = false; 2041 } else { 2042 break; 2043 } 2044 } 2045 } catch(e) {} 2046 2047 // Try merged item mapping 2048 var replacer = Zotero.Relations.getByPredicateAndObject( 2049 'item', Zotero.Relations.replacedItemPredicate, uri 2050 ); 2051 if (replacer.length && !replacer[0].deleted) { 2052 zoteroItem = replacer[0]; 2053 } 2054 2055 if(zoteroItem) break; 2056 } 2057 2058 if(zoteroItem) { 2059 // make sure URI is up to date (in case user just began synching) 2060 var newURI = Zotero.URI.getItemURI(zoteroItem); 2061 if(newURI != uris[i]) { 2062 uris[i] = newURI; 2063 needUpdate = true; 2064 } 2065 // cache uris 2066 this.itemIDURIs[zoteroItem.id] = uris; 2067 } else if(embeddedItem) { 2068 return [embeddedItem, false]; 2069 } 2070 2071 return [zoteroItem, needUpdate]; 2072 }); 2073 2074 Zotero.Integration.Field = class { 2075 constructor(field, rawCode) { 2076 if (field instanceof Zotero.Integration.Field) { 2077 throw new Error("Trying to instantiate Integration.Field with Integration.Field, not doc field"); 2078 } 2079 for (let func of Zotero.Integration.Field.INTERFACE) { 2080 if (!(func in this)) { 2081 this[func] = field[func].bind(field); 2082 } 2083 } 2084 this._field = field; 2085 this._code = rawCode; 2086 this.type = INTEGRATION_TYPE_TEMP; 2087 } 2088 2089 async setCode(code) { 2090 // Boo. Inconsistent order. 2091 if (this.type == INTEGRATION_TYPE_ITEM) { 2092 await this._field.setCode(`ITEM CSL_CITATION ${code}`); 2093 } else if (this.type == INTEGRATION_TYPE_BIBLIOGRAPHY) { 2094 await this._field.setCode(`BIBL ${code} CSL_BIBLIOGRAPHY`); 2095 } else { 2096 await this._field.setCode(`TEMP`); 2097 } 2098 this._code = code; 2099 } 2100 2101 getCode() { 2102 if (!this._code) { 2103 this._code = this._field.getCode(); 2104 } 2105 let start = this._code.indexOf('{'); 2106 if (start == -1) { 2107 return '{}'; 2108 } 2109 return this._code.substring(start, this._code.lastIndexOf('}')+1); 2110 } 2111 2112 async clearCode() { 2113 return await this.setCode('{}'); 2114 } 2115 2116 async setText(text) { 2117 var isRich = false; 2118 // If RTF wrap with RTF tags 2119 if (text.includes("\\")) { 2120 if (text.substr(0,5) != "{\\rtf") { 2121 text = "{\\rtf "+text+"}"; 2122 } 2123 isRich = true; 2124 } 2125 await this._field.setText(text, isRich); 2126 return isRich; 2127 } 2128 }; 2129 Zotero.Integration.Field.INTERFACE = ['delete', 'removeCode', 'select', 'setText', 2130 'getText', 'setCode', 'getCode', 'equals', 'getNoteIndex']; 2131 2132 /** 2133 * Load existing field in document and return correct instance of field type 2134 * @param docField 2135 * @param rawCode 2136 * @param idx 2137 * @returns {Zotero.Integration.Field|Zotero.Integration.CitationField|Zotero.Integration.BibliographyField} 2138 */ 2139 Zotero.Integration.Field.loadExisting = async function(docField) { 2140 var field; 2141 // Already loaded 2142 if (docField instanceof Zotero.Integration.Field) return docField; 2143 let rawCode = await docField.getCode(); 2144 2145 // ITEM/CITATION CSL_ITEM {json: 'data'} 2146 for (let type of ["ITEM", "CITATION"]) { 2147 if (rawCode.substr(0, type.length) === type) { 2148 field = new Zotero.Integration.CitationField(docField, rawCode); 2149 } 2150 } 2151 // BIBL {json: 'data'} CSL_BIBLIOGRAPHY 2152 if (rawCode.substr(0, 4) === "BIBL") { 2153 field = new Zotero.Integration.BibliographyField(docField, rawCode); 2154 } 2155 2156 if (!field) { 2157 field = new Zotero.Integration.Field(docField, rawCode); 2158 } 2159 2160 return field; 2161 }; 2162 2163 Zotero.Integration.CitationField = class extends Zotero.Integration.Field { 2164 constructor(field, rawCode) { 2165 super(field, rawCode); 2166 this.type = INTEGRATION_TYPE_ITEM; 2167 } 2168 2169 /** 2170 * Don't be fooled, this should be as simple as JSON.parse(). 2171 * The schema for the code is defined @ https://raw.githubusercontent.com/citation-style-language/schema/master/csl-citation.json 2172 * 2173 * However, over the years and different versions of Zotero there's been changes to the schema, 2174 * incorrect serialization, etc. Therefore this function is cruft-full and we can't get rid of it. 2175 * 2176 * @returns {{citationItems: Object[], properties: Object}} 2177 */ 2178 async unserialize() { 2179 function unserialize(code) { 2180 try { 2181 return JSON.parse(code); 2182 } catch(e) { 2183 // fix for corrupted fields (corrupted by 2.1b1) 2184 return JSON.parse(code.replace(/{{((?:\s*,?"unsorted":(?:true|false)|\s*,?"custom":"(?:(?:\\")?[^"]*\s*)*")*)}}/, "{$1}")); 2185 } 2186 } 2187 2188 function upgradeCruft(citation, code) { 2189 // fix for uppercase citation codes 2190 if(citation.CITATIONITEMS) { 2191 citation.citationItems = []; 2192 for (var i=0; i<citation.CITATIONITEMS.length; i++) { 2193 for (var j in citation.CITATIONITEMS[i]) { 2194 switch (j) { 2195 case 'ITEMID': 2196 var field = 'itemID'; 2197 break; 2198 2199 // 'position', 'custom' 2200 default: 2201 var field = j.toLowerCase(); 2202 } 2203 if (!citation.citationItems[i]) { 2204 citation.citationItems[i] = {}; 2205 } 2206 citation.citationItems[i][field] = citation.CITATIONITEMS[i][j]; 2207 } 2208 } 2209 } 2210 2211 if(!citation.properties) citation.properties = {}; 2212 2213 for (let citationItem of citation.citationItems) { 2214 // for upgrade from Zotero 2.0 or earlier 2215 if(citationItem.locatorType) { 2216 citationItem.label = citationItem.locatorType; 2217 delete citationItem.locatorType; 2218 } else if(citationItem.suppressAuthor) { 2219 citationItem["suppress-author"] = citationItem["suppressAuthor"]; 2220 delete citationItem.suppressAuthor; 2221 } 2222 2223 // fix for improper upgrade from Zotero 2.1 in <2.1.5 2224 if(parseInt(citationItem.label) == citationItem.label) { 2225 const locatorTypeTerms = ["page", "book", "chapter", "column", "figure", "folio", 2226 "issue", "line", "note", "opus", "paragraph", "part", "section", "sub verbo", 2227 "volume", "verse"]; 2228 citationItem.label = locatorTypeTerms[parseInt(citationItem.label)]; 2229 } 2230 2231 // for update from Zotero 2.1 or earlier 2232 if(citationItem.uri) { 2233 citationItem.uris = citationItem.uri; 2234 delete citationItem.uri; 2235 } 2236 } 2237 2238 // for upgrade from Zotero 2.0 or earlier 2239 if(citation.sort) { 2240 citation.properties.unsorted = !citation.sort; 2241 delete citation.sort; 2242 } 2243 if(citation.custom) { 2244 citation.properties.custom = citation.custom; 2245 delete citation.custom; 2246 } 2247 2248 citation.properties.field = code; 2249 return citation; 2250 } 2251 2252 function unserializePreZotero1_0(code) { 2253 var underscoreIndex = code.indexOf("_"); 2254 var itemIDs = code.substr(0, underscoreIndex).split("|"); 2255 2256 var lastIndex = code.lastIndexOf("_"); 2257 if(lastIndex != underscoreIndex+1) { 2258 var locatorString = code.substr(underscoreIndex+1, lastIndex-underscoreIndex-1); 2259 var locators = locatorString.split("|"); 2260 } 2261 2262 var citationItems = new Array(); 2263 for(var i=0; i<itemIDs.length; i++) { 2264 var citationItem = {id:itemIDs[i]}; 2265 if(locators) { 2266 citationItem.locator = locators[i].substr(1); 2267 citationItem.label = Zotero.Integration._oldCitationLocatorMap[locators[i][0]]; 2268 } 2269 citationItems.push(citationItem); 2270 } 2271 2272 return {"citationItems":citationItems, properties:{}}; 2273 } 2274 2275 2276 let code = this.getCode(); 2277 try { 2278 if (code[0] == '{') { // JSON field 2279 return upgradeCruft(unserialize(code), code); 2280 } else { // ye olde style field 2281 return unserializePreZotero1_0(code); 2282 } 2283 } catch (e) { 2284 return this.resolveCorrupt(code); 2285 } 2286 } 2287 2288 async clearCode() { 2289 await this.setCode(JSON.stringify({citationItems: [], properties: {}})); 2290 } 2291 2292 async resolveCorrupt(code) { 2293 Zotero.debug(`Integration: handling corrupt citation field ${code}`); 2294 var msg = Zotero.getString("integration.corruptField")+'\n\n'+ 2295 Zotero.getString('integration.corruptField.description'); 2296 await this.select(); 2297 Zotero.Integration.currentDoc.activate(); 2298 var result = await Zotero.Integration.currentSession.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_YES_NO_CANCEL); 2299 if (result == 0) { // Cancel 2300 return new Zotero.Exception.UserCancelled("corrupt citation resolution"); 2301 } else if (result == 1) { // No 2302 return false; 2303 } else { // Yes 2304 var fieldGetter = Zotero.Integration.currentSession.fields, 2305 oldWindow = Zotero.Integration.currentWindow, 2306 oldProgressCallback = this.progressCallback; 2307 // Clear current code and subsequent addEditCitation dialog will be the reselection 2308 await this.clearCode(); 2309 return this.unserialize(); 2310 } 2311 } 2312 }; 2313 2314 2315 Zotero.Integration.BibliographyField = class extends Zotero.Integration.Field { 2316 constructor(field, rawCode) { 2317 super(field, rawCode); 2318 this.type = INTEGRATION_TYPE_BIBLIOGRAPHY; 2319 }; 2320 2321 async unserialize() { 2322 var code = this.getCode(); 2323 try { 2324 return JSON.parse(code); 2325 } catch(e) { 2326 return this.resolveCorrupt(code); 2327 } 2328 } 2329 async resolveCorrupt(code) { 2330 Zotero.debug(`Integration: handling corrupt bibliography field ${code}`); 2331 var msg = Zotero.getString("integration.corruptBibliography")+'\n\n'+ 2332 Zotero.getString('integration.corruptBibliography.description'); 2333 var result = await Zotero.Integration.currentSession.displayAlert(msg, DIALOG_ICON_CAUTION, DIALOG_BUTTONS_OK_CANCEL); 2334 if (result == 0) { 2335 throw new Zotero.Exception.UserCancelled("corrupt bibliography resolution"); 2336 } else { 2337 await this.clearCode(); 2338 return this.unserialize(); 2339 } 2340 } 2341 }; 2342 2343 Zotero.Integration.Citation = class { 2344 constructor(citationField, data, noteIndex) { 2345 if (!data) { 2346 data = {citationItems: [], properties: {}}; 2347 } 2348 this.citationID = data.citationID; 2349 this.citationItems = data.citationItems; 2350 this.properties = data.properties; 2351 this.properties.noteIndex = noteIndex; 2352 2353 this._field = citationField; 2354 } 2355 2356 /** 2357 * Load citation item data 2358 * @param {Boolean} [promptToReselect=true] - will throw a MissingItemException if false 2359 * @returns {Promise{Number}} 2360 * - Zotero.Integration.NO_ACTION 2361 * - Zotero.Integration.UPDATE 2362 * - Zotero.Integration.REMOVE_CODE 2363 * - Zotero.Integration.DELETE 2364 */ 2365 loadItemData() { 2366 return Zotero.Promise.coroutine(function *(promptToReselect=true){ 2367 let items = []; 2368 var needUpdate = false; 2369 2370 if (!this.citationItems.length) { 2371 return Zotero.Integration.DELETE; 2372 } 2373 for (var i=0, n=this.citationItems.length; i<n; i++) { 2374 var citationItem = this.citationItems[i]; 2375 2376 // get Zotero item 2377 var zoteroItem = false; 2378 if (citationItem.uris) { 2379 let itemNeedsUpdate; 2380 [zoteroItem, itemNeedsUpdate] = yield Zotero.Integration.currentSession.uriMap.getZoteroItemForURIs(citationItem.uris); 2381 needUpdate = needUpdate || itemNeedsUpdate; 2382 2383 // Unfortunately, people do weird things with their documents. One weird thing people 2384 // apparently like to do (http://forums.zotero.org/discussion/22262/) is to copy and 2385 // paste citations from other documents created with earlier versions of Zotero into 2386 // their documents and then not refresh the document. Usually, this isn't a problem. If 2387 // document is edited by the same user, it will work without incident. If the first 2388 // citation of a given item doesn't contain itemData, the user will get a 2389 // MissingItemException. However, it may also happen that the first citation contains 2390 // itemData, but later citations don't, because the user inserted the item properly and 2391 // then copied and pasted the same citation from another document. We check for that 2392 // possibility here. 2393 if (zoteroItem.cslItemData && !citationItem.itemData) { 2394 citationItem.itemData = zoteroItem.cslItemData; 2395 needUpdate = true; 2396 } 2397 } else { 2398 if (citationItem.key && citationItem.libraryID) { 2399 // DEBUG: why no library id? 2400 zoteroItem = Zotero.Items.getByLibraryAndKey(citationItem.libraryID, citationItem.key); 2401 } else if (citationItem.itemID) { 2402 zoteroItem = Zotero.Items.get(citationItem.itemID); 2403 } else if (citationItem.id) { 2404 zoteroItem = Zotero.Items.get(citationItem.id); 2405 } 2406 if (zoteroItem) needUpdate = true; 2407 } 2408 2409 // Item no longer in library 2410 if (!zoteroItem) { 2411 // Use embedded item 2412 if (citationItem.itemData) { 2413 Zotero.debug(`Item ${JSON.stringify(citationItem.uris)} not in library. Using embedded data`); 2414 // add new embedded item 2415 var itemData = Zotero.Utilities.deepCopy(citationItem.itemData); 2416 2417 // assign a random string as an item ID 2418 var anonymousID = Zotero.randomString(); 2419 var globalID = itemData.id = citationItem.id = Zotero.Integration.currentSession.data.sessionID+"/"+anonymousID; 2420 Zotero.Integration.currentSession.embeddedItems[anonymousID] = itemData; 2421 2422 // assign a Zotero item 2423 var surrogateItem = Zotero.Integration.currentSession.embeddedZoteroItems[anonymousID] = new Zotero.Item(); 2424 Zotero.Utilities.itemFromCSLJSON(surrogateItem, itemData); 2425 surrogateItem.cslItemID = globalID; 2426 surrogateItem.cslURIs = citationItem.uris; 2427 surrogateItem.cslItemData = itemData; 2428 2429 for(var j=0, m=citationItem.uris.length; j<m; j++) { 2430 Zotero.Integration.currentSession.embeddedItemsByURI[citationItem.uris[j]] = surrogateItem; 2431 } 2432 } else if (promptToReselect) { 2433 zoteroItem = yield this.handleMissingItem(i); 2434 if (zoteroItem) needUpdate = true; 2435 else return Zotero.Integration.REMOVE_CODE; 2436 } else { 2437 // throw a MissingItemException 2438 throw (new Zotero.Integration.MissingItemException(this, this.citationItems[i])); 2439 } 2440 } 2441 2442 if (zoteroItem) { 2443 if (zoteroItem.cslItemID) { 2444 citationItem.id = zoteroItem.cslItemID; 2445 } 2446 else { 2447 citationItem.id = zoteroItem.id; 2448 items.push(zoteroItem); 2449 } 2450 } 2451 } 2452 2453 // Items may be in libraries that haven't been loaded, and retrieveItem() is synchronous, so load 2454 // all data (as required by toJSON(), which is used by itemToExportFormat(), which is used by 2455 // itemToCSLJSON()) now 2456 if (items.length) { 2457 yield Zotero.Items.loadDataTypes(items); 2458 } 2459 return needUpdate ? Zotero.Integration.UPDATE : Zotero.Integration.NO_ACTION; 2460 }).apply(this, arguments); 2461 } 2462 2463 async handleMissingItem(idx) { 2464 // Ask user what to do with this item 2465 if (this.citationItems.length == 1) { 2466 var msg = Zotero.getString("integration.missingItem.single"); 2467 } else { 2468 var msg = Zotero.getString("integration.missingItem.multiple", (idx).toString()); 2469 } 2470 msg += '\n\n'+Zotero.getString('integration.missingItem.description'); 2471 await this._field.select(); 2472 await Zotero.Integration.currentDoc.activate(); 2473 var result = await Zotero.Integration.currentSession.displayAlert(msg, 2474 DIALOG_ICON_WARNING, DIALOG_BUTTONS_YES_NO_CANCEL); 2475 if (result == 0) { // Cancel 2476 throw new Zotero.Exception.UserCancelled("document update"); 2477 } else if(result == 1) { // No 2478 return false; 2479 } 2480 2481 // Yes - prompt to reselect 2482 var io = new function() { this.wrappedJSObject = this; }; 2483 2484 io.addBorder = Zotero.isWin; 2485 io.singleSelection = true; 2486 2487 await Zotero.Integration.displayDialog('chrome://zotero/content/selectItemsDialog.xul', 'resizable', io); 2488 2489 if (io.dataOut && io.dataOut.length) { 2490 return Zotero.Items.get(io.dataOut[0]); 2491 } 2492 } 2493 2494 async prepareForEditing() { 2495 // Check for modified field text or dontUpdate flag 2496 if (this.properties.dontUpdate 2497 || (this.properties.plainCitation 2498 && await this._field.getText() !== this.properties.plainCitation)) { 2499 await Zotero.Integration.currentDoc.activate(); 2500 var fieldText = await this._field.getText(); 2501 Zotero.debug("[addEditCitation] Attempting to update manually modified citation.\n" 2502 + "citaion.properties.dontUpdate: " + this.properties.dontUpdate + "\n" 2503 + "Original: " + this.properties.plainCitation + "\n" 2504 + "Current: " + fieldText 2505 ); 2506 if (!await Zotero.Integration.currentDoc.displayAlert( 2507 Zotero.getString("integration.citationChanged.edit")+"\n\n" 2508 + Zotero.getString("integration.citationChanged.original", this.properties.plainCitation)+"\n" 2509 + Zotero.getString("integration.citationChanged.modified", fieldText)+"\n", 2510 DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL)) { 2511 throw new Zotero.Exception.UserCancelled("editing citation"); 2512 } 2513 } 2514 2515 // make sure it's going to get updated 2516 delete this.properties["formattedCitation"]; 2517 delete this.properties["plainCitation"]; 2518 delete this.properties["dontUpdate"]; 2519 2520 // Load items to be displayed in edit dialog 2521 await this.loadItemData(); 2522 } 2523 2524 toJSON() { 2525 const saveProperties = ["custom", "unsorted", "formattedCitation", "plainCitation", "dontUpdate", "noteIndex"]; 2526 const saveCitationItemKeys = ["locator", "label", "suppress-author", "author-only", "prefix", 2527 "suffix"]; 2528 2529 var citation = {}; 2530 2531 citation.citationID = this.citationID; 2532 2533 citation.properties = {}; 2534 for (let key of saveProperties) { 2535 if (key in this.properties) citation.properties[key] = this.properties[key]; 2536 } 2537 2538 citation.citationItems = new Array(this.citationItems.length); 2539 for (let i=0; i < this.citationItems.length; i++) { 2540 var citationItem = this.citationItems[i], 2541 serializeCitationItem = {}; 2542 2543 // add URI and itemData 2544 var slashIndex; 2545 if (typeof citationItem.id === "string" && (slashIndex = citationItem.id.indexOf("/")) !== -1) { 2546 // this is an embedded item 2547 serializeCitationItem.id = citationItem.id; 2548 serializeCitationItem.uris = citationItem.uris; 2549 2550 // XXX For compatibility with Zotero 2.0; to be removed at a later date 2551 serializeCitationItem.uri = serializeCitationItem.uris; 2552 2553 // always store itemData, since we have no way to get it back otherwise 2554 serializeCitationItem.itemData = citationItem.itemData; 2555 } else { 2556 serializeCitationItem.id = citationItem.id; 2557 serializeCitationItem.uris = Zotero.Integration.currentSession.uriMap.getURIsForItemID(citationItem.id); 2558 2559 // XXX For compatibility with Zotero 2.0; to be removed at a later date 2560 serializeCitationItem.uri = serializeCitationItem.uris; 2561 2562 serializeCitationItem.itemData = Zotero.Integration.currentSession.style.sys.retrieveItem(citationItem.id); 2563 } 2564 2565 for (let key of saveCitationItemKeys) { 2566 if (key in citationItem) serializeCitationItem[key] = citationItem[key]; 2567 } 2568 2569 citation.citationItems[i] = serializeCitationItem; 2570 } 2571 citation.schema = "https://github.com/citation-style-language/schema/raw/master/csl-citation.json"; 2572 2573 return citation; 2574 } 2575 2576 /** 2577 * Serializes the citation into CSL code representation 2578 * @returns {string} 2579 */ 2580 serialize() { 2581 return JSON.stringify(this.toJSON()); 2582 } 2583 }; 2584 2585 Zotero.Integration.Bibliography = class { 2586 constructor(bibliographyField, data) { 2587 this._field = bibliographyField; 2588 this.data = data; 2589 2590 this.uncitedItemIDs = new Set(); 2591 this.omittedItemIDs = new Set(); 2592 this.customEntryText = {}; 2593 this.dataLoaded = false; 2594 } 2595 2596 loadItemData() { 2597 return Zotero.Promise.coroutine(function* () { 2598 // set uncited 2599 var needUpdate = false; 2600 if (this.data.uncited) { 2601 if (this.data.uncited[0]) { 2602 // new style array of arrays with URIs 2603 let zoteroItem, itemNeedsUpdate; 2604 for (let uris of this.data.uncited) { 2605 [zoteroItem, itemNeedsUpdate] = yield Zotero.Integration.currentSession.uriMap.getZoteroItemForURIs(uris); 2606 var id = zoteroItem.cslItemID ? zoteroItem.cslItemID : zoteroItem.id; 2607 if(zoteroItem && !Zotero.Integration.currentSession.citationsByItemID[id]) { 2608 this.uncitedItemIDs.add(`${id}`); 2609 } else { 2610 needUpdate = true; 2611 } 2612 needUpdate |= itemNeedsUpdate; 2613 } 2614 } else { 2615 for(var itemID in this.data.uncited) { 2616 // if not yet in item set, add to item set 2617 // DEBUG: why no libraryID? 2618 var zoteroItem = Zotero.Items.getByLibraryAndKey(0, itemID); 2619 if (!zoteroItem) zoteroItem = Zotero.Items.get(itemID); 2620 if (zoteroItem) this.uncitedItemIDs.add(`${id}`); 2621 } 2622 needUpdate = true; 2623 } 2624 } 2625 2626 // set custom bibliography entries 2627 if(this.data.custom) { 2628 if(this.data.custom[0]) { 2629 // new style array of arrays with URIs 2630 var zoteroItem, itemNeedsUpdate; 2631 for (let custom of this.data.custom) { 2632 [zoteroItem, itemNeedsUpdate] = yield Zotero.Integration.currentSession.uriMap.getZoteroItemForURIs(custom[0]); 2633 if (!zoteroItem) continue; 2634 if (needUpdate) needUpdate = true; 2635 2636 var id = zoteroItem.cslItemID ? zoteroItem.cslItemID : zoteroItem.id; 2637 if (Zotero.Integration.currentSession.citationsByItemID[id] || id in this.uncitedItemIDs) { 2638 this.customEntryText[id] = custom[1]; 2639 } 2640 } 2641 } else { 2642 // old style hash 2643 for(var itemID in this.data.custom) { 2644 var zoteroItem = Zotero.Items.getByLibraryAndKey(0, itemID); 2645 if (!zoteroItem) zoteroItem = Zotero.Items.get(itemID); 2646 if (!zoteroItem) continue; 2647 2648 if(Zotero.Integration.currentSession.citationsByItemID[zoteroItem.id] || zoteroItem.id in this.uncitedItemIDs) { 2649 this.customEntryText[zoteroItem.id] = this.data.custom[itemID]; 2650 } 2651 } 2652 needUpdate = true; 2653 } 2654 } 2655 2656 // set entries to be omitted from bibliography 2657 if (this.data.omitted) { 2658 let zoteroItem, itemNeedsUpdate; 2659 for (let uris of this.data.omitted) { 2660 [zoteroItem, itemNeedsUpdate] = yield Zotero.Integration.currentSession.uriMap.getZoteroItemForURIs(uris); 2661 var id = zoteroItem.cslItemID ? zoteroItem.cslItemID : zoteroItem.id; 2662 if (zoteroItem && Zotero.Integration.currentSession.citationsByItemID[id]) { 2663 this.omittedItemIDs.add(`${id}`); 2664 } else { 2665 needUpdate = true; 2666 } 2667 needUpdate |= itemNeedsUpdate; 2668 } 2669 } 2670 this.dataLoaded = true; 2671 return needUpdate; 2672 }).apply(this, arguments); 2673 } 2674 2675 getCiteprocBibliography(citeproc) { 2676 if (Zotero.Utilities.isEmpty(Zotero.Integration.currentSession.citationsByItemID)) { 2677 throw new Error("Attempting to generate bibliography without having updated processor items"); 2678 }; 2679 if (!this.dataLoaded) { 2680 throw new Error("Attempting to generate bibliography without having loaded item data"); 2681 } 2682 2683 Zotero.debug(`Integration: style.updateUncitedItems ${Array.from(this.uncitedItemIDs.values()).toSource()}`); 2684 citeproc.updateUncitedItems(Array.from(this.uncitedItemIDs.values())); 2685 citeproc.setOutputFormat(Zotero.Integration.currentSession.outputFormat); 2686 let bibliography = citeproc.makeBibliography(); 2687 Zotero.Cite.removeFromBibliography(bibliography, this.omittedItemIDs); 2688 2689 for (let i in bibliography[0].entry_ids) { 2690 if (bibliography[0].entry_ids[i].length != 1) continue; 2691 let itemID = bibliography[0].entry_ids[i][0]; 2692 if (itemID in this.customEntryText) { 2693 bibliography[1][i] = this.customEntryText[itemID]; 2694 } 2695 } 2696 return bibliography; 2697 } 2698 2699 serialize() { 2700 if (!this.dataLoaded) { 2701 throw new Error("Attempting to generate bibliography without having loaded item data"); 2702 } 2703 var bibliography = { 2704 uncited: [], 2705 omitted: [], 2706 custom: [] 2707 }; 2708 2709 // add uncited if there is anything 2710 for (let itemID of this.uncitedItemIDs.values()) { 2711 bibliography.uncited.push(Zotero.Integration.currentSession.uriMap.getURIsForItemID(itemID)); 2712 } 2713 for (let itemID of this.omittedItemIDs.values()) { 2714 bibliography.omitted.push(Zotero.Integration.currentSession.uriMap.getURIsForItemID(itemID)); 2715 } 2716 2717 bibliography.custom = Object.keys(this.customEntryText) 2718 .map(id => [Zotero.Integration.currentSession.uriMap.getURIsForItemID(id), this.customEntryText[id]]); 2719 2720 2721 return JSON.stringify(bibliography); 2722 } 2723 } 2724 2725 // perhaps not the best place for a timer 2726 Zotero.Integration.Timer = class { 2727 start() { 2728 this.startTime = (new Date()).getTime(); 2729 } 2730 2731 stop() { 2732 this.resume(); 2733 this.finalTime = ((new Date()).getTime() - this.startTime) 2734 return this.finalTime/1000; 2735 } 2736 2737 pause() { 2738 this.pauseTime = (new Date()).getTime(); 2739 } 2740 2741 getSplit() { 2742 var pauseTime = 0; 2743 if (this.pauseTime) { 2744 var pauseTime = (new Date()).getTime() - this.pauseTime; 2745 } 2746 return ((new Date()).getTime() - this.startTime - pauseTime); 2747 } 2748 2749 resume() { 2750 if (this.pauseTime) { 2751 this.startTime += ((new Date()).getTime() - this.pauseTime); 2752 this.pauseTime; 2753 } 2754 } 2755 } 2756 2757 Zotero.Integration.Progress = class { 2758 /** 2759 * @param {Number} segmentCount 2760 * @param {Boolean} dontDisplay 2761 * On macOS closing an application window switches focus to the topmost window of the same application 2762 * instead of the previous window of any application. Since the progress window is opened and closed 2763 * between showing other integration windows, macOS will switch focus to the main Zotero window (and 2764 * move the word processor window to the background). Thus we avoid showing the progress window on macOS 2765 * except for http agents (i.e. google docs), where even opening the citation dialog may potentially take 2766 * a long time and having no indication of progress is worse than bringing the Zotero window to the front 2767 */ 2768 constructor(segmentCount=4, dontDisplay=false) { 2769 this.segments = Array.from({length: segmentCount}, () => undefined); 2770 this.timer = new Zotero.Integration.Timer(); 2771 this.segmentIdx = 0; 2772 this.dontDisplay = dontDisplay; 2773 } 2774 2775 update() { 2776 if (!this.onProgress) return; 2777 var currentSegment = this.segments[this.segmentIdx]; 2778 if (!currentSegment) return; 2779 var total = this.segments.reduce((acc, val) => acc+val, 0); 2780 var startProgress = 100*this.segments.slice(0, this.segmentIdx).reduce((acc, val) => acc+val, 0)/total; 2781 var maxProgress = 100.0*currentSegment/total; 2782 var split = this.timer.getSplit(); 2783 var curProgress = startProgress + maxProgress*Math.min(1, split/currentSegment); 2784 this.onProgress(curProgress); 2785 setTimeout(this.update.bind(this), 100); 2786 } 2787 2788 start() { 2789 this.timer.start(); 2790 } 2791 pause() {this.timer.pause();} 2792 resume() {this.timer.resume();} 2793 finishSegment() { 2794 this.timer.stop(); 2795 this.segments[this.segmentIdx++] = this.timer.finalTime; 2796 } 2797 reset() { 2798 this.segmentIdx = 0; 2799 } 2800 show() { 2801 if (this.dontDisplay) return; 2802 var options = 'chrome,centerscreen'; 2803 // without this, Firefox gets raised with our windows under Compiz 2804 if (Zotero.isLinux) options += ',dialog=no'; 2805 if (Zotero.isMac) options += ',resizable=false'; 2806 2807 var io = {onLoad: function(onProgress) { 2808 this.onProgress = onProgress; 2809 this.update(); 2810 }.bind(this)}; 2811 io.wrappedJSObject = io; 2812 this.window = Components.classes["@mozilla.org/embedcomp/window-watcher;1"] 2813 .getService(Components.interfaces.nsIWindowWatcher) 2814 .openWindow(null, 'chrome://zotero/content/integration/progressBar.xul', '', options, io); 2815 Zotero.Utilities.Internal.activate(this.window); 2816 } 2817 async hide(fast=false) { 2818 if (this.dontDisplay || !this.window) return; 2819 if (!fast) { 2820 this.onProgress && this.onProgress(100); 2821 this.onProgress = null; 2822 await Zotero.Promise.delay(300); 2823 } 2824 this.window.close(); 2825 } 2826 } 2827 2828 Zotero.Integration.LegacyPluginWrapper = function(application) { 2829 return { 2830 getDocument: 2831 async function() { 2832 return Zotero.Integration.LegacyPluginWrapper.wrapDocument( 2833 application.getDocument.apply(application, arguments)) 2834 }, 2835 getActiveDocument: 2836 async function() { 2837 return Zotero.Integration.LegacyPluginWrapper.wrapDocument( 2838 application.getActiveDocument.apply(application, arguments)) 2839 }, 2840 primaryFieldType: application.primaryFieldType, 2841 secondaryFieldType: application.secondaryFieldType, 2842 outputFormat: 'rtf', 2843 supportedNotes: ['footnotes', 'endnotes'] 2844 } 2845 } 2846 Zotero.Integration.LegacyPluginWrapper.wrapField = function (field) { 2847 var wrapped = {rawField: field}; 2848 var fns = ['getNoteIndex', 'setCode', 'getCode', 'setText', 2849 'getText', 'removeCode', 'delete', 'select']; 2850 for (let fn of fns) { 2851 wrapped[fn] = async function() { 2852 return field[fn].apply(field, arguments); 2853 } 2854 } 2855 wrapped.equals = async function(other) { 2856 return field.equals(other.rawField); 2857 } 2858 return wrapped; 2859 } 2860 Zotero.Integration.LegacyPluginWrapper.wrapDocument = function wrapDocument(doc) { 2861 var wrapped = {}; 2862 var fns = ['complete', 'cleanup', 'setBibliographyStyle', 'setDocumentData', 2863 'getDocumentData', 'canInsertField', 'activate', 'displayAlert']; 2864 for (let fn of fns) { 2865 wrapped[fn] = async function() { 2866 return doc[fn].apply(doc, arguments); 2867 } 2868 } 2869 // Should return an async array 2870 wrapped.getFields = async function(fieldType, progressCallback) { 2871 if ('getFieldsAsync' in doc) { 2872 var deferred = Zotero.Promise.defer(); 2873 var promise = deferred.promise; 2874 2875 var me = this; 2876 doc.getFieldsAsync(fieldType, 2877 {"observe":function(subject, topic, data) { 2878 if(topic === "fields-available") { 2879 if(progressCallback) { 2880 try { 2881 progressCallback(75); 2882 } catch(e) { 2883 Zotero.logError(e); 2884 }; 2885 } 2886 2887 try { 2888 // Add fields to fields array 2889 var fieldsEnumerator = subject.QueryInterface(Components.interfaces.nsISimpleEnumerator); 2890 var fields = []; 2891 while (fieldsEnumerator.hasMoreElements()) { 2892 let field = fieldsEnumerator.getNext(); 2893 try { 2894 fields.push(Zotero.Integration.LegacyPluginWrapper.wrapField( 2895 field.QueryInterface(Components.interfaces.zoteroIntegrationField))); 2896 } catch (e) { 2897 fields.push(Zotero.Integration.LegacyPluginWrapper.wrapField(field)); 2898 } 2899 } 2900 } catch(e) { 2901 deferred.reject(e); 2902 deferred = null; 2903 return; 2904 } 2905 2906 deferred.resolve(fields); 2907 deferred = null; 2908 } else if(topic === "fields-progress") { 2909 if(progressCallback) { 2910 try { 2911 progressCallback((data ? parseInt(data, 10)*(3/4) : null)); 2912 } catch(e) { 2913 Zotero.logError(e); 2914 }; 2915 } 2916 } else if(topic === "fields-error") { 2917 deferred.reject(data); 2918 deferred = null; 2919 } 2920 }, QueryInterface:XPCOMUtils.generateQI([Components.interfaces.nsIObserver, Components.interfaces.nsISupports])}); 2921 return promise; 2922 } else { 2923 var result = doc.getFields.apply(doc, arguments); 2924 var fields = []; 2925 if (result.hasMoreElements) { 2926 while (result.hasMoreElements()) { 2927 fields.push(Zotero.Integration.LegacyPluginWrapper.wrapField(result.getNext())); 2928 await Zotero.Promise.delay(); 2929 } 2930 } else { 2931 fields = result; 2932 } 2933 return fields; 2934 } 2935 } 2936 wrapped.insertField = async function() { 2937 return Zotero.Integration.LegacyPluginWrapper.wrapField(doc.insertField.apply(doc, arguments)); 2938 } 2939 wrapped.cursorInField = async function() { 2940 var result = doc.cursorInField.apply(doc, arguments); 2941 return !result ? result : Zotero.Integration.LegacyPluginWrapper.wrapField(result); 2942 } 2943 // Should take an arrayOfFields instead of an enumerator 2944 wrapped.convert = async function(arrayOfFields) { 2945 arguments[0] = new Zotero.Integration.JSEnumerator(arrayOfFields.map(f => f.rawField)); 2946 return doc.convert.apply(doc, arguments); 2947 } 2948 return wrapped; 2949 }