zotero/chrome/content/zotero/xpcom/utilities.js

1299 lines
41 KiB
JavaScript
Raw Normal View History

/*
***** BEGIN LICENSE BLOCK *****
2009-12-28 09:47:49 +00:00
Copyright © 2009 Center for History and New Media
George Mason University, Fairfax, Virginia, USA
http://zotero.org
2009-12-28 09:47:49 +00:00
This file is part of Zotero.
2009-12-28 09:47:49 +00:00
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
2009-12-28 09:47:49 +00:00
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
2009-12-28 09:47:49 +00:00
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
2009-12-28 09:47:49 +00:00
You should have received a copy of the GNU Affero General Public License
2009-12-28 09:47:49 +00:00
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
Utilities based in part on code taken from Piggy Bank 2.1.1 (BSD-licensed)
***** END LICENSE BLOCK *****
*/
/*
* Mappings for names
* Note that this is the reverse of the text variable map, since all mappings should be one to one
* and it makes the code cleaner
*/
const CSL_NAMES_MAPPINGS = {
"author":"author",
"bookAuthor":"container-author",
"composer":"composer",
"editor":"editor",
"interviewer":"interviewer",
"recipient":"recipient",
"seriesEditor":"collection-editor",
"translator":"translator"
}
/*
* Mappings for text variables
*/
const CSL_TEXT_MAPPINGS = {
"title":["title"],
"container-title":["publicationTitle", "reporter", "code"], /* reporter and code should move to SQL mapping tables */
"collection-title":["seriesTitle", "series"],
"collection-number":["seriesNumber"],
"publisher":["publisher", "distributor"], /* distributor should move to SQL mapping tables */
"publisher-place":["place"],
"authority":["court"],
"page":["pages"],
"volume":["volume"],
"issue":["issue"],
"number-of-volumes":["numberOfVolumes"],
"number-of-pages":["numPages"],
"edition":["edition"],
"version":["version"],
"section":["section"],
"genre":["type", "artworkSize"], /* artworkSize should move to SQL mapping tables, or added as a CSL variable */
"medium":["medium", "system"],
"archive":["archive"],
"archive_location":["archiveLocation"],
"event":["meetingName", "conferenceName"], /* these should be mapped to the same base field in SQL mapping tables */
"event-place":["place"],
"abstract":["abstractNote"],
"URL":["url"],
"DOI":["DOI"],
"ISBN":["ISBN"],
"call-number":["callNumber"],
"note":["extra"],
"number":["number"],
"references":["history"],
"shortTitle":["shortTitle"],
"journalAbbreviation":["journalAbbreviation"],
"language":["language"]
}
/*
* Mappings for dates
*/
const CSL_DATE_MAPPINGS = {
"issued":"date",
"accessed":"accessDate"
}
/*
* Mappings for types
*/
const CSL_TYPE_MAPPINGS = {
'book':"book",
'bookSection':'chapter',
'journalArticle':"article-journal",
'magazineArticle':"article-magazine",
'newspaperArticle':"article-newspaper",
'thesis':"thesis",
'encyclopediaArticle':"entry-encyclopedia",
'dictionaryEntry':"entry-dictionary",
'conferencePaper':"paper-conference",
'letter':"personal_communication",
'manuscript':"manuscript",
'interview':"interview",
'film':"motion_picture",
'artwork':"graphic",
'webpage':"webpage",
'report':"report",
'bill':"bill",
'case':"legal_case",
'hearing':"bill", // ??
'patent':"patent",
'statute':"bill", // ??
'email':"personal_communication",
'map':"map",
'blogPost':"webpage",
'instantMessage':"personal_communication",
'forumPost':"webpage",
'audioRecording':"song", // ??
'presentation':"speech",
'videoRecording':"motion_picture",
'tvBroadcast':"broadcast",
'radioBroadcast':"broadcast",
'podcast':"song", // ??
'computerProgram':"book" // ??
};
/**
* @class Functions for text manipulation and other miscellaneous purposes
*/
Zotero.Utilities = {
/**
* Cleans extraneous punctuation off a creator name and parse into first and last name
*
* @param {String} author Creator string
* @param {String} type Creator type string (e.g., "author" or "editor")
* @param {Boolean} useComma Whether the creator string is in inverted (Last, First) format
* @return {Object} firstName, lastName, and creatorType
*/
"cleanAuthor":function(author, type, useComma) {
const allCapsRe = /^[A-Z\u0400-\u042f]+$/;
2007-10-23 07:11:59 +00:00
if(typeof(author) != "string") {
throw "cleanAuthor: author must be a string";
}
author = author.replace(/^[\s\.\,\/\[\]\:]+/, '');
author = author.replace(/[\s\,\/\[\]\:\.]+$/, '');
author = author.replace(/ +/, ' ');
if(useComma) {
// Add spaces between periods
author = author.replace(/\.([^ ])/, ". $1");
var splitNames = author.split(/, ?/);
if(splitNames.length > 1) {
var lastName = splitNames[0];
var firstName = splitNames[1];
} else {
var lastName = author;
}
closes #78, figure out import/export architecture closes #100, migrate ingester to Scholar.Translate closes #88, migrate scrapers away from RDF closes #9, pull out LC subject heading tags references #87, add fromArray() and toArray() methods to item objects API changes: all translation (import/export/web) now goes through Scholar.Translate all Scholar-specific functions in scrapers start with "Scholar." rather than the jumbled up piggy bank un-namespaced confusion scrapers now longer specify items through RDF (the beginning of an item.fromArray()-like function exists in Scholar.Translate.prototype._itemDone()) scrapers can be any combination of import, export, and web (type is the sum of 1/2/4 respectively) scrapers now contain functions (doImport, doExport, doWeb) rather than loose code scrapers can call functions in other scrapers or just call the function to translate itself export accesses items item-by-item, rather than accepting a huge array of items MARC functions are now in the MARC import translator, and accessed by the web translators new features: import now works rudimentary RDF (unqualified dublin core only), RIS, and MARC import translators are implemented (although they are a little picky with respect to file extensions at the moment) items appear as they are scraped MARC import translator pulls out tags, although this seems to slow things down no icon appears next to a the URL when Scholar hasn't detected metadata, since this seemed somewhat confusing apologizes for the size of this diff. i figured if i was going to re-write the API, i might as well do it all at once and get everything working right.
2006-07-17 04:06:58 +00:00
} else {
var spaceIndex = author.lastIndexOf(" ");
var lastName = author.substring(spaceIndex+1);
var firstName = author.substring(0, spaceIndex);
}
if(firstName && allCapsRe.test(firstName) &&
firstName.length < 4 &&
(firstName.length == 1 || lastName.toUpperCase() != lastName)) {
// first name is probably initials
var newFirstName = "";
for(var i=0; i<firstName.length; i++) {
newFirstName += " "+firstName[i]+".";
}
firstName = newFirstName.substr(1);
closes #78, figure out import/export architecture closes #100, migrate ingester to Scholar.Translate closes #88, migrate scrapers away from RDF closes #9, pull out LC subject heading tags references #87, add fromArray() and toArray() methods to item objects API changes: all translation (import/export/web) now goes through Scholar.Translate all Scholar-specific functions in scrapers start with "Scholar." rather than the jumbled up piggy bank un-namespaced confusion scrapers now longer specify items through RDF (the beginning of an item.fromArray()-like function exists in Scholar.Translate.prototype._itemDone()) scrapers can be any combination of import, export, and web (type is the sum of 1/2/4 respectively) scrapers now contain functions (doImport, doExport, doWeb) rather than loose code scrapers can call functions in other scrapers or just call the function to translate itself export accesses items item-by-item, rather than accepting a huge array of items MARC functions are now in the MARC import translator, and accessed by the web translators new features: import now works rudimentary RDF (unqualified dublin core only), RIS, and MARC import translators are implemented (although they are a little picky with respect to file extensions at the moment) items appear as they are scraped MARC import translator pulls out tags, although this seems to slow things down no icon appears next to a the URL when Scholar hasn't detected metadata, since this seemed somewhat confusing apologizes for the size of this diff. i figured if i was going to re-write the API, i might as well do it all at once and get everything working right.
2006-07-17 04:06:58 +00:00
}
return {firstName:firstName, lastName:lastName, creatorType:type};
},
2007-10-23 07:11:59 +00:00
/**
* Removes leading and trailing whitespace from a string
* @type String
*/
"trim":function(/**String*/ s) {
if (typeof(s) != "string") {
throw "trim: argument must be a string";
2007-10-23 07:11:59 +00:00
}
s = s.replace(/^\s+/, "");
return s.replace(/\s+$/, "");
},
2009-06-01 20:13:09 +00:00
/**
* Cleans whitespace off a string and replaces multiple spaces with one
* @type String
*/
"trimInternal":function(/**String*/ s) {
if (typeof(s) != "string") {
throw "trimInternal: argument must be a string";
}
s = s.replace(/[\xA0\r\n\s]+/g, " ");
return this.trim(s);
},
2009-06-01 20:13:09 +00:00
/**
* Cleans any non-word non-parenthesis characters off the ends of a string
* @type String
*/
"superCleanString":function(/**String*/ x) {
if(typeof(x) != "string") {
throw "superCleanString: argument must be a string";
}
var x = x.replace(/^[\x00-\x27\x29-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/, "");
return x.replace(/[\x00-\x28\x2A-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+$/, "");
},
/**
* Eliminates HTML tags, replacing &lt;br&gt;s with newlines
* @type String
*/
"cleanTags":function(/**String*/ x) {
if(typeof(x) != "string") {
throw "cleanTags: argument must be a string";
}
x = x.replace(/<br[^>]*>/gi, "\n");
return x.replace(/<[^>]+>/g, "");
},
/**
* Strip info:doi prefix and any suffixes from a DOI
* @type String
*/
"cleanDOI":function(/**String**/ x) {
if(typeof(x) != "string") {
throw "cleanDOI: argument must be a string";
2007-10-23 07:11:59 +00:00
}
return x.match(/10\.[^\s\/]+\/[^\s]+/);
},
2007-10-23 07:11:59 +00:00
/**
* Convert plain text to HTML by replacing special characters and replacing newlines with BRs or
* P tags
* @param {String} str Plain text string
* @param {Boolean} singleNewlineIsParagraph Whether single newlines should be considered as
* paragraphs. If true, each newline is replaced with a P tag. If false, double newlines
* are replaced with P tags, while single newlines are replaced with BR tags.
* @type String
*/
"text2html":function (/**String**/ str, /**Boolean**/ singleNewlineIsParagraph) {
str = Zotero.Utilities.htmlSpecialChars(str);
// \n => <p>
if (singleNewlineIsParagraph) {
str = '<p>'
+ str.replace(/\n/g, '</p><p>')
.replace(/ /g, '&nbsp; ')
+ '</p>';
}
// \n\n => <p>, \n => <br/>
else {
str = '<p>'
+ str.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br/>')
.replace(/ /g, '&nbsp; ')
+ '</p>';
}
return str.replace(/<p>\s*<\/p>/g, '<p>&nbsp;</p>');
},
2007-10-23 07:11:59 +00:00
/**
* Encode special XML/HTML characters<br/>
* <br/>
* Certain entities can be inserted manually:<br/>
* <pre> &lt;ZOTEROBREAK/&gt; =&gt; &lt;br/&gt;
* &lt;ZOTEROHELLIP/&gt; =&gt; &amp;#8230;</pre>
* @type String
*/
"htmlSpecialChars":function(/**String*/ str) {
if (typeof str != 'string') {
throw "Argument '" + str + "' must be a string in Zotero.Utilities.htmlSpecialChars()";
}
if (!str) {
return '';
}
var chars = ['&', '"',"'",'<','>'];
var entities = ['amp', 'quot', 'apos', 'lt', 'gt'];
var newString = str;
for (var i = 0; i < chars.length; i++) {
var re = new RegExp(chars[i], 'g');
newString = newString.replace(re, '&' + entities[i] + ';');
}
newString = newString.replace(/&lt;ZOTERO([^\/]+)\/&gt;/g, function (str, p1, offset, s) {
switch (p1) {
case 'BREAK':
return '<br/>';
case 'HELLIP':
return '&#8230;';
default:
return p1;
}
});
return newString;
},
/**
* Decodes HTML entities within a string, returning plain text
* @type String
*/
"unescapeHTML":function(/**String*/ str) {
// If no tags, no need to unescape
if(str.indexOf("<") === -1 && str.indexOf("&") === -1) return str;
if(Zotero.isFx && !Zotero.isBookmarklet) {
if(!Zotero.Utilities._nsISUHTML) {
Zotero.Utilities._nsISUHTML = Components.classes["@mozilla.org/feed-unescapehtml;1"]
.getService(Components.interfaces.nsIScriptableUnescapeHTML);
}
return Zotero.Utilities._nsISUHTML.unescape(str);
} else if(Zotero.isNode) {
/*var doc = require('jsdom').jsdom(str, null, {
"features":{
"FetchExternalResources":false,
"ProcessExternalResources":false,
"MutationEvents":false,
"QuerySelector":false
}
});
if(!doc.documentElement) return str;
return doc.documentElement.textContent;*/
return Zotero.Utilities.cleanTags(str);
} else {
var node = document.createElement("div");
node.innerHTML = str;
return node.textContent;
}
},
/**
* Wrap URLs and DOIs in <a href=""> links in plain text
*
* Ignore URLs preceded by '>', just in case there are already links
* @type String
*/
"autoLink":function (/**String**/ str) {
// "http://www.google.com."
// "http://www.google.com. "
// "<http://www.google.com>" (and other characters, with or without a space after)
str = str.replace(/([^>])(https?:\/\/[^\s]+)([\."'>:\]\)](\s|$))/g, '$1<a href="$2">$2</a>$3');
// "http://www.google.com"
// "http://www.google.com "
str = str.replace(/([^">])(https?:\/\/[^\s]+)(\s|$)/g, '$1<a href="$2">$2</a>$3');
// DOI
str = str.replace(/(doi:[ ]*)(10\.[^\s]+[0-9a-zA-Z])/g, '$1<a href="http://dx.doi.org/$2">$2</a>');
return str;
},
2007-10-23 07:11:59 +00:00
/**
* Parses a text string for HTML/XUL markup and returns an array of parts. Currently only finds
* HTML links (&lt;a&gt; tags)
*
* @return {Array} An array of objects with the following form:<br>
* <pre> {
* type: 'text'|'link',
* text: "text content",
* [ attributes: { key1: val [ , key2: val, ...] }
* }</pre>
*/
"parseMarkup":function(/**String*/ str) {
var parts = [];
var splits = str.split(/(<a [^>]+>[^<]*<\/a>)/);
for(var i=0; i<splits.length; i++) {
// Link
if (splits[i].indexOf('<a ') == 0) {
var matches = splits[i].match(/<a ([^>]+)>([^<]*)<\/a>/);
if (matches) {
// Attribute pairs
var attributes = {};
var pairs = matches[1].match(/([^ =]+)="([^"]+")/g);
for(var j=0; j<pairs.length; j++) {
var keyVal = pairs[j].split(/=/);
attributes[keyVal[0]] = keyVal[1].substr(1, keyVal[1].length - 2);
}
parts.push({
type: 'link',
text: matches[2],
attributes: attributes
});
continue;
2007-10-23 07:11:59 +00:00
}
}
parts.push({
type: 'text',
text: splits[i]
});
2007-10-23 07:11:59 +00:00
}
return parts;
},
/**
* Calculates the Levenshtein distance between two strings
* @type Number
*/
"levenshtein":function (/**String*/ a, /**String**/ b) {
var aLen = a.length;
var bLen = b.length;
var arr = new Array(aLen+1);
var i, j, cost;
for (i = 0; i <= aLen; i++) {
arr[i] = new Array(bLen);
arr[i][0] = i;
}
for (j = 0; j <= bLen; j++) {
arr[0][j] = j;
}
for (i = 1; i <= aLen; i++) {
for (j = 1; j <= bLen; j++) {
cost = (a[i-1] == b[j-1]) ? 0 : 1;
arr[i][j] = Math.min(arr[i-1][j] + 1, Math.min(arr[i][j-1] + 1, arr[i-1][j-1] + cost));
}
}
return arr[aLen][bLen];
},
Zotero File Storage megacommit - Group file sync via Zotero File Storage - Split file syncing into separate modules for ZFS and WebDAV - Dragging items between libraries copies child notes, snapshots/files, and links based on checkboxes for each (enabled by default) in the Zotero preferences - Sync errors now trigger an exclamation/error icon separate from the sync icon, with a popup window displaying the error and an option to report it - Various errors that could cause perpetual sync icon spinning now stop the sync properly - Zotero.Utilities.md5(str) is now md5(strOrFile, base64) - doPost(), doHead(), and retrieveSource() now takes a headers parameter instead of requestContentType - doHead() can now accept an nsIURI (with login credentials), is a background request, and isn't cached - When library access or file writing access is denied during sync, display a warning and then reset local group to server version - Perform additional steps (e.g., removing local groups) when switching sync users to prevent errors - Compare hash as well as mod time when checking for modified local files - Don't trigger notifications when removing groups from the client - Clear relation links to items in removed groups - Zotero.Item.attachmentHash property to get file MD5 - importFromFile() now takes libraryID as a third parameter - Zotero.Attachments.getNumFiles() returns the number of files in the attachment directory - Zotero.Attachments.copyAttachmentToLibrary() copies an attachment item, including files, to another library - Removed Zotero.File.getFileHash() in favor of updated Zotero.Utilities.md5() - Zotero.File.copyDirectory(dir, newDir) copies all files from dir into newDir - Preferences shuffling: OpenURL to Advanced, import/export character set options to Export, "Include URLs of paper articles in references" to Styles - Other stuff I don't remember Suffice it to say, this could use testing.
2009-09-13 07:23:29 +00:00
/**
* Test if an object is empty
*
* @param {Object} obj
* @type Boolean
*/
"isEmpty":function (obj) {
for (var i in obj) {
return false;
}
return true;
},
/**
* Compares an array with another and returns an array with
* the values from array1 that don't exist in array2
*
* @param {Array} array1
* @param {Array} array2
* @param {Boolean} useIndex If true, return an array containing just
* the index of array2's elements;
* otherwise return the values
*/
"arrayDiff":function(array1, array2, useIndex) {
if (array1.constructor.name != 'Array') {
throw ("array1 is not an array in Zotero.Utilities.arrayDiff() (" + array1 + ")");
}
if (array2.constructor.name != 'Array') {
throw ("array2 is not an array in Zotero.Utilities.arrayDiff() (" + array2 + ")");
}
var val, pos, vals = [];
for (var i=0; i<array1.length; i++) {
val = array1[i];
pos = array2.indexOf(val);
if (pos == -1) {
vals.push(useIndex ? pos : val);
}
}
return vals;
},
2011-03-30 00:39:08 +00:00
/**
* Return new array with duplicate values removed
*
* From the JSLab Standard Library (JSL)
* Copyright 2007 - 2009 Tavs Dokkedahl
* Contact: http://www.jslab.dk/contact.php
*
* @param {Array} array
* @return {Array}
*/
"arrayUnique":function(arr) {
var a = [];
var l = arr.length;
for(var i=0; i<l; i++) {
for(var j=i+1; j<l; j++) {
// If this[i] is found later in the array
if (arr[i] === arr[j])
j = ++i;
}
a.push(arr[i]);
}
return a;
},
/**
* Generate a random integer between min and max inclusive
*
* @param {Integer} min
* @param {Integer} max
* @return {Integer}
*/
"rand":function (min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
},
/**
* Parse a page range
*
* @param {String} Page range to parse
* @return {Integer[]} Start and end pages
*/
"getPageRange":function(pages) {
const pageRangeRegexp = /^\s*([0-9]+) ?[-\u2013] ?([0-9]+)\s*$/
var pageNumbers;
var m = pageRangeRegexp.exec(pages);
if(m) {
// A page range
pageNumbers = [m[1], m[2]];
} else {
// Assume start and end are the same
pageNumbers = [pages, pages];
}
return pageNumbers;
},
2007-10-23 07:11:59 +00:00
/**
* Pads a number or other string with a given string on the left
*
* @param {String} string String to pad
* @param {String} pad String to use as padding
* @length {Integer} length Length of new padded string
* @type String
*/
"lpad":function(string, pad, length) {
string = string ? string + '' : '';
while(string.length < length) {
string = pad + string;
}
return string;
},
2007-10-23 07:11:59 +00:00
/**
* Shorten and add an ellipsis to a string if necessary
*
* @param {String} str
* @param {Integer} len
* @param {Boolean} [countChars=false]
*/
"ellipsize":function (str, len, countChars) {
if (!len) {
throw ("Length not specified in Zotero.Utilities.ellipsize()");
}
if (str.length > len) {
return str.substr(0, len) + '\u2026' + (countChars ? ' (' + str.length + ' chars)' : '');
}
return str;
},
/**
* Port of PHP's number_format()
*
* MIT Licensed
*
* From http://kevin.vanzonneveld.net
* + original by: Jonas Raoni Soares Silva (http://www.jsfromhell.com)
* + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net)
* + bugfix by: Michael White (http://getsprink.com)
* + bugfix by: Benjamin Lupton
* + bugfix by: Allan Jensen (http://www.winternet.no)
* + revised by: Jonas Raoni Soares Silva (http://www.jsfromhell.com)
* + bugfix by: Howard Yeend
* * example 1: number_format(1234.5678, 2, '.', '');
* * returns 1: 1234.57
*/
"numberFormat":function (number, decimals, dec_point, thousands_sep) {
var n = number, c = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals;
var d = dec_point == undefined ? "." : dec_point;
var t = thousands_sep == undefined ? "," : thousands_sep, s = n < 0 ? "-" : "";
var i = parseInt(n = Math.abs(+n || 0).toFixed(c)) + "", j = (j = i.length) > 3 ? j % 3 : 0;
return s + (j ? i.substr(0, j) + t : "") + i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + t) + (c ? d + Math.abs(n - i).toFixed(c).slice(2) : "");
},
/**
* Cleans a title, converting it to title case and replacing " :" with ":"
*
* @param {String} string
* @param {Boolean} force Forces title case conversion, even if the capitalizeTitles pref is off
* @type String
*/
"capitalizeTitle":function(string, force) {
const skipWords = ["but", "or", "yet", "so", "for", "and", "nor", "a", "an",
"the", "at", "by", "from", "in", "into", "of", "on", "to", "with", "up",
"down", "as"];
// this may only match a single character
const delimiterRegexp = /([ \/\-–—])/;
string = this.trimInternal(string);
string = string.replace(/ : /g, ": ");
if(force === false || (!Zotero.Prefs.get('capitalizeTitles') && !force)) return string;
if(!string) return "";
// split words
var words = string.split(delimiterRegexp);
var isUpperCase = string.toUpperCase() == string;
var newString = "";
var delimiterOffset = words[0].length;
var lastWordIndex = words.length-1;
var previousWordIndex = -1;
for(var i=0; i<=lastWordIndex; i++) {
// only do manipulation if not a delimiter character
if(words[i].length != 0 && (words[i].length != 1 || !delimiterRegexp.test(words[i]))) {
var upperCaseVariant = words[i].toUpperCase();
var lowerCaseVariant = words[i].toLowerCase();
// only use if word does not already possess some capitalization
if(isUpperCase || words[i] == lowerCaseVariant) {
if(
// a skip word
skipWords.indexOf(lowerCaseVariant.replace(/[^a-zA-Z]+/, "")) != -1
// not first or last word
&& i != 0 && i != lastWordIndex
// does not follow a colon
&& (previousWordIndex == -1 || words[previousWordIndex][words[previousWordIndex].length-1] != ":")
) {
words[i] = lowerCaseVariant;
} else {
// this is not a skip word or comes after a colon;
// we must capitalize
words[i] = upperCaseVariant[0] + lowerCaseVariant.substr(1);
}
}
previousWordIndex = i;
}
newString += words[i];
}
return newString;
},
Duplicate detection: - Adds a per-library "Duplicate Items" virtual search to the source list -- shows up by default for "My Library" but can be added to and removed from all libraries - Current matching algorithm is very basic: finds exact title matches (after normalizing case/diacritics/punctuation/spacing) and DOI/ISBN matches (untested) - In duplicates view, sets are selected automatically; in other views, duplicate items can be selected manually and the merge interface can be brought up with "Merge Items" in the context menu - Can select a master item and individual fields to merge from other versions - Word processor integration code will automatically find mapped replacements and update documents with new item keys Possible future improvements: - Improved detection algorithms - UI tweaks - Currently if any items differ, all available versions will be shown as master item options, even if only one item is different; probably the earliest equivalent item should be shown for each distinct version - Caching of results for performance - Confidence scale - Creator version selection (currently the creators from the chosen master item are kept) - Merging of matching child items - Better sorting of duplicates if not clustered together by the selected sort column - Relation path compression when merging items that are already mapped to previously removed duplicates Other changes in this commit: - Don't show Trash in word processor integration windows - Consider items in trash to be missing in word processor documents - Selection of special views (Trash, Unfiled, Duplicates) is now restored properly in new windows - Disabled field transform context menu when item isn't editable - Left/right arrow now expands/collapses all selected items instead of just the last-selected row - Relation deletions are now synced - The same items row is now reselected after item deletion - (dev) Zotero.Item.getNotes(), Zotero.Item.getAttachments(), and Zotero.Item.getTags() now return empty arrays rather than FALSE if no matches -- tests on those return values in third-party code will need to be changed - (dev) New function Zotero.Utilities.removeDiacritics(str, lowercaseOnly) -- could be used to generate ASCII BibTeX keys - (dev) New 'tempTable' search condition can take a table to join against -- useful for implementing virtual source lists - (dev) Significant UI code cleanup - (dev) Moved all item pane content into itemPane.xul - Probably various other things Needless to say, this needs testing.
2011-07-22 21:24:38 +00:00
/**
* Replaces accented characters in a string with ASCII equivalents
*
* @param {String} str
* @param {Boolean} [lowercaseOnly] Limit conversions to lowercase characters
* (for improved performance on lowercase input)
* @return {String}
*
* From http://lehelk.com/2011/05/06/script-to-remove-diacritics/
*/
"removeDiacritics": function (str, lowercaseOnly) {
var map = this._diacriticsRemovalMap.lowercase;
for (var i=0, len=map.length; i<len; i++) {
str = str.replace(map[i].letters, map[i].base);
}
if (!lowercaseOnly) {
var map = this._diacriticsRemovalMap.uppercase;
for (var i=0, len=map.length; i<len; i++) {
str = str.replace(map[i].letters, map[i].base);
}
}
return str;
},
"_diacriticsRemovalMap": {
uppercase: [
{'base':'A', 'letters':/[\u0041\u24B6\uFF21\u00C0\u00C1\u00C2\u1EA6\u1EA4\u1EAA\u1EA8\u00C3\u0100\u0102\u1EB0\u1EAE\u1EB4\u1EB2\u0226\u01E0\u00C4\u01DE\u1EA2\u00C5\u01FA\u01CD\u0200\u0202\u1EA0\u1EAC\u1EB6\u1E00\u0104\u023A\u2C6F]/g},
{'base':'AA','letters':/[\uA732]/g},
{'base':'AE','letters':/[\u00C6\u01FC\u01E2]/g},
{'base':'AO','letters':/[\uA734]/g},
{'base':'AU','letters':/[\uA736]/g},
{'base':'AV','letters':/[\uA738\uA73A]/g},
{'base':'AY','letters':/[\uA73C]/g},
{'base':'B', 'letters':/[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181]/g},
{'base':'C', 'letters':/[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E]/g},
{'base':'D', 'letters':/[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779]/g},
{'base':'DZ','letters':/[\u01F1\u01C4]/g},
{'base':'Dz','letters':/[\u01F2\u01C5]/g},
{'base':'E', 'letters':/[\u0045\u24BA\uFF25\u00C8\u00C9\u00CA\u1EC0\u1EBE\u1EC4\u1EC2\u1EBC\u0112\u1E14\u1E16\u0114\u0116\u00CB\u1EBA\u011A\u0204\u0206\u1EB8\u1EC6\u0228\u1E1C\u0118\u1E18\u1E1A\u0190\u018E]/g},
{'base':'F', 'letters':/[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B]/g},
{'base':'G', 'letters':/[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E]/g},
{'base':'H', 'letters':/[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D]/g},
{'base':'I', 'letters':/[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197]/g},
{'base':'J', 'letters':/[\u004A\u24BF\uFF2A\u0134\u0248]/g},
{'base':'K', 'letters':/[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2]/g},
{'base':'L', 'letters':/[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780]/g},
{'base':'LJ','letters':/[\u01C7]/g},
{'base':'Lj','letters':/[\u01C8]/g},
{'base':'M', 'letters':/[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C]/g},
{'base':'N', 'letters':/[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4]/g},
{'base':'NJ','letters':/[\u01CA]/g},
{'base':'Nj','letters':/[\u01CB]/g},
{'base':'O', 'letters':/[\u004F\u24C4\uFF2F\u00D2\u00D3\u00D4\u1ED2\u1ED0\u1ED6\u1ED4\u00D5\u1E4C\u022C\u1E4E\u014C\u1E50\u1E52\u014E\u022E\u0230\u00D6\u022A\u1ECE\u0150\u01D1\u020C\u020E\u01A0\u1EDC\u1EDA\u1EE0\u1EDE\u1EE2\u1ECC\u1ED8\u01EA\u01EC\u00D8\u01FE\u0186\u019F\uA74A\uA74C]/g},
{'base':'OI','letters':/[\u01A2]/g},
{'base':'OO','letters':/[\uA74E]/g},
{'base':'OU','letters':/[\u0222]/g},
{'base':'P', 'letters':/[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754]/g},
{'base':'Q', 'letters':/[\u0051\u24C6\uFF31\uA756\uA758\u024A]/g},
{'base':'R', 'letters':/[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782]/g},
{'base':'S', 'letters':/[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784]/g},
{'base':'T', 'letters':/[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786]/g},
{'base':'TZ','letters':/[\uA728]/g},
{'base':'U', 'letters':/[\u0055\u24CA\uFF35\u00D9\u00DA\u00DB\u0168\u1E78\u016A\u1E7A\u016C\u00DC\u01DB\u01D7\u01D5\u01D9\u1EE6\u016E\u0170\u01D3\u0214\u0216\u01AF\u1EEA\u1EE8\u1EEE\u1EEC\u1EF0\u1EE4\u1E72\u0172\u1E76\u1E74\u0244]/g},
{'base':'V', 'letters':/[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245]/g},
{'base':'VY','letters':/[\uA760]/g},
{'base':'W', 'letters':/[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72]/g},
{'base':'X', 'letters':/[\u0058\u24CD\uFF38\u1E8A\u1E8C]/g},
{'base':'Y', 'letters':/[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE]/g},
{'base':'Z', 'letters':/[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762]/g},
],
lowercase: [
{'base':'a', 'letters':/[\u0061\u24D0\uFF41\u1E9A\u00E0\u00E1\u00E2\u1EA7\u1EA5\u1EAB\u1EA9\u00E3\u0101\u0103\u1EB1\u1EAF\u1EB5\u1EB3\u0227\u01E1\u00E4\u01DF\u1EA3\u00E5\u01FB\u01CE\u0201\u0203\u1EA1\u1EAD\u1EB7\u1E01\u0105\u2C65\u0250]/g},
{'base':'aa','letters':/[\uA733]/g},
{'base':'ae','letters':/[\u00E6\u01FD\u01E3]/g},
{'base':'ao','letters':/[\uA735]/g},
{'base':'au','letters':/[\uA737]/g},
{'base':'av','letters':/[\uA739\uA73B]/g},
{'base':'ay','letters':/[\uA73D]/g},
{'base':'b', 'letters':/[\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253]/g},
{'base':'c', 'letters':/[\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184]/g},
{'base':'d', 'letters':/[\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A]/g},
{'base':'dz','letters':/[\u01F3\u01C6]/g},
{'base':'e', 'letters':/[\u0065\u24D4\uFF45\u00E8\u00E9\u00EA\u1EC1\u1EBF\u1EC5\u1EC3\u1EBD\u0113\u1E15\u1E17\u0115\u0117\u00EB\u1EBB\u011B\u0205\u0207\u1EB9\u1EC7\u0229\u1E1D\u0119\u1E19\u1E1B\u0247\u025B\u01DD]/g},
{'base':'f', 'letters':/[\u0066\u24D5\uFF46\u1E1F\u0192\uA77C]/g},
{'base':'g', 'letters':/[\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F]/g},
{'base':'h', 'letters':/[\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265]/g},
{'base':'hv','letters':/[\u0195]/g},
{'base':'i', 'letters':/[\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131]/g},
{'base':'j', 'letters':/[\u006A\u24D9\uFF4A\u0135\u01F0\u0249]/g},
{'base':'k', 'letters':/[\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3]/g},
{'base':'l', 'letters':/[\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747]/g},
{'base':'lj','letters':/[\u01C9]/g},
{'base':'m', 'letters':/[\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F]/g},
{'base':'n', 'letters':/[\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5]/g},
{'base':'nj','letters':/[\u01CC]/g},
{'base':'o', 'letters':/[\u006F\u24DE\uFF4F\u00F2\u00F3\u00F4\u1ED3\u1ED1\u1ED7\u1ED5\u00F5\u1E4D\u022D\u1E4F\u014D\u1E51\u1E53\u014F\u022F\u0231\u00F6\u022B\u1ECF\u0151\u01D2\u020D\u020F\u01A1\u1EDD\u1EDB\u1EE1\u1EDF\u1EE3\u1ECD\u1ED9\u01EB\u01ED\u00F8\u01FF\u0254\uA74B\uA74D\u0275]/g},
{'base':'oi','letters':/[\u01A3]/g},
{'base':'ou','letters':/[\u0223]/g},
{'base':'oo','letters':/[\uA74F]/g},
{'base':'p','letters':/[\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755]/g},
{'base':'q','letters':/[\u0071\u24E0\uFF51\u024B\uA757\uA759]/g},
{'base':'r','letters':/[\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783]/g},
{'base':'s','letters':/[\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B]/g},
{'base':'t','letters':/[\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787]/g},
{'base':'tz','letters':/[\uA729]/g},
{'base':'u','letters':/[\u0075\u24E4\uFF55\u00F9\u00FA\u00FB\u0169\u1E79\u016B\u1E7B\u016D\u00FC\u01DC\u01D8\u01D6\u01DA\u1EE7\u016F\u0171\u01D4\u0215\u0217\u01B0\u1EEB\u1EE9\u1EEF\u1EED\u1EF1\u1EE5\u1E73\u0173\u1E77\u1E75\u0289]/g},
{'base':'v','letters':/[\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C]/g},
{'base':'vy','letters':/[\uA761]/g},
{'base':'w','letters':/[\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73]/g},
{'base':'x','letters':/[\u0078\u24E7\uFF58\u1E8B\u1E8D]/g},
{'base':'y','letters':/[\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF]/g},
{'base':'z','letters':/[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763]/g}
]
},
/**
* Run sets of data through multiple asynchronous callbacks
*
* Each callback is passed the current set and a callback to call when done
*
* @param {Object[]} sets Sets of data
* @param {Function[]} callbacks
* @param {Function} onDone Function to call when done
*/
"processAsync":function (sets, callbacks, onDone) {
var currentSet;
var index = 0;
var nextSet = function () {
if (!sets.length) {
onDone();
return;
}
index = 0;
currentSet = sets.shift();
callbacks[0](currentSet, nextCallback);
};
var nextCallback = function () {
index++;
callbacks[index](currentSet, nextCallback);
};
// Add a final callback to proceed to the next set
callbacks[callbacks.length] = function () {
nextSet();
}
nextSet();
},
/**
* Performs a deep copy of a JavaScript object
* @param {Object} obj
* @return {Object}
*/
"deepCopy":function(obj) {
var obj2 = (obj instanceof Array ? [] : {});
for(var i in obj) {
if(!obj.hasOwnProperty(i)) continue;
if(typeof obj[i] === "object") {
obj2[i] = Zotero.Utilities.deepCopy(obj[i]);
} else {
obj2[i] = obj[i];
}
}
return obj2;
},
/**
* Tests if an item type exists
*
* @param {String} type Item type
* @type Boolean
*/
"itemTypeExists":function(type) {
2010-11-05 03:10:56 +00:00
if(Zotero.ItemTypes.getID(type)) {
return true;
} else {
2010-11-05 03:10:56 +00:00
return false;
}
},
/**
* Find valid creator types for a given item type
*
* @param {String} type Item type
* @return {String[]} Creator types
*/
"getCreatorsForType":function(type) {
2010-11-05 03:10:56 +00:00
var types = Zotero.CreatorTypes.getTypesForItemType(Zotero.ItemTypes.getID(type));
var cleanTypes = new Array();
for(var i=0; i<types.length; i++) {
cleanTypes.push(types[i].name);
}
2010-11-05 03:10:56 +00:00
return cleanTypes;
},
/**
2011-09-26 01:27:16 +00:00
* Determine whether a given field is valid for a given item type
*
2011-09-26 01:27:16 +00:00
* @param {String} field Field name
* @param {String} type Item type
2011-09-26 01:27:16 +00:00
* @type Boolean
*/
"fieldIsValidForType":function(field, type) {
2010-11-05 03:10:56 +00:00
return Zotero.ItemFields.isValidForType(field, Zotero.ItemTypes.getID(type));
},
/**
* Gets a creator type name, localized to the current locale
*
* @param {String} type Creator type
* @param {String} Localized creator type
* @type Boolean
*/
"getLocalizedCreatorType":function(type) {
2010-11-05 03:10:56 +00:00
try {
return Zotero.CreatorTypes.getLocalizedString(type);
} catch(e) {
return false;
}
},
/**
* Escapes metacharacters in a literal so that it may be used in a regular expression
*/
"quotemeta":function(literal) {
if(typeof literal !== "string") {
throw "Argument "+literal+" must be a string in Zotero.Utilities.quotemeta()";
}
const metaRegexp = /[-[\]{}()*+?.\\^$|,#\s]/g;
return literal.replace(metaRegexp, "\\$&");
},
/**
* Evaluate an XPath
*
* @param {element|element[]} elements The element(s) to use as the context for the XPath
* @param {String} xpath The XPath expression
* @param {Object} [namespaces] An object whose keys represent namespace prefixes, and whose
* values represent their URIs
* @return {element[]} DOM elements matching XPath
*/
2011-06-22 08:43:44 +00:00
"xpath":function(elements, xpath, namespaces) {
var nsResolver = null;
if(namespaces) {
nsResolver = function(prefix) {
return namespaces[prefix] || null;
};
}
if(!(elements instanceof Array)) elements = [elements];
var results = [];
for(var i=0, n=elements.length; i<n; i++) {
var element = elements[i];
// Firefox 5 hack, so we will preserve Fx5DOMWrappers
var useFx5DOMWrapper = !!element.__wrappedDOMObject;
if(useFx5DOMWrapper) element = element.__wrappedDOMObject;
if(element.ownerDocument) {
var rootDoc = element.ownerDocument;
} else if(element.documentElement) {
var rootDoc = element;
} else {
throw new Error("First argument must be either element(s) or document(s) in Zotero.Utilities.xpath(elements, '"+xpath+"')");
}
try {
var xpathObject = rootDoc.evaluate(xpath, element, nsResolver, 5, // 5 = ORDERED_NODE_ITERATOR_TYPE
null);
} catch(e) {
// rethrow so that we get a stack
throw new Error(e.name+": "+e.message);
}
var newEl;
while(newEl = xpathObject.iterateNext()) {
2011-06-22 07:52:06 +00:00
// Firefox 5 hack
results.push(useFx5DOMWrapper ? Zotero.Translate.SandboxManager.Fx5DOMWrapper(newEl) : newEl);
}
}
return results;
},
/**
* Generates a string from the content of nodes matching a given XPath
*
* @param {element} node The node representing the document and context
* @param {String} xpath The XPath expression
* @param {Object} [namespaces] An object whose keys represent namespace prefixes, and whose
* values represent their URIs
* @param {String} [delimiter] The string with which to join multiple matching nodes
* @return {String|null} DOM elements matching XPath, or null if no elements exist
*/
"xpathText":function(node, xpath, namespaces, delimiter) {
var elements = Zotero.Utilities.xpath(node, xpath, namespaces);
if(!elements.length) return null;
var strings = new Array(elements.length);
for(var i=0, n=elements.length; i<n; i++) {
strings[i] = elements[i].textContent;
}
return strings.join(delimiter ? delimiter : ", ");
},
/**
* Generate a random string of length 'len' (defaults to 8)
**/
"randomString":function(len, chars) {
if (!chars) {
chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
}
if (!len) {
len = 8;
}
var randomstring = '';
for (var i=0; i<len; i++) {
var rnum = Math.floor(Math.random() * chars.length);
randomstring += chars.substring(rnum,rnum+1);
}
return randomstring;
},
/**
* PHP var_dump equivalent for JS
*
* Adapted from http://binnyva.blogspot.com/2005/10/dump-function-javascript-equivalent-of.html
*/
"varDump":function(arr,level) {
var dumped_text = "";
if (!level){
level = 0;
}
// The padding given at the beginning of the line.
var level_padding = "";
for (var j=0;j<level+1;j++){
level_padding += " ";
}
if (typeof(arr) == 'object') { // Array/Hashes/Objects
for (var item in arr) {
var value = arr[item];
if (typeof(value) == 'object') { // If it is an array,
dumped_text += level_padding + "'" + item + "' ...\n";
dumped_text += arguments.callee(value,level+1);
}
else {
if (typeof value == 'function'){
dumped_text += level_padding + "'" + item + "' => function(...){...} \n";
}
else if (typeof value == 'number') {
dumped_text += level_padding + "'" + item + "' => " + value + "\n";
}
else {
dumped_text += level_padding + "'" + item + "' => \"" + value + "\"\n";
}
}
}
}
else { // Stings/Chars/Numbers etc.
dumped_text = "===>"+arr+"<===("+typeof(arr)+")";
}
return dumped_text;
},
/**
* Adds all fields to an item in toArray() format and adds a unique (base) fields to
* uniqueFields array
*/
"itemToExportFormat":function(item) {
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 content=json format used by the server
*/
"itemToServerJSON":function(item) {
var newItem = {};
var typeID = Zotero.ItemTypes.getID(item.itemType);
if(!typeID) {
Zotero.debug("Translate: Invalid itemType "+item.itemType+"; saving as webpage");
item.itemType = "webpage";
typeID = Zotero.ItemTypes.getID(item.itemType);
}
var fieldID, itemFieldID;
for(var field in item) {
if(field === "complete" || field === "itemID" || field === "attachments"
|| field === "seeAlso") continue;
var val = item[field];
if(field === "itemType") {
newItem[field] = val;
} else if(field === "creators") {
// normalize creators
var n = val.length;
var newCreators = newItem.creators = new Array(n);
for(var j=0; j<n; j++) {
var creator = val[j];
// Single-field mode
if (!creator.firstName || (creator.fieldMode && creator.fieldMode == 1)) {
var newCreator = {
name: creator.lastName
};
}
// Two-field mode
else {
var newCreator = {
firstName: creator.firstName,
lastName: creator.lastName
};
}
// ensure creatorType is present and valid
if(creator.creatorType) {
if(Zotero.CreatorTypes.getID(creator.creatorType)) {
newCreator.creatorType = creator.creatorType;
} else {
Zotero.debug("Translate: Invalid creator type "+creator.creatorType+"; falling back to author");
}
}
if(!newCreator.creatorType) newCreator.creatorType = "author";
newCreators[j] = newCreator;
}
} else if(field === "tags") {
// normalize tags
var n = val.length;
var newTags = newItem.tags = new Array(n);
for(var j=0; j<n; j++) {
var tag = val[j];
if(typeof tag === "object") {
if(tag.tag) {
tag = tag.tag;
} else if(tag.name) {
tag = tag.name;
} else {
Zotero.debug("Translate: Discarded invalid tag");
continue;
}
}
newTags[j] = {"tag":tag.toString(), "type":1};
}
} else if(field === "notes") {
// normalize notes
var n = val.length;
var newNotes = newItem.notes = new Array(n);
for(var j=0; j<n; j++) {
var note = val[j];
if(typeof note === "object") {
if(!note.note) {
Zotero.debug("Translate: Discarded invalid note");
continue;
}
note = note.note;
}
newNotes[j] = {"itemType":"note", "note":note.toString()};
}
} else if((fieldID = Zotero.ItemFields.getID(field))) {
// if content is not a string, either stringify it or delete it
if(typeof val !== "string") {
if(val || val === 0) {
val = val.toString();
} else {
continue;
}
}
// map from base field if possible
if((itemFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(typeID, fieldID))) {
newItem[Zotero.ItemFields.getName(itemFieldID)] = val;
continue; // already know this is valid
}
// if field is valid for this type, set field
if(Zotero.ItemFields.isValidForType(fieldID, typeID)) {
newItem[field] = val;
} else {
Zotero.debug("Translate: Discarded field "+field+": field not valid for type "+item.itemType, 3);
}
} else {
Zotero.debug("Translate: Discarded unknown field "+field, 3);
}
}
return newItem;
},
/**
* Converts an item from toArray() format to citeproc-js JSON
*/
"itemToCSLJSON":function(item) {
if(item instanceof Zotero.Item) {
item = item.toArray();
}
var itemType = item.itemType;
var cslType = CSL_TYPE_MAPPINGS[itemType];
if(!cslType) cslType = "article";
var cslItem = {
'id':item.itemID,
'type':cslType
};
// Map text fields
var itemTypeID = Zotero.ItemTypes.getID(itemType);
for(var variable in CSL_TEXT_MAPPINGS) {
var fields = CSL_TEXT_MAPPINGS[variable];
for(var i=0, n=fields.length; i<n; i++) {
var field = fields[i], value = undefined;
if(field in item) {
value = item[field];
} else {
var fieldID = Zotero.ItemFields.getID(field),
baseMapping
if(Zotero.ItemFields.isValidForType(fieldID, itemTypeID)
&& (baseMapping = Zotero.ItemFields.getBaseIDFromTypeAndField(itemTypeID, fieldID))) {
value = item[Zotero.ItemTypes.getName(baseMapping)];
}
}
if(!value) continue;
var valueLength = value.length;
if(valueLength) {
// Strip enclosing quotes
if(value[0] === '"' && value[valueLength-1] === '"') {
value = value.substr(1, valueLength-2);
}
}
cslItem[variable] = value;
break;
}
}
// separate name variables
var authorID = Zotero.CreatorTypes.getPrimaryIDForType(item.itemType);
var creators = item.creators;
if(creators) {
for(var i=0, n=creators.length; i<n; i++) {
var creator = creators[i];
if(creator.creatorTypeID == authorID) {
var creatorType = "author";
} else {
var creatorType = CSL_NAMES_MAPPINGS[creator.creatorType]
}
if(!creatorType) continue;
var nameObj = {'family':creator.lastName, 'given':creator.firstName};
if(cslItem[creatorType]) {
cslItem[creatorType].push(nameObj);
} else {
cslItem[creatorType] = [nameObj];
}
}
}
// get date variables
for(var variable in CSL_DATE_MAPPINGS) {
var date = item[CSL_DATE_MAPPINGS[variable]];
if(date) {
var dateObj = Zotero.Date.strToDate(date);
// otherwise, use date-parts
var dateParts = [];
if(dateObj.year) {
// add year, month, and day, if they exist
dateParts.push(dateObj.year);
if(dateObj.month !== undefined) {
dateParts.push(dateObj.month+1);
if(dateObj.day) {
dateParts.push(dateObj.day);
}
}
cslItem[variable] = {"date-parts":[dateParts]};
// if no month, use season as month
if(dateObj.part && !dateObj.month) {
cslItem[variable].season = dateObj.part;
}
} else {
// if no year, pass date literally
cslItem[variable] = {"literal":date};
}
}
}
//this._cache[item.id] = cslItem;
return cslItem;
}
}