www

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

commit f44d563a152f3837a2da2a231d3c1a71b83446f6
parent a1acbd403837b03e3ea7f17700b99cbd7e659cac
Author: Adomas VenĨkauskas <adomas.ven@gmail.com>
Date:   Thu, 25 May 2017 10:48:43 +0300

Add Zotero.Integration.Citation

- Moves a bunch of citation related processing from Integration.Session
- Replaces missing item handling with a function instead of exception
- Solves some really confusing flow issues in _processFields

Diffstat:
Mchrome/content/zotero/xpcom/cite.js | 2+-
Mchrome/content/zotero/xpcom/integration.js | 732++++++++++++++++++++++++++++++++++---------------------------------------------
Mtest/tests/integrationTest.js | 17++++++++++-------
3 files changed, 329 insertions(+), 422 deletions(-)

diff --git a/chrome/content/zotero/xpcom/cite.js b/chrome/content/zotero/xpcom/cite.js @@ -526,7 +526,7 @@ Zotero.Cite.System.prototype = { } if(!zoteroItem) { - throw "Zotero.Cite.System.retrieveItem called on non-item "+item; + throw new Error("Zotero.Cite.System.retrieveItem called on non-item "+item); } var cslItem = Zotero.Utilities.itemToCSLJSON(zoteroItem); diff --git a/chrome/content/zotero/xpcom/integration.js b/chrome/content/zotero/xpcom/integration.js @@ -305,8 +305,8 @@ Zotero.Integration = new function() { * @param {String} [io] Data to pass to the window * @return {Promise} Promise resolved when the window is closed */ - this.displayDialog = function displayDialog(doc, url, options, io) { - doc.cleanup(); + this.displayDialog = function displayDialog(url, options, io) { + Zotero.Integration.currentDoc.cleanup(); var allOptions = 'chrome,centerscreen'; // without this, Firefox gets raised with our windows under Compiz @@ -442,66 +442,13 @@ Zotero.Integration = new function() { /** * An exception thrown when a document contains an item that no longer exists in the current document. - * - * @param reselectKeys {Array} Keys representing the missing item - * @param reselectKeyType {Integer} The type of the keys (see RESELECT_KEY_* constants) - * @param citationIndex {Integer} The index of the missing item within the citation cluster - * @param citationLength {Integer} The number of items cited in this citation cluster */ -Zotero.Integration.MissingItemException = function(reselectKeys, reselectKeyType, citationIndex, citationLength) { - this.reselectKeys = reselectKeys; - this.reselectKeyType = reselectKeyType; - this.citationIndex = citationIndex; - this.citationLength = citationLength; -} +Zotero.Integration.MissingItemException = function() {}; Zotero.Integration.MissingItemException.prototype = { "name":"MissingItemException", "message":"An item in this document is missing from your Zotero library.", - "toString":function() { return this.message }, - "setContext":function(fieldGetter, fieldIndex) { - this.fieldGetter = fieldGetter; - this.fieldIndex = fieldIndex; - }, - - "attemptToResolve":function() { - Zotero.logError(this); - if(!this.fieldGetter) { - throw new Error("Could not resolve "+this.name+": setContext not called"); - } - - // Ask user what to do with this item - if(this.citationLength == 1) { - var msg = Zotero.getString("integration.missingItem.single"); - } else { - var msg = Zotero.getString("integration.missingItem.multiple", (this.citationIndex+1).toString()); - } - msg += '\n\n'+Zotero.getString('integration.missingItem.description'); - this.fieldGetter._fields[this.fieldIndex].select(); - this.fieldGetter._doc.activate(); - var result = this.fieldGetter._doc.displayAlert(msg, 1, 3); - if(result == 0) { // Cancel - return Zotero.Promise.reject(new Zotero.Exception.UserCancelled("document update")); - } else if(result == 1) { // No - for (let reselectKey of this.reselectKeys) { - this.fieldGetter._removeCodeKeys[reselectKey] = true; - } - this.fieldGetter._removeCodeFields[this.fieldIndex] = true; - return this.fieldGetter._processFields(this.fieldIndex+1); - } else { // Yes - // Display reselect item dialog - var fieldGetter = this.fieldGetter, - fieldIndex = this.fieldIndex, - oldCurrentWindow = Zotero.Integration.currentWindow; - return fieldGetter._session.reselectItem(fieldGetter._doc, this) - .then(function() { - // Now try again - Zotero.Integration.currentWindow = oldCurrentWindow; - fieldGetter._doc.activate(); - return fieldGetter._processFields(fieldIndex); - }); - } - } -} + "toString":function() { return this.message } +}; Zotero.Integration.CorruptFieldException = function(code, cause) { this.code = code; @@ -697,26 +644,23 @@ Zotero.Integration.Interface.prototype.addEditCitation = function() { * Adds a bibliography to the current document. * @return {Promise} */ -Zotero.Integration.Interface.prototype.addBibliography = function() { +Zotero.Integration.Interface.prototype.addBibliography = Zotero.Promise.coroutine(function* () { var me = this; - return this._prepareData(true, false).then(function() { - // Make sure we can have a bibliography - if(!me._session.data.style.hasBibliography) { - throw new Zotero.Exception.Alert("integration.error.noBibliography", [], - "integration.error.title"); - } - - var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); - return fieldGetter.addField().then(function(field) { - field.clearCode(); - field.type = INTEGRATION_TYPE_BIBLIOGRAPHY; - field.writeToDoc(); - return fieldGetter.updateSession().then(function() { - return fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); - }); - }); - }); -} + yield this._prepareData(true, false); + // Make sure we can have a bibliography + if(!me._session.data.style.hasBibliography) { + throw new Zotero.Exception.Alert("integration.error.noBibliography", [], + "integration.error.title"); + } + + var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); + let field = new Zotero.Integration.BibliographyField(yield fieldGetter.addField()); + field.clearCode(); + field.type = INTEGRATION_TYPE_BIBLIOGRAPHY; + field.writeToDoc(); + yield fieldGetter.updateSession(); + yield fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); +}) /** * Edits bibliography metadata. @@ -774,11 +718,10 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro if (haveBibliography) { yield fieldGetter.updateSession(); - yield this._session.editBibliography(this._doc); + yield this._session.editBibliography(); } else { var field = new Zotero.Integration.BibliographyField(yield fieldGetter.addField()); field.clearCode(); - field.type = INTEGRATION_TYPE_BIBLIOGRAPHY; field.writeToDoc(); yield fieldGetter.updateSession(); } @@ -921,7 +864,6 @@ Zotero.Integration.Fields = function(session, doc, fieldErrorHandler) { this._doc = doc; this._deferreds = null; - this._removeCodeKeys = {}; this._removeCodeFields = {}; this._bibliographyFields = []; this._bibliographyData = ""; @@ -1054,7 +996,6 @@ Zotero.Integration.Fields.prototype.updateSession = Zotero.Promise.coroutine(fun yield this.get(); this._session.resetRequest(this._doc); - this._removeCodeKeys = {}; this._removeCodeFields = {}; this._bibliographyFields = []; this._bibliographyData = ""; @@ -1104,31 +1045,28 @@ Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(fu for(var n = this._fields.length; i<n; i++) { let field = Zotero.Integration.Field.loadExisting(this._fields[i]); if (field.type === INTEGRATION_TYPE_ITEM) { - var noteIndex = field.getNoteIndex(); + var noteIndex = field.getNoteIndex(), + citation; try { - yield this._session.addCitation(i, noteIndex, field.unserialize()); - } catch(e) { - var removeCode = false; + citation = new Zotero.Integration.Citation(field); + let action = yield citation.loadItemData(); - if(e instanceof Zotero.Integration.CorruptFieldException) { - e.setContext(this, i) - } else if(e instanceof Zotero.Integration.MissingItemException) { - // Check if we've already decided to remove this field code - for (let reselectKey of e.reselectKeys) { - if(this._removeCodeKeys[reselectKey]) { - this._removeCodeFields[i] = true; - removeCode = true; - break; - } - } - if(!removeCode) e.setContext(this, i); + if (action == Zotero.Integration.Citation.DELETE) { + this._removeCodeFields[i] = true; + // Mark for removal and continue + continue; + } else if (action == Zotero.Integration.Citation.UPDATE) { + this._session.updateIndices[index] = true; } - - if(!removeCode) { - if(this.fieldErrorHandler) return this.fieldErrorHandler(e); - throw e; + } catch(e) { + if (e instanceof Zotero.Integration.CorruptFieldException) { + e.setContext(this, i); } + + if (this.fieldErrorHandler) return this.fieldErrorHandler(e); + throw e; } + yield this._session.addCitation(i, noteIndex, citation); } else if (field.type === INTEGRATION_TYPE_BIBLIOGRAPHY) { if (this.ignoreEmptyBibliography && field.text.trim() === "") { this._removeCodeFields[i] = true; @@ -1194,23 +1132,16 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, // If there is no citation, we're deleting it, or we shouldn't update it, ignore // it if(!citation || citation.properties.delete) continue; - var isRich = false; if(!citation.properties.dontUpdate) { var formattedCitation = citation.properties.custom ? citation.properties.custom : this._session.citationText[i]; - if(formattedCitation.indexOf("\\") !== -1) { - // need to set text as RTF - formattedCitation = "{\\rtf "+formattedCitation+"}" - isRich = true; - } - if(forceCitations === FORCE_CITATIONS_RESET_TEXT || citation.properties.formattedCitation !== formattedCitation) { // Check if citation has been manually modified if(!ignoreCitationChanges && citation.properties.plainCitation) { - var plainCitation = field.getText(); + var plainCitation = field.text; if(plainCitation !== citation.properties.plainCitation) { // Citation manually modified; ask user if they want to save changes Zotero.debug("[_updateDocument] Attempting to update manually modified citation.\n" @@ -1228,25 +1159,19 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, } if(!citation.properties.dontUpdate) { - field.setText(formattedCitation, isRich); + field.text = formattedCitation; citation.properties.formattedCitation = formattedCitation; - citation.properties.plainCitation = field.getText(); + citation.properties.plainCitation = field.text; } } } - var fieldCode = this._session.getCitationField(citation); - if(fieldCode != citation.properties.field) { - field.setCode(`ITEM CSL_CITATION ${fieldCode}`); - if(this._session.data.prefs.fieldType === "ReferenceMark" - && this._session.data.prefs.noteType != 0 && isRich - && !citation.properties.dontUpdate) { - // For ReferenceMarks with formatting, we need to set the text again, because - // setting the field code removes formatting from the mark. I don't like this. - field.setText(formattedCitation, isRich); - } + var serializedCitation = citation.serialize(); + if (serializedCitation != citation.properties.field) { + field.code = serializedCitation; } + field.writeToDoc(); nUpdated++; } @@ -1260,7 +1185,7 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, if(forceBibliography || this._session.bibliographyDataHasChanged) { var bibliographyData = this._session.getBibliographyData(); for (let field of bibliographyFields) { - field.setCode(`BIBL ${bibliographyData} CSL_BIBLIOGRAPHY`); + field.code = bibliographyData; } } @@ -1295,11 +1220,12 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, yield; } - if(bibliographyText) { - field.setText(bibliographyText, true); + if (bibliographyText) { + field.text = bibliographyText; } else { - field.setText("{Bibliography}", false); + field.text = "{Bibliography}"; } + field.writeToDoc(); nUpdated += 5; } } @@ -1321,64 +1247,24 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations, * Brings up the addCitationDialog, prepopulated if a citation is provided */ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(function* (field) { - var newField, citation; - - // TODO: refactor citation/field preparation - // Citation loading should be moved into Zotero.Integration.Citation + var newField; - // if there's already a citation, make sure we have item IDs in addition to keys if (field) { field = Zotero.Integration.Field.loadExisting(field); - if (field.type != INTEGRATION_TYPE_ITEM) { + + if (field.type != INTEGRATION_TYPE_ITEM) { throw new Zotero.Exception.Alert("integration.error.notInCitation"); } - - try { - citation = field.unserialize(); - } catch(e) {} - - if (citation) { - try { - yield this._session.lookupItems(citation); - } catch(e) { - if(e instanceof Zotero.Integration.MissingItemException) { - citation.citationItems = []; - } else { - throw e; - } - } - - if(citation.properties.dontUpdate - || (citation.properties.plainCitation - && field.getText() !== citation.properties.plainCitation)) { - this._doc.activate(); - Zotero.debug("[addEditCitation] Attempting to update manually modified citation.\n" - + "citation.properties.dontUpdate: " + citation.properties.dontUpdate + "\n" - + "Original: " + citation.properties.plainCitation + "\n" - + "Current: " + field.getText() - ); - if(!this._doc.displayAlert(Zotero.getString("integration.citationChanged.edit"), - DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL)) { - throw new Zotero.Exception.UserCancelled("editing citation"); - } - } - - // make sure it's going to get updated - delete citation.properties["formattedCitation"]; - delete citation.properties["plainCitation"]; - delete citation.properties["dontUpdate"]; - } } else { newField = true; field = new Zotero.Integration.CitationField(yield this.addField(true)); - } - - if (!citation) { field.clearCode(); field.writeToDoc(); - citation = {"citationItems":[], "properties":{}}; } + var citation = new Zotero.Integration.Citation(field); + yield citation.prepareForEditing(); + // ------------------- // Preparing stuff to pass into CitationEditInterface var fieldIndexPromise = this.get().then(function(fields) { @@ -1399,6 +1285,7 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f citation.properties.noteIndex = field.getNoteIndex(); var previewFn = Zotero.Promise.coroutine(function* (citation) { let idx = yield fieldIndexPromise; + yield citationsByItemIDPromise; let citationsPre, citationsPost, citationIndices; [citationsPre, citationsPost, citationIndices] = this._session._getPrePost(idx); try { @@ -1409,38 +1296,32 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f }.bind(this)); var io = new Zotero.Integration.CitationEditInterface( - // Clone citation - JSON.parse(JSON.stringify(citation)), + citation, this._session.style.opt.sort_citations, fieldIndexPromise, citationsByItemIDPromise, previewFn ); if (Zotero.Prefs.get("integration.useClassicAddCitationDialog")) { - Zotero.Integration.displayDialog(this._doc, - 'chrome://zotero/content/integration/addCitationDialog.xul', 'alwaysRaised,resizable', - io); + Zotero.Integration.displayDialog('chrome://zotero/content/integration/addCitationDialog.xul', + 'alwaysRaised,resizable', io); } else { var mode = (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised') ? 'popup' : 'alwaysRaised')+',resizable=false'; - Zotero.Integration.displayDialog(this._doc, - 'chrome://zotero/content/integration/quickFormat.xul', mode, io); + Zotero.Integration.displayDialog('chrome://zotero/content/integration/quickFormat.xul', + mode, io); } // ------------------- - // io.promise resolves/rejects when the citation dialog is closed - try { - this.progressCallback = yield io.promise; - } catch (e) { + // io.promise resolves when the citation dialog is closed + this.progressCallback = yield io.promise; + + if (!io.citation.citationItems.length) { + // Try to delete new field on cancel if (newField) { - // Try to delete new field on failure try { field.delete(); } catch(e) {} - throw e; } - } - - if (!io.citation.citationItems.length) { throw new Zotero.Exception.UserCancelled("inserting citation"); } @@ -1644,8 +1525,7 @@ Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(func // Make sure styles are initialized for new docs yield Zotero.Styles.init(); - yield Zotero.Integration.displayDialog(this.doc, - 'chrome://zotero/content/integration/integrationDocPrefs.xul', '', io); + yield Zotero.Integration.displayDialog('chrome://zotero/content/integration/integrationDocPrefs.xul', '', io); if (!io.style || !io.fieldType) { throw new Zotero.Exception.UserCancelled("document preferences window"); @@ -1682,97 +1562,6 @@ Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(func }) /** - * Reselects an item to replace a deleted item - * @param exception {Zotero.Integration.MissingItemException} - */ -Zotero.Integration.Session.prototype.reselectItem = function(doc, exception) { - var io = new function() { this.wrappedJSObject = this; }, - me = this; - io.addBorder = Zotero.isWin; - io.singleSelection = true; - - return Zotero.Integration.displayDialog(doc, 'chrome://zotero/content/selectItemsDialog.xul', - 'resizable', io).then(function() { - if(io.dataOut && io.dataOut.length) { - var itemID = io.dataOut[0]; - - // add reselected item IDs to hash, so they can be used - for (let reselectKey of exception.reselectKeys) { - me.reselectedItems[reselectKey] = itemID; - } - // add old URIs to map, so that they will be included - if(exception.reselectKeyType == RESELECT_KEY_URI) { - me.uriMap.add(itemID, exception.reselectKeys.concat(me.uriMap.getURIsForItemID(itemID))); - } - // flag for update - me.updateItemIDs[itemID] = true; - } - }); -} - -/** - * Generates a field from a citation object - */ -Zotero.Integration.Session.prototype.getCitationField = function(citation) { - const saveProperties = ["custom", "unsorted", "formattedCitation", "plainCitation", "dontUpdate"]; - const saveCitationItemKeys = ["locator", "label", "suppress-author", "author-only", "prefix", - "suffix"]; - - var type; - var field = []; - - field.push('"citationID":'+uneval(citation.citationID)); - - var properties = JSON.stringify(citation.properties, saveProperties); - if(properties != "{}") { - field.push('"properties":'+properties); - } - - var m = citation.citationItems.length; - var citationItems = new Array(m); - for(var j=0; j<m; j++) { - var citationItem = citation.citationItems[j], - serializeCitationItem = {}, - key, value; - - // add URI and itemData - var slashIndex; - if(typeof citationItem.id === "string" && (slashIndex = citationItem.id.indexOf("/")) !== -1) { - // this is an embedded item - serializeCitationItem.id = citationItem.itemData.id; - serializeCitationItem.uris = citationItem.uris; - - // XXX For compatibility with older versions of Zotero; to be removed at a later date - serializeCitationItem.uri = serializeCitationItem.uris; - - // always store itemData, since we have no way to get it back otherwise - serializeCitationItem.itemData = citationItem.itemData; - } else { - serializeCitationItem.id = citationItem.id; - serializeCitationItem.uris = this.uriMap.getURIsForItemID(citationItem.id); - - // XXX For compatibility with older versions of Zotero; to be removed at a later date - serializeCitationItem.uri = serializeCitationItem.uris; - - serializeCitationItem.itemData = this.style.sys.retrieveItem(citationItem.id); - } - - // copy saveCitationItemKeys - for(var i=0, n=saveCitationItemKeys.length; i<n; i++) { - if((value = citationItem[(key = saveCitationItemKeys[i])])) { - serializeCitationItem[key] = value; - } - } - - citationItems[j] = JSON.stringify(serializeCitationItem); - } - field.push('"citationItems":['+citationItems.join(",")+"]"); - field.push('"schema":"https://github.com/citation-style-language/schema/raw/master/csl-citation.json"'); - - return "{"+field.join(",")+"}"; -} - -/** * Adds a citation based on a serialized Word field */ Zotero.Integration._oldCitationLocatorMap = { @@ -1786,10 +1575,7 @@ Zotero.Integration._oldCitationLocatorMap = { */ Zotero.Integration.Session.prototype.addCitation = Zotero.Promise.coroutine(function* (index, noteIndex, citation) { var index = parseInt(index, 10); - - // get items - yield this.lookupItems(citation, index); - + citation.properties.added = true; citation.properties.zoteroIndex = index; citation.properties.noteIndex = noteIndex; @@ -1829,122 +1615,6 @@ Zotero.Integration.Session.prototype.addCitation = Zotero.Promise.coroutine(func }); /** - * Looks up item IDs to correspond with keys or generates embedded items for given citation object. - * Throws a MissingItemException if item was not found. - */ -Zotero.Integration.Session.prototype.lookupItems = Zotero.Promise.coroutine(function* (citation, index) { - let items = []; - - for(var i=0, n=citation.citationItems.length; i<n; i++) { - var citationItem = citation.citationItems[i]; - - // get Zotero item - var zoteroItem = false, - needUpdate; - if(citationItem.uris) { - [zoteroItem, needUpdate] = yield this.uriMap.getZoteroItemForURIs(citationItem.uris); - if(needUpdate && index) this.updateIndices[index] = true; - - // Unfortunately, people do weird things with their documents. One weird thing people - // apparently like to do (http://forums.zotero.org/discussion/22262/) is to copy and - // paste citations from other documents created with earlier versions of Zotero into - // their documents and then not refresh the document. Usually, this isn't a problem. If - // document is edited by the same user, it will work without incident. If the first - // citation of a given item doesn't contain itemData, the user will get a - // MissingItemException. However, it may also happen that the first citation contains - // itemData, but later citations don't, because the user inserted the item properly and - // then copied and pasted the same citation from another document. We check for that - // possibility here. - if(zoteroItem.cslItemData && !citationItem.itemData) { - citationItem.itemData = zoteroItem.cslItemData; - this.updateIndices[index] = true; - } - } else { - if(citationItem.key && citationItem.libraryID) { - // DEBUG: why no library id? - zoteroItem = Zotero.Items.getByLibraryAndKey(citationItem.libraryID, citationItem.key); - } else if(citationItem.itemID) { - zoteroItem = Zotero.Items.get(citationItem.itemID); - } else if(citationItem.id) { - zoteroItem = Zotero.Items.get(citationItem.id); - } - if(zoteroItem && index) this.updateIndices[index] = true; - } - - // if no item, check if it was already reselected and otherwise handle as a missing item - if(!zoteroItem) { - if(citationItem.uris) { - var reselectKeys = citationItem.uris; - var reselectKeyType = RESELECT_KEY_URI; - } else if(citationItem.key) { - var reselectKeys = [citationItem.key]; - var reselectKeyType = RESELECT_KEY_ITEM_KEY; - } else if(citationItem.id) { - var reselectKeys = [citationItem.id]; - var reselectKeyType = RESELECT_KEY_ITEM_ID; - } else { - var reselectKeys = [citationItem.itemID]; - var reselectKeyType = RESELECT_KEY_ITEM_ID; - } - - // look to see if item has already been reselected - for (let reselectKey of reselectKeys) { - if(this.reselectedItems[reselectKey]) { - zoteroItem = Zotero.Items.get(this.reselectedItems[reselectKey]); - citationItem.id = zoteroItem.id; - if(index) this.updateIndices[index] = true; - break; - } - } - - if(!zoteroItem) { - if(citationItem.itemData) { - // add new embedded item - var itemData = Zotero.Utilities.deepCopy(citationItem.itemData); - - // assign a random string as an item ID - var anonymousID = Zotero.randomString(); - var globalID = itemData.id = citationItem.id = this.data.sessionID+"/"+anonymousID; - this.embeddedItems[anonymousID] = itemData; - - // assign a Zotero item - var surrogateItem = this.embeddedZoteroItems[anonymousID] = new Zotero.Item(); - Zotero.Utilities.itemFromCSLJSON(surrogateItem, itemData); - surrogateItem.cslItemID = globalID; - surrogateItem.cslURIs = citationItem.uris; - surrogateItem.cslItemData = itemData; - - for(var j=0, m=citationItem.uris.length; j<m; j++) { - this.embeddedZoteroItemsByURI[citationItem.uris[j]] = surrogateItem; - } - } else { - // if not already reselected, throw a MissingItemException - throw(new Zotero.Integration.MissingItemException( - reselectKeys, reselectKeyType, i, citation.citationItems.length)); - } - } - } - - if(zoteroItem) { - if (zoteroItem.cslItemID) { - citationItem.id = zoteroItem.cslItemID; - } - else { - citationItem.id = zoteroItem.id; - items.push(zoteroItem); - } - } - } - - // Items may be in libraries that haven't been loaded, and retrieveItem() is synchronous, so load - // all data (as required by toJSON(), which is used by itemToExportFormat(), which is used by - // itemToCSLJSON()) now - if (items.length) { - yield Zotero.Items.loadDataTypes(items); - } -}); - -/** * Gets integration bibliography */ Zotero.Integration.Session.prototype.getBibliography = function() { @@ -2245,14 +1915,13 @@ Zotero.Integration.Session.prototype.getBibliographyData = function() { /** * Edits integration bibliography */ -Zotero.Integration.Session.prototype.editBibliography = function(doc) { +Zotero.Integration.Session.prototype.editBibliography = function() { var bibliographyEditor = new Zotero.Integration.Session.BibliographyEditInterface(this); var io = new function() { this.wrappedJSObject = bibliographyEditor; } this.bibliographyDataHasChanged = this.bibliographyHasChanged = true; - return Zotero.Integration.displayDialog(doc, - 'chrome://zotero/content/integration/editBibliographyDialog.xul', 'resizable', io); + return Zotero.Integration.displayDialog('chrome://zotero/content/integration/editBibliographyDialog.xul', 'resizable', io); } /** @@ -2620,16 +2289,8 @@ Zotero.Integration.Field = class { this.code = '{}'; }; - writeToDoc(doc) { - let text = this.text; - let isRich = false; - // If RTF wrap with RTF tags - if (text.indexOf("\\") !== -1) { - text = "{\\rtf "+text+"}"; - isRich = true; - } - this._field.setText(text, isRich); - + writeToDoc() { + if (!this.dirty) return; // Boo. Inconsistent. if (this.type == INTEGRATION_TYPE_ITEM) { this._field.setCode(`ITEM CSL_CITATION ${this.code}`); @@ -2639,6 +2300,16 @@ Zotero.Integration.Field = class { this._field.setCode(`TEMP`); } this.dirty = false; + + // NB: Setting code in LO removes rtf formatting, so the order here is important + let text = this.text; + let isRich = false; + // If RTF wrap with RTF tags + if (text.includes("\\")) { + text = "{\\rtf "+text+"}"; + isRich = true; + } + this._field.setText(text, isRich); // Retrigger retrieval from doc. this._text = null; @@ -2651,7 +2322,7 @@ Zotero.Integration.Field = class { if (start == -1) { return '{}'; } - return code.substring(start, code.indexOf('}')+1); + return code.substring(start, code.lastIndexOf('}')+1); }; }; @@ -2841,3 +2512,236 @@ Zotero.Integration.BibliographyField = class extends Zotero.Integration.Field { } } }; + +Zotero.Integration.Citation = class { + constructor(citationField) { + let data = citationField.unserialize(); + this.citationItems = data.citationItems; + this.properties = data.properties; + this.properties.noteIndex = citationField.getNoteIndex(); + + this._field = citationField; + } + + /** + * Load item data for current item + * @param {Boolean} [promptToReselect=true] - will throw a MissingItemException if false + * @returns {Promise{Number}} + * - Zotero.Integration.Citation.NO_ACTION + * - Zotero.Integration.Citation.UPDATE + * - Zotero.Integration.Citation.DELETE + */ + loadItemData() { + return Zotero.Promise.coroutine(function *(promptToReselect=true){ + let items = []; + var needUpdate = false; + + for (var i=0, n=this.citationItems.length; i<n; i++) { + var citationItem = this.citationItems[i]; + + // get Zotero item + var zoteroItem = false; + if (citationItem.uris) { + let itemNeedUpdate; + [zoteroItem, itemNeedUpdate] = yield Zotero.Integration.currentSession.uriMap.getZoteroItemForURIs(citationItem.uris); + needUpdate = needUpdate || itemNeedUpdate; + + // Unfortunately, people do weird things with their documents. One weird thing people + // apparently like to do (http://forums.zotero.org/discussion/22262/) is to copy and + // paste citations from other documents created with earlier versions of Zotero into + // their documents and then not refresh the document. Usually, this isn't a problem. If + // document is edited by the same user, it will work without incident. If the first + // citation of a given item doesn't contain itemData, the user will get a + // MissingItemException. However, it may also happen that the first citation contains + // itemData, but later citations don't, because the user inserted the item properly and + // then copied and pasted the same citation from another document. We check for that + // possibility here. + if (zoteroItem.cslItemData && !citationItem.itemData) { + citationItem.itemData = zoteroItem.cslItemData; + needUpdate = true; + } + } else { + if (citationItem.key && citationItem.libraryID) { + // DEBUG: why no library id? + zoteroItem = Zotero.Items.getByLibraryAndKey(citationItem.libraryID, citationItem.key); + } else if (citationItem.itemID) { + zoteroItem = Zotero.Items.get(citationItem.itemID); + } else if (citationItem.id) { + zoteroItem = Zotero.Items.get(citationItem.id); + } + if (zoteroItem) needUpdate = true; + } + + // Item no longer in library + if (!zoteroItem) { + // Use embedded item + if (citationItem.itemData) { + // add new embedded item + var itemData = Zotero.Utilities.deepCopy(citationItem.itemData); + + // assign a random string as an item ID + var anonymousID = Zotero.randomString(); + var globalID = itemData.id = citationItem.id = Zotero.Integration.currentSession.data.sessionID+"/"+anonymousID; + Zotero.Integration.currentSession.embeddedItems[anonymousID] = itemData; + + // assign a Zotero item + var surrogateItem = Zotero.Integration.currentSession.embeddedZoteroItems[anonymousID] = new Zotero.Item(); + Zotero.Utilities.itemFromCSLJSON(surrogateItem, itemData); + surrogateItem.cslItemID = globalID; + surrogateItem.cslURIs = citationItem.uris; + surrogateItem.cslItemData = itemData; + + for(var j=0, m=citationItem.uris.length; j<m; j++) { + Zotero.Integration.currentSession.embeddedZoteroItemsByURI[citationItem.uris[j]] = surrogateItem; + } + } else if (promptToReselect) { + zoteroItem = yield this.handleMissingItem(i); + if (zoteroItem) needUpdate = true; + else return Zotero.Integration.Citation.DELETE; + } else { + // throw a MissingItemException + throw (new Zotero.Integration.MissingItemException(this, i)); + } + } + + if (zoteroItem) { + if (zoteroItem.cslItemID) { + citationItem.id = zoteroItem.cslItemID; + } + else { + citationItem.id = zoteroItem.id; + items.push(zoteroItem); + } + } + } + + // Items may be in libraries that haven't been loaded, and retrieveItem() is synchronous, so load + // all data (as required by toJSON(), which is used by itemToExportFormat(), which is used by + // itemToCSLJSON()) now + if (items.length) { + yield Zotero.Items.loadDataTypes(items); + } + return needUpdate ? Zotero.Integration.Citation.UPDATE : Zotero.Integration.Citation.NO_ACTION; + }).apply(this, arguments); + } + + handleMissingItem() { + return Zotero.Promise.coroutine(function* (idx) { + // Ask user what to do with this item + if (this.citationItems.length == 1) { + var msg = Zotero.getString("integration.missingItem.single"); + } else { + var msg = Zotero.getString("integration.missingItem.multiple", (idx).toString()); + } + msg += '\n\n'+Zotero.getString('integration.missingItem.description'); + this._field.select(); + Zotero.Integration.currentDoc.activate(); + var result = Zotero.Integration.currentDoc.displayAlert(msg, 1, 3); + if (result == 0) { // Cancel + throw new Zotero.Exception.UserCancelled("document update"); + } else if(result == 1) { // No + return false; + } + + // Yes - prompt to reselect + var io = new function() { this.wrappedJSObject = this; }; + + io.addBorder = Zotero.isWin; + io.singleSelection = true; + + yield Zotero.Integration.displayDialog('chrome://zotero/content/selectItemsDialog.xul', 'resizable', io); + + if (io.dataOut && io.dataOut.length) { + return Zotero.Items.get(io.dataOut[0]); + } + }).apply(this, arguments); + } + + prepareForEditing() { + return Zotero.Promise.coroutine(function *(){ + // Check for modified field text or dontUpdate flag + if (this.properties.dontUpdate + || (this.properties.plainCitation + && this._field.text !== this.properties.plainCitation)) { + Zotero.Integration.currentDoc.activate(); + Zotero.debug("[addEditCitation] Attempting to update manually modified citation.\n" + + "citaion.properties.dontUpdate: " + this.properties.dontUpdate + "\n" + + "Original: " + this.properties.plainCitation + "\n" + + "Current: " + this._field.text + ); + if (!Zotero.Integration.currentDoc.displayAlert(Zotero.getString("integration.citationChanged.edit"), + DIALOG_ICON_WARNING, DIALOG_BUTTONS_OK_CANCEL)) { + throw new Zotero.Exception.UserCancelled("editing citation"); + } + } + + // make sure it's going to get updated + delete this.properties["formattedCitation"]; + delete this.properties["plainCitation"]; + delete this.properties["dontUpdate"]; + + // Load items to be displayed in edit dialog + yield this.loadItemData(); + + }).apply(this, arguments); + } + + /** + * Serializes the citation into CSL code representation + * @returns {string} + */ + serialize() { + const saveProperties = ["custom", "unsorted", "formattedCitation", "plainCitation", "dontUpdate"]; + const saveCitationItemKeys = ["locator", "label", "suppress-author", "author-only", "prefix", + "suffix"]; + + var citation = {}; + + citation.citationID = this.citationID; + + citation.properties = {}; + for (let key of saveProperties) { + if (key in this.properties) citation.properties[key] = this.properties[key]; + } + + citation.citationItems = new Array(this.citationItems.length); + for (let i=0; i < this.citationItems.length; i++) { + var citationItem = this.citationItems[i], + serializeCitationItem = {}; + + // add URI and itemData + var slashIndex; + if (typeof citationItem.id === "string" && (slashIndex = citationItem.id.indexOf("/")) !== -1) { + // this is an embedded item + serializeCitationItem.id = citationItem.itemData.id; + serializeCitationItem.uris = citationItem.uris; + + // XXX For compatibility with older versions of Zotero; to be removed at a later date + serializeCitationItem.uri = serializeCitationItem.uris; + + // always store itemData, since we have no way to get it back otherwise + serializeCitationItem.itemData = citationItem.itemData; + } else { + serializeCitationItem.id = citationItem.id; + serializeCitationItem.uris = Zotero.Integration.currentSession.uriMap.getURIsForItemID(citationItem.id); + + // XXX For compatibility with older versions of Zotero; to be removed at a later date + serializeCitationItem.uri = serializeCitationItem.uris; + + serializeCitationItem.itemData = Zotero.Integration.currentSession.style.sys.retrieveItem(citationItem.id); + } + + for (let key of saveCitationItemKeys) { + if (key in citationItem) serializeCitationItem[key] = citationItem[key]; + } + + citation.citationItems[i] = serializeCitationItem; + } + citation.schema = "https://github.com/citation-style-language/schema/raw/master/csl-citation.json"; + + return JSON.stringify(citation); + } +}; +Zotero.Integration.Citation.NO_ACTION = 0; +Zotero.Integration.Citation.UPDATE = 1; +Zotero.Integration.Citation.DELETE = 2; diff --git a/test/tests/integrationTest.js b/test/tests/integrationTest.js @@ -273,14 +273,16 @@ describe("Zotero.Integration", function () { function setAddEditItems(items) { if (items.length == undefined) items = [items]; - dialogResults.quickFormat = function(doc, dialogName) { + dialogResults.quickFormat = function(dialogName) { var citationItems = items.map((i) => {return {id: i.id} }); - var field = doc.insertField("Field", 0); - field.setCode('TEMP'); + var field = new Zotero.Integration.CitationField(Zotero.Integration.currentDoc.insertField("Field", 0)); + field.clearCode(); + field.writeToDoc(); + var citation = new Zotero.Integration.Citation(field); var integrationDoc = addEditCitationSpy.lastCall.thisValue; var fieldGetter = new Zotero.Integration.Fields(integrationDoc._session, integrationDoc._doc, () => 0); var io = new Zotero.Integration.CitationEditInterface( - { citationItems, properties: {} }, + citation, field, fieldGetter, integrationDoc._session @@ -313,10 +315,11 @@ describe("Zotero.Integration", function () { // possible bug that reset() erases callsFake. // @NOTE: https://github.com/sinonjs/sinon/issues/1341 // displayDialogStub.callsFake(function(doc, dialogName, prefs, io) { - function(doc, dialogName, prefs, io) { + function(dialogName, prefs, io) { + Zotero.debug(`Display dialog: ${dialogName}`, 2); var ioResult = dialogResults[dialogName.substring(dialogName.lastIndexOf('/')+1, dialogName.length-4)]; if (typeof ioResult == 'function') { - ioResult = ioResult(doc, dialogName); + ioResult = ioResult(dialogName); } Object.assign(io, ioResult); return Zotero.Promise.resolve(); @@ -460,7 +463,7 @@ describe("Zotero.Integration", function () { displayDialogStub.reset(); yield execCommand('addEditBibliography', docID); assert.isTrue(displayDialogStub.calledOnce); - assert.isTrue(displayDialogStub.lastCall.args[1].includes('editBibliographyDialog')); + assert.isTrue(displayDialogStub.lastCall.args[0].includes('editBibliographyDialog')); }); }); });