cite.js (21254B)
1 "use strict"; 2 3 /** 4 * Utility functions for dealing with citations 5 * @namespace 6 */ 7 Zotero.Cite = { 8 /** 9 * Locator labels 10 */ 11 "labels":["page", "book", "chapter", "column", "figure", "folio", 12 "issue", "line", "note", "opus", "paragraph", "part", "section", "sub verbo", 13 "volume", "verse"], 14 15 /** 16 * Remove specified item IDs in-place from a citeproc-js bibliography object returned 17 * by makeBibliography() 18 * @param {bib} citeproc-js bibliography object 19 * @param {Set} itemsToRemove Set of items to remove 20 */ 21 "removeFromBibliography":function(bib, itemsToRemove) { 22 var removeItems = []; 23 for(let i in bib[0].entry_ids) { 24 for(let j in bib[0].entry_ids[i]) { 25 if(itemsToRemove.has(`${bib[0].entry_ids[i][j]}`)) { 26 removeItems.push(i); 27 break; 28 } 29 } 30 } 31 for(let i=removeItems.length-1; i>=0; i--) { 32 bib[0].entry_ids.splice(removeItems[i], 1); 33 bib[1].splice(removeItems[i], 1); 34 } 35 }, 36 37 /** 38 * Convert formatting data from citeproc-js bibliography object into explicit format 39 * parameters for RTF or word processors 40 * @param {bib} citeproc-js bibliography object 41 * @return {Object} Bibliography style parameters. 42 */ 43 "getBibliographyFormatParameters":function getBibliographyFormatParameters(bib) { 44 var bibStyle = {"tabStops":[], "indent":0, "firstLineIndent":0, 45 "lineSpacing":(240*bib[0].linespacing), 46 "entrySpacing":(240*bib[0].entryspacing)}; 47 if(bib[0].hangingindent) { 48 bibStyle.indent = 720; // 720 twips = 0.5 in 49 bibStyle.firstLineIndent = -720; // -720 twips = -0.5 in 50 } else if(bib[0]["second-field-align"]) { 51 // this is a really sticky issue. the below works for first fields that look like "[1]" 52 // and "1." otherwise, i have no idea. luckily, this will be good enough 99% of the time. 53 var alignAt = 24+bib[0].maxoffset*120; 54 bibStyle.firstLineIndent = -alignAt; 55 if(bib[0]["second-field-align"] == "margin") { 56 bibStyle.tabStops = [0]; 57 } else { 58 bibStyle.indent = alignAt; 59 bibStyle.tabStops = [alignAt]; 60 } 61 } 62 63 return bibStyle; 64 }, 65 66 /** 67 * Makes a formatted bibliography, if the style defines one; otherwise makes a 68 * formatted list of items 69 * @param {Zotero.Style} style The style to use 70 * @param {Zotero.Item[]} items An array of items 71 * @param {String} format The format of the output (html, text, or rtf) 72 * @return {String} Bibliography or item list in specified format 73 */ 74 "makeFormattedBibliographyOrCitationList":function(cslEngine, items, format, asCitationList) { 75 cslEngine.setOutputFormat(format); 76 cslEngine.updateItems(items.map(item => item.id)); 77 78 if(!asCitationList) { 79 var bibliography = Zotero.Cite.makeFormattedBibliography(cslEngine, format); 80 if(bibliography) return bibliography; 81 } 82 83 var styleClass = cslEngine.opt.class; 84 var citations=[]; 85 for (var i=0, ilen=items.length; i<ilen; i++) { 86 var item = items[i]; 87 var outList = cslEngine.appendCitationCluster({"citationItems":[{"id":item.id}], "properties":{}}, true); 88 for (var j=0, jlen=outList.length; j<jlen; j++) { 89 var citationPos = outList[j][0]; 90 citations[citationPos] = outList[j][1]; 91 } 92 } 93 94 if(styleClass == "note") { 95 if(format == "html") { 96 return "<ol>\n\t<li>"+citations.join("</li>\n\t<li>")+"</li>\n</ol>"; 97 } else if(format == "text") { 98 var output = []; 99 for(var i=0; i<citations.length; i++) { 100 output.push((i+1)+". "+citations[i]+"\r\n"); 101 } 102 return output.join(""); 103 } else if(format == "rtf") { 104 var output = ["{\\rtf \n{\\*\\listtable{\\list\\listtemplateid1\\listhybrid{\\listlevel"+ 105 "\\levelnfc0\\levelnfcn0\\leveljc0\\leveljcn0\\levelfollow0\\levelstartat1"+ 106 "\\levelspace360\\levelindent0{\\*\\levelmarker \\{decimal\\}.}{\\leveltext"+ 107 "\\leveltemplateid1\\'02\\'00.;}{\\levelnumbers\\'01;}\\fi-360\\li720\\lin720 }"+ 108 "{\\listname ;}\\listid1}}\n{\\*\\listoverridetable{\\listoverride\\listid1"+ 109 "\\listoverridecount0\\ls1}}\n\\tx720\\li720\\fi-480\\ls1\\ilvl0\n"]; 110 for(var i=0; i<citations.length; i++) { 111 output.push("{\\listtext "+(i+1)+". }"+citations[i]+"\\\n"); 112 } 113 output.push("}"); 114 return output.join(""); 115 } else { 116 throw "Unimplemented bibliography format "+format; 117 } 118 } else { 119 if(format == "html") { 120 return citations.join("<br />"); 121 } else if(format == "text") { 122 return citations.join("\r\n"); 123 } else if(format == "rtf") { 124 return "<\\rtf \n"+citations.join("\\\n")+"\n}"; 125 } 126 } 127 }, 128 129 /** 130 * Makes a formatted bibliography 131 * @param {Zotero.Style} style The style 132 * @param {String} format The format of the output (html, text, or rtf) 133 * @return {String} Bibliography in specified format 134 */ 135 "makeFormattedBibliography":function makeFormattedBibliography(cslEngine, format) { 136 cslEngine.setOutputFormat(format); 137 var bib = cslEngine.makeBibliography(); 138 if(!bib) return false; 139 140 if(format == "html") { 141 var output = [bib[0].bibstart]; 142 for(var i in bib[1]) { 143 output.push(bib[1][i]); 144 145 // add COinS 146 for (let itemID of bib[0].entry_ids[i]) { 147 try { 148 var co = Zotero.OpenURL.createContextObject(Zotero.Items.get(itemID), "1.0"); 149 if(!co) continue; 150 output.push(' <span class="Z3988" title="'+ 151 co.replace("&", "&", "g").replace("<", "<", "g").replace(">", ">", "g")+ 152 '"></span>\n'); 153 } catch(e) { 154 Zotero.logError(e); 155 } 156 } 157 } 158 output.push(bib[0].bibend); 159 var html = output.join(""); 160 161 var inlineCSS = true; 162 if (!inlineCSS) { 163 return html; 164 } 165 166 //Zotero.debug("maxoffset: " + bib[0].maxoffset); 167 //Zotero.debug("entryspacing: " + bib[0].entryspacing); 168 //Zotero.debug("linespacing: " + bib[0].linespacing); 169 //Zotero.debug("hangingindent: " + bib[0].hangingindent); 170 //Zotero.debug("second-field-align: " + bib[0]["second-field-align"]); 171 172 var maxOffset = parseInt(bib[0].maxoffset); 173 var entrySpacing = parseInt(bib[0].entryspacing); 174 var lineSpacing = parseInt(bib[0].linespacing); 175 var hangingIndent = parseInt(bib[0].hangingindent); 176 var secondFieldAlign = bib[0]["second-field-align"]; 177 178 // Validate input 179 if(maxOffset == NaN) throw "Invalid maxoffset"; 180 if(entrySpacing == NaN) throw "Invalid entryspacing"; 181 if(lineSpacing == NaN) throw "Invalid linespacing"; 182 183 var str; 184 var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] 185 .createInstance(Components.interfaces.nsIDOMParser), 186 doc = parser.parseFromString("<!DOCTYPE html><html><body></body></html>", "text/html"); 187 doc.body.insertAdjacentHTML("afterbegin", html); 188 var div = doc.body.firstChild, 189 leftMarginDivs = Zotero.Utilities.xpath(doc, '//div[@class="csl-left-margin"]'), 190 multiField = !!leftMarginDivs.length, 191 clearEntries = multiField; 192 193 // One of the characters is usually a period, so we can adjust this down a bit 194 maxOffset = Math.max(1, maxOffset - 2); 195 196 // Force a minimum line height 197 if(lineSpacing <= 1.35) lineSpacing = 1.35; 198 199 var style = div.getAttribute("style"); 200 if(!style) style = ""; 201 style += "line-height: " + lineSpacing + "; "; 202 203 if(hangingIndent) { 204 if (multiField && !secondFieldAlign) { 205 throw ("second-field-align=false and hangingindent=true combination is not currently supported"); 206 } 207 // If only one field, apply hanging indent on root 208 else if (!multiField) { 209 style += "margin-left: " + hangingIndent + "em; text-indent:-" + hangingIndent + "em;"; 210 } 211 } 212 213 if(style) div.setAttribute("style", style); 214 215 // csl-entry 216 var divs = Zotero.Utilities.xpath(doc, '//div[@class="csl-entry"]'); 217 for(var i=0, n=divs.length; i<n; i++) { 218 var div = divs[i], 219 divStyle = div.getAttribute("style"); 220 if(!divStyle) divStyle = ""; 221 222 if (clearEntries) { 223 divStyle += "clear: left; "; 224 } 225 226 if(entrySpacing && i !== n - 1) { 227 divStyle += "margin-bottom: " + entrySpacing + "em;"; 228 } 229 230 if(divStyle) div.setAttribute("style", divStyle); 231 } 232 233 // Padding on the label column, which we need to include when 234 // calculating offset of right column 235 var rightPadding = .5; 236 237 // div.csl-left-margin 238 for (let div of leftMarginDivs) { 239 var divStyle = div.getAttribute("style"); 240 if(!divStyle) divStyle = ""; 241 242 divStyle = "float: left; padding-right: " + rightPadding + "em;"; 243 244 // Right-align the labels if aligning second line, since it looks 245 // better and we don't need the second line of text to align with 246 // the left edge of the label 247 if (secondFieldAlign) { 248 divStyle += "text-align: right; width: " + maxOffset + "em;"; 249 } 250 251 div.setAttribute("style", divStyle); 252 } 253 254 // div.csl-right-inline 255 for (let div of Zotero.Utilities.xpath(doc, '//div[@class="csl-right-inline"]')) { 256 var divStyle = div.getAttribute("style"); 257 if(!divStyle) divStyle = ""; 258 259 divStyle = "margin: 0 .4em 0 " + (secondFieldAlign ? maxOffset + rightPadding : "0") + "em;"; 260 261 if (hangingIndent) { 262 divStyle += "padding-left: " + hangingIndent + "em; text-indent:-" + hangingIndent + "em;"; 263 } 264 265 div.setAttribute("style", divStyle); 266 } 267 268 // div.csl-indent 269 for (let div of Zotero.Utilities.xpath(doc, '//div[@class="csl-indent"]')) { 270 div.setAttribute("style", "margin: .5em 0 0 2em; padding: 0 0 .2em .5em; border-left: 5px solid #ccc;"); 271 } 272 273 return doc.body.innerHTML; 274 } else if(format == "text") { 275 return bib[0].bibstart+bib[1].join("")+bib[0].bibend; 276 } else if(format == "rtf") { 277 var bibStyle = Zotero.Cite.getBibliographyFormatParameters(bib); 278 279 var preamble = (bibStyle.tabStops.length ? "\\tx"+bibStyle.tabStops.join(" \\tx")+" " : ""); 280 preamble += "\\li"+bibStyle.indent+" \\fi"+bibStyle.firstLineIndent+" " 281 +"\\sl"+bibStyle.lineSpacing+" \\slmult1 " 282 +"\\sa"+bibStyle.entrySpacing+" "; 283 284 return bib[0].bibstart+preamble+bib[1].join("\\\r\n")+"\\\r\n"+bib[0].bibend; 285 } else { 286 throw "Unimplemented bibliography format "+format; 287 } 288 }, 289 290 /** 291 * Get an item by ID, either by retrieving it from the library or looking for the document it 292 * belongs to. 293 * @param {String|Number|Array} id 294 * @return {Zotero.Item} item 295 */ 296 "getItem":function getItem(id) { 297 var slashIndex; 298 299 if(id instanceof Array) { 300 return id.map(anId => Zotero.Cite.getItem(anId)); 301 } else if(typeof id === "string" && (slashIndex = id.indexOf("/")) !== -1) { 302 var sessionID = id.substr(0, slashIndex), 303 session = Zotero.Integration.sessions[sessionID], 304 item; 305 if (session) { 306 item = session.embeddedZoteroItems[id.substr(slashIndex+1)]; 307 } 308 309 if(!item) { 310 item = new Zotero.Item("document"); 311 item.setField("title", "Missing Item"); 312 Zotero.log("CSL item "+id+" not found"); 313 } 314 return item; 315 } else { 316 return Zotero.Items.get(id); 317 } 318 }, 319 320 extraToCSL: function (extra) { 321 return extra.replace(/^([A-Za-z \-]+)(:\s*.+)/gm, function (_, field, value) { 322 var originalField = field; 323 var field = field.toLowerCase().replace(/ /g, '-'); 324 // Fields from https://aurimasv.github.io/z2csl/typeMap.xml 325 switch (field) { 326 // Standard fields 327 case 'abstract': 328 case 'accessed': 329 case 'annote': 330 case 'archive': 331 case 'archive-place': 332 case 'author': 333 case 'authority': 334 case 'call-number': 335 case 'chapter-number': 336 case 'citation-label': 337 case 'citation-number': 338 case 'collection-editor': 339 case 'collection-number': 340 case 'collection-title': 341 case 'composer': 342 case 'container': 343 case 'container-author': 344 case 'container-title': 345 case 'container-title-short': 346 case 'dimensions': 347 case 'director': 348 case 'edition': 349 case 'editor': 350 case 'editorial-director': 351 case 'event': 352 case 'event-date': 353 case 'event-place': 354 case 'first-reference-note-number': 355 case 'genre': 356 case 'illustrator': 357 case 'interviewer': 358 case 'issue': 359 case 'issued': 360 case 'jurisdiction': 361 case 'keyword': 362 case 'language': 363 case 'locator': 364 case 'medium': 365 case 'note': 366 case 'number': 367 case 'number-of-pages': 368 case 'number-of-volumes': 369 case 'original-author': 370 case 'original-date': 371 case 'original-publisher': 372 case 'original-publisher-place': 373 case 'original-title': 374 case 'page': 375 case 'page-first': 376 case 'publisher': 377 case 'publisher-place': 378 case 'recipient': 379 case 'references': 380 case 'reviewed-author': 381 case 'reviewed-title': 382 case 'scale': 383 case 'section': 384 case 'source': 385 case 'status': 386 case 'submitted': 387 case 'title': 388 case 'title-short': 389 case 'translator': 390 case 'version': 391 case 'volume': 392 case 'year-suffix': 393 break; 394 395 // Uppercase fields 396 case 'doi': 397 case 'isbn': 398 case 'issn': 399 case 'pmcid': 400 case 'pmid': 401 case 'url': 402 field = field.toUpperCase(); 403 break; 404 405 // Weirdo 406 case 'archive-location': 407 field = 'archive_location'; 408 break; 409 410 // Don't change other lines 411 default: 412 field = originalField; 413 } 414 return field + value; 415 }); 416 } 417 }; 418 419 /** 420 * Get a CSL abbreviation in the format expected by citeproc-js 421 */ 422 Zotero.Cite.getAbbreviation = new function() { 423 var abbreviations, 424 abbreviationCategories; 425 426 /** 427 * Initialize abbreviations database. 428 */ 429 function init() { 430 if(!abbreviations) loadAbbreviations(); 431 } 432 433 function loadAbbreviations() { 434 var file = Zotero.File.pathToFile(Zotero.DataDirectory.dir); 435 file.append("abbreviations.json"); 436 437 var json, origin; 438 if(file.exists()) { 439 json = Zotero.File.getContents(file); 440 origin = file.path; 441 } else { 442 json = Zotero.File.getContentsFromURL("resource://zotero/schema/abbreviations.json"); 443 origin = "resource://zotero/schema/abbreviations.json"; 444 } 445 446 try { 447 abbreviations = JSON.parse(json); 448 } catch(e) { 449 throw new Zotero.Exception.Alert("styles.abbreviations.parseError", origin, 450 "styles.abbreviations.title", e); 451 } 452 453 if(!abbreviations.info || !abbreviations.info.name || !abbreviations.info.URI) { 454 throw new Zotero.Exception.Alert("styles.abbreviations.missingInfo", origin, 455 "styles.abbreviations.title"); 456 } 457 458 abbreviationCategories = {}; 459 for(var jurisdiction in abbreviations) { 460 for(var category in abbreviations[jurisdiction]) { 461 abbreviationCategories[category] = true; 462 } 463 } 464 } 465 466 /** 467 * Normalizes a key 468 */ 469 function normalizeKey(key) { 470 // Strip periods, normalize spacing, and convert to lowercase 471 return key.toString(). 472 replace(/(?:\b|^)(?:and|et|y|und|l[ae]|the|[ld]')(?:\b|$)|[\x21-\x2C.\/\x3A-\x40\x5B-\x60\\\x7B-\x7E]/ig, ""). 473 replace(/\s+/g, " ").trim(); 474 } 475 476 function lookupKey(key) { 477 return key.toLowerCase().replace(/\s*\./g, "." ); 478 } 479 480 /** 481 * Replace getAbbreviation on citeproc-js with our own handler. 482 */ 483 return function getAbbreviation(listname, obj, jurisdiction, category, key) { 484 init(); 485 486 // Short circuit if we know we don't handle this kind of abbreviation 487 if(!abbreviationCategories[category] && !abbreviationCategories[category+"-word"]) return; 488 489 var normalizedKey = normalizeKey(key), 490 lcNormalizedKey = lookupKey(normalizedKey), 491 abbreviation; 492 if(!normalizedKey) return; 493 494 var jurisdictions = ["default"]; 495 if(jurisdiction !== "default" && abbreviations[jurisdiction]) { 496 jurisdictions.unshift(jurisdiction); 497 } 498 499 // Look for full abbreviation 500 var jur, cat; 501 for(var i=0; i<jurisdictions.length && !abbreviation; i++) { 502 if((jur = abbreviations[jurisdictions[i]]) && (cat = jur[category])) { 503 abbreviation = cat[lcNormalizedKey]; 504 } 505 } 506 507 if(!abbreviation) { 508 // Abbreviate words individually 509 var words = normalizedKey.split(/([ \-])/); 510 511 if(words.length > 1) { 512 var lcWords = []; 513 for(var j=0; j<words.length; j+=2) { 514 lcWords[j] = lookupKey(words[j]); 515 } 516 for(var j=0; j<words.length; j+=2) { 517 var word = words[j], 518 lcWord = lcWords[j], 519 newWord = undefined, 520 exactMatch = false; 521 522 for(var i=0; i<jurisdictions.length && newWord === undefined; i++) { 523 if(!(jur = abbreviations[jurisdictions[i]])) continue; 524 if(!(cat = jur[category+"-word"])) continue; 525 526 if(cat.hasOwnProperty(lcWord)) { 527 // Complete match 528 newWord = cat[lcWord]; 529 exactMatch = true; 530 } else if(lcWord.charAt(lcWord.length-1) == 's' && cat.hasOwnProperty(lcWord.substr(0, lcWord.length-1))) { 531 // Try dropping 's' 532 newWord = cat[lcWord.substr(0, lcWord.length-1)]; 533 exactMatch = true; 534 } else { 535 if(j < words.length-2) { 536 // Two-word match 537 newWord = cat[lcWord+words[j+1]+lcWords[j+2]]; 538 if(newWord !== undefined) { 539 words.splice(j+1, 2); 540 lcWords.splice(j+1, 2); 541 exactMatch = true; 542 } 543 } 544 545 if(newWord === undefined) { 546 // Partial match 547 for(var k=lcWord.length; k>0 && newWord === undefined; k--) { 548 newWord = cat[lcWord.substr(0, k)+"-"]; 549 } 550 } 551 } 552 } 553 554 // Don't substitute with a longer word 555 if(newWord && !exactMatch && word.length - newWord.length < 1) { 556 newWord = word; 557 } 558 559 // Fall back to full word 560 if(newWord === undefined) newWord = word; 561 562 // Don't discard last word (e.g. Climate of the Past => Clim. Past) 563 if(!newWord && j == words.length-1) newWord = word; 564 565 words[j] = newWord.substr(0, 1).toUpperCase() + newWord.substr(1); 566 } 567 abbreviation = words.join("").replace(/\s+/g, " ").trim(); 568 } else { 569 abbreviation = key; 570 } 571 } 572 573 if(!abbreviation) abbreviation = key; //this should never happen, but just in case 574 575 Zotero.debug("Abbreviated "+key+" as "+abbreviation); 576 577 // Add to jurisdiction object 578 if(!obj[jurisdiction]) { 579 obj[jurisdiction] = new Zotero.CiteProc.CSL.AbbreviationSegments(); 580 } 581 obj[jurisdiction][category][key] = abbreviation; 582 } 583 }; 584 585 /** 586 * citeproc-js system object 587 * @class 588 */ 589 Zotero.Cite.System = function(automaticJournalAbbreviations) { 590 if(automaticJournalAbbreviations) { 591 this.getAbbreviation = Zotero.Cite.getAbbreviation; 592 } 593 } 594 595 Zotero.Cite.System.prototype = { 596 /** 597 * citeproc-js system function for getting items 598 * See http://gsl-nagoya-u.net/http/pub/citeproc-doc.html#retrieveitem 599 * @param {String|Integer} item - Item ID, or string item for embedded citations 600 * @return {Object} citeproc-js item 601 */ 602 "retrieveItem":function retrieveItem(item) { 603 var zoteroItem, slashIndex; 604 if(typeof item === "object" && item !== null && item instanceof Zotero.Item) { 605 //if(this._cache[item.id]) return this._cache[item.id]; 606 zoteroItem = item; 607 } else if(typeof item === "string" && (slashIndex = item.indexOf("/")) !== -1) { 608 // is an embedded item 609 var sessionID = item.substr(0, slashIndex); 610 var session = Zotero.Integration.sessions[sessionID]; 611 if(session) { 612 var embeddedCitation = session.embeddedItems[item.substr(slashIndex+1)]; 613 if (embeddedCitation) { 614 embeddedCitation.id = item; 615 return embeddedCitation; 616 } 617 } 618 } else { 619 // is an item ID 620 //if(this._cache[item]) return this._cache[item]; 621 try { 622 zoteroItem = Zotero.Items.get(item); 623 } catch(e) {} 624 } 625 626 if(!zoteroItem) { 627 throw new Error("Zotero.Cite.System.retrieveItem called on non-item "+item); 628 } 629 630 var cslItem = Zotero.Utilities.itemToCSLJSON(zoteroItem); 631 632 // TEMP: citeproc-js currently expects the id property to be the item DB id 633 cslItem.id = zoteroItem.id; 634 635 if (!Zotero.Prefs.get("export.citePaperJournalArticleURL")) { 636 var itemType = Zotero.ItemTypes.getName(zoteroItem.itemTypeID); 637 // don't return URL or accessed information for journal articles if a 638 // pages field exists 639 if (["journalArticle", "newspaperArticle", "magazineArticle"].indexOf(itemType) !== -1 640 && zoteroItem.getField("pages") 641 ) { 642 delete cslItem.URL; 643 delete cslItem.accessed; 644 } 645 } 646 647 return cslItem; 648 }, 649 650 /** 651 * citeproc-js system function for getting locale 652 * See http://gsl-nagoya-u.net/http/pub/citeproc-doc.html#retrieveLocale 653 * @param {String} lang Language to look for a locale for 654 * @return {String|Boolean} The locale as a string if it exists, or false if it doesn't 655 */ 656 "retrieveLocale":function retrieveLocale(lang) { 657 return Zotero.Cite.Locale.get(lang); 658 } 659 }; 660 661 Zotero.Cite.Locale = { 662 _cache: new Map(), 663 664 get: function (locale) { 665 var str = this._cache.get(locale); 666 if (str) { 667 return str; 668 } 669 var uri = `chrome://zotero/content/locale/csl/locales-${locale}.xml`; 670 try { 671 let protHandler = Components.classes["@mozilla.org/network/protocol;1?name=chrome"] 672 .createInstance(Components.interfaces.nsIProtocolHandler); 673 let channel = protHandler.newChannel(protHandler.newURI(uri)); 674 let cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"] 675 .createInstance(Components.interfaces.nsIConverterInputStream); 676 cstream.init(channel.open(), "UTF-8", 0, 0); 677 let obj = {}; 678 let read = 0; 679 let str = ""; 680 do { 681 // Read as much as we can and put it in obj.value 682 read = cstream.readString(0xffffffff, obj); 683 str += obj.value; 684 } while (read != 0); 685 cstream.close(); 686 this._cache.set(locale, str); 687 return str; 688 } 689 catch (e) { 690 //Zotero.debug(e); 691 return false; 692 } 693 } 694 };