www

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

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 }