diff --git a/chrome/content/zotero/xpcom/cite.js b/chrome/content/zotero/xpcom/cite.js index 6d3fb66633..fd8adbcd28 100644 --- a/chrome/content/zotero/xpcom/cite.js +++ b/chrome/content/zotero/xpcom/cite.js @@ -524,6 +524,9 @@ Zotero.Cite.System.prototype = { var cslItem = Zotero.Utilities.itemToCSLJSON(zoteroItem); + // TEMP: citeproc-js currently expects the id property to be the item DB id + cslItem.id = zoteroItem.id; + if (!Zotero.Prefs.get("export.citePaperJournalArticleURL")) { var itemType = Zotero.ItemTypes.getName(zoteroItem.itemTypeID); // don't return URL or accessed information for journal articles if a diff --git a/chrome/content/zotero/xpcom/data/item.js b/chrome/content/zotero/xpcom/data/item.js index 68e488bc5e..242901415d 100644 --- a/chrome/content/zotero/xpcom/data/item.js +++ b/chrome/content/zotero/xpcom/data/item.js @@ -4918,6 +4918,196 @@ Zotero.Item.prototype.serialize = function(mode) { return arr; } +/** + * Serializes Zotero Item into Zotero web server API JSON format + * + * @param {Object} options + * mode {String}: [new|full|patch] "new" is default. "full" mode includes all + * fields even if empty. "patch" returns only fields that are different from + * those in patchBase + * patchBase {Object}: Item in API JSON format to be compared to in + * "patch" mode. Required if "patch" mode is specified + */ +Zotero.Item.prototype.toJSON = function(options) { + if (this.id || this.key) { + if (!this._primaryDataLoaded) { + this.loadPrimaryData(true); + } + + if (this.id) { + if (!this._itemDataLoaded) this._loadItemData(); + if (this.isRegularItem() && !this._creatorsLoaded) this._loadCreators(); + if (!this._relatedItemsLoaded) this._loadRelatedItems(); + } + } + + if (this.hasChanged()) { + throw new Error("Cannot generate JSON from changed item"); + } + + options = options || {}; + let mode = options.mode || 'new'; + let patchBase = options.patchBase; + + if (mode == 'patch') { + if (!patchBase) { + throw new Error('Cannot use "patch" mode if patchBase not provided'); + } + } + else if (patchBase) { + Zotero.debug('Zotero.Item.toJSON: ignoring provided patchBase in "' + mode + '" mode', 2); + } + + let obj = { + key: this.key || false, + version: 1, + itemType: Zotero.ItemTypes.getName(this.itemTypeID), + tags: [], + collections: [], + relations: {} + }; + + // Type-specific fields + for (let i in this._itemData) { + let val = '' + this.getField(i); + if (val !== '' || mode == 'full') { + let name = Zotero.ItemFields.getName(i); + if (name == 'version') { + // Changed in API v3 to avoid clash with 'version' above + // Remove this after https://github.com/zotero/zotero/issues/670 + name = 'versionNumber'; + } + + if (name == 'accessDate') { + val = Zotero.Date.dateToISO(Zotero.Date.sqlToDate(val)); + } + + obj[name] = val; + } + } + + if (this.isRegularItem()) { + // Creators + obj.creators = []; + let creators = this.getCreators(); + for (let i=0; i 0; + var configOptions = this._translatorInfo.configOptions || {}, getCollections = configOptions.getCollections || false; switch (this._export.type) { diff --git a/chrome/content/zotero/xpcom/translation/translate_item.js b/chrome/content/zotero/xpcom/translation/translate_item.js index 616040b870..5eed86e2e4 100644 --- a/chrome/content/zotero/xpcom/translation/translate_item.js +++ b/chrome/content/zotero/xpcom/translation/translate_item.js @@ -745,9 +745,10 @@ Zotero.Translate.ItemSaver.prototype = { } Zotero.Translate.ItemGetter = function() { - this._itemsLeft = null; + this._itemsLeft = []; 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); diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js index eec16d1bb1..3204930261 100644 --- a/chrome/content/zotero/xpcom/utilities.js +++ b/chrome/content/zotero/xpcom/utilities.js @@ -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 m.toLowerCase()); // not all-caps words + } + + itemFields[name] = value; + } + + let creatorTypes = Zotero.CreatorTypes.getTypesForItemType(itemTypes[i].id), + creators = itemFields.creators = []; + for (let j = 0; j < creatorTypes.length; j++) { + let typeName = creatorTypes[j].name; + creators.push({ + creatorType: typeName, + firstName: typeName + 'First', + lastName: typeName + 'Last' + }); + } + } + + return data; +} + +/** + * Populates the database with sample items + * The field values should be in the form exactly as they would appear in Zotero + */ +function populateDBWithSampleData(data) { + Zotero.DB.beginTransaction(); + + for (let itemName in data) { + let item = data[itemName]; + let zItem = new Zotero.Item(item.itemType); + for (let itemField in item) { + if (itemField == 'itemType') continue; + + if (itemField == 'creators') { + let creators = item[itemField]; + for (let i=0; i&2 < Zotero.URI.getItemURI(i)); + + Zotero.DB.commitTransaction(); + + getter._itemsLeft = items; + + assert.equal(getter.nextItem().uri, itemURIs[0], 'first item comes out first'); + assert.equal(getter.nextItem().uri, itemURIs[1], 'second item comes out second'); + assert.isFalse(getter.nextItem(), 'end of item queue'); + }); + it('should return items with tags in expected format', function() { + let getter = new Zotero.Translate.ItemGetter(); + + Zotero.DB.beginTransaction(); + + let itemWithAutomaticTag = Zotero.Items.get((new Zotero.Item('journalArticle')).save()); + itemWithAutomaticTag.addTag('automatic tag', 0); + + let itemWithManualTag = Zotero.Items.get((new Zotero.Item('journalArticle')).save()); + itemWithManualTag.addTag('manual tag', 1); + + let itemWithMultipleTags = Zotero.Items.get((new Zotero.Item('journalArticle')).save()); + itemWithMultipleTags.addTag('tag1', 0); + itemWithMultipleTags.addTag('tag2', 1); + + Zotero.DB.commitTransaction(); + + let legacyMode = [false, true]; + for (let i=0; iISSN:1234\xA0-\t5679(print)\neISSN (electronic):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"'); + assert.equal(cslJSONNote.title, note.getNoteTitle(), 'note title is set to Zotero pseudo-title'); + }); + 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'); + }); + }); });