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
This commit is contained in:
Adomas Venčkauskas 2017-05-25 10:48:43 +03:00
parent a1acbd4038
commit f44d563a15
3 changed files with 329 additions and 422 deletions

View file

@ -526,7 +526,7 @@ Zotero.Cite.System.prototype = {
} }
if(!zoteroItem) { 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); var cslItem = Zotero.Utilities.itemToCSLJSON(zoteroItem);

View file

@ -305,8 +305,8 @@ Zotero.Integration = new function() {
* @param {String} [io] Data to pass to the window * @param {String} [io] Data to pass to the window
* @return {Promise} Promise resolved when the window is closed * @return {Promise} Promise resolved when the window is closed
*/ */
this.displayDialog = function displayDialog(doc, url, options, io) { this.displayDialog = function displayDialog(url, options, io) {
doc.cleanup(); Zotero.Integration.currentDoc.cleanup();
var allOptions = 'chrome,centerscreen'; var allOptions = 'chrome,centerscreen';
// without this, Firefox gets raised with our windows under Compiz // 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. * 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) { Zotero.Integration.MissingItemException = function() {};
this.reselectKeys = reselectKeys;
this.reselectKeyType = reselectKeyType;
this.citationIndex = citationIndex;
this.citationLength = citationLength;
}
Zotero.Integration.MissingItemException.prototype = { Zotero.Integration.MissingItemException.prototype = {
"name":"MissingItemException", "name":"MissingItemException",
"message":"An item in this document is missing from your Zotero library.", "message":"An item in this document is missing from your Zotero library.",
"toString":function() { return this.message }, "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);
});
}
}
}
Zotero.Integration.CorruptFieldException = function(code, cause) { Zotero.Integration.CorruptFieldException = function(code, cause) {
this.code = code; this.code = code;
@ -697,9 +644,9 @@ Zotero.Integration.Interface.prototype.addEditCitation = function() {
* Adds a bibliography to the current document. * Adds a bibliography to the current document.
* @return {Promise} * @return {Promise}
*/ */
Zotero.Integration.Interface.prototype.addBibliography = function() { Zotero.Integration.Interface.prototype.addBibliography = Zotero.Promise.coroutine(function* () {
var me = this; var me = this;
return this._prepareData(true, false).then(function() { yield this._prepareData(true, false);
// Make sure we can have a bibliography // Make sure we can have a bibliography
if(!me._session.data.style.hasBibliography) { if(!me._session.data.style.hasBibliography) {
throw new Zotero.Exception.Alert("integration.error.noBibliography", [], throw new Zotero.Exception.Alert("integration.error.noBibliography", [],
@ -707,16 +654,13 @@ Zotero.Integration.Interface.prototype.addBibliography = function() {
} }
var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError); var fieldGetter = new Zotero.Integration.Fields(me._session, me._doc, Zotero.Integration.onFieldError);
return fieldGetter.addField().then(function(field) { let field = new Zotero.Integration.BibliographyField(yield fieldGetter.addField());
field.clearCode(); field.clearCode();
field.type = INTEGRATION_TYPE_BIBLIOGRAPHY; field.type = INTEGRATION_TYPE_BIBLIOGRAPHY;
field.writeToDoc(); field.writeToDoc();
return fieldGetter.updateSession().then(function() { yield fieldGetter.updateSession();
return fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false); yield fieldGetter.updateDocument(FORCE_CITATIONS_FALSE, true, false);
}); })
});
});
}
/** /**
* Edits bibliography metadata. * Edits bibliography metadata.
@ -774,11 +718,10 @@ Zotero.Integration.Interface.prototype.addEditBibliography = Zotero.Promise.coro
if (haveBibliography) { if (haveBibliography) {
yield fieldGetter.updateSession(); yield fieldGetter.updateSession();
yield this._session.editBibliography(this._doc); yield this._session.editBibliography();
} else { } else {
var field = new Zotero.Integration.BibliographyField(yield fieldGetter.addField()); var field = new Zotero.Integration.BibliographyField(yield fieldGetter.addField());
field.clearCode(); field.clearCode();
field.type = INTEGRATION_TYPE_BIBLIOGRAPHY;
field.writeToDoc(); field.writeToDoc();
yield fieldGetter.updateSession(); yield fieldGetter.updateSession();
} }
@ -921,7 +864,6 @@ Zotero.Integration.Fields = function(session, doc, fieldErrorHandler) {
this._doc = doc; this._doc = doc;
this._deferreds = null; this._deferreds = null;
this._removeCodeKeys = {};
this._removeCodeFields = {}; this._removeCodeFields = {};
this._bibliographyFields = []; this._bibliographyFields = [];
this._bibliographyData = ""; this._bibliographyData = "";
@ -1054,7 +996,6 @@ Zotero.Integration.Fields.prototype.updateSession = Zotero.Promise.coroutine(fun
yield this.get(); yield this.get();
this._session.resetRequest(this._doc); this._session.resetRequest(this._doc);
this._removeCodeKeys = {};
this._removeCodeFields = {}; this._removeCodeFields = {};
this._bibliographyFields = []; this._bibliographyFields = [];
this._bibliographyData = ""; this._bibliographyData = "";
@ -1104,31 +1045,28 @@ Zotero.Integration.Fields.prototype._processFields = Zotero.Promise.coroutine(fu
for(var n = this._fields.length; i<n; i++) { for(var n = this._fields.length; i<n; i++) {
let field = Zotero.Integration.Field.loadExisting(this._fields[i]); let field = Zotero.Integration.Field.loadExisting(this._fields[i]);
if (field.type === INTEGRATION_TYPE_ITEM) { if (field.type === INTEGRATION_TYPE_ITEM) {
var noteIndex = field.getNoteIndex(); var noteIndex = field.getNoteIndex(),
citation;
try { try {
yield this._session.addCitation(i, noteIndex, field.unserialize()); citation = new Zotero.Integration.Citation(field);
} catch(e) { let action = yield citation.loadItemData();
var removeCode = false;
if(e instanceof Zotero.Integration.CorruptFieldException) { if (action == Zotero.Integration.Citation.DELETE) {
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; this._removeCodeFields[i] = true;
removeCode = true; // Mark for removal and continue
break; continue;
} else if (action == Zotero.Integration.Citation.UPDATE) {
this._session.updateIndices[index] = true;
} }
} } catch(e) {
if(!removeCode) e.setContext(this, i); if (e instanceof Zotero.Integration.CorruptFieldException) {
e.setContext(this, i);
} }
if(!removeCode) {
if (this.fieldErrorHandler) return this.fieldErrorHandler(e); if (this.fieldErrorHandler) return this.fieldErrorHandler(e);
throw e; throw e;
} }
} yield this._session.addCitation(i, noteIndex, citation);
} else if (field.type === INTEGRATION_TYPE_BIBLIOGRAPHY) { } else if (field.type === INTEGRATION_TYPE_BIBLIOGRAPHY) {
if (this.ignoreEmptyBibliography && field.text.trim() === "") { if (this.ignoreEmptyBibliography && field.text.trim() === "") {
this._removeCodeFields[i] = true; 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 // If there is no citation, we're deleting it, or we shouldn't update it, ignore
// it // it
if(!citation || citation.properties.delete) continue; if(!citation || citation.properties.delete) continue;
var isRich = false;
if(!citation.properties.dontUpdate) { if(!citation.properties.dontUpdate) {
var formattedCitation = citation.properties.custom var formattedCitation = citation.properties.custom
? citation.properties.custom : this._session.citationText[i]; ? 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 if(forceCitations === FORCE_CITATIONS_RESET_TEXT
|| citation.properties.formattedCitation !== formattedCitation) { || citation.properties.formattedCitation !== formattedCitation) {
// Check if citation has been manually modified // Check if citation has been manually modified
if(!ignoreCitationChanges && citation.properties.plainCitation) { if(!ignoreCitationChanges && citation.properties.plainCitation) {
var plainCitation = field.getText(); var plainCitation = field.text;
if(plainCitation !== citation.properties.plainCitation) { if(plainCitation !== citation.properties.plainCitation) {
// Citation manually modified; ask user if they want to save changes // Citation manually modified; ask user if they want to save changes
Zotero.debug("[_updateDocument] Attempting to update manually modified citation.\n" 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) { if(!citation.properties.dontUpdate) {
field.setText(formattedCitation, isRich); field.text = formattedCitation;
citation.properties.formattedCitation = formattedCitation; citation.properties.formattedCitation = formattedCitation;
citation.properties.plainCitation = field.getText(); citation.properties.plainCitation = field.text;
} }
} }
} }
var fieldCode = this._session.getCitationField(citation); var serializedCitation = citation.serialize();
if(fieldCode != citation.properties.field) { if (serializedCitation != citation.properties.field) {
field.setCode(`ITEM CSL_CITATION ${fieldCode}`); field.code = serializedCitation;
if(this._session.data.prefs.fieldType === "ReferenceMark"
&& this._session.data.prefs.noteType != 0 && isRich
&& !citation.properties.dontUpdate) {
// For ReferenceMarks with formatting, we need to set the text again, because
// setting the field code removes formatting from the mark. I don't like this.
field.setText(formattedCitation, isRich);
}
} }
field.writeToDoc();
nUpdated++; nUpdated++;
} }
@ -1260,7 +1185,7 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations,
if(forceBibliography || this._session.bibliographyDataHasChanged) { if(forceBibliography || this._session.bibliographyDataHasChanged) {
var bibliographyData = this._session.getBibliographyData(); var bibliographyData = this._session.getBibliographyData();
for (let field of bibliographyFields) { for (let field of bibliographyFields) {
field.setCode(`BIBL ${bibliographyData} CSL_BIBLIOGRAPHY`); field.code = bibliographyData;
} }
} }
@ -1296,10 +1221,11 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations,
} }
if (bibliographyText) { if (bibliographyText) {
field.setText(bibliographyText, true); field.text = bibliographyText;
} else { } else {
field.setText("{Bibliography}", false); field.text = "{Bibliography}";
} }
field.writeToDoc();
nUpdated += 5; nUpdated += 5;
} }
} }
@ -1321,64 +1247,24 @@ Zotero.Integration.Fields.prototype._updateDocument = function* (forceCitations,
* Brings up the addCitationDialog, prepopulated if a citation is provided * Brings up the addCitationDialog, prepopulated if a citation is provided
*/ */
Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(function* (field) { Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(function* (field) {
var newField, citation; var newField;
// TODO: refactor citation/field preparation
// Citation loading should be moved into Zotero.Integration.Citation
// if there's already a citation, make sure we have item IDs in addition to keys
if (field) { if (field) {
field = Zotero.Integration.Field.loadExisting(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"); 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 { } else {
newField = true; newField = true;
field = new Zotero.Integration.CitationField(yield this.addField(true)); field = new Zotero.Integration.CitationField(yield this.addField(true));
}
if (!citation) {
field.clearCode(); field.clearCode();
field.writeToDoc(); field.writeToDoc();
citation = {"citationItems":[], "properties":{}};
} }
var citation = new Zotero.Integration.Citation(field);
yield citation.prepareForEditing();
// ------------------- // -------------------
// Preparing stuff to pass into CitationEditInterface // Preparing stuff to pass into CitationEditInterface
var fieldIndexPromise = this.get().then(function(fields) { 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(); citation.properties.noteIndex = field.getNoteIndex();
var previewFn = Zotero.Promise.coroutine(function* (citation) { var previewFn = Zotero.Promise.coroutine(function* (citation) {
let idx = yield fieldIndexPromise; let idx = yield fieldIndexPromise;
yield citationsByItemIDPromise;
let citationsPre, citationsPost, citationIndices; let citationsPre, citationsPost, citationIndices;
[citationsPre, citationsPost, citationIndices] = this._session._getPrePost(idx); [citationsPre, citationsPost, citationIndices] = this._session._getPrePost(idx);
try { try {
@ -1409,38 +1296,32 @@ Zotero.Integration.Fields.prototype.addEditCitation = Zotero.Promise.coroutine(f
}.bind(this)); }.bind(this));
var io = new Zotero.Integration.CitationEditInterface( var io = new Zotero.Integration.CitationEditInterface(
// Clone citation citation,
JSON.parse(JSON.stringify(citation)),
this._session.style.opt.sort_citations, fieldIndexPromise, citationsByItemIDPromise, this._session.style.opt.sort_citations, fieldIndexPromise, citationsByItemIDPromise,
previewFn previewFn
); );
if (Zotero.Prefs.get("integration.useClassicAddCitationDialog")) { if (Zotero.Prefs.get("integration.useClassicAddCitationDialog")) {
Zotero.Integration.displayDialog(this._doc, Zotero.Integration.displayDialog('chrome://zotero/content/integration/addCitationDialog.xul',
'chrome://zotero/content/integration/addCitationDialog.xul', 'alwaysRaised,resizable', 'alwaysRaised,resizable', io);
io);
} else { } else {
var mode = (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised') var mode = (!Zotero.isMac && Zotero.Prefs.get('integration.keepAddCitationDialogRaised')
? 'popup' : 'alwaysRaised')+',resizable=false'; ? 'popup' : 'alwaysRaised')+',resizable=false';
Zotero.Integration.displayDialog(this._doc, Zotero.Integration.displayDialog('chrome://zotero/content/integration/quickFormat.xul',
'chrome://zotero/content/integration/quickFormat.xul', mode, io); mode, io);
} }
// ------------------- // -------------------
// io.promise resolves/rejects when the citation dialog is closed // io.promise resolves when the citation dialog is closed
try {
this.progressCallback = yield io.promise; this.progressCallback = yield io.promise;
} catch (e) {
if (!io.citation.citationItems.length) {
// Try to delete new field on cancel
if (newField) { if (newField) {
// Try to delete new field on failure
try { try {
field.delete(); field.delete();
} catch(e) {} } catch(e) {}
throw e;
} }
}
if (!io.citation.citationItems.length) {
throw new Zotero.Exception.UserCancelled("inserting citation"); 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 // Make sure styles are initialized for new docs
yield Zotero.Styles.init(); yield Zotero.Styles.init();
yield Zotero.Integration.displayDialog(this.doc, yield Zotero.Integration.displayDialog('chrome://zotero/content/integration/integrationDocPrefs.xul', '', io);
'chrome://zotero/content/integration/integrationDocPrefs.xul', '', io);
if (!io.style || !io.fieldType) { if (!io.style || !io.fieldType) {
throw new Zotero.Exception.UserCancelled("document preferences window"); throw new Zotero.Exception.UserCancelled("document preferences window");
@ -1681,97 +1561,6 @@ Zotero.Integration.Session.prototype.setDocPrefs = Zotero.Promise.coroutine(func
return oldData || null; return oldData || null;
}) })
/**
* Reselects an item to replace a deleted item
* @param exception {Zotero.Integration.MissingItemException}
*/
Zotero.Integration.Session.prototype.reselectItem = function(doc, exception) {
var io = new function() { this.wrappedJSObject = this; },
me = this;
io.addBorder = Zotero.isWin;
io.singleSelection = true;
return Zotero.Integration.displayDialog(doc, 'chrome://zotero/content/selectItemsDialog.xul',
'resizable', io).then(function() {
if(io.dataOut && io.dataOut.length) {
var itemID = io.dataOut[0];
// add reselected item IDs to hash, so they can be used
for (let reselectKey of exception.reselectKeys) {
me.reselectedItems[reselectKey] = itemID;
}
// add old URIs to map, so that they will be included
if(exception.reselectKeyType == RESELECT_KEY_URI) {
me.uriMap.add(itemID, exception.reselectKeys.concat(me.uriMap.getURIsForItemID(itemID)));
}
// flag for update
me.updateItemIDs[itemID] = true;
}
});
}
/**
* Generates a field from a citation object
*/
Zotero.Integration.Session.prototype.getCitationField = function(citation) {
const saveProperties = ["custom", "unsorted", "formattedCitation", "plainCitation", "dontUpdate"];
const saveCitationItemKeys = ["locator", "label", "suppress-author", "author-only", "prefix",
"suffix"];
var type;
var field = [];
field.push('"citationID":'+uneval(citation.citationID));
var properties = JSON.stringify(citation.properties, saveProperties);
if(properties != "{}") {
field.push('"properties":'+properties);
}
var m = citation.citationItems.length;
var citationItems = new Array(m);
for(var j=0; j<m; j++) {
var citationItem = citation.citationItems[j],
serializeCitationItem = {},
key, value;
// add URI and itemData
var slashIndex;
if(typeof citationItem.id === "string" && (slashIndex = citationItem.id.indexOf("/")) !== -1) {
// this is an embedded item
serializeCitationItem.id = citationItem.itemData.id;
serializeCitationItem.uris = citationItem.uris;
// XXX For compatibility with older versions of Zotero; to be removed at a later date
serializeCitationItem.uri = serializeCitationItem.uris;
// always store itemData, since we have no way to get it back otherwise
serializeCitationItem.itemData = citationItem.itemData;
} else {
serializeCitationItem.id = citationItem.id;
serializeCitationItem.uris = this.uriMap.getURIsForItemID(citationItem.id);
// XXX For compatibility with older versions of Zotero; to be removed at a later date
serializeCitationItem.uri = serializeCitationItem.uris;
serializeCitationItem.itemData = this.style.sys.retrieveItem(citationItem.id);
}
// copy saveCitationItemKeys
for(var i=0, n=saveCitationItemKeys.length; i<n; i++) {
if((value = citationItem[(key = saveCitationItemKeys[i])])) {
serializeCitationItem[key] = value;
}
}
citationItems[j] = JSON.stringify(serializeCitationItem);
}
field.push('"citationItems":['+citationItems.join(",")+"]");
field.push('"schema":"https://github.com/citation-style-language/schema/raw/master/csl-citation.json"');
return "{"+field.join(",")+"}";
}
/** /**
* Adds a citation based on a serialized Word field * Adds a citation based on a serialized Word field
*/ */
@ -1787,9 +1576,6 @@ Zotero.Integration._oldCitationLocatorMap = {
Zotero.Integration.Session.prototype.addCitation = Zotero.Promise.coroutine(function* (index, noteIndex, citation) { Zotero.Integration.Session.prototype.addCitation = Zotero.Promise.coroutine(function* (index, noteIndex, citation) {
var index = parseInt(index, 10); var index = parseInt(index, 10);
// get items
yield this.lookupItems(citation, index);
citation.properties.added = true; citation.properties.added = true;
citation.properties.zoteroIndex = index; citation.properties.zoteroIndex = index;
citation.properties.noteIndex = noteIndex; citation.properties.noteIndex = noteIndex;
@ -1828,122 +1614,6 @@ Zotero.Integration.Session.prototype.addCitation = Zotero.Promise.coroutine(func
this.documentCitationIDs[citation.citationID] = citation.citationID; this.documentCitationIDs[citation.citationID] = citation.citationID;
}); });
/**
* Looks up item IDs to correspond with keys or generates embedded items for given citation object.
* Throws a MissingItemException if item was not found.
*/
Zotero.Integration.Session.prototype.lookupItems = Zotero.Promise.coroutine(function* (citation, index) {
let items = [];
for(var i=0, n=citation.citationItems.length; i<n; i++) {
var citationItem = citation.citationItems[i];
// get Zotero item
var zoteroItem = false,
needUpdate;
if(citationItem.uris) {
[zoteroItem, needUpdate] = yield this.uriMap.getZoteroItemForURIs(citationItem.uris);
if(needUpdate && index) this.updateIndices[index] = true;
// Unfortunately, people do weird things with their documents. One weird thing people
// apparently like to do (http://forums.zotero.org/discussion/22262/) is to copy and
// paste citations from other documents created with earlier versions of Zotero into
// their documents and then not refresh the document. Usually, this isn't a problem. If
// document is edited by the same user, it will work without incident. If the first
// citation of a given item doesn't contain itemData, the user will get a
// MissingItemException. However, it may also happen that the first citation contains
// itemData, but later citations don't, because the user inserted the item properly and
// then copied and pasted the same citation from another document. We check for that
// possibility here.
if(zoteroItem.cslItemData && !citationItem.itemData) {
citationItem.itemData = zoteroItem.cslItemData;
this.updateIndices[index] = true;
}
} else {
if(citationItem.key && citationItem.libraryID) {
// DEBUG: why no library id?
zoteroItem = Zotero.Items.getByLibraryAndKey(citationItem.libraryID, citationItem.key);
} else if(citationItem.itemID) {
zoteroItem = Zotero.Items.get(citationItem.itemID);
} else if(citationItem.id) {
zoteroItem = Zotero.Items.get(citationItem.id);
}
if(zoteroItem && index) this.updateIndices[index] = true;
}
// if no item, check if it was already reselected and otherwise handle as a missing item
if(!zoteroItem) {
if(citationItem.uris) {
var reselectKeys = citationItem.uris;
var reselectKeyType = RESELECT_KEY_URI;
} else if(citationItem.key) {
var reselectKeys = [citationItem.key];
var reselectKeyType = RESELECT_KEY_ITEM_KEY;
} else if(citationItem.id) {
var reselectKeys = [citationItem.id];
var reselectKeyType = RESELECT_KEY_ITEM_ID;
} else {
var reselectKeys = [citationItem.itemID];
var reselectKeyType = RESELECT_KEY_ITEM_ID;
}
// look to see if item has already been reselected
for (let reselectKey of reselectKeys) {
if(this.reselectedItems[reselectKey]) {
zoteroItem = Zotero.Items.get(this.reselectedItems[reselectKey]);
citationItem.id = zoteroItem.id;
if(index) this.updateIndices[index] = true;
break;
}
}
if(!zoteroItem) {
if(citationItem.itemData) {
// add new embedded item
var itemData = Zotero.Utilities.deepCopy(citationItem.itemData);
// assign a random string as an item ID
var anonymousID = Zotero.randomString();
var globalID = itemData.id = citationItem.id = this.data.sessionID+"/"+anonymousID;
this.embeddedItems[anonymousID] = itemData;
// assign a Zotero item
var surrogateItem = this.embeddedZoteroItems[anonymousID] = new Zotero.Item();
Zotero.Utilities.itemFromCSLJSON(surrogateItem, itemData);
surrogateItem.cslItemID = globalID;
surrogateItem.cslURIs = citationItem.uris;
surrogateItem.cslItemData = itemData;
for(var j=0, m=citationItem.uris.length; j<m; j++) {
this.embeddedZoteroItemsByURI[citationItem.uris[j]] = surrogateItem;
}
} else {
// if not already reselected, throw a MissingItemException
throw(new Zotero.Integration.MissingItemException(
reselectKeys, reselectKeyType, i, citation.citationItems.length));
}
}
}
if(zoteroItem) {
if (zoteroItem.cslItemID) {
citationItem.id = zoteroItem.cslItemID;
}
else {
citationItem.id = zoteroItem.id;
items.push(zoteroItem);
}
}
}
// Items may be in libraries that haven't been loaded, and retrieveItem() is synchronous, so load
// all data (as required by toJSON(), which is used by itemToExportFormat(), which is used by
// itemToCSLJSON()) now
if (items.length) {
yield Zotero.Items.loadDataTypes(items);
}
});
/** /**
* Gets integration bibliography * Gets integration bibliography
*/ */
@ -2245,14 +1915,13 @@ Zotero.Integration.Session.prototype.getBibliographyData = function() {
/** /**
* Edits integration bibliography * 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 bibliographyEditor = new Zotero.Integration.Session.BibliographyEditInterface(this);
var io = new function() { this.wrappedJSObject = bibliographyEditor; } var io = new function() { this.wrappedJSObject = bibliographyEditor; }
this.bibliographyDataHasChanged = this.bibliographyHasChanged = true; this.bibliographyDataHasChanged = this.bibliographyHasChanged = true;
return Zotero.Integration.displayDialog(doc, return Zotero.Integration.displayDialog('chrome://zotero/content/integration/editBibliographyDialog.xul', 'resizable', io);
'chrome://zotero/content/integration/editBibliographyDialog.xul', 'resizable', io);
} }
/** /**
@ -2620,16 +2289,8 @@ Zotero.Integration.Field = class {
this.code = '{}'; this.code = '{}';
}; };
writeToDoc(doc) { writeToDoc() {
let text = this.text; if (!this.dirty) return;
let isRich = false;
// If RTF wrap with RTF tags
if (text.indexOf("\\") !== -1) {
text = "{\\rtf "+text+"}";
isRich = true;
}
this._field.setText(text, isRich);
// Boo. Inconsistent. // Boo. Inconsistent.
if (this.type == INTEGRATION_TYPE_ITEM) { if (this.type == INTEGRATION_TYPE_ITEM) {
this._field.setCode(`ITEM CSL_CITATION ${this.code}`); this._field.setCode(`ITEM CSL_CITATION ${this.code}`);
@ -2640,6 +2301,16 @@ Zotero.Integration.Field = class {
} }
this.dirty = false; 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. // Retrigger retrieval from doc.
this._text = null; this._text = null;
this._code = null; this._code = null;
@ -2651,7 +2322,7 @@ Zotero.Integration.Field = class {
if (start == -1) { if (start == -1) {
return '{}'; 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;

View file

@ -273,14 +273,16 @@ describe("Zotero.Integration", function () {
function setAddEditItems(items) { function setAddEditItems(items) {
if (items.length == undefined) items = [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 citationItems = items.map((i) => {return {id: i.id} });
var field = doc.insertField("Field", 0); var field = new Zotero.Integration.CitationField(Zotero.Integration.currentDoc.insertField("Field", 0));
field.setCode('TEMP'); field.clearCode();
field.writeToDoc();
var citation = new Zotero.Integration.Citation(field);
var integrationDoc = addEditCitationSpy.lastCall.thisValue; var integrationDoc = addEditCitationSpy.lastCall.thisValue;
var fieldGetter = new Zotero.Integration.Fields(integrationDoc._session, integrationDoc._doc, () => 0); var fieldGetter = new Zotero.Integration.Fields(integrationDoc._session, integrationDoc._doc, () => 0);
var io = new Zotero.Integration.CitationEditInterface( var io = new Zotero.Integration.CitationEditInterface(
{ citationItems, properties: {} }, citation,
field, field,
fieldGetter, fieldGetter,
integrationDoc._session integrationDoc._session
@ -313,10 +315,11 @@ describe("Zotero.Integration", function () {
// possible bug that reset() erases callsFake. // possible bug that reset() erases callsFake.
// @NOTE: https://github.com/sinonjs/sinon/issues/1341 // @NOTE: https://github.com/sinonjs/sinon/issues/1341
// displayDialogStub.callsFake(function(doc, dialogName, prefs, io) { // 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)]; var ioResult = dialogResults[dialogName.substring(dialogName.lastIndexOf('/')+1, dialogName.length-4)];
if (typeof ioResult == 'function') { if (typeof ioResult == 'function') {
ioResult = ioResult(doc, dialogName); ioResult = ioResult(dialogName);
} }
Object.assign(io, ioResult); Object.assign(io, ioResult);
return Zotero.Promise.resolve(); return Zotero.Promise.resolve();
@ -460,7 +463,7 @@ describe("Zotero.Integration", function () {
displayDialogStub.reset(); displayDialogStub.reset();
yield execCommand('addEditBibliography', docID); yield execCommand('addEditBibliography', docID);
assert.isTrue(displayDialogStub.calledOnce); assert.isTrue(displayDialogStub.calledOnce);
assert.isTrue(displayDialogStub.lastCall.args[1].includes('editBibliographyDialog')); assert.isTrue(displayDialogStub.lastCall.args[0].includes('editBibliographyDialog'));
}); });
}); });
}); });