www

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | Submodules | README | LICENSE

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 });