www

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

quickCopy.js (15950B)


      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 "use strict";
     27 
     28 Zotero.QuickCopy = new function() {
     29 	this.lastActiveURL = null;
     30 	
     31 	var _initTimeoutID
     32 	var _initPromise;
     33 	var _initialized = false;
     34 	var _initCancelled = false;
     35 	var _siteSettings;
     36 	var _formattedNames;
     37 	
     38 	this.init = Zotero.Promise.coroutine(function* () {
     39 		Zotero.debug("Initializing Quick Copy");
     40 		
     41 		if (!_initialized) {
     42 			// Make sure export translator code is loaded whenever the output format changes
     43 			Zotero.Prefs.registerObserver("export.quickCopy.setting", _loadOutputFormat);
     44 			_initialized = true;
     45 		}
     46 		
     47 		// Load code for selected export translators ahead of time
     48 		// (in the background, because it requires translator initialization)
     49 		Zotero.Schema.schemaUpdatePromise
     50 		.then(function () {
     51 			if (_initCancelled) return;
     52 			
     53 			// Avoid random translator initialization during tests, which can result in timeouts,
     54 			// if an export format is selected
     55 			if (Zotero.test) return;
     56 			
     57 			_initPromise = Zotero.Promise.each(
     58 				[
     59 					() => _loadOutputFormat(),
     60 					() => this.loadSiteSettings()
     61 				],
     62 				f => f()
     63 			);
     64 		}.bind(this));
     65 	});
     66 	
     67 	
     68 	this.uninit = function () {
     69 		_initCancelled = true;
     70 		// Cancel load if in progress
     71 		if (_initPromise) {
     72 			_initPromise.cancel();
     73 		}
     74 		Zotero.Prefs.unregisterObserver("export.quickCopy.setting", _loadOutputFormat);
     75 	};
     76 	
     77 	
     78 	this.loadSiteSettings = Zotero.Promise.coroutine(function* () {
     79 		var sql = "SELECT key AS domainPath, value AS format FROM settings "
     80 			+ "WHERE setting='quickCopySite'";
     81 		var rows = yield Zotero.DB.queryAsync(sql);
     82 		// Unproxify storage row
     83 		_siteSettings = rows.map(row => {
     84 			return {
     85 				domainPath: row.domainPath,
     86 				format: row.format 
     87 			};
     88 		});
     89 		yield Zotero.Promise.map(rows, row => _preloadFormat(row.format));
     90 	});
     91 	
     92 	
     93 	this.hasSiteSettings = function () {
     94 		return _siteSettings && _siteSettings.length > 0;
     95 	};
     96 	
     97 	
     98 	/*
     99 	 * Return Quick Copy setting object from string, stringified object, or object
    100 	 * 
    101 	 * Example string format: "bibliography/html=http://www.zotero.org/styles/apa"
    102 	 *
    103 	 * Quick Copy setting object has the following properties:
    104 	 * - "mode": "bibliography" (for styles) or "export" (for export translators)
    105 	 * - "contentType: "" (plain text output) or "html" (HTML output; for styles
    106 	 *   only)
    107 	 * - "id": style ID or export translator ID
    108 	 * - "locale": locale code (for styles only)
    109 	 */
    110 	this.unserializeSetting = function (setting) {
    111 		var settingObject = {};
    112 		
    113 		if (typeof setting === 'string') {
    114 			try {
    115 				// First test if string input is a stringified object
    116 				settingObject = JSON.parse(setting);
    117 			} catch (e) {
    118 				// Try parsing as formatted string
    119 				var parsedSetting = setting.match(/(bibliography|export)(?:\/([^=]+))?=(.+)$/);
    120 				if (parsedSetting) {
    121 					settingObject.mode = parsedSetting[1];
    122 					settingObject.contentType = parsedSetting[2] || '';
    123 					settingObject.id = parsedSetting[3];
    124 					settingObject.locale = '';
    125 				}
    126 			}
    127 		} else {
    128 			// Return input if not a string; it might already be an object
    129 			return setting;
    130 		}
    131 		
    132 		return settingObject;
    133 	};
    134 	
    135 	
    136 	this.getFormattedNameFromSetting = Zotero.Promise.coroutine(function* (setting) {
    137 		if (!_formattedNames) {
    138 			yield _loadFormattedNames();
    139 		}
    140 		var format = this.unserializeSetting(setting);
    141 		
    142 		var name = _formattedNames[format.mode + "=" + format.id];
    143 		return name ? name : '';
    144 	});
    145 	
    146 	this.getSettingFromFormattedName = Zotero.Promise.coroutine(function* (name) {
    147 		if (!_formattedNames) {
    148 			yield _loadFormattedNames();
    149 		}
    150 		for (var setting in _formattedNames) {
    151 			if (_formattedNames[setting] == name) {
    152 				return setting;
    153 			}
    154 		}
    155 		return '';
    156 	});
    157 	
    158 	
    159 	this.getFormatFromURL = function(url) {
    160 		var quickCopyPref = Zotero.Prefs.get("export.quickCopy.setting");
    161 		quickCopyPref = JSON.stringify(this.unserializeSetting(quickCopyPref));
    162 		
    163 		if (!url) {
    164 			return quickCopyPref;
    165 		}
    166 		
    167 		var ioService = Components.classes["@mozilla.org/network/io-service;1"]
    168 			.getService(Components.interfaces.nsIIOService);
    169 		var nsIURI;
    170 		try {
    171 			nsIURI = ioService.newURI(url, null, null);
    172 			// Accessing some properties may throw for URIs that do not support those
    173 			// parts. E.g. hostPort throws NS_ERROR_FAILURE for about:blank
    174 			var urlHostPort = nsIURI.hostPort;
    175 			var urlPath = nsIURI.path;
    176 		}
    177 		catch (e) {}
    178 		
    179 		// Skip non-HTTP URLs
    180 		if (!nsIURI || !/^https?$/.test(nsIURI.scheme)) {
    181 			return quickCopyPref;
    182 		}
    183 		
    184 		if (!_siteSettings) {
    185 			throw new Zotero.Exception.UnloadedDataException("Quick Copy site settings not loaded");
    186 		}
    187 		
    188 		var matches = [];
    189 		for (let i=0; i<_siteSettings.length; i++) {
    190 			let row = _siteSettings[i];
    191 			let domain = row.domainPath.split('/',1)[0];
    192 			let path = row.domainPath.substr(domain.length) || '/';
    193 			if (urlHostPort.endsWith(domain) && urlPath.startsWith(path)) {
    194 				matches.push({
    195 					format: JSON.stringify(this.unserializeSetting(row.format)),
    196 					domainLength: domain.length,
    197 					pathLength: path.length
    198 				});
    199 			}
    200 		}
    201 		
    202 		// Give priority to longer domains, then longer paths
    203 		var sort = function(a, b) {
    204 			if (a.domainLength > b.domainLength) {
    205 				return -1;
    206 			}
    207 			else if (a.domainLength < b.domainLength) {
    208 				return 1;
    209 			}
    210 			
    211 			if (a.pathLength > b.pathLength) {
    212 				return -1;
    213 			}
    214 			else if (a.pathLength < b.pathLength) {
    215 				return 1;
    216 			}
    217 			
    218 			return -1;
    219 		};
    220 		
    221 		if (matches.length) {
    222 			matches.sort(sort);
    223 			return matches[0].format;
    224 		} else {
    225 			return quickCopyPref;
    226 		}
    227 	};
    228 	
    229 	
    230 	/*
    231 	 * Get text and (when applicable) HTML content from items
    232 	 *
    233 	 * |items| is an array of Zotero.Item objects
    234 	 *
    235 	 * |format| may be a Quick Copy format string
    236 	 * (e.g. "bibliography=http://www.zotero.org/styles/apa")
    237 	 * or an Quick Copy format object
    238 	 *
    239 	 * |callback| is only necessary if using an export format and should be
    240 	 * a function suitable for Zotero.Translate.setHandler, taking parameters
    241 	 * |obj| and |worked|. The generated content should be placed in obj.string
    242 	 * and |worked| should be true if the operation is successful.
    243 	 *
    244 	 * If bibliography format, the process is synchronous and an object
    245 	 * contain properties 'text' and 'html' is returned.
    246 	 */
    247 	this.getContentFromItems = function (items, format, callback, modified) {
    248 		if (items.length > Zotero.Prefs.get('export.quickCopy.dragLimit')) {
    249 			Zotero.debug("Skipping quick copy for " + items.length + " items");
    250 			return false;
    251 		}
    252 		
    253 		format = this.unserializeSetting(format);
    254 		
    255 		if (format.mode == 'export') {
    256 			var translation = new Zotero.Translate.Export;
    257 			translation.noWait = true;	// needed not to break drags
    258 			translation.setItems(items);
    259 			translation.setTranslator(format.id);
    260 			translation.setHandler("done", callback);
    261 			translation.translate();
    262 			return true;
    263 		}
    264 		else if (format.mode == 'bibliography') {
    265 			// Move notes to separate array
    266 			var allNotes = true;
    267 			var notes = [];
    268 			for (var i=0; i<items.length; i++) {
    269 				if (items[i].isNote()) {
    270 					notes.push(items.splice(i, 1)[0]);
    271 					i--;
    272 				}
    273 				else {
    274 					allNotes = false;
    275 				}
    276 			}
    277 			
    278 			// If all notes, export full content
    279 			if (allNotes) {
    280 				var content = [],
    281 					parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
    282 						.createInstance(Components.interfaces.nsIDOMParser),
    283 					doc = parser.parseFromString('<div class="zotero-notes"/>', 'text/html'),
    284 					textDoc = parser.parseFromString('<div class="zotero-notes"/>', 'text/html'),
    285 					container = doc.documentElement,
    286 					textContainer = textDoc.documentElement;
    287 				for (var i=0; i<notes.length; i++) {
    288 					var div = doc.createElement("div");
    289 					div.className = "zotero-note";
    290 					// AMO reviewer: This documented is never rendered (and the inserted markup
    291 					// is sanitized anyway)
    292 					div.insertAdjacentHTML('afterbegin', notes[i].getNote());
    293 					container.appendChild(div);
    294 					textContainer.appendChild(textDoc.importNode(div, true));
    295 				}
    296 				
    297 				// Raw HTML output
    298 				var html = container.outerHTML;
    299 				
    300 				// Add placeholders for newlines between notes
    301 				if (notes.length > 1) {
    302 					var divs = Zotero.Utilities.xpath(container, "div"),
    303 						textDivs = Zotero.Utilities.xpath(textContainer, "div");
    304 					for (var i=1, len=divs.length; i<len; i++) {
    305 						var p = doc.createElement("p");
    306 						p.appendChild(doc.createTextNode("--------------------------------------------------"));
    307 						container.insertBefore(p, divs[i]);
    308 						textContainer.insertBefore(textDoc.importNode(p, true), textDivs[i]);
    309 					}
    310 				}
    311 				
    312 				const BLOCKQUOTE_PREFS = {
    313 					'export.quickCopy.quoteBlockquotes.richText':doc,
    314 					'export.quickCopy.quoteBlockquotes.plainText':textDoc
    315 				};
    316 				for(var pref in BLOCKQUOTE_PREFS) {
    317 					if (Zotero.Prefs.get(pref)) {
    318 						var currentDoc = BLOCKQUOTE_PREFS[pref];
    319 						// Add quotes around blockquote paragraphs
    320 						var addOpenQuote = Zotero.Utilities.xpath(currentDoc, "//blockquote/p[1]"),
    321 							addCloseQuote = Zotero.Utilities.xpath(currentDoc, "//blockquote/p[last()]");
    322 						for(var i=0; i<addOpenQuote.length; i++) {
    323 							addOpenQuote[i].insertBefore(currentDoc.createTextNode("\u201c"),
    324 								addOpenQuote[i].firstChild);
    325 						}
    326 						for(var i=0; i<addCloseQuote.length; i++) {
    327 							addCloseQuote[i].appendChild(currentDoc.createTextNode("\u201d"));
    328 						}
    329 					}
    330 				}
    331 				
    332 				//
    333 				// Text-only adjustments
    334 				//
    335 				
    336 				// Replace span styles with characters
    337 				var spans = textDoc.getElementsByTagName("span");
    338 				for(var i=0; i<spans.length; i++) {
    339 					var span = spans[i];
    340 					if(span.style.textDecoration == "underline") {
    341 						span.insertBefore(textDoc.createTextNode("_"), span.firstChild);
    342 						span.appendChild(textDoc.createTextNode("_"));
    343 					}
    344 				}
    345 				
    346 				//
    347 				// And add spaces for indents
    348 				//
    349 				// Placeholder for 4 spaces in final output
    350 				const ZTAB = "%%ZOTEROTAB%%";
    351 				var ps = textDoc.getElementsByTagName("p");
    352 				for(var i=0; i<ps.length; i++) {
    353 					var p = ps[i],
    354 						paddingLeft = p.style.paddingLeft;
    355 					if(paddingLeft && paddingLeft.substr(paddingLeft.length-2) === "px") {
    356 						var paddingPx = parseInt(paddingLeft, 10),
    357 							ztabs = "";
    358 						for (let j = 30; j <= paddingPx; j += 30) ztabs += ZTAB;
    359 						p.insertBefore(textDoc.createTextNode(ztabs), p.firstChild);
    360 					}
    361 				}
    362 				
    363 				// Use plaintext serializer to output formatted text
    364 				var docEncoder = Components.classes["@mozilla.org/layout/documentEncoder;1?type=text/html"]
    365 					.createInstance(Components.interfaces.nsIDocumentEncoder);
    366 				docEncoder.init(textDoc, "text/plain", docEncoder.OutputFormatted);
    367 				var text = docEncoder.encodeToString().trim().replace(ZTAB, "    ", "g");
    368 				
    369 				//
    370 				// Adjustments for the HTML copied to the clipboard
    371 				//
    372 				
    373 				// Everything seems to like margin-left better than padding-left
    374 				var ps = Zotero.Utilities.xpath(doc, "p");
    375 				for(var i=0; i<ps.length; i++) {
    376 					var p = ps[i];
    377 					if(p.style.paddingLeft) {
    378 						p.style.marginLeft = p.style.paddingLeft;
    379 						p.style.paddingLeft = "";
    380 					}
    381 				}
    382 				
    383 				// Word and TextEdit don't indent blockquotes on their own and need this
    384 				//
    385 				// OO gets it right, so this results in an extra indent
    386 				if (Zotero.Prefs.get('export.quickCopy.compatibility.indentBlockquotes')) {
    387 					var ps = Zotero.Utilities.xpath(doc, "//blockquote/p");
    388 					for(var i=0; i<ps.length; i++) ps[i].style.marginLeft = "30px";
    389 				}
    390 				
    391 				// Add Word Normal style to paragraphs and add double-spacing
    392 				//
    393 				// OO inserts the conditional style code as a document comment
    394 				if (Zotero.Prefs.get('export.quickCopy.compatibility.word')) {
    395 					var ps = doc.getElementsByTagName("p");
    396 					for (var i=0; i<ps.length; i++) ps[i].className = "msoNormal";
    397 					var copyHTML = "<!--[if gte mso 0]>"
    398 									+ "<style>"
    399 									+ "p { margin-top:.1pt;margin-right:0in;margin-bottom:.1pt;margin-left:0in; line-height: 200%; }"
    400 									+ "li { margin-top:.1pt;margin-right:0in;margin-bottom:.1pt;margin-left:0in; line-height: 200%; }"
    401 									+ "blockquote p { margin-left: 11px; margin-right: 11px }"
    402 									+ "</style>"
    403 									+ "<![endif]-->\n"
    404 									+ container.outerHTML;
    405 				}
    406 				else {
    407 					var copyHTML = container.outerHTML;
    408 				}
    409 				
    410 				var content = {
    411 					text: format.contentType == "html" ? html : text,
    412 					html: copyHTML
    413 				};
    414 				
    415 				return content;
    416 			}
    417 			
    418 			// determine locale preference
    419 			var locale = format.locale ? format.locale : Zotero.Prefs.get('export.quickCopy.locale');
    420 			
    421 			// Copy citations if shift key pressed
    422 			if (modified) {
    423 				var csl = Zotero.Styles.get(format.id).getCiteProc(locale);
    424 				csl.updateItems(items.map(item => item.id));
    425 				var citation = {
    426 					citationItems: items.map(item => ({ id: item.id })),
    427 					properties: {}
    428 				};
    429 				var html = csl.previewCitationCluster(citation, [], [], "html"); 
    430 				var text = csl.previewCitationCluster(citation, [], [], "text"); 
    431 			} else {
    432 				var style = Zotero.Styles.get(format.id);
    433 				var cslEngine = style.getCiteProc(locale);
    434  				var html = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, "html");
    435 				cslEngine = style.getCiteProc(locale);
    436 				var text = Zotero.Cite.makeFormattedBibliographyOrCitationList(cslEngine, items, "text");
    437 			}
    438 			
    439 			return {text:(format.contentType == "html" ? html : text), html:html};
    440 		}
    441 		
    442 		throw ("Invalid mode '" + format.mode + "' in Zotero.QuickCopy.getContentFromItems()");
    443 	};
    444 	
    445 	
    446 	/**
    447 	 * If an export translator is the selected output format, load its code (which must be done
    448 	 * asynchronously) ahead of time, since drag-and-drop requires synchronous operation
    449 	 *
    450 	 * @return {Promise}
    451 	 */
    452 	var _loadOutputFormat = Zotero.Promise.coroutine(function* () {
    453 		var format = Zotero.Prefs.get("export.quickCopy.setting");
    454 		return _preloadFormat(format);
    455 	});
    456 	
    457 	
    458 	var _preloadFormat = async function (format) {
    459 		format = Zotero.QuickCopy.unserializeSetting(format);
    460 		if (format.mode == 'export') {
    461 			Zotero.debug(`Preloading ${format.id} for Quick Copy`);
    462 			await Zotero.Translators.init();
    463 			let translator = Zotero.Translators.get(format.id);
    464 			translator.cacheCode = true;
    465 			await translator.getCode();
    466 		}
    467 	};
    468 	
    469 	
    470 	var _loadFormattedNames = Zotero.Promise.coroutine(function* () {
    471 		var t = new Date;
    472 		Zotero.debug("Loading formatted names for Quick Copy");
    473 		
    474 		var translation = new Zotero.Translate.Export;
    475 		var translators = yield translation.getTranslators();
    476 		
    477 		// add styles to list
    478 		_formattedNames = {};
    479 		var styles = Zotero.Styles.getVisible();
    480 		for (let style of styles) {
    481 			_formattedNames['bibliography=' + style.styleID] = style.title;
    482 		}
    483 		
    484 		for (var i=0; i<translators.length; i++) {
    485 			// Skip RDF formats
    486 			switch (translators[i].translatorID) {
    487 				case '6e372642-ed9d-4934-b5d1-c11ac758ebb7':
    488 				case '14763d24-8ba0-45df-8f52-b8d1108e7ac9':
    489 					continue;
    490 			}
    491 			_formattedNames['export=' + translators[i].translatorID] = translators[i].label;
    492 		}
    493 		
    494 		Zotero.debug("Loaded formatted names for Quick Copy in " + (new Date - t) + " ms");
    495 	});
    496 }