Transition item Export Format to Zotero web API item JSON
* Enable legacy mode for export translators compatible with pre-4.0.27: * Add compatibility mappings, so that current translators don't break if they specify minVersion lower than 4.0.27. This does introduce non-compatible changes, specifically, "version" field in legacy mode is "versionNumber" in the new format. "version" in the new format corresponds to the "version" as specified for Zotero API JSON format. New translators should expect Zotero web API JSON format and should specify minVersion 4.0.27. * Update CSL mappings to comply with new itemToExportFormat * CSL JSON export translator needs to be updated to be compatible with 4.0.27 to export correct CSL JSON * Use item URI for id in CSL JSON instead of item ID * Fix note and attachment handling in itemToCSLJSON
This commit is contained in:
parent
12db2e6c51
commit
47bf9c38e9
5 changed files with 264 additions and 95 deletions
|
@ -2186,6 +2186,10 @@ Zotero.Translate.Export.prototype._prepareTranslation = function() {
|
|||
|
||||
// initialize ItemGetter
|
||||
this._itemGetter = new Zotero.Translate.ItemGetter();
|
||||
|
||||
// Toggle legacy mode for translators pre-4.0.27
|
||||
this._itemGetter.legacy = Services.vc.compare('4.0.27', this._translatorInfo.minVersion) > 0;
|
||||
|
||||
var configOptions = this._translatorInfo.configOptions || {},
|
||||
getCollections = configOptions.getCollections || false;
|
||||
switch (this._export.type) {
|
||||
|
|
|
@ -748,6 +748,7 @@ Zotero.Translate.ItemGetter = function() {
|
|||
this._itemsLeft = null;
|
||||
this._collectionsLeft = null;
|
||||
this._exportFileDirectory = null;
|
||||
this.legacy = false;
|
||||
};
|
||||
|
||||
Zotero.Translate.ItemGetter.prototype = {
|
||||
|
@ -828,13 +829,8 @@ Zotero.Translate.ItemGetter.prototype = {
|
|||
* Converts an attachment to array format and copies it to the export folder if desired
|
||||
*/
|
||||
"_attachmentToArray":function(attachment) {
|
||||
var attachmentArray = this._itemToArray(attachment);
|
||||
var attachmentArray = Zotero.Utilities.Internal.itemToExportFormat(attachment, this.legacy);
|
||||
var linkMode = attachment.attachmentLinkMode;
|
||||
|
||||
// Get mime type
|
||||
attachmentArray.mimeType = attachmentArray.uniqueFields.mimeType = attachment.attachmentMIMEType;
|
||||
// Get charset
|
||||
attachmentArray.charset = attachmentArray.uniqueFields.charset = attachment.attachmentCharset;
|
||||
if(linkMode != Zotero.Attachments.LINK_MODE_LINKED_URL) {
|
||||
var attachFile = attachment.getFile();
|
||||
attachmentArray.localPath = attachFile.path;
|
||||
|
@ -845,7 +841,7 @@ Zotero.Translate.ItemGetter.prototype = {
|
|||
// Add path and filename if not an internet link
|
||||
var attachFile = attachment.getFile();
|
||||
if(attachFile) {
|
||||
attachmentArray.defaultPath = "files/" + attachmentArray.itemID + "/" + attachFile.leafName;
|
||||
attachmentArray.defaultPath = "files/" + attachment.id + "/" + attachFile.leafName;
|
||||
attachmentArray.filename = attachFile.leafName;
|
||||
|
||||
/**
|
||||
|
@ -959,39 +955,8 @@ Zotero.Translate.ItemGetter.prototype = {
|
|||
}
|
||||
}
|
||||
|
||||
attachmentArray.itemType = "attachment";
|
||||
|
||||
return attachmentArray;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts an item to array format
|
||||
*/
|
||||
"_itemToArray":function(returnItem) {
|
||||
// TODO use Zotero.Item#serialize()
|
||||
var returnItemArray = returnItem.toArray();
|
||||
|
||||
// Remove SQL date from multipart dates
|
||||
if (returnItemArray.date) {
|
||||
returnItemArray.date = Zotero.Date.multipartToStr(returnItemArray.date);
|
||||
}
|
||||
|
||||
var returnItemArray = Zotero.Utilities.itemToExportFormat(returnItemArray);
|
||||
|
||||
// TODO: Change tag.tag references in translators to tag.name
|
||||
// once translators are 1.5-only
|
||||
// TODO: Preserve tag type?
|
||||
if (returnItemArray.tags) {
|
||||
for (var i in returnItemArray.tags) {
|
||||
returnItemArray.tags[i].tag = returnItemArray.tags[i].fields.name;
|
||||
}
|
||||
}
|
||||
|
||||
// add URI
|
||||
returnItemArray.uri = Zotero.URI.getItemURI(returnItem);
|
||||
|
||||
return returnItemArray;
|
||||
},
|
||||
|
||||
/**
|
||||
* Retrieves the next available item
|
||||
|
@ -1004,10 +969,10 @@ Zotero.Translate.ItemGetter.prototype = {
|
|||
var returnItemArray = this._attachmentToArray(returnItem);
|
||||
if(returnItemArray) return returnItemArray;
|
||||
} else {
|
||||
var returnItemArray = this._itemToArray(returnItem);
|
||||
var returnItemArray = Zotero.Utilities.Internal.itemToExportFormat(returnItem, this.legacy);
|
||||
|
||||
// get attachments, although only urls will be passed if exportFileData is off
|
||||
returnItemArray.attachments = new Array();
|
||||
returnItemArray.attachments = [];
|
||||
var attachments = returnItem.getAttachments();
|
||||
for each(var attachmentID in attachments) {
|
||||
var attachment = Zotero.Items.get(attachmentID);
|
||||
|
|
|
@ -61,7 +61,7 @@ const CSL_TEXT_MAPPINGS = {
|
|||
"number-of-volumes":["numberOfVolumes"],
|
||||
"number-of-pages":["numPages"],
|
||||
"edition":["edition"],
|
||||
"version":["version"],
|
||||
"version":["versionNumber"],
|
||||
"section":["section", "committee"],
|
||||
"genre":["type", "programmingLanguage"],
|
||||
"source":["libraryCatalog"],
|
||||
|
@ -133,7 +133,10 @@ const CSL_TYPE_MAPPINGS = {
|
|||
'tvBroadcast':"broadcast",
|
||||
'radioBroadcast':"broadcast",
|
||||
'podcast':"song", // ??
|
||||
'computerProgram':"book" // ??
|
||||
'computerProgram':"book", // ??
|
||||
'document':"article",
|
||||
'note':"article",
|
||||
'attachment':"article"
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -1345,49 +1348,6 @@ Zotero.Utilities = {
|
|||
return dumpedText;
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds all fields to an item in toArray() format and adds a unique (base) fields to
|
||||
* uniqueFields array
|
||||
*/
|
||||
"itemToExportFormat":function(item) {
|
||||
const CREATE_ARRAYS = ['creators', 'notes', 'tags', 'seeAlso', 'attachments'];
|
||||
for(var i=0; i<CREATE_ARRAYS.length; i++) {
|
||||
var createArray = CREATE_ARRAYS[i];
|
||||
if(!item[createArray]) item[createArray] = [];
|
||||
}
|
||||
|
||||
item.uniqueFields = {};
|
||||
|
||||
// get base fields, not just the type-specific ones
|
||||
var itemTypeID = (item.itemTypeID ? item.itemTypeID : Zotero.ItemTypes.getID(item.itemType));
|
||||
var allFields = Zotero.ItemFields.getItemTypeFields(itemTypeID);
|
||||
for(var i in allFields) {
|
||||
var field = allFields[i];
|
||||
var fieldName = Zotero.ItemFields.getName(field);
|
||||
|
||||
if(item[fieldName] !== undefined) {
|
||||
var baseField = Zotero.ItemFields.getBaseIDFromTypeAndField(itemTypeID, field);
|
||||
|
||||
var baseName = null;
|
||||
if(baseField && baseField != field) {
|
||||
baseName = Zotero.ItemFields.getName(baseField);
|
||||
}
|
||||
|
||||
if(baseName) {
|
||||
item[baseName] = item[fieldName];
|
||||
item.uniqueFields[baseName] = item[fieldName];
|
||||
} else {
|
||||
item.uniqueFields[fieldName] = item[fieldName];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// preserve notes
|
||||
if(item.note) item.uniqueFields.note = item.note;
|
||||
|
||||
return item;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts an item from toArray() format to an array of items in
|
||||
* the content=json format used by the server
|
||||
|
@ -1527,14 +1487,16 @@ Zotero.Utilities = {
|
|||
*/
|
||||
"itemToCSLJSON":function(zoteroItem) {
|
||||
if (zoteroItem instanceof Zotero.Item) {
|
||||
zoteroItem = zoteroItem.toArray();
|
||||
zoteroItem = Zotero.Utilities.Internal.itemToExportFormat(zoteroItem);
|
||||
}
|
||||
|
||||
var cslType = CSL_TYPE_MAPPINGS[zoteroItem.itemType] || "article";
|
||||
var cslType = CSL_TYPE_MAPPINGS[zoteroItem.itemType];
|
||||
if (!cslType) throw new Error('Unexpected Zotero Item type "' + zoteroItem.itemType + '"');
|
||||
|
||||
var itemTypeID = Zotero.ItemTypes.getID(zoteroItem.itemType);
|
||||
|
||||
var cslItem = {
|
||||
'id':zoteroItem.itemID,
|
||||
'id':zoteroItem.uri,
|
||||
'type':cslType
|
||||
};
|
||||
|
||||
|
@ -1548,11 +1510,13 @@ Zotero.Utilities = {
|
|||
if(field in zoteroItem) {
|
||||
value = zoteroItem[field];
|
||||
} else {
|
||||
if (field == 'versionNumber') field = 'version'; // Until https://github.com/zotero/zotero/issues/670
|
||||
var fieldID = Zotero.ItemFields.getID(field),
|
||||
baseMapping;
|
||||
if(Zotero.ItemFields.isValidForType(fieldID, itemTypeID)
|
||||
&& (baseMapping = Zotero.ItemFields.getBaseIDFromTypeAndField(itemTypeID, fieldID))) {
|
||||
value = zoteroItem[Zotero.ItemTypes.getName(baseMapping)];
|
||||
typeFieldID;
|
||||
if(fieldID
|
||||
&& (typeFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, fieldID))
|
||||
) {
|
||||
value = zoteroItem[Zotero.ItemFields.getName(typeFieldID)];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1578,7 +1542,7 @@ Zotero.Utilities = {
|
|||
// separate name variables
|
||||
var author = Zotero.CreatorTypes.getName(Zotero.CreatorTypes.getPrimaryIDForType(itemTypeID));
|
||||
var creators = zoteroItem.creators;
|
||||
for(var i=0; i<creators.length; i++) {
|
||||
for(var i=0; creators && i<creators.length; i++) {
|
||||
var creator = creators[i];
|
||||
var creatorType = creator.creatorType;
|
||||
if(creatorType == author) {
|
||||
|
@ -1600,6 +1564,13 @@ Zotero.Utilities = {
|
|||
// get date variables
|
||||
for(var variable in CSL_DATE_MAPPINGS) {
|
||||
var date = zoteroItem[CSL_DATE_MAPPINGS[variable]];
|
||||
if (!date) {
|
||||
var typeSpecificFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, CSL_DATE_MAPPINGS[variable]);
|
||||
if (typeSpecificFieldID) {
|
||||
date = zoteroItem[Zotero.ItemFields.getName(typeSpecificFieldID)];
|
||||
}
|
||||
}
|
||||
|
||||
if(date) {
|
||||
var dateObj = Zotero.Date.strToDate(date);
|
||||
// otherwise, use date-parts
|
||||
|
@ -1625,7 +1596,7 @@ Zotero.Utilities = {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// extract PMID
|
||||
var extra = zoteroItem.extra;
|
||||
if(typeof extra === "string") {
|
||||
|
|
|
@ -220,7 +220,6 @@ Zotero.Utilities.Internal = {
|
|||
return s;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Display a prompt from an error with custom buttons and a callback
|
||||
*/
|
||||
|
@ -370,6 +369,136 @@ Zotero.Utilities.Internal = {
|
|||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts Zotero.Item to a format expected by translators
|
||||
* This is mostly the Zotero web API item JSON format, but with an attachments
|
||||
* and notes arrays and optional compatibility mappings for older translators.
|
||||
*
|
||||
* @param {Zotero.Item} zoteroItem
|
||||
* @param {Boolean} legacy Add mappings for legacy (pre-4.0.27) translators
|
||||
* @return {Object}
|
||||
*/
|
||||
"itemToExportFormat": new function() {
|
||||
return function(zoteroItem, legacy) {
|
||||
var item = zoteroItem.toJSON();
|
||||
item.uri = Zotero.URI.getItemURI(zoteroItem);
|
||||
delete item.key;
|
||||
|
||||
if (!zoteroItem.isAttachment() && !zoteroItem.isNote()) {
|
||||
// Include attachments
|
||||
item.attachments = [];
|
||||
let attachments = zoteroItem.getAttachments();
|
||||
for (let i=0; i<attachments.length; i++) {
|
||||
let zoteroAttachment = Zotero.Items.get(attachments[i]),
|
||||
attachment = zoteroAttachment.toJSON();
|
||||
if (legacy) addCompatibilityMappings(attachment, zoteroAttachment);
|
||||
|
||||
item.attachments.push(attachment);
|
||||
}
|
||||
|
||||
// Include notes
|
||||
item.notes = [];
|
||||
let notes = zoteroItem.getNotes();
|
||||
for (let i=0; i<notes.length; i++) {
|
||||
let zoteroNote = Zotero.Items.get(notes[i]),
|
||||
note = zoteroNote.toJSON();
|
||||
if (legacy) addCompatibilityMappings(note, zoteroNote);
|
||||
|
||||
item.notes.push(note);
|
||||
}
|
||||
}
|
||||
|
||||
if (legacy) addCompatibilityMappings(item, zoteroItem);
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
function addCompatibilityMappings(item, zoteroItem) {
|
||||
item.uniqueFields = {};
|
||||
|
||||
// Meaningless local item ID, but some older export translators depend on it
|
||||
item.itemID = zoteroItem.id;
|
||||
item.key = zoteroItem.key; // CSV translator exports this
|
||||
|
||||
// "version" is expected to be a field for "computerProgram", which is now
|
||||
// called "versionNumber"
|
||||
delete item.version;
|
||||
if (item.versionNumber) {
|
||||
item.version = item.uniqueFields.version = item.versionNumber;
|
||||
delete item.versionNumber;
|
||||
}
|
||||
|
||||
// SQL instead of ISO-8601
|
||||
item.dateAdded = zoteroItem.dateAdded;
|
||||
item.dateModified = zoteroItem.dateModified;
|
||||
|
||||
// Map base fields
|
||||
for (let field in item) {
|
||||
let id = Zotero.ItemFields.getID(field);
|
||||
if (!id || !Zotero.ItemFields.isValidForType(id, zoteroItem.itemTypeID)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let baseField = Zotero.ItemFields.getName(
|
||||
Zotero.ItemFields.getBaseIDFromTypeAndField(item.itemType, field)
|
||||
);
|
||||
|
||||
if (!baseField || baseField == field) {
|
||||
item.uniqueFields[field] = item[field];
|
||||
} else {
|
||||
item[baseField] = item[field];
|
||||
item.uniqueFields[baseField] = item[field];
|
||||
}
|
||||
}
|
||||
|
||||
// Add various fields for compatibility with translators pre-4.0.27
|
||||
item.itemID = zoteroItem.id;
|
||||
item.libraryID = zoteroItem.libraryID;
|
||||
|
||||
// Creators
|
||||
if (item.creators) {
|
||||
for (let i=0; i<item.creators.length; i++) {
|
||||
let creator = item.creators[i];
|
||||
|
||||
if (creator.name) {
|
||||
creator.fieldMode = 1;
|
||||
creator.lastName = creator.name;
|
||||
delete creator.name;
|
||||
}
|
||||
|
||||
// Old format used to supply creatorID (the database ID), but no
|
||||
// translator ever used it
|
||||
}
|
||||
}
|
||||
|
||||
if (!zoteroItem.isRegularItem()) {
|
||||
item.sourceItemKey = item.parentItem;
|
||||
}
|
||||
|
||||
// Tags
|
||||
for (let i=0; i<item.tags.length; i++) {
|
||||
if (!item.tags[i].type) {
|
||||
item.tags[i].type = 0;
|
||||
}
|
||||
// No translator ever used "primary", "fields", or "linkedItems" objects
|
||||
}
|
||||
|
||||
// "related" was never used (array of itemIDs)
|
||||
|
||||
// seeAlso was always present, but it was always an empty array.
|
||||
// Zotero RDF translator pretended to use it
|
||||
item.seeAlso = [];
|
||||
|
||||
// Fix linkMode
|
||||
if (zoteroItem.isAttachment()) {
|
||||
item.linkMode = zoteroItem.attachmentLinkMode;
|
||||
item.mimeType = item.contentType;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Hyphenate an ISBN based on the registrant table available from
|
||||
* https://www.isbn-international.org/range_file_generation
|
||||
|
|
|
@ -172,4 +172,104 @@ describe("Zotero.Utilities", function() {
|
|||
assert.equal(cleanISSN('<b>ISSN</b>:1234\xA0-\t5679(print)\n<b>eISSN (electronic)</b>:0028-0836'), '1234-5679');
|
||||
});
|
||||
});
|
||||
describe("itemToCSLJSON", function() {
|
||||
it("should accept Zotero.Item and Zotero export item format", function() {
|
||||
let data = populateDBWithSampleData(loadSampleData('journalArticle'));
|
||||
let item = Zotero.Items.get(data.journalArticle.id);
|
||||
|
||||
let fromZoteroItem;
|
||||
try {
|
||||
fromZoteroItem = Zotero.Utilities.itemToCSLJSON(item);
|
||||
} catch(e) {
|
||||
assert.fail(e, null, 'accepts Zotero Item');
|
||||
}
|
||||
assert.isObject(fromZoteroItem, 'converts Zotero Item to object');
|
||||
assert.isNotNull(fromZoteroItem, 'converts Zotero Item to non-null object');
|
||||
|
||||
|
||||
let fromExportItem;
|
||||
try {
|
||||
fromExportItem = Zotero.Utilities.itemToCSLJSON(
|
||||
Zotero.Utilities.Internal.itemToExportFormat(item)
|
||||
);
|
||||
} catch(e) {
|
||||
assert.fail(e, null, 'accepts Zotero export item');
|
||||
}
|
||||
assert.isObject(fromExportItem, 'converts Zotero export item to object');
|
||||
assert.isNotNull(fromExportItem, 'converts Zotero export item to non-null object');
|
||||
|
||||
assert.deepEqual(fromZoteroItem, fromExportItem, 'conversion from Zotero Item and from export item are the same');
|
||||
});
|
||||
it("should convert standalone notes to expected format", function() {
|
||||
let note = new Zotero.Item('note');
|
||||
note.setNote('Some note longer than 50 characters, which will become the title.');
|
||||
note = Zotero.Items.get(note.save());
|
||||
|
||||
let cslJSONNote = Zotero.Utilities.itemToCSLJSON(note);
|
||||
assert.equal(cslJSONNote.type, 'article', 'note is exported as "article"');
|
||||
});
|
||||
it("should convert standalone attachments to expected format", function() {
|
||||
let file = getTestDataDirectory();
|
||||
file.append("empty.pdf");
|
||||
|
||||
let attachment = Zotero.Items.get(Zotero.Attachments.importFromFile(file));
|
||||
attachment.setField('title', 'Empty');
|
||||
attachment.setField('accessDate', '2001-02-03 12:13:14');
|
||||
attachment.setField('url', 'http://example.com');
|
||||
attachment.setNote('Note');
|
||||
|
||||
attachment.save();
|
||||
|
||||
cslJSONAttachment = Zotero.Utilities.itemToCSLJSON(attachment);
|
||||
assert.equal(cslJSONAttachment.type, 'article', 'attachment is exported as "article"');
|
||||
assert.equal(cslJSONAttachment.title, 'Empty', 'attachment title is correct');
|
||||
assert.deepEqual(cslJSONAttachment.accessed, {"date-parts":[["2001",2,3]]}, 'attachment access date is mapped correctly');
|
||||
});
|
||||
it("should refuse to convert unexpected item types", function() {
|
||||
let data = populateDBWithSampleData(loadSampleData('journalArticle'));
|
||||
let item = Zotero.Items.get(data.journalArticle.id);
|
||||
|
||||
let exportFormat = Zotero.Utilities.Internal.itemToExportFormat(item);
|
||||
exportFormat.itemType = 'foo';
|
||||
|
||||
assert.throws(Zotero.Utilities.itemToCSLJSON.bind(Zotero.Utilities, exportFormat), /^Unexpected Zotero Item type ".*"$/, 'throws an error when trying to map invalid item types');
|
||||
});
|
||||
it("should map additional fields from Extra field", function() {
|
||||
let item = new Zotero.Item('journalArticle');
|
||||
item.setField('extra', 'PMID: 12345\nPMCID:123456');
|
||||
item = Zotero.Items.get(item.save());
|
||||
|
||||
let cslJSON = Zotero.Utilities.itemToCSLJSON(item);
|
||||
|
||||
assert.equal(cslJSON.PMID, '12345', 'PMID from Extra is mapped to PMID');
|
||||
assert.equal(cslJSON.PMCID, '123456', 'PMCID from Extra is mapped to PMCID');
|
||||
|
||||
item.setField('extra', 'PMID: 12345');
|
||||
item.save();
|
||||
cslJSON = Zotero.Utilities.itemToCSLJSON(item);
|
||||
|
||||
assert.equal(cslJSON.PMID, '12345', 'single-line entry is extracted correctly');
|
||||
|
||||
item.setField('extra', 'some junk: note\nPMID: 12345\nstuff in-between\nPMCID: 123456\nlast bit of junk!');
|
||||
item.save();
|
||||
cslJSON = Zotero.Utilities.itemToCSLJSON(item);
|
||||
|
||||
assert.equal(cslJSON.PMID, '12345', 'PMID from mixed Extra field is mapped to PMID');
|
||||
assert.equal(cslJSON.PMCID, '123456', 'PMCID from mixed Extra field is mapped to PMCID');
|
||||
|
||||
item.setField('extra', 'a\n PMID: 12345\nfoo PMCID: 123456');
|
||||
item.save();
|
||||
cslJSON = Zotero.Utilities.itemToCSLJSON(item);
|
||||
|
||||
assert.isUndefined(cslJSON.PMCID, 'field label must not be preceded by other text');
|
||||
assert.isUndefined(cslJSON.PMID, 'field label must not be preceded by a space');
|
||||
assert.equal(cslJSON.note, 'a\n PMID: 12345\nfoo PMCID: 123456', 'note is left untouched if nothing is extracted');
|
||||
|
||||
item.setField('extra', 'something\npmid: 12345\n');
|
||||
item.save();
|
||||
cslJSON = Zotero.Utilities.itemToCSLJSON(item);
|
||||
|
||||
assert.isUndefined(cslJSON.PMID, 'field labels are case-sensitive');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue