utilities.js (62803B)
1 /* 2 ***** BEGIN LICENSE BLOCK ***** 3 4 Copyright © 2009 Center for History and New Media 5 George Mason University, Fairfax, Virginia, USA 6 http://zotero.org 7 8 This file is part of Zotero. 9 10 Zotero is free software: you can redistribute it and/or modify 11 it under the terms of the GNU Affero General Public License as published by 12 the Free Software Foundation, either version 3 of the License, or 13 (at your option) any later version. 14 15 Zotero is distributed in the hope that it will be useful, 16 but WITHOUT ANY WARRANTY; without even the implied warranty of 17 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 18 GNU Affero General Public License for more details. 19 20 You should have received a copy of the GNU Affero General Public License 21 along with Zotero. If not, see <http://www.gnu.org/licenses/>. 22 23 24 Utilities based in part on code taken from Piggy Bank 2.1.1 (BSD-licensed) 25 26 ***** END LICENSE BLOCK ***** 27 */ 28 29 /* 30 * Mappings for names 31 * Note that this is the reverse of the text variable map, since all mappings should be one to one 32 * and it makes the code cleaner 33 */ 34 var CSL_NAMES_MAPPINGS = { 35 "author":"author", 36 "editor":"editor", 37 "bookAuthor":"container-author", 38 "composer":"composer", 39 "director":"director", 40 "interviewer":"interviewer", 41 "recipient":"recipient", 42 "reviewedAuthor":"reviewed-author", 43 "seriesEditor":"collection-editor", 44 "translator":"translator" 45 } 46 47 /* 48 * Mappings for text variables 49 */ 50 var CSL_TEXT_MAPPINGS = { 51 "title":["title"], 52 "container-title":["publicationTitle", "reporter", "code"], /* reporter and code should move to SQL mapping tables */ 53 "collection-title":["seriesTitle", "series"], 54 "collection-number":["seriesNumber"], 55 "publisher":["publisher", "distributor"], /* distributor should move to SQL mapping tables */ 56 "publisher-place":["place"], 57 "authority":["court","legislativeBody", "issuingAuthority"], 58 "page":["pages"], 59 "volume":["volume", "codeNumber"], 60 "issue":["issue", "priorityNumbers"], 61 "number-of-volumes":["numberOfVolumes"], 62 "number-of-pages":["numPages"], 63 "edition":["edition"], 64 "version":["versionNumber"], 65 "section":["section", "committee"], 66 "genre":["type", "programmingLanguage"], 67 "source":["libraryCatalog"], 68 "dimensions": ["artworkSize", "runningTime"], 69 "medium":["medium", "system"], 70 "scale":["scale"], 71 "archive":["archive"], 72 "archive_location":["archiveLocation"], 73 "event":["meetingName", "conferenceName"], /* these should be mapped to the same base field in SQL mapping tables */ 74 "event-place":["place"], 75 "abstract":["abstractNote"], 76 "URL":["url"], 77 "DOI":["DOI"], 78 "ISBN":["ISBN"], 79 "ISSN":["ISSN"], 80 "call-number":["callNumber", "applicationNumber"], 81 "note":["extra"], 82 "number":["number"], 83 "chapter-number":["session"], 84 "references":["history", "references"], 85 "shortTitle":["shortTitle"], 86 "journalAbbreviation":["journalAbbreviation"], 87 "status":["legalStatus"], 88 "language":["language"] 89 } 90 91 /* 92 * Mappings for dates 93 */ 94 var CSL_DATE_MAPPINGS = { 95 "issued":"date", 96 "accessed":"accessDate", 97 "submitted":"filingDate" 98 } 99 100 /* 101 * Mappings for types 102 * Also see itemFromCSLJSON 103 */ 104 var CSL_TYPE_MAPPINGS = { 105 'book':"book", 106 'bookSection':'chapter', 107 'journalArticle':"article-journal", 108 'magazineArticle':"article-magazine", 109 'newspaperArticle':"article-newspaper", 110 'thesis':"thesis", 111 'encyclopediaArticle':"entry-encyclopedia", 112 'dictionaryEntry':"entry-dictionary", 113 'conferencePaper':"paper-conference", 114 'letter':"personal_communication", 115 'manuscript':"manuscript", 116 'interview':"interview", 117 'film':"motion_picture", 118 'artwork':"graphic", 119 'webpage':"webpage", 120 'report':"report", 121 'bill':"bill", 122 'case':"legal_case", 123 'hearing':"bill", // ?? 124 'patent':"patent", 125 'statute':"legislation", // ?? 126 'email':"personal_communication", 127 'map':"map", 128 'blogPost':"post-weblog", 129 'instantMessage':"personal_communication", 130 'forumPost':"post", 131 'audioRecording':"song", // ?? 132 'presentation':"speech", 133 'videoRecording':"motion_picture", 134 'tvBroadcast':"broadcast", 135 'radioBroadcast':"broadcast", 136 'podcast':"song", // ?? 137 'computerProgram':"book", // ?? 138 'document':"article", 139 'note':"article", 140 'attachment':"article" 141 }; 142 143 /** 144 * @class Functions for text manipulation and other miscellaneous purposes 145 */ 146 Zotero.Utilities = { 147 /** 148 * Cleans extraneous punctuation off a creator name and parse into first and last name 149 * 150 * @param {String} author Creator string 151 * @param {String} type Creator type string (e.g., "author" or "editor") 152 * @param {Boolean} useComma Whether the creator string is in inverted (Last, First) format 153 * @return {Object} firstName, lastName, and creatorType 154 */ 155 "cleanAuthor":function(author, type, useComma) { 156 var allCaps = 'A-Z' + 157 '\u0400-\u042f'; //cyrilic 158 159 var allCapsRe = new RegExp('^[' + allCaps + ']+$'); 160 var initialRe = new RegExp('^-?[' + allCaps + ']$'); 161 162 if(typeof(author) != "string") { 163 throw "cleanAuthor: author must be a string"; 164 } 165 166 author = author.replace(/^[\s\u00A0\.\,\/\[\]\:]+/, '') 167 .replace(/[\s\u00A0\.\,\/\[\]\:]+$/, '') 168 .replace(/[\s\u00A0]+/, ' '); 169 170 if(useComma) { 171 // Add spaces between periods 172 author = author.replace(/\.([^ ])/, ". $1"); 173 174 var splitNames = author.split(/, ?/); 175 if(splitNames.length > 1) { 176 var lastName = splitNames[0]; 177 var firstName = splitNames[1]; 178 } else { 179 var lastName = author; 180 } 181 } else { 182 // Don't parse "Firstname Lastname [Country]" as "[Country], Firstname Lastname" 183 var spaceIndex = author.length; 184 do { 185 spaceIndex = author.lastIndexOf(" ", spaceIndex-1); 186 var lastName = author.substring(spaceIndex + 1); 187 var firstName = author.substring(0, spaceIndex); 188 } while (!Zotero.Utilities.XRegExp('\\pL').test(lastName[0]) && spaceIndex > 0) 189 } 190 191 if(firstName && allCapsRe.test(firstName) && 192 firstName.length < 4 && 193 (firstName.length == 1 || lastName.toUpperCase() != lastName)) { 194 // first name is probably initials 195 var newFirstName = ""; 196 for(var i=0; i<firstName.length; i++) { 197 newFirstName += " "+firstName[i]+"."; 198 } 199 firstName = newFirstName.substr(1); 200 } 201 202 //add periods after all the initials 203 if(firstName) { 204 var names = firstName.replace(/^[\s\.]+/,'') 205 .replace(/[\s\,]+$/,'') 206 //remove spaces surronding any dashes 207 .replace(/\s*([\u002D\u00AD\u2010-\u2015\u2212\u2E3A\u2E3B])\s*/,'-') 208 .split(/(?:[\s\.]+|(?=-))/); 209 var newFirstName = ''; 210 for(var i=0, n=names.length; i<n; i++) { 211 newFirstName += names[i]; 212 if(initialRe.test(names[i])) newFirstName += '.'; 213 newFirstName += ' '; 214 } 215 firstName = newFirstName.replace(/ -/g,'-').trim(); 216 } 217 218 return {firstName:firstName, lastName:lastName, creatorType:type}; 219 }, 220 221 /** 222 * Removes leading and trailing whitespace from a string 223 * @type String 224 */ 225 "trim":function(/**String*/ s) { 226 if (typeof(s) != "string") { 227 throw "trim: argument must be a string"; 228 } 229 230 s = s.replace(/^\s+/, ""); 231 return s.replace(/\s+$/, ""); 232 }, 233 234 /** 235 * Cleans whitespace off a string and replaces multiple spaces with one 236 * @type String 237 */ 238 "trimInternal":function(/**String*/ s) { 239 if (typeof(s) != "string") { 240 throw new Error("trimInternal: argument must be a string"); 241 } 242 243 s = s.replace(/[\xA0\r\n\s]+/g, " "); 244 return this.trim(s); 245 }, 246 247 /** 248 * Cleans any non-word non-parenthesis characters off the ends of a string 249 * @type String 250 */ 251 "superCleanString":function(/**String*/ x) { 252 if(typeof(x) != "string") { 253 throw "superCleanString: argument must be a string"; 254 } 255 256 var x = x.replace(/^[\x00-\x27\x29-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F\s]+/, ""); 257 return x.replace(/[\x00-\x28\x2A-\x2F\x3A-\x40\x5B-\x60\x7B-\x7F\s]+$/, ""); 258 }, 259 260 /** 261 * Cleans a http url string 262 * @param url {String} 263 * @params tryHttp {Boolean} Attempt prepending 'http://' to the url 264 * @returns {String} 265 */ 266 cleanURL: function(url, tryHttp=false) { 267 url = url.trim(); 268 if (!url) return false; 269 270 var ios = Components.classes["@mozilla.org/network/io-service;1"] 271 .getService(Components.interfaces.nsIIOService); 272 try { 273 return ios.newURI(url, null, null).spec; // Valid URI if succeeds 274 } catch (e) { 275 if (e instanceof Components.Exception 276 && e.result == Components.results.NS_ERROR_MALFORMED_URI 277 ) { 278 if (tryHttp && /\w\.\w/.test(url)) { 279 // Assume it's a URL missing "http://" part 280 try { 281 return ios.newURI('http://' + url, null, null).spec; 282 } catch (e) {} 283 } 284 285 Zotero.debug('cleanURL: Invalid URI: ' + url, 2); 286 return false; 287 } 288 throw e; 289 } 290 }, 291 292 /** 293 * Eliminates HTML tags, replacing <br>s with newlines 294 * @type String 295 */ 296 "cleanTags":function(/**String*/ x) { 297 if(typeof(x) != "string") { 298 throw "cleanTags: argument must be a string"; 299 } 300 301 x = x.replace(/<br[^>]*>/gi, "\n"); 302 x = x.replace(/<\/p>/gi, "\n\n"); 303 return x.replace(/<[^>]+>/g, ""); 304 }, 305 306 /** 307 * Strip info:doi prefix and any suffixes from a DOI 308 * @type String 309 */ 310 "cleanDOI":function(/**String**/ x) { 311 if(typeof(x) != "string") { 312 throw "cleanDOI: argument must be a string"; 313 } 314 315 var doi = x.match(/10(?:\.[0-9]{4,})?\/[^\s]*[^\s\.,]/); 316 return doi ? doi[0] : null; 317 }, 318 319 /** 320 * Clean and validate ISBN. 321 * Return isbn if valid, otherwise return false 322 * @param {String} isbn 323 * @param {Boolean} [dontValidate=false] Do not validate check digit 324 * @return {String|Boolean} Valid ISBN or false 325 */ 326 "cleanISBN":function(isbnStr, dontValidate) { 327 isbnStr = isbnStr.toUpperCase() 328 .replace(/[\x2D\xAD\u2010-\u2015\u2043\u2212]+/g, ''); // Ignore dashes 329 var isbnRE = /\b(?:97[89]\s*(?:\d\s*){9}\d|(?:\d\s*){9}[\dX])\b/g, 330 isbnMatch; 331 while(isbnMatch = isbnRE.exec(isbnStr)) { 332 var isbn = isbnMatch[0].replace(/\s+/g, ''); 333 334 if (dontValidate) { 335 return isbn; 336 } 337 338 if(isbn.length == 10) { 339 // Verify ISBN-10 checksum 340 var sum = 0; 341 for (var i = 0; i < 9; i++) { 342 sum += isbn[i] * (10-i); 343 } 344 //check digit might be 'X' 345 sum += (isbn[9] == 'X')? 10 : isbn[9]*1; 346 347 if (sum % 11 == 0) return isbn; 348 } else { 349 // Verify ISBN 13 checksum 350 var sum = 0; 351 for (var i = 0; i < 12; i+=2) sum += isbn[i]*1; //to make sure it's int 352 for (var i = 1; i < 12; i+=2) sum += isbn[i]*3; 353 sum += isbn[12]*1; //add the check digit 354 355 if (sum % 10 == 0 ) return isbn; 356 } 357 358 isbnRE.lastIndex = isbnMatch.index + 1; // Retry the same spot + 1 359 } 360 361 return false; 362 }, 363 364 /* 365 * Convert ISBN 10 to ISBN 13 366 * @param {String} isbn ISBN 10 or ISBN 13 367 * cleanISBN 368 * @return {String} ISBN-13 369 */ 370 "toISBN13": function(isbnStr) { 371 var isbn; 372 if (!(isbn = Zotero.Utilities.cleanISBN(isbnStr, true))) { 373 throw new Error('ISBN not found in "' + isbnStr + '"'); 374 } 375 376 if (isbn.length == 13) { 377 isbn = isbn.substr(0,12); // Strip off check digit and re-calculate it 378 } else { 379 isbn = '978' + isbn.substr(0,9); 380 } 381 382 var sum = 0; 383 for (var i = 0; i < 12; i++) { 384 sum += isbn[i] * (i%2 ? 3 : 1); 385 } 386 387 var checkDigit = 10 - (sum % 10); 388 if (checkDigit == 10) checkDigit = 0; 389 390 return isbn + checkDigit; 391 }, 392 393 /** 394 * Clean and validate ISSN. 395 * Return issn if valid, otherwise return false 396 */ 397 "cleanISSN":function(/**String*/ issnStr) { 398 issnStr = issnStr.toUpperCase() 399 .replace(/[\x2D\xAD\u2010-\u2015\u2043\u2212]+/g, ''); // Ignore dashes 400 var issnRE = /\b(?:\d\s*){7}[\dX]\b/g, 401 issnMatch; 402 while (issnMatch = issnRE.exec(issnStr)) { 403 var issn = issnMatch[0].replace(/\s+/g, ''); 404 405 // Verify ISSN checksum 406 var sum = 0; 407 for (var i = 0; i < 7; i++) { 408 sum += issn[i] * (8-i); 409 } 410 //check digit might be 'X' 411 sum += (issn[7] == 'X')? 10 : issn[7]*1; 412 413 if (sum % 11 == 0) { 414 return issn.substring(0,4) + '-' + issn.substring(4); 415 } 416 417 issnRE.lastIndex = issnMatch.index + 1; // Retry same spot + 1 418 } 419 420 return false; 421 }, 422 423 /** 424 * Convert plain text to HTML by replacing special characters and replacing newlines with BRs or 425 * P tags 426 * @param {String} str Plain text string 427 * @param {Boolean} singleNewlineIsParagraph Whether single newlines should be considered as 428 * paragraphs. If true, each newline is replaced with a P tag. If false, double newlines 429 * are replaced with P tags, while single newlines are replaced with BR tags. 430 * @type String 431 */ 432 "text2html":function (/**String**/ str, /**Boolean**/ singleNewlineIsParagraph) { 433 str = Zotero.Utilities.htmlSpecialChars(str); 434 435 // \n => <p> 436 if (singleNewlineIsParagraph) { 437 str = '<p>' 438 + str.replace(/\n/g, '</p><p>') 439 .replace(/ /g, ' ') 440 + '</p>'; 441 } 442 // \n\n => <p>, \n => <br/> 443 else { 444 str = '<p>' 445 + str.replace(/\n\n/g, '</p><p>') 446 .replace(/\n/g, '<br/>') 447 .replace(/ /g, ' ') 448 + '</p>'; 449 } 450 return str.replace(/<p>\s*<\/p>/g, '<p> </p>'); 451 }, 452 453 /** 454 * Encode special XML/HTML characters 455 * Certain entities can be inserted manually: 456 * <ZOTEROBREAK/> => <br/> 457 * <ZOTEROHELLIP/> => … 458 * 459 * @param {String} str 460 * @return {String} 461 */ 462 "htmlSpecialChars":function(str) { 463 if (str && typeof str != 'string') { 464 Zotero.debug('#htmlSpecialChars: non-string arguments are deprecated. Update your code', 465 1, undefined, true); 466 str = str.toString(); 467 } 468 469 if (!str) return ''; 470 471 return str 472 .replace(/&/g, '&') 473 .replace(/"/g, '"') 474 .replace(/'/g, ''') 475 .replace(/</g, '<') 476 .replace(/>/g, '>') 477 .replace(/<ZOTERO([^\/]+)\/>/g, function (str, p1, offset, s) { 478 switch (p1) { 479 case 'BREAK': 480 return '<br/>'; 481 case 'HELLIP': 482 return '…'; 483 default: 484 return p1; 485 } 486 }); 487 }, 488 489 /** 490 * Decodes HTML entities within a string, returning plain text 491 * @type String 492 */ 493 "unescapeHTML":new function() { 494 var nsIScriptableUnescapeHTML, node; 495 496 return function(/**String*/ str) { 497 // If no tags, no need to unescape 498 if(str.indexOf("<") === -1 && str.indexOf("&") === -1) return str; 499 500 if(Zotero.isFx && !Zotero.isBookmarklet) { 501 // Create a node and use the textContent property to do unescaping where 502 // possible, because this approach preserves line endings in the HTML 503 if(node === undefined) { 504 node = Zotero.Utilities.Internal.getDOMDocument().createElement("div"); 505 } 506 507 node.innerHTML = str; 508 return node.textContent.replace(/ {2,}/g, " "); 509 } else if(Zotero.isNode) { 510 /*var doc = require('jsdom').jsdom(str, null, { 511 "features":{ 512 "FetchExternalResources":false, 513 "ProcessExternalResources":false, 514 "MutationEvents":false, 515 "QuerySelector":false 516 } 517 }); 518 if(!doc.documentElement) return str; 519 return doc.documentElement.textContent;*/ 520 return Zotero.Utilities.cleanTags(str); 521 } else { 522 if(!node) node = document.createElement("div"); 523 node.innerHTML = str; 524 return ("textContent" in node ? node.textContent : node.innerText).replace(/ {2,}/g, " "); 525 } 526 }; 527 }, 528 529 /** 530 * Converts text inside a DOM object to plain text preserving text formatting 531 * appropriate for given field 532 * 533 * @param {DOMNode} rootNode Node containing all the text that needs to be extracted 534 * @param {String} targetField Zotero item field that the text is meant for 535 * 536 * @return {String} Zotero formatted string 537 */ 538 "dom2text": function(rootNode, targetField) { 539 // TODO: actually do this 540 return Zotero.Utilities.trimInternal(rootNode.textContent); 541 }, 542 543 /** 544 * Wrap URLs and DOIs in <a href=""> links in plain text 545 * 546 * Ignore URLs preceded by '>', just in case there are already links 547 * @type String 548 */ 549 "autoLink":function (/**String**/ str) { 550 // "http://www.google.com." 551 // "http://www.google.com. " 552 // "<http://www.google.com>" (and other characters, with or without a space after) 553 str = str.replace(/([^>])(https?:\/\/[^\s]+)([\."'>:\]\)](\s|$))/g, '$1<a href="$2">$2</a>$3'); 554 // "http://www.google.com" 555 // "http://www.google.com " 556 str = str.replace(/([^">])(https?:\/\/[^\s]+)(\s|$)/g, '$1<a href="$2">$2</a>$3'); 557 558 // DOI 559 str = str.replace(/(doi:[ ]*)(10\.[^\s]+[0-9a-zA-Z])/g, '$1<a href="http://dx.doi.org/$2">$2</a>'); 560 return str; 561 }, 562 563 /** 564 * Parses a text string for HTML/XUL markup and returns an array of parts. Currently only finds 565 * HTML links (<a> tags) 566 * 567 * @return {Array} An array of objects with the following form:<br> 568 * <pre> { 569 * type: 'text'|'link', 570 * text: "text content", 571 * [ attributes: { key1: val [ , key2: val, ...] } 572 * }</pre> 573 */ 574 "parseMarkup":function(/**String*/ str) { 575 var parts = []; 576 var splits = str.split(/(<a [^>]+>[^<]*<\/a>)/); 577 578 for(var i=0; i<splits.length; i++) { 579 // Link 580 if (splits[i].indexOf('<a ') == 0) { 581 var matches = splits[i].match(/<a ([^>]+)>([^<]*)<\/a>/); 582 if (matches) { 583 // Attribute pairs 584 var attributes = {}; 585 var pairs = matches[1].match(/([^ =]+)="([^"]+")/g); 586 for(var j=0; j<pairs.length; j++) { 587 var keyVal = pairs[j].split(/=/); 588 attributes[keyVal[0]] = keyVal[1].substr(1, keyVal[1].length - 2); 589 } 590 591 parts.push({ 592 type: 'link', 593 text: matches[2], 594 attributes: attributes 595 }); 596 continue; 597 } 598 } 599 600 parts.push({ 601 type: 'text', 602 text: splits[i] 603 }); 604 } 605 606 return parts; 607 }, 608 609 /** 610 * Calculates the Levenshtein distance between two strings 611 * @type Number 612 */ 613 "levenshtein":function (/**String*/ a, /**String**/ b) { 614 var aLen = a.length; 615 var bLen = b.length; 616 617 var arr = new Array(aLen+1); 618 var i, j, cost; 619 620 for (i = 0; i <= aLen; i++) { 621 arr[i] = new Array(bLen); 622 arr[i][0] = i; 623 } 624 625 for (j = 0; j <= bLen; j++) { 626 arr[0][j] = j; 627 } 628 629 for (i = 1; i <= aLen; i++) { 630 for (j = 1; j <= bLen; j++) { 631 cost = (a[i-1] == b[j-1]) ? 0 : 1; 632 arr[i][j] = Math.min(arr[i-1][j] + 1, Math.min(arr[i][j-1] + 1, arr[i-1][j-1] + cost)); 633 } 634 } 635 636 return arr[aLen][bLen]; 637 }, 638 639 /** 640 * Test if an object is empty 641 * 642 * @param {Object} obj 643 * @type Boolean 644 */ 645 "isEmpty":function (obj) { 646 for (var i in obj) { 647 return false; 648 } 649 return true; 650 }, 651 652 /** 653 * Compares an array with another and returns an array with 654 * the values from array1 that don't exist in array2 655 * 656 * @param {Array} array1 657 * @param {Array} array2 658 * @param {Boolean} useIndex If true, return an array containing just 659 * the index of array2's elements; 660 * otherwise return the values 661 */ 662 "arrayDiff":function(array1, array2, useIndex) { 663 if (!Array.isArray(array1)) { 664 throw new Error("array1 is not an array (" + array1 + ")"); 665 } 666 if (!Array.isArray(array2)) { 667 throw new Error("array2 is not an array (" + array2 + ")"); 668 } 669 670 var val, pos, vals = []; 671 for (var i=0; i<array1.length; i++) { 672 val = array1[i]; 673 pos = array2.indexOf(val); 674 if (pos == -1) { 675 vals.push(useIndex ? pos : val); 676 } 677 } 678 return vals; 679 }, 680 681 682 /** 683 * Determine whether two arrays are identical 684 * 685 * Modified from http://stackoverflow.com/a/14853974 686 * 687 * @return {Boolean} 688 */ 689 "arrayEquals": function (array1, array2) { 690 // If either array is a falsy value, return 691 if (!array1 || !array2) 692 return false; 693 694 // Compare lengths - can save a lot of time 695 if (array1.length != array2.length) 696 return false; 697 698 for (var i = 0, l=array1.length; i < l; i++) { 699 // Check if we have nested arrays 700 if (array1[i] instanceof Array && array2[i] instanceof Array) { 701 // Recurse into the nested arrays 702 if (!this.arrayEquals(array1[i], array2[i])) { 703 return false; 704 } 705 } 706 else if (array1[i] != array2[i]) { 707 // Warning - two different object instances will never be equal: {x:20} != {x:20} 708 return false; 709 } 710 } 711 return true; 712 }, 713 714 715 /** 716 * Return new array with values shuffled 717 * 718 * From http://stackoverflow.com/a/6274398 719 * 720 * @param {Array} arr 721 * @return {Array} 722 */ 723 "arrayShuffle": function (array) { 724 var counter = array.length, temp, index; 725 726 // While there are elements in the array 727 while (counter--) { 728 // Pick a random index 729 index = (Math.random() * counter) | 0; 730 731 // And swap the last element with it 732 temp = array[counter]; 733 array[counter] = array[index]; 734 array[index] = temp; 735 } 736 737 return array; 738 }, 739 740 741 /** 742 * Return new array with duplicate values removed 743 * 744 * @param {Array} array 745 * @return {Array} 746 */ 747 arrayUnique: function (arr) { 748 return [...new Set(arr)]; 749 }, 750 751 /** 752 * Run a function on chunks of a given size of an array's elements. 753 * 754 * @param {Array} arr 755 * @param {Integer} chunkSize 756 * @param {Function} func 757 * @return {Array} The return values from the successive runs 758 */ 759 "forEachChunk":function(arr, chunkSize, func) { 760 var retValues = []; 761 var tmpArray = arr.concat(); 762 var num = arr.length; 763 var done = 0; 764 765 do { 766 var chunk = tmpArray.splice(0, chunkSize); 767 done += chunk.length; 768 retValues.push(func(chunk)); 769 } 770 while (done < num); 771 772 return retValues; 773 }, 774 775 /** 776 * Assign properties to an object 777 * 778 * @param {Object} target 779 * @param {Object} source 780 * @param {String[]} [props] Properties to assign. Assign all otherwise 781 */ 782 "assignProps": function(target, source, props) { 783 if (!props) props = Object.keys(source); 784 785 for (var i=0; i<props.length; i++) { 786 if (source[props[i]] === undefined) continue; 787 target[props[i]] = source[props[i]]; 788 } 789 }, 790 791 /** 792 * Generate a random integer between min and max inclusive 793 * 794 * @param {Integer} min 795 * @param {Integer} max 796 * @return {Integer} 797 */ 798 "rand":function (min, max) { 799 return Math.floor(Math.random() * (max - min + 1)) + min; 800 }, 801 802 /** 803 * Parse a page range 804 * 805 * @param {String} Page range to parse 806 * @return {Integer[]} Start and end pages 807 */ 808 "getPageRange":function(pages) { 809 const pageRangeRegexp = /^\s*([0-9]+) ?[-\u2013] ?([0-9]+)\s*$/ 810 811 var pageNumbers; 812 var m = pageRangeRegexp.exec(pages); 813 if(m) { 814 // A page range 815 pageNumbers = [m[1], m[2]]; 816 } else { 817 // Assume start and end are the same 818 pageNumbers = [pages, pages]; 819 } 820 return pageNumbers; 821 }, 822 823 /** 824 * Pads a number or other string with a given string on the left 825 * 826 * @param {String} string String to pad 827 * @param {String} pad String to use as padding 828 * @length {Integer} length Length of new padded string 829 * @type String 830 */ 831 "lpad":function(string, pad, length) { 832 string = string ? string + '' : ''; 833 while(string.length < length) { 834 string = pad + string; 835 } 836 return string; 837 }, 838 839 /** 840 * Shorten and add an ellipsis to a string if necessary 841 * 842 * @param {String} str 843 * @param {Integer} len 844 * @param {Boolean} [wordBoundary=false] 845 * @param {Boolean} [countChars=false] 846 */ 847 ellipsize: function (str, len, wordBoundary = false, countChars) { 848 if (!len) { 849 throw ("Length not specified in Zotero.Utilities.ellipsize()"); 850 } 851 if (str.length <= len) { 852 return str; 853 } 854 let radius = Math.min(len, 5); 855 if (wordBoundary) { 856 let min = len - radius; 857 // If next character is a space, include that so we stop at len 858 if (str.charAt(len).match(/\s/)) { 859 radius++; 860 } 861 // Remove trailing characters and spaces, up to radius 862 str = str.substr(0, min) + str.substr(min, radius).replace(/\W*\s\S*$/, ""); 863 } 864 else { 865 str = str.substr(0, len) 866 } 867 return str + '\u2026' + (countChars ? ' (' + str.length + ' chars)' : ''); 868 }, 869 870 871 /** 872 * Return the proper plural form of a string 873 * 874 * For now, this is only used for debug output in English. 875 * 876 * @param {Integer} num 877 * @param {String[]|String} forms - If an array, an array of plural forms (e.g., ['object', 'objects']); 878 * currently only the two English forms are supported, for 1 and 0/many. If a single string, 879 * 's' is added automatically for 0/many. 880 * @return {String} 881 */ 882 pluralize: function (num, forms) { 883 if (typeof forms == 'string') { 884 forms = [forms, forms + 's']; 885 } 886 return num == 1 ? forms[0] : forms[1]; 887 }, 888 889 890 /** 891 * Port of PHP's number_format() 892 * 893 * MIT Licensed 894 * 895 * From http://kevin.vanzonneveld.net 896 * + original by: Jonas Raoni Soares Silva (http://www.jsfromhell.com) 897 * + improved by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) 898 * + bugfix by: Michael White (http://getsprink.com) 899 * + bugfix by: Benjamin Lupton 900 * + bugfix by: Allan Jensen (http://www.winternet.no) 901 * + revised by: Jonas Raoni Soares Silva (http://www.jsfromhell.com) 902 * + bugfix by: Howard Yeend 903 * * example 1: number_format(1234.5678, 2, '.', ''); 904 * * returns 1: 1234.57 905 */ 906 "numberFormat":function (number, decimals, dec_point, thousands_sep) { 907 var n = number, c = isNaN(decimals = Math.abs(decimals)) ? 2 : decimals; 908 var d = dec_point == undefined ? "." : dec_point; 909 var t = thousands_sep == undefined ? "," : thousands_sep, s = n < 0 ? "-" : ""; 910 var i = parseInt(n = Math.abs(+n || 0).toFixed(c)) + "", j = (j = i.length) > 3 ? j % 3 : 0; 911 912 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) : ""); 913 }, 914 915 /** 916 * Cleans a title, converting it to title case and replacing " :" with ":" 917 * 918 * @param {String} string 919 * @param {Boolean} force Forces title case conversion, even if the capitalizeTitles pref is off 920 * @type String 921 */ 922 "capitalizeTitle":function(string, force) { 923 const skipWords = ["but", "or", "yet", "so", "for", "and", "nor", "a", "an", 924 "the", "at", "by", "from", "in", "into", "of", "on", "to", "with", "up", 925 "down", "as"]; 926 927 // this may only match a single character 928 const delimiterRegexp = /([ \/\u002D\u00AD\u2010-\u2015\u2212\u2E3A\u2E3B])/; 929 930 string = this.trimInternal(string); 931 string = string.replace(/ : /g, ": "); 932 if(force === false || (!Zotero.Prefs.get('capitalizeTitles') && !force)) return string; 933 if(!string) return ""; 934 935 // split words 936 var words = string.split(delimiterRegexp); 937 var isUpperCase = string.toUpperCase() == string; 938 939 var newString = ""; 940 var delimiterOffset = words[0].length; 941 var lastWordIndex = words.length-1; 942 var previousWordIndex = -1; 943 for(var i=0; i<=lastWordIndex; i++) { 944 // only do manipulation if not a delimiter character 945 if(words[i].length != 0 && (words[i].length != 1 || !delimiterRegexp.test(words[i]))) { 946 var upperCaseVariant = words[i].toUpperCase(); 947 var lowerCaseVariant = words[i].toLowerCase(); 948 949 // only use if word does not already possess some capitalization 950 if(isUpperCase || words[i] == lowerCaseVariant) { 951 if( 952 // a skip word 953 skipWords.indexOf(lowerCaseVariant.replace(/[^a-zA-Z]+/, "")) != -1 954 // not first or last word 955 && i != 0 && i != lastWordIndex 956 // does not follow a colon 957 && (previousWordIndex == -1 || words[previousWordIndex][words[previousWordIndex].length-1].search(/[:\?!]/)==-1) 958 ) { 959 words[i] = lowerCaseVariant; 960 } else { 961 // this is not a skip word or comes after a colon; 962 // we must capitalize 963 // handle punctuation in the beginning, including multiple, as in "¿Qué pasa?" 964 var punct = words[i].match(/^[\'\"¡¿“‘„«\s]+/); 965 punct = punct ? punct[0].length+1 : 1; 966 words[i] = words[i].length ? words[i].substr(0, punct).toUpperCase() + 967 words[i].substr(punct).toLowerCase() : words[i]; 968 } 969 } 970 971 previousWordIndex = i; 972 } 973 974 newString += words[i]; 975 } 976 977 return newString; 978 }, 979 980 "capitalize": function (str) { 981 if (typeof str != 'string') throw new Error("Argument must be a string"); 982 if (!str) return str; // Empty string 983 return str[0].toUpperCase() + str.substr(1); 984 }, 985 986 /** 987 * Replaces accented characters in a string with ASCII equivalents 988 * 989 * @param {String} str 990 * @param {Boolean} [lowercaseOnly] Limit conversions to lowercase characters 991 * (for improved performance on lowercase input) 992 * @return {String} 993 * 994 * From http://lehelk.com/2011/05/06/script-to-remove-diacritics/ 995 */ 996 "removeDiacritics": function (str, lowercaseOnly) { 997 // Short-circuit on the most basic input 998 if (/^[a-zA-Z0-9_-]*$/.test(str)) return str; 999 1000 var map = this._diacriticsRemovalMap.lowercase; 1001 for (var i=0, len=map.length; i<len; i++) { 1002 str = str.replace(map[i].letters, map[i].base); 1003 } 1004 1005 if (!lowercaseOnly) { 1006 var map = this._diacriticsRemovalMap.uppercase; 1007 for (var i=0, len=map.length; i<len; i++) { 1008 str = str.replace(map[i].letters, map[i].base); 1009 } 1010 } 1011 1012 return str; 1013 }, 1014 1015 "_diacriticsRemovalMap": { 1016 uppercase: [ 1017 {'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}, 1018 {'base':'AA','letters':/[\uA732]/g}, 1019 {'base':'AE','letters':/[\u00C6\u01FC\u01E2]/g}, 1020 {'base':'AO','letters':/[\uA734]/g}, 1021 {'base':'AU','letters':/[\uA736]/g}, 1022 {'base':'AV','letters':/[\uA738\uA73A]/g}, 1023 {'base':'AY','letters':/[\uA73C]/g}, 1024 {'base':'B', 'letters':/[\u0042\u24B7\uFF22\u1E02\u1E04\u1E06\u0243\u0182\u0181]/g}, 1025 {'base':'C', 'letters':/[\u0043\u24B8\uFF23\u0106\u0108\u010A\u010C\u00C7\u1E08\u0187\u023B\uA73E]/g}, 1026 {'base':'D', 'letters':/[\u0044\u24B9\uFF24\u1E0A\u010E\u1E0C\u1E10\u1E12\u1E0E\u0110\u018B\u018A\u0189\uA779]/g}, 1027 {'base':'DZ','letters':/[\u01F1\u01C4]/g}, 1028 {'base':'Dz','letters':/[\u01F2\u01C5]/g}, 1029 {'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}, 1030 {'base':'F', 'letters':/[\u0046\u24BB\uFF26\u1E1E\u0191\uA77B]/g}, 1031 {'base':'G', 'letters':/[\u0047\u24BC\uFF27\u01F4\u011C\u1E20\u011E\u0120\u01E6\u0122\u01E4\u0193\uA7A0\uA77D\uA77E]/g}, 1032 {'base':'H', 'letters':/[\u0048\u24BD\uFF28\u0124\u1E22\u1E26\u021E\u1E24\u1E28\u1E2A\u0126\u2C67\u2C75\uA78D]/g}, 1033 {'base':'I', 'letters':/[\u0049\u24BE\uFF29\u00CC\u00CD\u00CE\u0128\u012A\u012C\u0130\u00CF\u1E2E\u1EC8\u01CF\u0208\u020A\u1ECA\u012E\u1E2C\u0197]/g}, 1034 {'base':'J', 'letters':/[\u004A\u24BF\uFF2A\u0134\u0248]/g}, 1035 {'base':'K', 'letters':/[\u004B\u24C0\uFF2B\u1E30\u01E8\u1E32\u0136\u1E34\u0198\u2C69\uA740\uA742\uA744\uA7A2]/g}, 1036 {'base':'L', 'letters':/[\u004C\u24C1\uFF2C\u013F\u0139\u013D\u1E36\u1E38\u013B\u1E3C\u1E3A\u0141\u023D\u2C62\u2C60\uA748\uA746\uA780]/g}, 1037 {'base':'LJ','letters':/[\u01C7]/g}, 1038 {'base':'Lj','letters':/[\u01C8]/g}, 1039 {'base':'M', 'letters':/[\u004D\u24C2\uFF2D\u1E3E\u1E40\u1E42\u2C6E\u019C]/g}, 1040 {'base':'N', 'letters':/[\u004E\u24C3\uFF2E\u01F8\u0143\u00D1\u1E44\u0147\u1E46\u0145\u1E4A\u1E48\u0220\u019D\uA790\uA7A4]/g}, 1041 {'base':'NJ','letters':/[\u01CA]/g}, 1042 {'base':'Nj','letters':/[\u01CB]/g}, 1043 {'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}, 1044 {'base':'OE','letters':/[\u0152]/g}, 1045 {'base':'OI','letters':/[\u01A2]/g}, 1046 {'base':'OO','letters':/[\uA74E]/g}, 1047 {'base':'OU','letters':/[\u0222]/g}, 1048 {'base':'P', 'letters':/[\u0050\u24C5\uFF30\u1E54\u1E56\u01A4\u2C63\uA750\uA752\uA754]/g}, 1049 {'base':'Q', 'letters':/[\u0051\u24C6\uFF31\uA756\uA758\u024A]/g}, 1050 {'base':'R', 'letters':/[\u0052\u24C7\uFF32\u0154\u1E58\u0158\u0210\u0212\u1E5A\u1E5C\u0156\u1E5E\u024C\u2C64\uA75A\uA7A6\uA782]/g}, 1051 {'base':'S', 'letters':/[\u0053\u24C8\uFF33\u1E9E\u015A\u1E64\u015C\u1E60\u0160\u1E66\u1E62\u1E68\u0218\u015E\u2C7E\uA7A8\uA784]/g}, 1052 {'base':'T', 'letters':/[\u0054\u24C9\uFF34\u1E6A\u0164\u1E6C\u021A\u0162\u1E70\u1E6E\u0166\u01AC\u01AE\u023E\uA786]/g}, 1053 {'base':'TZ','letters':/[\uA728]/g}, 1054 {'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}, 1055 {'base':'V', 'letters':/[\u0056\u24CB\uFF36\u1E7C\u1E7E\u01B2\uA75E\u0245]/g}, 1056 {'base':'VY','letters':/[\uA760]/g}, 1057 {'base':'W', 'letters':/[\u0057\u24CC\uFF37\u1E80\u1E82\u0174\u1E86\u1E84\u1E88\u2C72]/g}, 1058 {'base':'X', 'letters':/[\u0058\u24CD\uFF38\u1E8A\u1E8C]/g}, 1059 {'base':'Y', 'letters':/[\u0059\u24CE\uFF39\u1EF2\u00DD\u0176\u1EF8\u0232\u1E8E\u0178\u1EF6\u1EF4\u01B3\u024E\u1EFE]/g}, 1060 {'base':'Z', 'letters':/[\u005A\u24CF\uFF3A\u0179\u1E90\u017B\u017D\u1E92\u1E94\u01B5\u0224\u2C7F\u2C6B\uA762]/g}, 1061 ], 1062 1063 lowercase: [ 1064 {'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}, 1065 {'base':'aa','letters':/[\uA733]/g}, 1066 {'base':'ae','letters':/[\u00E6\u01FD\u01E3]/g}, 1067 {'base':'ao','letters':/[\uA735]/g}, 1068 {'base':'au','letters':/[\uA737]/g}, 1069 {'base':'av','letters':/[\uA739\uA73B]/g}, 1070 {'base':'ay','letters':/[\uA73D]/g}, 1071 {'base':'b', 'letters':/[\u0062\u24D1\uFF42\u1E03\u1E05\u1E07\u0180\u0183\u0253]/g}, 1072 {'base':'c', 'letters':/[\u0063\u24D2\uFF43\u0107\u0109\u010B\u010D\u00E7\u1E09\u0188\u023C\uA73F\u2184]/g}, 1073 {'base':'d', 'letters':/[\u0064\u24D3\uFF44\u1E0B\u010F\u1E0D\u1E11\u1E13\u1E0F\u0111\u018C\u0256\u0257\uA77A]/g}, 1074 {'base':'dz','letters':/[\u01F3\u01C6]/g}, 1075 {'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}, 1076 {'base':'f', 'letters':/[\u0066\u24D5\uFF46\u1E1F\u0192\uA77C]/g}, 1077 {'base':'g', 'letters':/[\u0067\u24D6\uFF47\u01F5\u011D\u1E21\u011F\u0121\u01E7\u0123\u01E5\u0260\uA7A1\u1D79\uA77F]/g}, 1078 {'base':'h', 'letters':/[\u0068\u24D7\uFF48\u0125\u1E23\u1E27\u021F\u1E25\u1E29\u1E2B\u1E96\u0127\u2C68\u2C76\u0265]/g}, 1079 {'base':'hv','letters':/[\u0195]/g}, 1080 {'base':'i', 'letters':/[\u0069\u24D8\uFF49\u00EC\u00ED\u00EE\u0129\u012B\u012D\u00EF\u1E2F\u1EC9\u01D0\u0209\u020B\u1ECB\u012F\u1E2D\u0268\u0131]/g}, 1081 {'base':'j', 'letters':/[\u006A\u24D9\uFF4A\u0135\u01F0\u0249]/g}, 1082 {'base':'k', 'letters':/[\u006B\u24DA\uFF4B\u1E31\u01E9\u1E33\u0137\u1E35\u0199\u2C6A\uA741\uA743\uA745\uA7A3]/g}, 1083 {'base':'l', 'letters':/[\u006C\u24DB\uFF4C\u0140\u013A\u013E\u1E37\u1E39\u013C\u1E3D\u1E3B\u017F\u0142\u019A\u026B\u2C61\uA749\uA781\uA747]/g}, 1084 {'base':'lj','letters':/[\u01C9]/g}, 1085 {'base':'m', 'letters':/[\u006D\u24DC\uFF4D\u1E3F\u1E41\u1E43\u0271\u026F]/g}, 1086 {'base':'n', 'letters':/[\u006E\u24DD\uFF4E\u01F9\u0144\u00F1\u1E45\u0148\u1E47\u0146\u1E4B\u1E49\u019E\u0272\u0149\uA791\uA7A5]/g}, 1087 {'base':'nj','letters':/[\u01CC]/g}, 1088 {'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}, 1089 {'base':'oe','letters':/[\u0153]/g}, 1090 {'base':'oi','letters':/[\u01A3]/g}, 1091 {'base':'ou','letters':/[\u0223]/g}, 1092 {'base':'oo','letters':/[\uA74F]/g}, 1093 {'base':'p','letters':/[\u0070\u24DF\uFF50\u1E55\u1E57\u01A5\u1D7D\uA751\uA753\uA755]/g}, 1094 {'base':'q','letters':/[\u0071\u24E0\uFF51\u024B\uA757\uA759]/g}, 1095 {'base':'r','letters':/[\u0072\u24E1\uFF52\u0155\u1E59\u0159\u0211\u0213\u1E5B\u1E5D\u0157\u1E5F\u024D\u027D\uA75B\uA7A7\uA783]/g}, 1096 {'base':'s','letters':/[\u0073\u24E2\uFF53\u00DF\u015B\u1E65\u015D\u1E61\u0161\u1E67\u1E63\u1E69\u0219\u015F\u023F\uA7A9\uA785\u1E9B]/g}, 1097 {'base':'t','letters':/[\u0074\u24E3\uFF54\u1E6B\u1E97\u0165\u1E6D\u021B\u0163\u1E71\u1E6F\u0167\u01AD\u0288\u2C66\uA787]/g}, 1098 {'base':'tz','letters':/[\uA729]/g}, 1099 {'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}, 1100 {'base':'v','letters':/[\u0076\u24E5\uFF56\u1E7D\u1E7F\u028B\uA75F\u028C]/g}, 1101 {'base':'vy','letters':/[\uA761]/g}, 1102 {'base':'w','letters':/[\u0077\u24E6\uFF57\u1E81\u1E83\u0175\u1E87\u1E85\u1E98\u1E89\u2C73]/g}, 1103 {'base':'x','letters':/[\u0078\u24E7\uFF58\u1E8B\u1E8D]/g}, 1104 {'base':'y','letters':/[\u0079\u24E8\uFF59\u1EF3\u00FD\u0177\u1EF9\u0233\u1E8F\u00FF\u1EF7\u1E99\u1EF5\u01B4\u024F\u1EFF]/g}, 1105 {'base':'z','letters':/[\u007A\u24E9\uFF5A\u017A\u1E91\u017C\u017E\u1E93\u1E95\u01B6\u0225\u0240\u2C6C\uA763]/g} 1106 ] 1107 }, 1108 1109 /** 1110 * Run sets of data through multiple asynchronous callbacks 1111 * 1112 * Each callback is passed the current set and a callback to call when done 1113 * 1114 * @param {Object[]} sets Sets of data 1115 * @param {Function[]} callbacks 1116 * @param {Function} onDone Function to call when done 1117 */ 1118 "processAsync":function (sets, callbacks, onDone) { 1119 if(sets.wrappedJSObject) sets = sets.wrappedJSObject; 1120 if(callbacks.wrappedJSObject) callbacks = callbacks.wrappedJSObject; 1121 1122 var currentSet; 1123 var index = 0; 1124 1125 var nextSet = function () { 1126 if (!sets.length) { 1127 onDone(); 1128 return; 1129 } 1130 index = 0; 1131 currentSet = sets.shift(); 1132 callbacks[0](currentSet, nextCallback); 1133 }; 1134 var nextCallback = function () { 1135 index++; 1136 callbacks[index](currentSet, nextCallback); 1137 }; 1138 1139 // Add a final callback to proceed to the next set 1140 callbacks[callbacks.length] = function () { 1141 nextSet(); 1142 } 1143 nextSet(); 1144 }, 1145 1146 /** 1147 * Performs a deep copy of a JavaScript object 1148 * @param {Object} obj 1149 * @return {Object} 1150 */ 1151 "deepCopy":function(obj) { 1152 var obj2 = (obj instanceof Array ? [] : {}); 1153 for(var i in obj) { 1154 if(!obj.hasOwnProperty(i)) continue; 1155 1156 if(typeof obj[i] === "object" && obj[i] !== null) { 1157 obj2[i] = Zotero.Utilities.deepCopy(obj[i]); 1158 } else { 1159 obj2[i] = obj[i]; 1160 } 1161 } 1162 return obj2; 1163 }, 1164 1165 /** 1166 * Tests if an item type exists 1167 * 1168 * @param {String} type Item type 1169 * @type Boolean 1170 */ 1171 "itemTypeExists":function(type) { 1172 if(Zotero.ItemTypes.getID(type)) { 1173 return true; 1174 } else { 1175 return false; 1176 } 1177 }, 1178 1179 /** 1180 * Find valid creator types for a given item type 1181 * 1182 * @param {String} type Item type 1183 * @return {String[]} Creator types 1184 */ 1185 "getCreatorsForType":function(type) { 1186 if(type === "attachment" || type === "note") return []; 1187 var types = Zotero.CreatorTypes.getTypesForItemType(Zotero.ItemTypes.getID(type)); 1188 var cleanTypes = new Array(); 1189 for(var i=0; i<types.length; i++) { 1190 cleanTypes.push(types[i].name); 1191 } 1192 return cleanTypes; 1193 }, 1194 1195 /** 1196 * Determine whether a given field is valid for a given item type 1197 * 1198 * @param {String} field Field name 1199 * @param {String} type Item type 1200 * @type Boolean 1201 */ 1202 "fieldIsValidForType":function(field, type) { 1203 return Zotero.ItemFields.isValidForType(field, Zotero.ItemTypes.getID(type)); 1204 }, 1205 1206 /** 1207 * Gets a creator type name, localized to the current locale 1208 * 1209 * @param {String} type Creator type 1210 * @param {String} Localized creator type 1211 * @type Boolean 1212 */ 1213 "getLocalizedCreatorType":function(type) { 1214 try { 1215 return Zotero.CreatorTypes.getLocalizedString(type); 1216 } catch(e) { 1217 return false; 1218 } 1219 }, 1220 1221 /** 1222 * Escapes metacharacters in a literal so that it may be used in a regular expression 1223 */ 1224 "quotemeta":function(literal) { 1225 if(typeof literal !== "string") { 1226 throw "Argument "+literal+" must be a string in Zotero.Utilities.quotemeta()"; 1227 } 1228 const metaRegexp = /[-[\]{}()*+?.\\^$|,#\s]/g; 1229 return literal.replace(metaRegexp, "\\$&"); 1230 }, 1231 1232 /** 1233 * Evaluate an XPath 1234 * 1235 * @param {element|element[]} elements The element(s) to use as the context for the XPath 1236 * @param {String} xpath The XPath expression 1237 * @param {Object} [namespaces] An object whose keys represent namespace prefixes, and whose 1238 * values represent their URIs 1239 * @return {element[]} DOM elements matching XPath 1240 */ 1241 "xpath":function(elements, xpath, namespaces) { 1242 var nsResolver = null; 1243 if(namespaces) { 1244 nsResolver = function(prefix) { 1245 return namespaces[prefix] || null; 1246 }; 1247 } 1248 1249 if(!("length" in elements)) elements = [elements]; 1250 1251 var results = []; 1252 for(var i=0, n=elements.length; i<n; i++) { 1253 // For some reason, if elements is wrapped by an object 1254 // Xray, we won't be able to unwrap the DOMWrapper around 1255 // the element. So waive the object Xray. 1256 var maybeWrappedEl = elements.wrappedJSObject ? elements.wrappedJSObject[i] : elements[i]; 1257 1258 // Firefox 5 hack, so we will preserve Fx5DOMWrappers 1259 var isWrapped = Zotero.Translate.DOMWrapper && Zotero.Translate.DOMWrapper.isWrapped(maybeWrappedEl); 1260 var element = isWrapped ? Zotero.Translate.DOMWrapper.unwrap(maybeWrappedEl) : maybeWrappedEl; 1261 1262 // We waived the object Xray above, which will waive the 1263 // DOM Xray, so make sure we have a DOM Xray wrapper. 1264 if(Zotero.isFx) { 1265 element = new XPCNativeWrapper(element); 1266 } 1267 1268 if(element.ownerDocument) { 1269 var rootDoc = element.ownerDocument; 1270 } else if(element.documentElement) { 1271 var rootDoc = element; 1272 } else if(Zotero.isIE && element.documentElement === null) { 1273 // IE: documentElement may be null if there is a parse error. In this 1274 // case, we don't match anything to mimic what would happen with DOMParser 1275 continue; 1276 } else { 1277 throw new Error("First argument must be either element(s) or document(s) in Zotero.Utilities.xpath(elements, '"+xpath+"')"); 1278 } 1279 1280 if(!Zotero.isIE || "evaluate" in rootDoc) { 1281 try { 1282 // This may result in a deprecation warning in the console due to 1283 // https://bugzilla.mozilla.org/show_bug.cgi?id=674437 1284 var xpathObject = rootDoc.evaluate(xpath, element, nsResolver, 5 /*ORDERED_NODE_ITERATOR_TYPE*/, null); 1285 } catch(e) { 1286 // rethrow so that we get a stack 1287 throw new Error(e.name+": "+e.message); 1288 } 1289 1290 var newEl; 1291 while(newEl = xpathObject.iterateNext()) { 1292 // Firefox 5 hack 1293 results.push(isWrapped ? Zotero.Translate.DOMWrapper.wrapIn(newEl, maybeWrappedEl) : newEl); 1294 } 1295 } else if("selectNodes" in element) { 1296 // We use JavaScript-XPath in IE for HTML documents, but with an XML 1297 // document, we need to use selectNodes 1298 if(namespaces) { 1299 var ieNamespaces = []; 1300 for(var j in namespaces) { 1301 if(!j) continue; 1302 ieNamespaces.push('xmlns:'+j+'="'+Zotero.Utilities.htmlSpecialChars(namespaces[j])+'"'); 1303 } 1304 rootDoc.setProperty("SelectionNamespaces", ieNamespaces.join(" ")); 1305 } 1306 var nodes = element.selectNodes(xpath); 1307 for(var j=0; j<nodes.length; j++) { 1308 results.push(nodes[j]); 1309 } 1310 } else { 1311 throw new Error("XPath functionality not available"); 1312 } 1313 } 1314 1315 return results; 1316 }, 1317 1318 /** 1319 * Generates a string from the content of nodes matching a given XPath 1320 * 1321 * @param {element} node The node representing the document and context 1322 * @param {String} xpath The XPath expression 1323 * @param {Object} [namespaces] An object whose keys represent namespace prefixes, and whose 1324 * values represent their URIs 1325 * @param {String} [delimiter] The string with which to join multiple matching nodes 1326 * @return {String|null} DOM elements matching XPath, or null if no elements exist 1327 */ 1328 "xpathText":function(node, xpath, namespaces, delimiter) { 1329 var elements = Zotero.Utilities.xpath(node, xpath, namespaces); 1330 if(!elements.length) return null; 1331 1332 var strings = new Array(elements.length); 1333 for(var i=0, n=elements.length; i<n; i++) { 1334 var el = elements[i]; 1335 if(el.wrappedJSObject) el = el.wrappedJSObject; 1336 if(Zotero.Translate.DOMWrapper) el = Zotero.Translate.DOMWrapper.unwrap(el); 1337 strings[i] = 1338 (el.nodeType === 2 /*ATTRIBUTE_NODE*/ && "value" in el) ? el.value 1339 : "textContent" in el ? el.textContent 1340 : "innerText" in el ? el.innerText 1341 : "text" in el ? el.text 1342 : el.nodeValue; 1343 } 1344 1345 return strings.join(delimiter !== undefined ? delimiter : ", "); 1346 }, 1347 1348 /** 1349 * Generate a random string of length 'len' (defaults to 8) 1350 **/ 1351 "randomString":function(len, chars) { 1352 if (!chars) { 1353 chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; 1354 } 1355 if (!len) { 1356 len = 8; 1357 } 1358 var randomstring = ''; 1359 for (var i=0; i<len; i++) { 1360 var rnum = Math.floor(Math.random() * chars.length); 1361 randomstring += chars.substring(rnum,rnum+1); 1362 } 1363 return randomstring; 1364 }, 1365 1366 /** 1367 * PHP var_dump equivalent for JS 1368 * 1369 * Adapted from http://binnyva.blogspot.com/2005/10/dump-function-javascript-equivalent-of.html 1370 */ 1371 "varDump": function(obj,level,maxLevel,parentObjects,path) { 1372 // Simple dump 1373 var type = typeof obj; 1374 if (type == 'number' || type == 'undefined' || type == 'boolean' || obj === null) { 1375 if (!level) { 1376 // When dumping these directly, make sure to distinguish them from regular 1377 // strings as output by Zotero.debug (i.e. no quotes) 1378 return '===>' + obj + '<=== (' + type + ')'; 1379 } 1380 else { 1381 return '' + obj; 1382 } 1383 } 1384 else if (type == 'string') { 1385 return JSON.stringify(obj); 1386 } 1387 else if (type == 'function') { 1388 var funcStr = ('' + obj).trim(); 1389 if (!level) { 1390 // Dump function contents as well if only dumping function 1391 return funcStr; 1392 } 1393 1394 // Display [native code] label for native functions, but make it one line 1395 if (/^[^{]+{\s*\[native code\]\s*}$/i.test(funcStr)) { 1396 return funcStr.replace(/\s*(\[native code\])\s*/i, ' $1 '); 1397 } 1398 1399 // For non-native functions, display an elipsis 1400 return ('' + obj).replace(/{[\s\S]*}/, '{...}'); 1401 } 1402 else if (type != 'object') { 1403 return '<<Unknown type: ' + type + '>> ' + obj; 1404 } 1405 1406 // Don't descend into global object cache for data objects 1407 if (Zotero.isClient && typeof obj == 'object' && obj instanceof Zotero.DataObject) { 1408 maxLevel = 1; 1409 } 1410 1411 // More complex dump with indentation for objects 1412 if (level === undefined) { 1413 level = 0; 1414 } 1415 1416 if (maxLevel === undefined) { 1417 maxLevel = 5; 1418 } 1419 1420 var objType = Object.prototype.toString.call(obj); 1421 1422 if (level > maxLevel) { 1423 return objType + " <<Maximum depth reached>>"; 1424 } 1425 1426 // The padding given at the beginning of the line. 1427 var level_padding = ""; 1428 for (var j=0; j<level+1; j++) { 1429 level_padding += " "; 1430 } 1431 1432 //Special handling for Error or Exception 1433 var isException = Zotero.isFx && !Zotero.isBookmarklet && obj instanceof Components.interfaces.nsIException; 1434 var isError = obj instanceof Error; 1435 if (!isException && !isError && obj.message !== undefined && obj.stack !== undefined) { 1436 isError = true; 1437 } 1438 1439 if (isError || isException) { 1440 var header = ''; 1441 if (isError) { 1442 header = (obj.constructor && obj.constructor.name) ? obj.constructor.name : 'Error'; 1443 } else { 1444 header = (obj.name ? obj.name + ' ' : '') + 'Exception'; 1445 } 1446 1447 let msg = (obj.message ? ('' + obj.message).replace(/^/gm, level_padding).trim() : ''); 1448 if (obj.stack) { 1449 let stack = obj.stack.trim().replace(/^(?=.)/gm, level_padding); 1450 stack = Zotero.Utilities.Internal.filterStack(stack); 1451 1452 msg += '\n\n'; 1453 1454 // At least with Zotero.HTTP.UnexpectedStatusException, the stack contains "Error:" 1455 // and the message in addition to the trace. I'm not sure what's causing that 1456 // (Bluebird?), but fix it here. 1457 if (stack.startsWith('Error:')) { 1458 msg += stack.replace('Error: ' + obj.message + '\n', ''); 1459 } 1460 else { 1461 msg += stack; 1462 } 1463 } 1464 1465 return header + ': ' + msg; 1466 } 1467 1468 // Only dump single level for nsIDOMNode objects (including document) 1469 if (Zotero.isFx && !Zotero.isBookmarklet 1470 && (obj instanceof Components.interfaces.nsIDOMNode 1471 || obj instanceof Components.interfaces.nsIDOMWindow) 1472 ) { 1473 level = maxLevel; 1474 } 1475 1476 // Recursion checking 1477 if(!parentObjects) { 1478 parentObjects = [obj]; 1479 path = ['ROOT']; 1480 } 1481 1482 var isArray = objType == '[object Array]' 1483 if (isArray) { 1484 var dumpedText = '['; 1485 } 1486 else if (objType == '[object Object]') { 1487 var dumpedText = '{'; 1488 } 1489 else { 1490 var dumpedText = objType + ' {'; 1491 } 1492 for (var prop in obj) { 1493 dumpedText += '\n' + level_padding + JSON.stringify(prop) + ": "; 1494 1495 try { 1496 var value = obj[prop]; 1497 } catch(e) { 1498 dumpedText += "<<Access Denied>>"; 1499 continue; 1500 } 1501 1502 // Check for recursion 1503 if (typeof(value) == 'object') { 1504 var i = parentObjects.indexOf(value); 1505 if(i != -1) { 1506 var parentName = path.slice(0,i+1).join('->'); 1507 dumpedText += "<<Reference to parent object " + parentName + " >>"; 1508 continue; 1509 } 1510 } 1511 1512 try { 1513 dumpedText += Zotero.Utilities.varDump(value,level+1,maxLevel,parentObjects.concat([value]),path.concat([prop])); 1514 } catch(e) { 1515 dumpedText += "<<Error processing property: " + e.message + " (" + value + ")>>"; 1516 } 1517 } 1518 1519 var lastChar = dumpedText.charAt(dumpedText.length - 1); 1520 if (lastChar != '[' && lastChar != '{') { 1521 dumpedText += '\n' + level_padding.substr(4); 1522 } 1523 dumpedText += isArray ? ']' : '}'; 1524 1525 return dumpedText; 1526 }, 1527 1528 /** 1529 * Converts an item from toArray() format to citeproc-js JSON 1530 * @param {Zotero.Item} zoteroItem 1531 * @return {Object|Promise<Object>} A CSL item, or a promise for a CSL item if a Zotero.Item 1532 * is passed 1533 */ 1534 "itemToCSLJSON":function(zoteroItem) { 1535 // If a Zotero.Item was passed, convert it to the proper format (skipping child items) and 1536 // call this function again with that object 1537 // 1538 // (Zotero.Item won't be defined in translation-server) 1539 if (typeof Zotero.Item !== 'undefined' && zoteroItem instanceof Zotero.Item) { 1540 return this.itemToCSLJSON( 1541 Zotero.Utilities.Internal.itemToExportFormat(zoteroItem, false, true) 1542 ); 1543 } 1544 1545 var cslType = CSL_TYPE_MAPPINGS[zoteroItem.itemType]; 1546 if (!cslType) { 1547 throw new Error('Unexpected Zotero Item type "' + zoteroItem.itemType + '"'); 1548 } 1549 1550 var itemTypeID = Zotero.ItemTypes.getID(zoteroItem.itemType); 1551 1552 var cslItem = { 1553 'id':zoteroItem.uri, 1554 'type':cslType 1555 }; 1556 1557 // get all text variables (there must be a better way) 1558 for(var variable in CSL_TEXT_MAPPINGS) { 1559 var fields = CSL_TEXT_MAPPINGS[variable]; 1560 for(var i=0, n=fields.length; i<n; i++) { 1561 var field = fields[i], 1562 value = null; 1563 1564 if(field in zoteroItem) { 1565 value = zoteroItem[field]; 1566 } else { 1567 if (field == 'versionNumber') field = 'version'; // Until https://github.com/zotero/zotero/issues/670 1568 var fieldID = Zotero.ItemFields.getID(field), 1569 typeFieldID; 1570 if(fieldID 1571 && (typeFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, fieldID)) 1572 ) { 1573 value = zoteroItem[Zotero.ItemFields.getName(typeFieldID)]; 1574 } 1575 } 1576 1577 if (!value) continue; 1578 1579 if (typeof value == 'string') { 1580 if (field == 'ISBN') { 1581 // Only use the first ISBN in CSL JSON 1582 var isbn = value.match(/^(?:97[89]-?)?(?:\d-?){9}[\dx](?!-)\b/i); 1583 if (isbn) value = isbn[0]; 1584 } 1585 else if (field == 'extra') { 1586 value = Zotero.Cite.extraToCSL(value); 1587 } 1588 1589 // Strip enclosing quotes 1590 if(value.charAt(0) == '"' && value.indexOf('"', 1) == value.length - 1) { 1591 value = value.substring(1, value.length-1); 1592 } 1593 cslItem[variable] = value; 1594 break; 1595 } 1596 } 1597 } 1598 1599 // separate name variables 1600 if (zoteroItem.type != "attachment" && zoteroItem.type != "note") { 1601 var author = Zotero.CreatorTypes.getName(Zotero.CreatorTypes.getPrimaryIDForType(itemTypeID)); 1602 var creators = zoteroItem.creators; 1603 for(var i=0; creators && i<creators.length; i++) { 1604 var creator = creators[i]; 1605 var creatorType = creator.creatorType; 1606 if(creatorType == author) { 1607 creatorType = "author"; 1608 } 1609 1610 creatorType = CSL_NAMES_MAPPINGS[creatorType]; 1611 if(!creatorType) continue; 1612 1613 var nameObj; 1614 if (creator.lastName || creator.firstName) { 1615 nameObj = { 1616 family: creator.lastName || '', 1617 given: creator.firstName || '' 1618 }; 1619 1620 // Parse name particles 1621 // Replicate citeproc-js logic for what should be parsed so we don't 1622 // break current behavior. 1623 if (nameObj.family && nameObj.given) { 1624 // Don't parse if last name is quoted 1625 if (nameObj.family.length > 1 1626 && nameObj.family.charAt(0) == '"' 1627 && nameObj.family.charAt(nameObj.family.length - 1) == '"' 1628 ) { 1629 nameObj.family = nameObj.family.substr(1, nameObj.family.length - 2); 1630 } else { 1631 Zotero.CiteProc.CSL.parseParticles(nameObj, true); 1632 } 1633 } 1634 } else if (creator.name) { 1635 nameObj = {'literal': creator.name}; 1636 } 1637 1638 if(cslItem[creatorType]) { 1639 cslItem[creatorType].push(nameObj); 1640 } else { 1641 cslItem[creatorType] = [nameObj]; 1642 } 1643 } 1644 } 1645 1646 // get date variables 1647 for(var variable in CSL_DATE_MAPPINGS) { 1648 var date = zoteroItem[CSL_DATE_MAPPINGS[variable]]; 1649 if (!date) { 1650 var typeSpecificFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, CSL_DATE_MAPPINGS[variable]); 1651 if (typeSpecificFieldID) { 1652 date = zoteroItem[Zotero.ItemFields.getName(typeSpecificFieldID)]; 1653 } 1654 } 1655 1656 if(date) { 1657 var dateObj = Zotero.Date.strToDate(date); 1658 // otherwise, use date-parts 1659 var dateParts = []; 1660 if(dateObj.year) { 1661 // add year, month, and day, if they exist 1662 dateParts.push(dateObj.year); 1663 if(dateObj.month !== undefined) { 1664 // strToDate() returns a JS-style 0-indexed month, so we add 1 to it 1665 dateParts.push(dateObj.month+1); 1666 if(dateObj.day) { 1667 dateParts.push(dateObj.day); 1668 } 1669 } 1670 cslItem[variable] = {"date-parts":[dateParts]}; 1671 1672 // if no month, use season as month 1673 if(dateObj.part && dateObj.month === undefined) { 1674 cslItem[variable].season = dateObj.part; 1675 } 1676 } else { 1677 // if no year, pass date literally 1678 cslItem[variable] = {"literal":date}; 1679 } 1680 } 1681 } 1682 1683 // Special mapping for note title 1684 if (zoteroItem.itemType == 'note' && zoteroItem.note) { 1685 cslItem.title = Zotero.Notes.noteToTitle(zoteroItem.note); 1686 } 1687 1688 //this._cache[zoteroItem.id] = cslItem; 1689 return cslItem; 1690 }, 1691 1692 /** 1693 * Converts an item in CSL JSON format to a Zotero item 1694 * @param {Zotero.Item} item 1695 * @param {Object} cslItem 1696 */ 1697 "itemFromCSLJSON":function(item, cslItem) { 1698 var isZoteroItem = !!item.setType, 1699 zoteroType; 1700 1701 // Some special cases to help us map item types correctly 1702 // This ensures that we don't lose data on import. The fields 1703 // we check are incompatible with the alternative item types 1704 if (cslItem.type == 'book') { 1705 zoteroType = 'book'; 1706 if (cslItem.version) { 1707 zoteroType = 'computerProgram'; 1708 } 1709 } else if (cslItem.type == 'bill') { 1710 zoteroType = 'bill'; 1711 if (cslItem.publisher || cslItem['number-of-volumes']) { 1712 zoteroType = 'hearing'; 1713 } 1714 } else if (cslItem.type == 'song') { 1715 zoteroType = 'audioRecording'; 1716 if (cslItem.number) { 1717 zoteroType = 'podcast'; 1718 } 1719 } else if (cslItem.type == 'motion_picture') { 1720 zoteroType = 'film'; 1721 if (cslItem['collection-title'] || cslItem['publisher-place'] 1722 || cslItem['event-place'] || cslItem.volume 1723 || cslItem['number-of-volumes'] || cslItem.ISBN 1724 ) { 1725 zoteroType = 'videoRecording'; 1726 } 1727 } else { 1728 for(var type in CSL_TYPE_MAPPINGS) { 1729 if(CSL_TYPE_MAPPINGS[type] == cslItem.type) { 1730 zoteroType = type; 1731 break; 1732 } 1733 } 1734 } 1735 1736 if(!zoteroType) zoteroType = "document"; 1737 1738 var itemTypeID = Zotero.ItemTypes.getID(zoteroType); 1739 if(isZoteroItem) { 1740 item.setType(itemTypeID); 1741 } else { 1742 item.itemID = cslItem.id; 1743 item.itemType = zoteroType; 1744 } 1745 1746 // map text fields 1747 for(var variable in CSL_TEXT_MAPPINGS) { 1748 if(variable in cslItem) { 1749 var textMappings = CSL_TEXT_MAPPINGS[variable]; 1750 for(var i=0; i<textMappings.length; i++) { 1751 var field = textMappings[i]; 1752 var fieldID = Zotero.ItemFields.getID(field); 1753 1754 if(Zotero.ItemFields.isBaseField(fieldID)) { 1755 var newFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, fieldID); 1756 if(newFieldID) fieldID = newFieldID; 1757 } 1758 1759 if(Zotero.ItemFields.isValidForType(fieldID, itemTypeID)) { 1760 // TODO: Convert restrictive Extra cheater syntax ('original-date: 2018') 1761 // to nicer format we allow ('Original Date: 2018'), unless we've added 1762 // those fields before we get to that 1763 if(isZoteroItem) { 1764 item.setField(fieldID, cslItem[variable]); 1765 } else { 1766 item[field] = cslItem[variable]; 1767 } 1768 1769 break; 1770 } 1771 } 1772 } 1773 } 1774 1775 // separate name variables 1776 for(var field in CSL_NAMES_MAPPINGS) { 1777 if(CSL_NAMES_MAPPINGS[field] in cslItem) { 1778 var creatorTypeID = Zotero.CreatorTypes.getID(field); 1779 if(!Zotero.CreatorTypes.isValidForItemType(creatorTypeID, itemTypeID)) { 1780 creatorTypeID = Zotero.CreatorTypes.getPrimaryIDForType(itemTypeID); 1781 } 1782 1783 var nameMappings = cslItem[CSL_NAMES_MAPPINGS[field]]; 1784 for(var i in nameMappings) { 1785 var cslAuthor = nameMappings[i]; 1786 let creator = {}; 1787 if(cslAuthor.family || cslAuthor.given) { 1788 creator.lastName = cslAuthor.family || ''; 1789 creator.firstName = cslAuthor.given || ''; 1790 } else if(cslAuthor.literal) { 1791 creator.lastName = cslAuthor.literal; 1792 creator.fieldMode = 1; 1793 } else { 1794 continue; 1795 } 1796 creator.creatorTypeID = creatorTypeID; 1797 1798 if(isZoteroItem) { 1799 item.setCreator(item.getCreators().length, creator); 1800 } else { 1801 creator.creatorType = Zotero.CreatorTypes.getName(creatorTypeID); 1802 if (Zotero.isFx && !Zotero.isBookmarklet) { 1803 creator = Components.utils.cloneInto(creator, item); 1804 } 1805 item.creators.push(creator); 1806 } 1807 } 1808 } 1809 } 1810 1811 // get date variables 1812 for(var variable in CSL_DATE_MAPPINGS) { 1813 if(variable in cslItem) { 1814 var field = CSL_DATE_MAPPINGS[variable], 1815 fieldID = Zotero.ItemFields.getID(field), 1816 cslDate = cslItem[variable]; 1817 var fieldID = Zotero.ItemFields.getID(field); 1818 if(Zotero.ItemFields.isBaseField(fieldID)) { 1819 var newFieldID = Zotero.ItemFields.getFieldIDFromTypeAndBase(itemTypeID, fieldID); 1820 if(newFieldID) fieldID = newFieldID; 1821 } 1822 1823 if(Zotero.ItemFields.isValidForType(fieldID, itemTypeID)) { 1824 var date = ""; 1825 if(cslDate.literal || cslDate.raw) { 1826 date = cslDate.literal || cslDate.raw; 1827 if(variable === "accessed") { 1828 date = Zotero.Date.strToISO(date); 1829 } 1830 } else { 1831 var newDate = Zotero.Utilities.deepCopy(cslDate); 1832 if(cslDate["date-parts"] && typeof cslDate["date-parts"] === "object" 1833 && cslDate["date-parts"] !== null 1834 && typeof cslDate["date-parts"][0] === "object" 1835 && cslDate["date-parts"][0] !== null) { 1836 if(cslDate["date-parts"][0][0]) newDate.year = cslDate["date-parts"][0][0]; 1837 if(cslDate["date-parts"][0][1]) newDate.month = cslDate["date-parts"][0][1]; 1838 if(cslDate["date-parts"][0][2]) newDate.day = cslDate["date-parts"][0][2]; 1839 } 1840 1841 if(newDate.year) { 1842 if(variable === "accessed") { 1843 // Need to convert to SQL 1844 var date = Zotero.Utilities.lpad(newDate.year, "0", 4); 1845 if(newDate.month) { 1846 date += "-"+Zotero.Utilities.lpad(newDate.month, "0", 2); 1847 if(newDate.day) { 1848 date += "-"+Zotero.Utilities.lpad(newDate.day, "0", 2); 1849 } 1850 } 1851 } else { 1852 if(newDate.month) newDate.month--; 1853 date = Zotero.Date.formatDate(newDate); 1854 if(newDate.season) { 1855 date = newDate.season+" "+date; 1856 } 1857 } 1858 } 1859 } 1860 1861 if(isZoteroItem) { 1862 item.setField(fieldID, date); 1863 } else { 1864 item[field] = date; 1865 } 1866 } 1867 } 1868 } 1869 }, 1870 1871 /** 1872 * Get the real target URL from an intermediate URL 1873 */ 1874 "resolveIntermediateURL":function(url) { 1875 var patterns = [ 1876 // Google search results 1877 { 1878 regexp: /^https?:\/\/(www.)?google\.(com|(com?\.)?[a-z]{2})\/url\?/, 1879 variable: "url" 1880 } 1881 ]; 1882 1883 for (var i=0, len=patterns.length; i<len; i++) { 1884 if (!url.match(patterns[i].regexp)) { 1885 continue; 1886 } 1887 var matches = url.match(new RegExp("&" + patterns[i].variable + "=(.+?)(&|$)")); 1888 if (!matches) { 1889 continue; 1890 } 1891 return decodeURIComponent(matches[1]); 1892 } 1893 1894 return url; 1895 }, 1896 1897 /** 1898 * Adds a string to a given array at a given offset, converted to UTF-8 1899 * @param {String} string The string to convert to UTF-8 1900 * @param {Array|Uint8Array} array The array to which to add the string 1901 * @param {Integer} [offset] Offset at which to add the string 1902 */ 1903 "stringToUTF8Array":function(string, array, offset) { 1904 if(!offset) offset = 0; 1905 var n = string.length; 1906 for(var i=0; i<n; i++) { 1907 var val = string.charCodeAt(i); 1908 if(val >= 128) { 1909 if(val >= 2048) { 1910 array[offset] = (val >>> 12) | 224; 1911 array[offset+1] = ((val >>> 6) & 63) | 128; 1912 array[offset+2] = (val & 63) | 128; 1913 offset += 3; 1914 } else { 1915 array[offset] = ((val >>> 6) | 192); 1916 array[offset+1] = (val & 63) | 128; 1917 offset += 2; 1918 } 1919 } else { 1920 array[offset++] = val; 1921 } 1922 } 1923 }, 1924 1925 /** 1926 * Gets the byte length of the UTF-8 representation of a given string 1927 * @param {String} string 1928 * @return {Integer} 1929 */ 1930 "getStringByteLength":function(string) { 1931 var length = 0, n = string.length; 1932 for(var i=0; i<n; i++) { 1933 var val = string.charCodeAt(i); 1934 if(val >= 128) { 1935 if(val >= 2048) { 1936 length += 3; 1937 } else { 1938 length += 2; 1939 } 1940 } else { 1941 length += 1; 1942 } 1943 } 1944 return length; 1945 }, 1946 1947 /** 1948 * Gets the icon for a JSON-style attachment 1949 */ 1950 "determineAttachmentIcon":function(attachment) { 1951 if(attachment.linkMode === "linked_url") { 1952 return Zotero.ItemTypes.getImageSrc("attachment-web-link"); 1953 } 1954 return Zotero.ItemTypes.getImageSrc(attachment.mimeType === "application/pdf" 1955 ? "attachment-pdf" : "attachment-snapshot"); 1956 }, 1957 1958 "allowedKeyChars": "23456789ABCDEFGHIJKLMNPQRSTUVWXYZ", 1959 1960 /** 1961 * Generates a valid object key for the server API 1962 */ 1963 "generateObjectKey":function generateObjectKey() { 1964 return Zotero.Utilities.randomString(8, Zotero.Utilities.allowedKeyChars); 1965 }, 1966 1967 /** 1968 * Check if an object key is in a valid format 1969 */ 1970 "isValidObjectKey":function(key) { 1971 if (!Zotero.Utilities.objectKeyRegExp) { 1972 Zotero.Utilities.objectKeyRegExp = new RegExp('^[' + Zotero.Utilities.allowedKeyChars + ']{8}$'); 1973 } 1974 return Zotero.Utilities.objectKeyRegExp.test(key); 1975 }, 1976 1977 /** 1978 * Provides unicode support and other additional features for regular expressions 1979 * See https://github.com/slevithan/xregexp for usage 1980 */ 1981 "XRegExp": XRegExp 1982 }