style.js (29362B)
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 ***** END LICENSE BLOCK ***** 24 */ 25 26 Zotero.Styles = new function() { 27 var _initialized = false; 28 var _initializationDeferred = false; 29 var _styles, _visibleStyles; 30 31 var _renamedStyles = null; 32 33 Components.utils.import("resource://gre/modules/Services.jsm"); 34 Components.utils.import("resource://gre/modules/FileUtils.jsm"); 35 36 this.xsltProcessor = null; 37 this.ns = { 38 "csl":"http://purl.org/net/xbiblio/csl" 39 }; 40 41 42 /** 43 * Initializes styles cache, loading metadata for styles into memory 44 */ 45 this.init = Zotero.Promise.coroutine(function* (options = {}) { 46 // Wait until bundled files have been updated, except when this is called by the schema update 47 // code itself 48 if (!options.fromSchemaUpdate) { 49 yield Zotero.Schema.schemaUpdatePromise; 50 } 51 52 // If an initialization has already started, a regular init() call should return the promise 53 // for that (which may already be resolved). A reinit should yield on that but then continue 54 // with reinitialization. 55 if (_initializationDeferred) { 56 let promise = _initializationDeferred.promise; 57 if (options.reinit) { 58 yield promise; 59 } 60 else { 61 return promise; 62 } 63 } 64 65 _initializationDeferred = Zotero.Promise.defer(); 66 67 Zotero.debug("Initializing styles"); 68 var start = new Date; 69 70 // Upgrade style locale prefs for 4.0.27 71 var bibliographyLocale = Zotero.Prefs.get("export.bibliographyLocale"); 72 if (bibliographyLocale) { 73 Zotero.Prefs.set("export.lastLocale", bibliographyLocale); 74 Zotero.Prefs.set("export.quickCopy.locale", bibliographyLocale); 75 Zotero.Prefs.clear("export.bibliographyLocale"); 76 } 77 78 _styles = {}; 79 _visibleStyles = []; 80 81 // main dir 82 var dir = Zotero.getStylesDirectory().path; 83 var num = yield _readStylesFromDirectory(dir, false); 84 85 // hidden dir 86 var hiddenDir = OS.Path.join(dir, 'hidden'); 87 if (yield OS.File.exists(hiddenDir)) { 88 num += yield _readStylesFromDirectory(hiddenDir, true); 89 } 90 91 // Sort visible styles by title 92 _visibleStyles.sort(function(a, b) { 93 return a.title.localeCompare(b.title); 94 }) 95 // .. and freeze, so they can be returned directly 96 _visibleStyles = Object.freeze(_visibleStyles); 97 98 Zotero.debug("Cached " + num + " styles in " + (new Date - start) + " ms"); 99 100 // load available CSL locales 101 var localeFile = {}; 102 var locales = {}; 103 var primaryDialects = {}; 104 var localesLocation = "chrome://zotero/content/locale/csl/locales.json"; 105 localeFile = JSON.parse(yield Zotero.File.getContentsFromURLAsync(localesLocation)); 106 107 primaryDialects = localeFile["primary-dialects"]; 108 109 // only keep localized language name 110 for (let locale in localeFile["language-names"]) { 111 locales[locale] = localeFile["language-names"][locale][0]; 112 } 113 114 this.locales = locales; 115 this.primaryDialects = primaryDialects; 116 117 // Load renamed styles 118 _renamedStyles = {}; 119 var xmlhttp = yield Zotero.HTTP.request( 120 "GET", 121 "resource://zotero/schema/renamed-styles.json", 122 { 123 responseType: 'json' 124 } 125 ); 126 // Map some obsolete styles to current ones 127 if (xmlhttp.response) { 128 _renamedStyles = xmlhttp.response; 129 } 130 131 _initializationDeferred.resolve(); 132 _initialized = true; 133 134 // Styles are fully loaded, but we still need to trigger citeproc reloads in Integration 135 // so that style updates are reflected in open documents 136 Zotero.Integration.resetSessionStyles(); 137 }); 138 139 this.reinit = function (options = {}) { 140 return this.init(Object.assign({}, options, { reinit: true })); 141 }; 142 143 // This is used by bibliography.js to work around a weird interaction between Bluebird and modal 144 // dialogs in tests. Calling `yield Zotero.Styles.init()` from `Zotero_File_Interface_Bibliography.init()` 145 // in the modal Create Bibliography dialog results in a hang, so instead use a synchronous check for 146 // initialization. The hang doesn't seem to happen (at least in the same way) outside of tests. 147 this.initialized = function () { 148 return _initialized; 149 }; 150 151 /** 152 * Reads all styles from a given directory and caches their metadata 153 * @private 154 */ 155 var _readStylesFromDirectory = Zotero.Promise.coroutine(function* (dir, hidden) { 156 var numCached = 0; 157 158 var iterator = new OS.File.DirectoryIterator(dir); 159 try { 160 while (true) { 161 let entries = yield iterator.nextBatch(10); // TODO: adjust as necessary 162 if (!entries.length) break; 163 164 for (let i = 0; i < entries.length; i++) { 165 let entry = entries[i]; 166 let path = entry.path; 167 let fileName = entry.name; 168 if (!fileName || fileName[0] === "." 169 || fileName.substr(-4).toLowerCase() !== ".csl" 170 || entry.isDir) continue; 171 172 try { 173 let code = yield Zotero.File.getContentsAsync(path); 174 var style = new Zotero.Style(code, path); 175 } 176 catch (e) { 177 Components.utils.reportError(e); 178 Zotero.debug(e, 1); 179 continue; 180 } 181 if(style.styleID) { 182 // same style is already cached 183 if (_styles[style.styleID]) { 184 Components.utils.reportError('Style with ID ' + style.styleID 185 + ' already loaded from ' + _styles[style.styleID].fileName); 186 } else { 187 // add to cache 188 _styles[style.styleID] = style; 189 _styles[style.styleID].hidden = hidden; 190 if(!hidden) _visibleStyles.push(style); 191 } 192 } 193 numCached++; 194 } 195 } 196 } 197 finally { 198 iterator.close(); 199 } 200 return numCached; 201 }); 202 203 /** 204 * Gets a style with a given ID 205 * @param {String} id 206 * @param {Boolean} skipMappings Don't automatically return renamed style 207 */ 208 this.get = function (id, skipMappings) { 209 if (!_initialized) { 210 throw new Zotero.Exception.UnloadedDataException("Styles not yet loaded", 'styles'); 211 } 212 213 if(!skipMappings) { 214 var prefix = "http://www.zotero.org/styles/"; 215 var shortName = id.replace(prefix, ""); 216 if(_renamedStyles.hasOwnProperty(shortName) && _styles[prefix + _renamedStyles[shortName]]) { 217 let newID = prefix + _renamedStyles[shortName]; 218 Zotero.debug("Mapping " + id + " to " + newID); 219 return _styles[newID]; 220 } 221 } 222 223 return _styles[id] || false; 224 }; 225 226 /** 227 * Gets all visible styles 228 * @return {Zotero.Style[]} - An immutable array of Zotero.Style objects 229 */ 230 this.getVisible = function () { 231 if (!_initialized) { 232 throw new Zotero.Exception.UnloadedDataException("Styles not yet loaded", 'styles'); 233 } 234 return _visibleStyles; // Immutable 235 } 236 237 /** 238 * Gets all styles 239 * 240 * @return {Object} - An object with style IDs for keys and Zotero.Style objects for values 241 */ 242 this.getAll = function () { 243 if (!_initialized) { 244 throw new Zotero.Exception.UnloadedDataException("Styles not yet loaded", 'styles'); 245 } 246 return _styles; 247 } 248 249 /** 250 * Validates a style 251 * @param {String} style The style, as a string 252 * @return {Promise} A promise representing the style file. This promise is rejected 253 * with the validation error if validation fails, or resolved if it is not. 254 */ 255 this.validate = function(style) { 256 var deferred = Zotero.Promise.defer(), 257 worker = new Worker("resource://zotero/csl-validator.js"); 258 worker.onmessage = function(event) { 259 if(event.data) { 260 deferred.reject(event.data); 261 } else { 262 deferred.resolve(); 263 } 264 }; 265 worker.postMessage(style); 266 return deferred.promise; 267 } 268 269 /** 270 * Installs a style file, getting the contents of an nsIFile and showing appropriate 271 * error messages 272 * @param {Object} style - An object with one of the following properties 273 * - file: An nsIFile or string path representing a style on disk 274 * - path: A string path 275 * - url: A url of the location of the style (local or remote) 276 * - string: A string containing the style data 277 * @param {String} origin The origin of the style, either a filename or URL, to be 278 * displayed in dialogs referencing the style 279 * @param {Boolean} [silent=false] Skip prompts 280 */ 281 this.install = Zotero.Promise.coroutine(function* (style, origin, silent=false) { 282 var styleTitle; 283 var warnDeprecated; 284 if (style instanceof Components.interfaces.nsIFile) { 285 warnDeprecated = true; 286 style = {file: style}; 287 } else if (typeof style == 'string') { 288 warnDeprecated = true; 289 style = {string: style}; 290 } 291 if (warnDeprecated) { 292 Zotero.debug("Zotero.Styles.install() now takes a style object as first argument -- update your code", 2); 293 } 294 295 try { 296 if (style.file) { 297 style.string = yield Zotero.File.getContentsAsync(style.file); 298 } 299 else if (style.url) { 300 style.string = yield Zotero.File.getContentsFromURLAsync(style.url); 301 } 302 styleTitle = yield _install(style.string, origin, false, silent); 303 } 304 catch (error) { 305 // Unless user cancelled, show an alert with the error 306 if(typeof error === "object" && error instanceof Zotero.Exception.UserCancelled) return; 307 if(typeof error === "object" && error instanceof Zotero.Exception.Alert) { 308 Zotero.logError(error); 309 if (silent) { 310 throw error; 311 } else { 312 error.present(); 313 } 314 } else { 315 Zotero.logError(error); 316 if (silent) { 317 throw error 318 } else { 319 (new Zotero.Exception.Alert("styles.install.unexpectedError", 320 origin, "styles.install.title", error)).present(); 321 } 322 } 323 } 324 return styleTitle; 325 }); 326 327 /** 328 * Installs a style 329 * @param {String} style The style as a string 330 * @param {String} origin The origin of the style, either a filename or URL, to be 331 * displayed in dialogs referencing the style 332 * @param {Boolean} [hidden] Whether style is to be hidden. 333 * @param {Boolean} [silent=false] Skip prompts 334 * @return {Promise} 335 */ 336 var _install = Zotero.Promise.coroutine(function* (style, origin, hidden, silent=false) { 337 if (!_initialized) yield Zotero.Styles.init(); 338 339 var existingFile, destFile, source; 340 341 // First, parse style and make sure it's valid XML 342 var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] 343 .createInstance(Components.interfaces.nsIDOMParser), 344 doc = parser.parseFromString(style, "application/xml"); 345 346 var styleID = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:id[1]', 347 Zotero.Styles.ns), 348 // Get file name from URL 349 m = /[^\/]+$/.exec(styleID), 350 fileName = Zotero.File.getValidFileName(m ? m[0] : styleID), 351 title = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:title[1]', 352 Zotero.Styles.ns); 353 354 if(!styleID || !title) { 355 // If it's not valid XML, we'll return a promise that immediately resolves 356 // to an error 357 throw new Zotero.Exception.Alert("styles.installError", origin, 358 "styles.install.title", "Style is not valid XML, or the styleID or title is missing"); 359 } 360 361 // look for a parent 362 source = Zotero.Utilities.xpathText(doc, 363 '/csl:style/csl:info[1]/csl:link[@rel="source" or @rel="independent-parent"][1]/@href', 364 Zotero.Styles.ns); 365 if(source == styleID) { 366 throw new Zotero.Exception.Alert("styles.installError", origin, 367 "styles.install.title", "Style references itself as source"); 368 } 369 370 // ensure csl extension 371 if(fileName.substr(-4).toLowerCase() != ".csl") fileName += ".csl"; 372 373 destFile = Zotero.getStylesDirectory(); 374 var destFileHidden = destFile.clone(); 375 destFile.append(fileName); 376 destFileHidden.append("hidden"); 377 if(hidden) Zotero.File.createDirectoryIfMissing(destFileHidden); 378 destFileHidden.append(fileName); 379 380 // look for an existing style with the same styleID or filename 381 var existingTitle; 382 if(_styles[styleID]) { 383 existingFile = _styles[styleID].file; 384 existingTitle = _styles[styleID].title; 385 } else { 386 if(destFile.exists()) { 387 existingFile = destFile; 388 } else if(destFileHidden.exists()) { 389 existingFile = destFileHidden; 390 } 391 392 if(existingFile) { 393 // find associated style 394 for (let existingStyle of _styles) { 395 if(destFile.equals(existingStyle.file)) { 396 existingTitle = existingStyle.title; 397 break; 398 } 399 } 400 } 401 } 402 403 // also look for an existing style with the same title 404 if(!existingFile) { 405 let styles = Zotero.Styles.getAll(); 406 for (let i in styles) { 407 let existingStyle = styles[i]; 408 if(title === existingStyle.title) { 409 existingFile = existingStyle.file; 410 existingTitle = existingStyle.title; 411 break; 412 } 413 } 414 } 415 416 // display a dialog to tell the user we're about to install the style 417 if(hidden) { 418 destFile = destFileHidden; 419 } else if (!silent) { 420 if(existingTitle) { 421 var text = Zotero.getString('styles.updateStyle', [existingTitle, title, origin]); 422 } else { 423 var text = Zotero.getString('styles.installStyle', [title, origin]); 424 } 425 426 var index = Services.prompt.confirmEx(null, Zotero.getString('styles.install.title'), 427 text, 428 ((Services.prompt.BUTTON_POS_0) * (Services.prompt.BUTTON_TITLE_IS_STRING) 429 + (Services.prompt.BUTTON_POS_1) * (Services.prompt.BUTTON_TITLE_CANCEL)), 430 Zotero.getString('general.install'), null, null, null, {} 431 ); 432 433 if(index !== 0) { 434 throw new Zotero.Exception.UserCancelled("style installation"); 435 } 436 } 437 438 yield Zotero.Styles.validate(style) 439 .catch(function(validationErrors) { 440 Zotero.logError("Style from " + origin + " failed to validate:\n\n" + validationErrors); 441 442 // If validation fails on the parent of a dependent style, ignore it (for now) 443 if(hidden) return; 444 445 // If validation fails on a different style, we ask the user if s/he really 446 // wants to install it 447 Components.utils.import("resource://gre/modules/Services.jsm"); 448 var shouldInstall = Services.prompt.confirmEx(null, 449 Zotero.getString('styles.install.title'), 450 Zotero.getString('styles.validationWarning', origin), 451 (Services.prompt.BUTTON_POS_0) * (Services.prompt.BUTTON_TITLE_OK) 452 + (Services.prompt.BUTTON_POS_1) * (Services.prompt.BUTTON_TITLE_CANCEL) 453 + Services.prompt.BUTTON_POS_1_DEFAULT + Services.prompt.BUTTON_DELAY_ENABLE, 454 null, null, null, null, {} 455 ); 456 if(shouldInstall !== 0) { 457 throw new Zotero.Exception.UserCancelled("style installation"); 458 } 459 }); 460 461 // User wants to install/update 462 if(source && !_styles[source]) { 463 // Need to fetch source 464 if(source.substr(0, 7) === "http://" || source.substr(0, 8) === "https://") { 465 try { 466 let xmlhttp = yield Zotero.HTTP.request("GET", source); 467 yield _install(xmlhttp.responseText, origin, true); 468 } 469 catch (e) { 470 if (typeof e === "object" && e instanceof Zotero.Exception.Alert) { 471 throw new Zotero.Exception.Alert( 472 "styles.installSourceError", 473 [origin, source], 474 "styles.install.title", 475 e 476 ); 477 } 478 throw e; 479 } 480 } else { 481 throw new Zotero.Exception.Alert("styles.installSourceError", [origin, source], 482 "styles.install.title", "Source CSL URI is invalid"); 483 } 484 } 485 486 // Dependent style has been retrieved if there was one, so we're ready to 487 // continue 488 489 // Remove any existing file with a different name 490 if(existingFile) existingFile.remove(false); 491 492 yield Zotero.File.putContentsAsync(destFile, style); 493 494 yield Zotero.Styles.reinit(); 495 496 // Refresh preferences windows 497 var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]. 498 getService(Components.interfaces.nsIWindowMediator); 499 var enumerator = wm.getEnumerator("zotero:pref"); 500 while(enumerator.hasMoreElements()) { 501 var win = enumerator.getNext(); 502 if(win.Zotero_Preferences.Cite) { 503 yield win.Zotero_Preferences.Cite.refreshStylesList(styleID); 504 } 505 } 506 return existingTitle || title; 507 }); 508 509 /** 510 * Populate menulist with locales 511 * 512 * @param {xul:menulist} menulist 513 * @return {Promise} 514 */ 515 this.populateLocaleList = function (menulist) { 516 if (!_initialized) { 517 throw new Zotero.Exception.UnloadedDataException("Styles not yet loaded", 'styles'); 518 } 519 520 // Reset menulist 521 menulist.selectedItem = null; 522 menulist.removeAllItems(); 523 524 let fallbackLocale = Zotero.Styles.primaryDialects[Zotero.locale] 525 || Zotero.locale; 526 527 let menuLocales = Zotero.Utilities.deepCopy(Zotero.Styles.locales); 528 let menuLocalesKeys = Object.keys(menuLocales).sort(); 529 530 // Make sure that client locale is always available as a choice 531 if (fallbackLocale && !(fallbackLocale in menuLocales)) { 532 menuLocales[fallbackLocale] = fallbackLocale; 533 menuLocalesKeys.unshift(fallbackLocale); 534 } 535 536 for (let i=0; i<menuLocalesKeys.length; i++) { 537 menulist.appendItem(menuLocales[menuLocalesKeys[i]], menuLocalesKeys[i]); 538 } 539 } 540 541 /** 542 * Update locale list state based on style selection. 543 * For styles that do not define a locale, enable the list and select a 544 * preferred locale. 545 * For styles that define a locale, disable the list and select the 546 * specified locale. If the locale does not exist, it is added to the list. 547 * If null is passed instead of style, the list and its label are disabled, 548 * and set to blank value. 549 * 550 * Note: Do not call this function synchronously immediately after 551 * populateLocaleList. The menulist items are added, but the values are not 552 * yet set. 553 * 554 * @param {xul:menulist} menulist Menulist object that will be manipulated 555 * @param {Zotero.Style} style Currently selected style 556 * @param {String} prefLocale Preferred locale if not overridden by the style 557 * 558 * @return {String} The locale that was selected 559 */ 560 this.updateLocaleList = function (menulist, style, prefLocale) { 561 if (!_initialized) { 562 throw new Zotero.Exception.UnloadedDataException("Styles not yet loaded", 'styles'); 563 } 564 565 // Remove any nodes that were manually added to menulist 566 let availableLocales = []; 567 for (let i=0; i<menulist.itemCount; i++) { 568 let item = menulist.getItemAtIndex(i); 569 if (item.getAttributeNS('zotero:', 'customLocale')) { 570 menulist.removeItemAt(i); 571 i--; 572 continue; 573 } 574 575 availableLocales.push(item.value); 576 } 577 578 if (!style) { 579 // disable menulist and label 580 menulist.disabled = true; 581 if (menulist.labelElement) menulist.labelElement.disabled = true; 582 583 // set node to blank node 584 // If we just set value to "", the internal label is collapsed and the dropdown list becomes shorter 585 let blankListNode = menulist.appendItem('', ''); 586 blankListNode.setAttributeNS('zotero:', 'customLocale', true); 587 588 menulist.selectedItem = blankListNode; 589 return menulist.value; 590 } 591 592 menulist.disabled = !!style.locale; 593 if (menulist.labelElement) menulist.labelElement.disabled = false; 594 595 let selectLocale = style.locale || prefLocale || Zotero.locale; 596 selectLocale = Zotero.Styles.primaryDialects[selectLocale] || selectLocale; 597 598 // Make sure the locale we want to select is in the menulist 599 if (availableLocales.indexOf(selectLocale) == -1) { 600 let customLocale = menulist.insertItemAt(0, selectLocale, selectLocale); 601 customLocale.setAttributeNS('zotero:', 'customLocale', true); 602 } 603 604 return menulist.value = selectLocale; 605 } 606 } 607 608 /** 609 * @class Represents a style file and its metadata 610 * @property {String} path The path to the style file 611 * @property {String} fileName The name of the style file 612 * @property {String} styleID 613 * @property {String} url The URL where the style can be found (rel="self") 614 * @property {String} type "csl" for CSL styles 615 * @property {String} title 616 * @property {String} updated SQL-style date updated 617 * @property {String} class "in-text" or "note" 618 * @property {String} source The CSL that contains the formatting information for this one, or null 619 * if this CSL contains formatting information 620 * @property {Zotero.CSL} csl The Zotero.CSL object used to format using this style 621 * @property {Boolean} hidden True if this style is hidden in style selection dialogs, false if it 622 * is not 623 */ 624 Zotero.Style = function (style, path) { 625 if (typeof style != "string") { 626 throw new Error("Style code must be a string"); 627 } 628 629 this.type = "csl"; 630 631 var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] 632 .createInstance(Components.interfaces.nsIDOMParser), 633 doc = parser.parseFromString(style, "application/xml"); 634 if(doc.documentElement.localName === "parsererror") { 635 throw new Error("File is not valid XML"); 636 } 637 638 if (path) { 639 this.path = path; 640 this.fileName = OS.Path.basename(path); 641 } 642 else { 643 this.string = style; 644 } 645 646 this.styleID = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:id[1]', 647 Zotero.Styles.ns); 648 this.url = Zotero.Utilities.xpathText(doc, 649 '/csl:style/csl:info[1]/csl:link[@rel="self"][1]/@href', 650 Zotero.Styles.ns); 651 this.title = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:title[1]', 652 Zotero.Styles.ns); 653 this.updated = Zotero.Utilities.xpathText(doc, '/csl:style/csl:info[1]/csl:updated[1]', 654 Zotero.Styles.ns).replace(/(.+)T([^\+]+)\+?.*/, "$1 $2"); 655 this.locale = Zotero.Utilities.xpathText(doc, '/csl:style/@default-locale', 656 Zotero.Styles.ns) || null; 657 this._class = doc.documentElement.getAttribute("class"); 658 this._usesAbbreviation = !!Zotero.Utilities.xpath(doc, 659 '//csl:text[(@variable="container-title" and @form="short") or (@variable="container-title-short")][1]', 660 Zotero.Styles.ns).length; 661 this._hasBibliography = !!doc.getElementsByTagName("bibliography").length; 662 this._version = doc.documentElement.getAttribute("version"); 663 if(!this._version) { 664 this._version = "0.8"; 665 666 //In CSL 0.8.1, the "term" attribute on cs:category stored both 667 //citation formats and fields. 668 this.categories = Zotero.Utilities.xpath( 669 doc, '/csl:style/csl:info[1]/csl:category', Zotero.Styles.ns) 670 .filter(category => category.hasAttribute("term")) 671 .map(category => category.getAttribute("term")); 672 } else { 673 //CSL 1.0 introduced a dedicated "citation-format" attribute on cs:category 674 this.categories = Zotero.Utilities.xpathText(doc, 675 '/csl:style/csl:info[1]/csl:category[@citation-format][1]/@citation-format', 676 Zotero.Styles.ns); 677 } 678 679 this.source = Zotero.Utilities.xpathText(doc, 680 '/csl:style/csl:info[1]/csl:link[@rel="source" or @rel="independent-parent"][1]/@href', 681 Zotero.Styles.ns); 682 if(this.source === this.styleID) { 683 throw "Style with ID "+this.styleID+" references itself as source"; 684 } 685 } 686 687 /** 688 * Get a citeproc-js CSL.Engine instance 689 * @param {String} locale Locale code 690 * @param {Boolean} automaticJournalAbbreviations Whether to automatically abbreviate titles 691 */ 692 Zotero.Style.prototype.getCiteProc = function(locale, automaticJournalAbbreviations) { 693 if(!locale) { 694 var locale = Zotero.Prefs.get('export.lastLocale') || Zotero.locale; 695 if(!locale) { 696 var locale = 'en-US'; 697 } 698 } 699 700 // determine version of parent style 701 var overrideLocale = false; // to force dependent style locale 702 if(this.source) { 703 var parentStyle = Zotero.Styles.get(this.source); 704 if(!parentStyle) { 705 throw new Error( 706 'Style references ' + this.source + ', but this style is not installed', 707 Zotero.Utilities.pathToFileURI(this.path) 708 ); 709 } 710 var version = parentStyle._version; 711 712 // citeproc-js will not know anything about the dependent style, including 713 // the default-locale, so we need to force locale if a dependent style 714 // contains one 715 if(this.locale) { 716 overrideLocale = true; 717 locale = this.locale; 718 } 719 } else { 720 var version = this._version; 721 } 722 723 if(version === "0.8") { 724 // get XSLT processor from updateCSL.xsl file 725 if(!Zotero.Styles.xsltProcessor) { 726 let protHandler = Components.classes["@mozilla.org/network/protocol;1?name=chrome"] 727 .createInstance(Components.interfaces.nsIProtocolHandler); 728 let channel = protHandler.newChannel(protHandler.newURI("chrome://zotero/content/updateCSL.xsl", "UTF-8", null)); 729 let updateXSLT = Components.classes["@mozilla.org/xmlextras/domparser;1"] 730 .createInstance(Components.interfaces.nsIDOMParser) 731 .parseFromStream(channel.open(), "UTF-8", channel.contentLength, "application/xml"); 732 733 // load XSLT file into XSLTProcessor 734 Zotero.Styles.xsltProcessor = Components.classes["@mozilla.org/document-transformer;1?type=xslt"] 735 .createInstance(Components.interfaces.nsIXSLTProcessor); 736 Zotero.Styles.xsltProcessor.importStylesheet(updateXSLT); 737 } 738 739 // read style file as DOM XML 740 let styleDOMXML = Components.classes["@mozilla.org/xmlextras/domparser;1"] 741 .createInstance(Components.interfaces.nsIDOMParser) 742 .parseFromString(this.getXML(), "text/xml"); 743 744 // apply XSLT and serialize output 745 let newDOMXML = Zotero.Styles.xsltProcessor.transformToDocument(styleDOMXML); 746 var xml = Components.classes["@mozilla.org/xmlextras/xmlserializer;1"] 747 .createInstance(Components.interfaces.nsIDOMSerializer).serializeToString(newDOMXML); 748 } else { 749 var xml = this.getXML(); 750 } 751 752 try { 753 var citeproc = new Zotero.CiteProc.CSL.Engine( 754 new Zotero.Cite.System(automaticJournalAbbreviations), 755 xml, 756 locale, 757 overrideLocale 758 ); 759 760 citeproc.opt.development_extensions.wrap_url_and_doi = true; 761 // Don't try to parse author names. We parse them in itemToCSLJSON 762 citeproc.opt.development_extensions.parse_names = false; 763 764 return citeproc; 765 } catch(e) { 766 Zotero.logError(e); 767 throw e; 768 } 769 }; 770 771 Zotero.Style.prototype.__defineGetter__("class", 772 /** 773 * Retrieves the style class, either from the metadata that's already loaded or by loading the file 774 * @type String 775 */ 776 function() { 777 if(this.source) { 778 // use class from source style 779 var parentStyle = Zotero.Styles.get(this.source); 780 if(!parentStyle) { 781 throw new Error('Style references missing parent ' + this.source); 782 } 783 return parentStyle.class; 784 } 785 return this._class; 786 }); 787 788 Zotero.Style.prototype.__defineGetter__("hasBibliography", 789 /** 790 * Determines whether or not this style has a bibliography, either from the metadata that's already\ 791 * loaded or by loading the file 792 * @type String 793 */ 794 function() { 795 if(this.source) { 796 // use hasBibliography from source style 797 var parentStyle = Zotero.Styles.get(this.source); 798 if(!parentStyle) { 799 throw new Error('Style references missing parent ' + this.source); 800 } 801 return parentStyle.hasBibliography; 802 } 803 return this._hasBibliography; 804 }); 805 806 Zotero.Style.prototype.__defineGetter__("usesAbbreviation", 807 /** 808 * Retrieves the style class, either from the metadata that's already loaded or by loading the file 809 * @type String 810 */ 811 function() { 812 if(this.source) { 813 var parentStyle = Zotero.Styles.get(this.source); 814 if(!parentStyle) return false; 815 return parentStyle.usesAbbreviation; 816 } 817 return this._usesAbbreviation; 818 }); 819 820 Zotero.Style.prototype.__defineGetter__("independentFile", 821 /** 822 * Retrieves the file corresponding to the independent CSL 823 * (the parent if this style is dependent, or this style if it is not) 824 */ 825 function() { 826 if(this.source) { 827 // parent/child 828 var formatCSL = Zotero.Styles.get(this.source); 829 if(!formatCSL) { 830 throw new Error('Style references missing parent ' + this.source); 831 } 832 return formatCSL.path; 833 } else if (this.path) { 834 return this.path; 835 } 836 return null; 837 }); 838 839 /** 840 * Retrieves the XML corresponding to this style 841 * @type String 842 */ 843 Zotero.Style.prototype.getXML = function() { 844 var indepFile = this.independentFile; 845 if(indepFile) return Zotero.File.getContents(indepFile); 846 return this.string; 847 }; 848 849 /** 850 * Deletes a style 851 */ 852 Zotero.Style.prototype.remove = Zotero.Promise.coroutine(function* () { 853 if (!this.path) { 854 throw new Error("Cannot delete a style with no associated file") 855 } 856 857 // make sure no styles depend on this one 858 var dependentStyles = false; 859 var styles = Zotero.Styles.getAll(); 860 for (let i in styles) { 861 let style = styles[i]; 862 if(style.source == this.styleID) { 863 dependentStyles = true; 864 break; 865 } 866 } 867 868 if(dependentStyles) { 869 // copy dependent styles to hidden directory 870 let hiddenDir = OS.Path.join(Zotero.getStylesDirectory().path, 'hidden'); 871 yield Zotero.File.createDirectoryIfMissingAsync(hiddenDir); 872 yield OS.File.move(this.path, OS.Path.join(hiddenDir, OS.Path.basename(this.path))); 873 } else { 874 // remove defunct files 875 yield OS.File.remove(this.path); 876 } 877 878 // check to see if this style depended on a hidden one 879 if(this.source) { 880 var source = Zotero.Styles.get(this.source); 881 if(source && source.hidden) { 882 var deleteSource = true; 883 884 // check to see if any other styles depend on the hidden one 885 let styles = Zotero.Styles.getAll(); 886 for (let i in styles) { 887 let style = styles[i]; 888 if(style.source == this.source && style.styleID != this.styleID) { 889 deleteSource = false; 890 break; 891 } 892 } 893 894 // if it was only this style with the dependency, delete the source 895 if(deleteSource) { 896 yield source.remove(); 897 } 898 } 899 } 900 901 return Zotero.Styles.reinit(); 902 });