diff --git a/chrome/content/zotero/browser.js b/chrome/content/zotero/browser.js index 134b86089a..28d197b4cd 100644 --- a/chrome/content/zotero/browser.js +++ b/chrome/content/zotero/browser.js @@ -723,6 +723,7 @@ var Zotero_Browser = new function() { Zotero_Browser.progress.show(); Zotero_Browser.progress.changeHeadline(Zotero.getString("ingester.lookup.performing")); tab.page.translate.translate(false); + e.stopPropagation(); } } diff --git a/chrome/content/zotero/xpcom/cite.js b/chrome/content/zotero/xpcom/cite.js index 6d3fb66633..bdbf2d52e6 100644 --- a/chrome/content/zotero/xpcom/cite.js +++ b/chrome/content/zotero/xpcom/cite.js @@ -510,12 +510,6 @@ Zotero.Cite.System.prototype = { return embeddedCitation; } } - } else { - // is an item ID - //if(this._cache[item]) return this._cache[item]; - try { - zoteroItem = Zotero.Items.get(item); - } catch(e) {} } if(!zoteroItem) { @@ -524,6 +518,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 c3f2aced9a..02a60a6925 100644 --- a/chrome/content/zotero/xpcom/data/item.js +++ b/chrome/content/zotero/xpcom/data/item.js @@ -4181,6 +4181,7 @@ Zotero.Item.prototype.toJSON = Zotero.Promise.coroutine(function* (options) { obj.dateAdded = Zotero.Date.sqlToISO8601(this.dateAdded); obj.dateModified = Zotero.Date.sqlToISO8601(this.dateModified); + if (obj.accessDate) obj.accessDate = Zotero.Date.sqlToISO8601(obj.accessDate); if (mode == 'patch') { for (let i in options.patchBase) { diff --git a/chrome/content/zotero/xpcom/translation/translate.js b/chrome/content/zotero/xpcom/translation/translate.js index e30d9ae3ba..60b7672122 100644 --- a/chrome/content/zotero/xpcom/translation/translate.js +++ b/chrome/content/zotero/xpcom/translation/translate.js @@ -2161,6 +2161,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) { diff --git a/chrome/content/zotero/xpcom/translation/translate_item.js b/chrome/content/zotero/xpcom/translation/translate_item.js index 09640915d3..4efb64623a 100644 --- a/chrome/content/zotero/xpcom/translation/translate_item.js +++ b/chrome/content/zotero/xpcom/translation/translate_item.js @@ -83,7 +83,8 @@ Zotero.Translate.ItemSaver.prototype = { * @param items Items in Zotero.Item.toArray() format * @param {Function} callback A callback to be executed when saving is complete. If saving * succeeded, this callback will be passed true as the first argument and a list of items - * saved as the second. If saving failed, the callback will be passed false as the first + * saved as the second. If + saving failed, the callback will be passed false as the first * argument and an error object as the second * @param {Function} [attachmentCallback] A callback that receives information about attachment * save progress. The callback will be called as attachmentCallback(attachment, false, error) @@ -700,9 +701,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 = { @@ -782,14 +784,9 @@ 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); + "_attachmentToArray":Zotero.Promise.coroutine(function* (attachment) { + var attachmentArray = yield 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; @@ -800,7 +797,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; /** @@ -914,59 +911,28 @@ 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 */ - "nextItem":function() { + "nextItem":Zotero.Promise.coroutine(function* () { while(this._itemsLeft.length != 0) { var returnItem = this._itemsLeft.shift(); // export file data for single files if(returnItem.isAttachment()) { // an independent attachment - var returnItemArray = this._attachmentToArray(returnItem); + var returnItemArray = yield this._attachmentToArray(returnItem); if(returnItemArray) return returnItemArray; } else { - var returnItemArray = this._itemToArray(returnItem); + var returnItemArray = yield 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); - var attachmentInfo = this._attachmentToArray(attachment); + var attachment = yield Zotero.Items.getAsync(attachmentID); + var attachmentInfo = yield this._attachmentToArray(attachment); if(attachmentInfo) { returnItemArray.attachments.push(attachmentInfo); @@ -977,7 +943,7 @@ Zotero.Translate.ItemGetter.prototype = { } } return false; - }, + }), "nextCollection":function() { if(!this._collectionsLeft || this._collectionsLeft.length == 0) return false; diff --git a/chrome/content/zotero/xpcom/utilities.js b/chrome/content/zotero/xpcom/utilities.js index 8d96711afc..13ed5e3410 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" }; /** @@ -1425,49 +1428,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) { + return Zotero.DB.executeTransaction(function* () { + 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') { + zItem.setCreators(item[itemField]); + continue; + } + + if (itemField == 'tags') { + // Must save item first + continue; + } + + zItem.setField(itemField, item[itemField]); + } + + if (item.tags && item.tags.length) { + for (let i=0; i testfile.append(part)); return Zotero.Attachments.importFromFile({file: testfile}); } - diff --git a/test/runtests.sh b/test/runtests.sh index 53890a3e9f..201d44bc9e 100755 --- a/test/runtests.sh +++ b/test/runtests.sh @@ -15,48 +15,60 @@ function makePath { } DEBUG=false -if [ "`uname`" == "Darwin" ]; then - FX_EXECUTABLE="/Applications/Firefox.app/Contents/MacOS/firefox" -else - FX_EXECUTABLE="firefox" +if [ -z "$FX_EXECUTABLE" ]; then + if [ "`uname`" == "Darwin" ]; then + FX_EXECUTABLE="/Applications/Firefox.app/Contents/MacOS/firefox" + else + FX_EXECUTABLE="firefox" + fi fi + FX_ARGS="" function usage { cat >&2 < Zotero.URI.getItemURI(i)); + }); + + getter._itemsLeft = items; + + assert.equal((yield getter.nextItem()).uri, itemURIs[0], 'first item comes out first'); + assert.equal((yield getter.nextItem()).uri, itemURIs[1], 'second item comes out second'); + assert.isFalse((yield getter.nextItem()), 'end of item queue'); + })); + it('should return items with tags in expected format', Zotero.Promise.coroutine(function* () { + let getter = new Zotero.Translate.ItemGetter(); + let itemWithAutomaticTag, itemWithManualTag, itemWithMultipleTags + + yield Zotero.DB.executeTransaction(function* () { + itemWithAutomaticTag = new Zotero.Item('journalArticle'); + itemWithAutomaticTag.addTag('automatic tag', 0); + yield itemWithAutomaticTag.save(); + + itemWithManualTag = new Zotero.Item('journalArticle'); + itemWithManualTag.addTag('manual tag', 1); + yield itemWithManualTag.save(); + + itemWithMultipleTags = new Zotero.Item('journalArticle'); + itemWithMultipleTags.addTag('tag1', 0); + itemWithMultipleTags.addTag('tag2', 1); + yield itemWithMultipleTags.save(); + }); + + let legacyMode = [false, true]; + for (let i=0; i item.save())); + + collections = [ + new Zotero.Collection, + new Zotero.Collection, + new Zotero.Collection, + new Zotero.Collection + ]; + collections[0].name = "test1"; + collections[1].name = "test2"; + collections[2].name = "subTest1"; + collections[3].name = "subTest2"; + yield collections[0].save(); + yield collections[1].save(); + collections[2].parentID = collections[0].id; + collections[3].parentID = collections[1].id; + yield collections[2].save(); + yield collections[3].save(); + + yield collections[0].addItems([items[1].id, items[2].id]); + yield collections[1].addItem(items[2].id); + yield collections[2].addItem(items[3].id); + }); + + let translatorItem = yield getter.nextItem(); + assert.isArray(translatorItem.collections, 'item in library root has a collections array'); + assert.equal(translatorItem.collections.length, 0, 'item in library root does not list any collections'); + + translatorItem = yield getter.nextItem(); + assert.isArray(translatorItem.collections, 'item in a single collection has a collections array'); + assert.equal(translatorItem.collections.length, 1, 'item in a single collection lists one collection'); + assert.equal(translatorItem.collections[0], collections[0].key, 'item in a single collection identifies correct collection'); + + translatorItem = yield getter.nextItem(); + assert.isArray(translatorItem.collections, 'item in two collections has a collections array'); + assert.equal(translatorItem.collections.length, 2, 'item in two collections lists two collections'); + assert.deepEqual( + translatorItem.collections.sort(), + [collections[0].key, collections[1].key].sort(), + 'item in two collections identifies correct collections' + ); + + translatorItem = yield getter.nextItem(); + assert.isArray(translatorItem.collections, 'item in a nested collection has a collections array'); + assert.equal(translatorItem.collections.length, 1, 'item in a single nested collection lists one collection'); + assert.equal(translatorItem.collections[0], collections[2].key, 'item in a single collection identifies correct collection'); + })); + // it('should return item relations in expected format', Zotero.Promise.coroutine(function* () { + // let getter = new Zotero.Translate.ItemGetter(); + // let items; + + // yield Zotero.DB.executeTransaction(function* () { + // items = [ + // new Zotero.Item('journalArticle'), // Item with no relations + + // new Zotero.Item('journalArticle'), // Relation set on this item + // new Zotero.Item('journalArticle'), // To this item + + // new Zotero.Item('journalArticle'), // This item is related to two items below + // new Zotero.Item('journalArticle'), // But this item is not related to the item below + // new Zotero.Item('journalArticle') + // ]; + // yield Zotero.Promise.all(items.map(item => item.save())); + + // yield items[1].addRelatedItem(items[2].id); + + // yield items[3].addRelatedItem(items[4].id); + // yield items[3].addRelatedItem(items[5].id); + // }); + + // getter._itemsLeft = items.slice(); + + // let translatorItem = yield getter.nextItem(); + // assert.isObject(translatorItem.relations, 'item with no relations has a relations object'); + // assert.equal(Object.keys(translatorItem.relations).length, 0, 'item with no relations does not list any relations'); + + // translatorItem = yield getter.nextItem(); + // assert.isObject(translatorItem.relations, 'item that is the subject of a single relation has a relations object'); + // assert.equal(Object.keys(translatorItem.relations).length, 1, 'item that is the subject of a single relation list one relations predicate'); + // assert.isDefined(translatorItem.relations['dc:relation'], 'item that is the subject of a single relation uses "dc:relation" as the predicate'); + // assert.isString(translatorItem.relations['dc:relation'], 'item that is the subject of a single relation lists "dc:relation" object as a string'); + // assert.equal(translatorItem.relations['dc:relation'], Zotero.URI.getItemURI(items[2]), 'item that is the subject of a single relation identifies correct object URI'); + + // translatorItem = yield getter.nextItem(); + // assert.isObject(translatorItem.relations, 'item that is the object of a single relation has a relations object'); + // assert.equal(Object.keys(translatorItem.relations).length, 1, 'item that is the object of a single relation list one relations predicate'); + // assert.isDefined(translatorItem.relations['dc:relation'], 'item that is the object of a single relation uses "dc:relation" as the predicate'); + // assert.isString(translatorItem.relations['dc:relation'], 'item that is the object of a single relation lists "dc:relation" object as a string'); + // assert.equal(translatorItem.relations['dc:relation'], Zotero.URI.getItemURI(items[1]), 'item that is the object of a single relation identifies correct subject URI'); + + // translatorItem = yield getter.nextItem(); + // assert.isObject(translatorItem.relations, 'item that is the subject of two relations has a relations object'); + // assert.equal(Object.keys(translatorItem.relations).length, 1, 'item that is the subject of two relations list one relations predicate'); + // assert.isDefined(translatorItem.relations['dc:relation'], 'item that is the subject of two relations uses "dc:relation" as the predicate'); + // assert.isArray(translatorItem.relations['dc:relation'], 'item that is the subject of two relations lists "dc:relation" object as an array'); + // assert.equal(translatorItem.relations['dc:relation'].length, 2, 'item that is the subject of two relations lists two relations in the "dc:relation" array'); + // assert.deepEqual(translatorItem.relations['dc:relation'].sort(), + // [Zotero.URI.getItemURI(items[4]), Zotero.URI.getItemURI(items[5])].sort(), + // 'item that is the subject of two relations identifies correct object URIs' + // ); + + // translatorItem = yield getter.nextItem(); + // assert.isObject(translatorItem.relations, 'item that is the object of one relation from item with two relations has a relations object'); + // assert.equal(Object.keys(translatorItem.relations).length, 1, 'item that is the object of one relation from item with two relations list one relations predicate'); + // assert.isDefined(translatorItem.relations['dc:relation'], 'item that is the object of one relation from item with two relations uses "dc:relation" as the predicate'); + // assert.isString(translatorItem.relations['dc:relation'], 'item that is the object of one relation from item with two relations lists "dc:relation" object as a string'); + // assert.equal(translatorItem.relations['dc:relation'], Zotero.URI.getItemURI(items[3]), 'item that is the object of one relation from item with two relations identifies correct subject URI'); + // })); + it('should return standalone note in expected format', Zotero.Promise.coroutine(function* () { + let relatedItem, note, collection; + + yield Zotero.DB.executeTransaction(function* () { + relatedItem = new Zotero.Item('journalArticle'); + yield relatedItem.save(); + + note = new Zotero.Item('note'); + note.setNote('Note'); + note.addTag('automaticTag', 0); + note.addTag('manualTag', 1); + // note.addRelatedItem(relatedItem.id); + yield note.save(); + + collection = new Zotero.Collection; + collection.name = 'test'; + yield collection.save(); + yield collection.addItem(note.id); + }); + + let legacyMode = [false, true]; + for (let i=0; i item.save())); + + collection = new Zotero.Collection; + collection.name = 'test'; + yield collection.save(); + yield collection.addItem(items[0].id); + yield collection.addItem(items[1].id); + + note = new Zotero.Item('note'); + note.setNote('Note'); + note.addTag('automaticTag', 0); + note.addTag('manualTag', 1); + yield note.save(); + + // note.addRelatedItem(relatedItem.id); + }); + + 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", Zotero.Promise.coroutine(function* () { + let data = yield populateDBWithSampleData(loadSampleData('journalArticle')); + let item = yield Zotero.Items.getAsync(data.journalArticle.id); + + let fromZoteroItem; + try { + fromZoteroItem = yield 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( + yield 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", Zotero.Promise.coroutine(function* () { + let note = new Zotero.Item('note'); + note.setNote('Some note longer than 50 characters, which will become the title.'); + yield note.saveTx(); + + let cslJSONNote = yield 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", Zotero.Promise.coroutine(function* () { + let file = getTestDataDirectory(); + file.append("empty.pdf"); + + let attachment = yield Zotero.Attachments.importFromFile({"file":file}); + attachment.setField('title', 'Empty'); + attachment.setField('accessDate', '2001-02-03 12:13:14'); + attachment.setField('url', 'http://example.com'); + attachment.setNote('Note'); + + yield attachment.saveTx(); + + cslJSONAttachment = yield 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", Zotero.Promise.coroutine(function* () { + let data = yield populateDBWithSampleData(loadSampleData('journalArticle')); + let item = yield Zotero.Items.getAsync(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", Zotero.Promise.coroutine(function* () { + let item = new Zotero.Item('journalArticle'); + item.setField('extra', 'PMID: 12345\nPMCID:123456'); + yield item.saveTx(); + + let cslJSON = yield 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'); + yield item.saveTx(); + cslJSON = yield 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!'); + yield item.saveTx(); + cslJSON = yield 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'); + yield item.saveTx(); + cslJSON = yield 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'); + yield item.saveTx(); + cslJSON = yield Zotero.Utilities.itemToCSLJSON(item); + + assert.isUndefined(cslJSON.PMID, 'field labels are case-sensitive'); + })); + }); });