www

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

commit bf8e4eae285471fdd0a8bd0be10451b2d1d68141
parent 0769e0e1f885d7592b8e28d86b29eed6f51c572b
Author: Simon Kornblith <simon@simonster.com>
Date:   Tue, 16 Sep 2008 19:14:52 +0000

- implement Zotero.Styles.install, refine Zotero.Style.delete, and restore functionality to Styles prefpane
- allow deletion of multiple styles simultaneously
- split Zotero.Styles/Zotero.Style and Zotero.CSL into style.js and csl.js respectively
- add Zotero.File.getBinaryContents for binary-safe file reading
- add Zotero.MIMETypeHandler to provide a unified interface for registering observers and capturing MIME types with Zotero


Diffstat:
Mchrome/content/zotero/preferences/preferences.js | 96++++++++++++++++++++++++++++++++++---------------------------------------------
Dchrome/content/zotero/xpcom/cite.js | 3278-------------------------------------------------------------------------------
Achrome/content/zotero/xpcom/csl.js | 2942+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/file.js | 16++++++++++++++++
Mchrome/content/zotero/xpcom/ingester.js | 208++++++++++++-------------------------------------------------------------------
Achrome/content/zotero/xpcom/mimeTypeHandler.js | 221+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/proxy.js | 136+++++++++++++++++++++++++++++++++----------------------------------------------
Achrome/content/zotero/xpcom/style.js | 474+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/zotero.js | 3+--
Mchrome/locale/en-US/zotero/zotero.properties | 6+++---
Mchrome/skin/default/zotero/preferences.css | 5+++++
Mcomponents/zotero-service.js | 4+++-
12 files changed, 3794 insertions(+), 3595 deletions(-)

diff --git a/chrome/content/zotero/preferences/preferences.js b/chrome/content/zotero/preferences/preferences.js @@ -1110,16 +1110,18 @@ function onOpenURLCustomized() /** * Refreshes the list of styles in the styles pane - **/ + * @param {String} cslID Style to select + */ function refreshStylesList(cslID) { var treechildren = document.getElementById('styleManager-rows'); while (treechildren.hasChildNodes()) { treechildren.removeChild(treechildren.firstChild); } - var styles = Zotero.Styles.getAll(); + var styles = Zotero.Styles.getVisible(); var selectIndex = false; + var i = 0; for each(var style in styles) { var treeitem = document.createElement('treeitem'); var treerow = document.createElement('treerow'); @@ -1138,8 +1140,7 @@ function refreshStylesList(cslID) { titleCell.setAttribute('label', style.title); updatedCell.setAttribute('label', updatedDate); // if not EN - if (style.styleID.length < Zotero.ENConverter.uriPrefix.length || - style.styleID.substr(0, Zotero.ENConverter.uriPrefix.length) != Zotero.ENConverter.uriPrefix) { + if(style.type == "csl") { cslCell.setAttribute('src', 'chrome://zotero/skin/tick.png'); } @@ -1152,6 +1153,7 @@ function refreshStylesList(cslID) { if (cslID == style.styleID) { document.getElementById('styleManager').view.selection.select(i); } + i++; } } @@ -1169,70 +1171,52 @@ function addStyle() { var rv = fp.show(); if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) { - var file = fp.file; - - // read file - var iStream = Components.classes["@mozilla.org/network/file-input-stream;1"] - .createInstance(Components.interfaces.nsIFileInputStream); - iStream.init(file, 0x01, 0664, 0); - var bStream = Components.classes["@mozilla.org/binaryinputstream;1"] - .createInstance(Components.interfaces.nsIBinaryInputStream); - bStream.setInputStream(iStream); - - var read = bStream.readBytes(6); - - if(read == "\x00\x08\xFF\x00\x00\x00") { - // EndNote style - - // read the rest of the bytes in the file - read += bStream.readBytes(file.fileSize-6); - - // get name and modification date - var name = file.leafName.replace(/\.ens$/i, ""); - var date = new Date(file.lastModifiedTime); - - var cslID = Zotero.Styles.install(read, false, date, name); - } else { - // This _should_ get the right charset for us automatically - var fileURI = Components.classes["@mozilla.org/network/protocol;1?name=file"] - .getService(Components.interfaces.nsIFileProtocolHandler) - .getURLSpecFromFile(file); - var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]. - createInstance(); - req.open("GET", fileURI, false); - req.overrideMimeType("text/plain"); - try { - req.send(null); - } catch(e) { - styleImportError(); - throw e; - } - - var cslID = Zotero.Styles.install(req.responseText); - } + Zotero.Styles.install(fp.file); } - - if(cslID !== false) this.refreshStylesList(cslID); } /** - * Deletes a style from the style pane + * Deletes selected styles from the styles pane **/ function deleteStyle() { + // get selected cslIDs var tree = document.getElementById('styleManager'); - if(tree.currentIndex == -1) return; - var treeitem = tree.lastChild.childNodes[tree.currentIndex]; - var cslID = treeitem.getAttribute('id').substr(11); + var treeItems = tree.lastChild.childNodes; + var cslIDs = []; + var start = {}; + var end = {}; + var nRanges = tree.view.selection.getRangeCount(); + for(var i=0; i<nRanges; i++) { + tree.view.selection.getRangeAt(i, start, end); + for(var j=start.value; j<=end.value; j++) { + cslIDs.push(treeItems[j].getAttribute('id').substr(11)); + } + } + + if(cslIDs.length == 0) { + return; + } else if(cslIDs.length == 1) { + var selectedStyle = Zotero.Styles.get(cslIDs[0]) + var text = Zotero.getString('styles.deleteStyle', selectedStyle.title); + } else { + var text = Zotero.getString('styles.deleteStyles'); + } var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] .getService(Components.interfaces.nsIPromptService); - var text = Zotero.getString('styles.deleteStyle', [title]); if(ps.confirm(null, '', text)) { - Zotero.Styles.get(cslID).delete(); + // delete if requested + if(cslIDs.length == 1) { + selectedStyle.delete(); + } else { + for(var i=0; i<cslIDs.length; i++) { + Zotero.Styles.get(cslIDs[i]).delete(); + } + } + this.refreshStylesList(); + document.getElementById('styleManager-delete').disabled = true; } - - document.getElementById('styleManager-delete').disabled = true; } /** @@ -1242,6 +1226,8 @@ function styleImportError() { alert(Zotero.getString('styles.installError', "This")); } +/**PROXIES**/ + /** * Adds a proxy to the proxy pane */ diff --git a/chrome/content/zotero/xpcom/cite.js b/chrome/content/zotero/xpcom/cite.js @@ -1,3277 +0,0 @@ -/* - ***** BEGIN LICENSE BLOCK ***** - - Copyright (c) 2006 Center for History and New Media - George Mason University, Fairfax, Virginia, USA - http://chnm.gmu.edu - - Licensed under the Educational Community License, Version 1.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.opensource.org/licenses/ecl1.php - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - ***** END LICENSE BLOCK ***** -*/ - -/** - * @property {Boolean} cacheTranslatorData Whether translator data should be cached or reloaded - * every time a translator is accessed - * @property {Zotero.CSL} lastCSL - */ -Zotero.Styles = new function() { - var _initialized = false; - var _styles, _visibleStyles; - - this.ios = Components.classes["@mozilla.org/network/io-service;1"]. - getService(Components.interfaces.nsIIOService); - - /** - * Initializes styles cache, loading metadata for styles into memory - */ - this.init = function() { - _initialized = true; - - var start = (new Date()).getTime() - - _styles = {}; - _visibleStyles = []; - this.cacheTranslatorData = Zotero.Prefs.get("cacheTranslatorData"); - this.lastCSL = null; - - // main dir - var dir = Zotero.getStylesDirectory(); - var i = _readStylesFromDirectory(dir, false); - - // hidden dir - dir.append("hidden"); - if(dir.exists()) i += _readStylesFromDirectory(dir, true); - - Zotero.debug("Cached "+i+" styles in "+((new Date()).getTime() - start)+" ms"); - } - - /** - * Reads all styles from a given directory and caches their metadata - */ - function _readStylesFromDirectory(dir, hidden) { - var i = 0; - var contents = dir.directoryEntries; - while(contents.hasMoreElements()) { - var file = contents.getNext().QueryInterface(Components.interfaces.nsIFile); - if(!file.leafName || file.leafName[0] == "." || file.isDirectory()) continue; - - var style = new Zotero.Style(file); - if(style.styleID) { - if(_styles[style.styleID]) { - // same style is already cached - Zotero.log('Style with ID '+style.styleID+' already loaded from "'+ - _styles[style.styleID].file.leafName+'"', "error", - Zotero.Styles.ios.newFileURI(style.file).spec); - } else { - // add to cache - _styles[style.styleID] = style; - _styles[style.styleID].hidden = hidden; - if(!hidden) _visibleStyles.push(style); - } - } - i++; - } - return i; - } - - /** - * Gets a style with a given ID - * @param {String} id - */ - this.get = function(id) { - if(!_initialized) this.init(); - return _styles[id] ? _styles[id] : false; - } - - /** - * Gets all visible styles - */ - this.getVisible = function() { - if(!_initialized || !this.cacheTranslatorData) this.init(); - return _visibleStyles.slice(0); - } - - /** - * Gets all styles - */ - this.getAll = function() { - if(!_initialized || !this.cacheTranslatorData) this.init(); - return _styles; - } -} - -/** - * @class Represents a style file and its metadata - * @property {String} styleID - * @property {String} type "csl" for CSL styles, "ens" for legacy styles - * @property {String} title - * @property {String} updated SQL-style date updated - * @property {String} class "in-text" or "note" - * @property {String} source The CSL that contains the formatting information for this one, or null - * if this CSL contains formatting information - * @property {Zotero.CSL} csl The Zotero.CSL object used to format using this style - * @property {Boolean} hidden True if this style is hidden in style selection dialogs, false if it - * is not - */ -Zotero.Style = function(file) { - this.file = file; - - var extension = file.leafName.substr(-4).toLowerCase(); - if(extension == ".ens") { - this.type = "ens"; - - this.styleID = Zotero.Styles.ios.newFileURI(this.file).spec; - this.title = file.leafName.substr(0, file.leafName.length-4); - this.updated = Zotero.Date.dateToSQL(new Date(file.lastModifiedTime)); - } else if(extension == ".csl") { - // "with ({});" needed to fix default namespace scope issue - // See https://bugzilla.mozilla.org/show_bug.cgi?id=330572 - default xml namespace = "http://purl.org/net/xbiblio/csl"; with ({}); - - this.type = "csl"; - - var xml = Zotero.CSL.Global.cleanXML(Zotero.File.getContents(file)); - try { - xml = new XML(xml); - } catch(e) { - Zotero.log(e.toString(), "error", - Zotero.Styles.ios.newFileURI(this.file).spec, xml.split(/\r?\n/)[e.lineNumber-1], - e.lineNumber); - return; - } - - this.styleID = xml.info.id.toString(); - this.title = xml.info.title.toString(); - this.updated = xml.info.updated.toString().replace(/(.+)T([^\+]+)\+?.*/, "$1 $2"); - this._class = xml.@class.toString(); - - this.source = null; - for each(var link in xml.info.link) { - if(link.@rel == "source") { - this.source = link.@href.toString(); - } - } - } -} - -Zotero.Style.prototype.__defineGetter__("csl", -/** - * Retrieves the Zotero.CSL object for this style - * @type Zotero.CSL - */ -function() { - // cache last style - if(Zotero.Styles.cacheTranslatorData && Zotero.Styles.lastCSL && - Zotero.Styles.lastCSL.styleID == this.styleID) { - return Zotero.Styles.lastCSL; - } - - if(this.type == "ens") { - // EN style - var iStream = Components.classes["@mozilla.org/network/file-input-stream;1"] - .createInstance(Components.interfaces.nsIFileInputStream); - iStream.init(this.file, 0x01, 0664, 0); - var bStream = Components.classes["@mozilla.org/binaryinputstream;1"] - .createInstance(Components.interfaces.nsIBinaryInputStream); - bStream.setInputStream(iStream); - var string = bStream.readBytes(this.file.fileSize); - iStream.close(); - - var enConverter = new Zotero.ENConverter(string, null, this.title); - var xml = enConverter.parse(); - } else { - // "with ({});" needed to fix default namespace scope issue - // See https://bugzilla.mozilla.org/show_bug.cgi?id=330572 - default xml namespace = "http://purl.org/net/xbiblio/csl"; with ({}); - - if(this.source) { - // parent/child - var formatCSL = Zotero.Styles.get(this.source); - if(!formatCSL) { - throw(new Error('Style references '+this.source+', but this style is not installed', - Zotero.Styles.ios.newFileURI(this.file).spec, null)); - } - var file = formatCSL.file; - } else { - var file = this.file; - } - - var cslString = Zotero.File.getContents(file); - var xml = new XML(Zotero.CSL.Global.cleanXML(cslString)); - } - - return (Zotero.Styles.lastCSL = new Zotero.CSL(xml)); -}); - -Zotero.Style.prototype.__defineGetter__("class", -/** - * Retrieves the style class, either from the metadata that's already loaded or by loading the file - * @type String - */ -function() { - if(this._class) return this._class; - return (this._class = this.csl.class); -}); - -/** - * Deletes a style - */ -Zotero.Style.prototype.delete = function() { - this.file.remove(false); - Zotero.Styles.init(); -} - - -Zotero.Styles.MIMEHandler = new function () { - this.init = init; - - /* - * registers URIContentListener to handle MIME types - */ - function init() { - Zotero.debug("Registering URIContentListener for text/x-csl"); - var uriLoader = Components.classes["@mozilla.org/uriloader;1"] - .getService(Components.interfaces.nsIURILoader); - uriLoader.registerContentListener(Zotero.Styles.MIMEHandler.URIContentListener); - } -} - - -/* - * Zotero.Styles.MIMEHandler.URIContentListener: implements - * nsIURIContentListener interface to grab MIME types - */ -Zotero.Styles.MIMEHandler.URIContentListener = new function() { - // list of content types to capture - // NOTE: must be from shortest to longest length - this.desiredContentTypes = ["text/x-csl"]; - - this.QueryInterface = QueryInterface; - this.canHandleContent = canHandleContent; - this.doContent = doContent; - this.isPreferred = isPreferred; - this.onStartURIOpen = onStartURIOpen; - - function QueryInterface(iid) { - if (iid.equals(Components.interfaces.nsISupports) - || iid.equals(Components.interfaces.nsISupportsWeakReference) - || iid.equals(Components.interfaces.nsIURIContentListener)) { - return this; - } - throw Components.results.NS_ERROR_NO_INTERFACE; - } - - function canHandleContent(contentType, isContentPreferred, desiredContentType) { - if (this.desiredContentTypes.indexOf(contentType) != -1) { - return true; - } - return false; - } - - function doContent(contentType, isContentPreferred, request, contentHandler) { - Zotero.debug("Running doContent() for " + request.name); - contentHandler.value = new Zotero.Styles.MIMEHandler.StreamListener(request, contentType); - return false; - } - - function isPreferred(contentType, desiredContentType) { - if (this.desiredContentTypes.indexOf(contentType) != -1) { - return true; - } - return false; - } - - function onStartURIOpen(URI) { - return true; - } -} - -/* - * Zotero.Styles.MIMEHandler.StreamListener: implements nsIStreamListener and - * nsIRequestObserver interfaces to download MIME types we've grabbed - */ -Zotero.Styles.MIMEHandler.StreamListener = function(request, contentType) { - this._request = request; - this._contentType = contentType - this._readString = ""; - this._scriptableStream = null; - this._scriptableStreamInput = null - - Zotero.debug("Prepared to grab content type " + contentType); -} - -Zotero.Styles.MIMEHandler.StreamListener.prototype.QueryInterface = function(iid) { - if (iid.equals(Components.interfaces.nsISupports) - || iid.equals(Components.interfaces.nsIRequestObserver) - || iid.equals(Components.interfaces.nsIStreamListener)) { - return this; - } - throw Components.results.NS_ERROR_NO_INTERFACE; -} - -Zotero.Styles.MIMEHandler.StreamListener.prototype.onStartRequest = function(channel, context) {} - -/* - * Called when there's data available; basically, we just want to collect this data - */ -Zotero.Styles.MIMEHandler.StreamListener.prototype.onDataAvailable = function(request, context, inputStream, offset, count) { - Zotero.debug(count + " bytes available"); - - if (inputStream != this._scriptableStreamInput) { - this._scriptableStream = Components.classes["@mozilla.org/scriptableinputstream;1"] - .createInstance(Components.interfaces.nsIScriptableInputStream); - this._scriptableStream.init(inputStream); - this._scriptableStreamInput = inputStream; - } - this._readString += this._scriptableStream.read(count); -} - -/* - * Called when the request is done - */ -Zotero.Styles.MIMEHandler.StreamListener.prototype.onStopRequest = function(channel, context, status) { - Zotero.debug("Request finished"); - var externalHelperAppService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"] - .getService(Components.interfaces.nsIExternalHelperAppService); - - if (this._request.name) { - var loadURI = this._request.name; - } - else { - var loadURI = ''; - } - - Zotero.Styles.install(this._readString, loadURI); -} - - -/* - * CSL: a class for creating bibliographies from CSL files - * this is abstracted as a separate class for the benefit of anyone who doesn't - * want to use the Scholar data model, but does want to use CSL in JavaScript - */ -Zotero.CSL = function(csl) { - // "with ({});" needed to fix default namespace scope issue - // See https://bugzilla.mozilla.org/show_bug.cgi?id=330572 - default xml namespace = "http://purl.org/net/xbiblio/csl"; with ({}); - - if(typeof csl != "XML") { - this._csl = new XML(Zotero.CSL.Global.cleanXML(csl)); - } else { - this._csl = CSL; - } - - // initialize CSL - Zotero.CSL.Global.init(); - - // load localizations - this._terms = Zotero.CSL.Global.parseLocales(this._csl.terms); - - // load class and styleID - this.styleID = this._csl.info.id.toString(); - this.class = this._csl["@class"].toString(); - Zotero.debug("CSL: style class is "+this.class); - - this.hasBibliography = (this._csl.bibliography.length() ? 1 : 0); -} - -/* - * Constants for citation positions - */ -Zotero.CSL.POSITION_FIRST = 0; -Zotero.CSL.POSITION_SUBSEQUENT = 1; -Zotero.CSL.POSITION_IBID = 2; -Zotero.CSL.POSITION_IBID_WITH_LOCATOR = 3; - - -Zotero.CSL._dateVariables = { - "issued":true, - "accessDate":true -} - -Zotero.CSL._namesVariables = { - "editor":true, - "translator":true, - "recipient":true, - "interviewer":true, - "collection-editor":true, - "author":true -} - -/* - * Constants for name (used for disambiguate-add-givenname) - */ -Zotero.CSL.NAME_USE_INITIAL = 1; -Zotero.CSL.NAME_USE_FULL = 2; - -/* - * generate an item set - */ -Zotero.CSL.prototype.createItemSet = function(items) { - return new Zotero.CSL.ItemSet(items, this); -} - -/* - * generate a citation object - */ -Zotero.CSL.prototype.createCitation = function(citationItems) { - return new Zotero.CSL.Citation(citationItems, this); -} - -/* - * create a citation (in-text or footnote) - */ -Zotero.CSL._firstNameRegexp = /^[^\s]*/; -Zotero.CSL._textCharRegexp = /[a-zA-Z0-9]/; -Zotero.CSL._numberRegexp = /\d+/; -Zotero.CSL.prototype.formatCitation = function(citation, format) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); - - var context = this._csl.citation; - if(!context) { - throw "CSL: formatCitation called on style with no citation context"; - } - if(!citation.citationItems.length) { - throw "CSL: formatCitation called with empty citation"; - } - - // clone citationItems, so as not to disturb the citation - var citationItems = citation.citationItems; - - // handle collapse - var cslAdded = []; - - var collapse = context.option.(@name == "collapse").@value.toString(); - if(collapse) { - // clone citationItems, so as not to disturb the citation - citationItems = new Array(); - - if(collapse == "citation-number") { - // loop through, collecting citation numbers - var citationNumbers = new Object(); - for(var i=0; i<citation.citationItems.length; i++) { - citationNumbers[citation.citationItems[i].item.getProperty("citation-number")] = i; - } - // add -1 at the end so that the last span gets added (loop below - // must be run once more) - citationNumbers[-1] = false; - - var previousI = -1; - var span = []; - // loop through citation numbers and collect ranges in span - for(var i in citationNumbers) { - if(i != -1 && !citation.citationItems[citationNumbers[i]].prefix - && !citation.citationItems[citationNumbers[i]].suffix - && i == parseInt(previousI, 10)+1) { - // could be part of a range including the previous number - span.push(citationNumbers[i]); - } else { // not part of a range - if(span.length) citationItems[span[0]] = citation.citationItems[span[0]]; - if(span.length > 2) { - // if previous set of citations was a range, collapse them - var firstNumber = citationItems[span[0]].item.getProperty("citation-number"); - citationItems[span[0]]._csl = {"citation-number":(firstNumber+"-"+(parseInt(firstNumber, 10)+span.length-1))}; - cslAdded.push(span[0]); - } else if(span.length == 2) { - citationItems[span[1]] = citation.citationItems[span[1]]; - } - - span = [citationNumbers[i]]; - } - previousI = i; - } - } else if(collapse.substr(0, 4) == "year") { - // loop through, collecting citations (sans date) in an array - var lastNames = {}; - for(var i=0; i<citation.citationItems.length; i++) { - var citationString = new Zotero.CSL.FormattedString(context, format); - this._processElements(citation.citationItems[i].item, context.layout, citationString, - context, null, [{"issued":true}, {}]); - var cite = citationString.get(); - - // put into lastNames array - if(!lastNames[cite]) { - lastNames[cite] = [i]; - } else { - lastNames[cite].push(i); - } - } - - for(var i in lastNames) { - var itemsSharingName = lastNames[i]; - if(itemsSharingName.length == 1) { - // if only one, don't worry about grouping - citationItems[itemsSharingName[0]] = citation.citationItems[itemsSharingName[0]]; - } else { - var years = []; - // if grouping by year-suffix, we need to do more (to pull - // together various letters) - if(collapse == "year-suffix" && context.option.(@name == "disambiguate-add-year-suffix").@value == "true") { - var yearsArray = new Object(); - for(var j=0; j<itemsSharingName.length; j++) { - var year = citation.citationItems[itemsSharingName[j]].item.getDate("issued"); - if(year) { - year = year.getDateVariable("year"); - if(year) { - // add to years - if(!yearsArray[year]) { - yearsArray[year] = [itemsSharingName[j]]; - } else { - yearsArray[year].push(itemsSharingName[j]); - } - } - } - - if(!year) { - // if no year, just copy - years.push(""); - } - } - - // loop through all years - for(var j in yearsArray) { - var citationItem = citation.citationItems[yearsArray[j][0]]; - - // push first year with any suffix - var year = j; - var suffix = citationItem.item.getProperty("disambiguate-add-year-suffix"); - if(suffix) year += suffix; - years.push(year); - - // also push subsequent years - if(yearsArray[j].length > 1) { - for(k=1; k<yearsArray[j].length; k++) { - var suffix = citation.citationItems[yearsArray[j][k]].item.getProperty("disambiguate-add-year-suffix"); - if(suffix) years.push(suffix); - } - } - } - } else { - // just add years - for(var j=0; j<itemsSharingName.length; j++) { - var item = citation.citationItems[itemsSharingName[j]].item; - var year = item.getDate("issued"); - if(year) { - years[j] = year.getDateVariable("year"); - var suffix = item.getProperty("disambiguate-add-year-suffix"); - if(suffix) years[j] += suffix; - } - } - } - citation.citationItems[itemsSharingName[0]]._csl = {"issued":{"year":years.join(", ")}}; - citationItems[itemsSharingName[0]] = citation.citationItems[itemsSharingName[0]]; - cslAdded.push(itemsSharingName[0]); - } - } - } - } - - var string = new Zotero.CSL.FormattedString(context, format, context.layout.@delimiter.toString()); - for(var i=0; i<citationItems.length; i++) { - var citationItem = citationItems[i]; - if(!citationItem) continue; - - var citationString = string.clone(); - - // suppress author if requested - var ignore = citationItem.suppressAuthor ? [{"author":true}, {}] : undefined; - - // add prefix - if(citationItem.prefix) { - var prefix = citationItem.prefix; - - // add space to prefix if last char is alphanumeric - if(Zotero.CSL._textCharRegexp.test(prefix[prefix.length-1])) prefix += " "; - - citationString.append(prefix); - } - - this._processElements(citationItem.item, context.layout, citationString, - context, citationItem, ignore); - - // add suffix - if(citationItem.suffix) { - var suffix = citationItem.suffix; - - // add space to suffix if last char is alphanumeric - if(Zotero.CSL._textCharRegexp.test(suffix[0])) suffix = " "+suffix; - - citationString.append(suffix); - } - - string.concat(citationString); - } - - var returnString = string.clone(); - returnString.concat(string, context.layout); - var returnString = returnString.get(); - - // loop through to remove _csl property - for(var i=0; i<cslAdded.length; i++) { - citationItems[cslAdded[i]]._csl = undefined; - } - - return returnString; -} - -/* - * create a bibliography - */ -Zotero.CSL.prototype.formatBibliography = function(itemSet, format) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); - - var context = this._csl.bibliography; - if(!context.length()) { - context = this._csl.citation; - var isCitation = true; - } - if(!context) { - throw "CSL: formatBibliography called on style with no bibliography context"; - } - - if(!itemSet.items.length) return ""; - - var hangingIndent = !!(context.option.(@name == "hanging-indent").@value == "true"); - var secondFieldAlign = context.option.(@name == "second-field-align").@value.toString(); - var lineSpacing = context.option.(@name == "line-spacing").@value.toString(); - lineSpacing = (lineSpacing === "" ? 1 : parseInt(lineSpacing, 10)); - if(lineSpacing == NaN) throw "Invalid line spacing"; - var entrySpacing = context.option.(@name == "entry-spacing").@value.toString(); - entrySpacing = (entrySpacing === "" ? 1 : parseInt(entrySpacing, 10)); - if(entrySpacing == NaN) throw "Invalid entry spacing"; - - var index = 0; - var output = ""; - var preamble = ""; - if(format == "HTML") { - if(this.class == "note" && isCitation) { - // note citations are formatted as an ordered list - preamble = '<ol>\r\n'; - secondFieldAlign = false; - } else { - // needed bc HTML doesn't force lines to be at least as big as the - // tallest character - if(lineSpacing <= 1.1) lineSpacing = 1.1; - - // add style - var style = 'line-height:'+lineSpacing+'em;' - if(hangingIndent) { - style += 'margin-left:0.5in;text-indent:-0.5in;'; - } - - if(secondFieldAlign) { - preamble += '<table style="border-collapse:collapse;'+style+'">\r\n'; - } else { - preamble += '<div style="'+style+'">\r\n'; - } - } - } else { - if(format == "RTF" || format == "Integration") { - if(format == "RTF") { - preamble = "{\\rtf\\ansi{\\fonttbl\\f0\\froman Times New Roman;}{\\colortbl;\\red255\\green255\\blue255;}\\pard\\f0\r\n"; - } - - var tabStop = null; - if(hangingIndent) { - var indent = 720; // 720 twips = 0.5 in - var firstLineIndent = -720; // -720 twips = -0.5 in - } else { - var indent = 0; - var firstLineIndent = 0; - } - } - - var returnChars = ""; - for(j=0; j<=entrySpacing; j++) { - if(format == "RTF") { - returnChars += "\\\r\n"; - } else if(Zotero.isWin) { - returnChars += "\r\n"; - } else { - returnChars += "\n"; - } - } - } - - var maxFirstFieldLength = 0; - for(var i in itemSet.items) { - var item = itemSet.items[i]; - if(item == undefined) continue; - - // try to get custom bibliography - var string = item.getProperty("bibliography-"+(format == "Integration" ? "RTF" : format)); - if(!string) { - string = new Zotero.CSL.FormattedString(context, format); - this._processElements(item, context.layout, string, context); - if(!string) { - continue; - } - - // add format - string.string = context.layout.@prefix.toString() + string.string; - if(context.layout.@suffix.length()) { - string.append(context.layout.@suffix.toString()); - } - - string = string.get(); - } - - if(secondFieldAlign && (format == "RTF" || format == "Integration")) { - if(format == "RTF") { - var tab = string.indexOf("\\tab "); - } else { - var tab = string.indexOf("\t"); - } - if(tab > maxFirstFieldLength) { - maxFirstFieldLength = tab; - } - } - - // add line feeds - if(format == "HTML") { - var coins = Zotero.OpenURL.createContextObject(item.zoteroItem, "1.0"); - - var span = (coins ? ' <span class="Z3988" title="'+coins.replace("&", "&amp;", "g")+'">&nbsp;</span>' : ''); - - if(this.class == "note" && isCitation) { - output += "<li>"+string+span+"</li>\r\n"; - } else if(secondFieldAlign) { - output += '<tr style="vertical-align:top;"><td>'+string+span+"</td></tr>\r\n"; - for(var j=0; j<entrySpacing; j++) { - output += '<tr><td colspan="2">&nbsp;</td></tr>\r\n'; - } - } else { - if(i == 0) { - // first p has no margins - var margin = "0"; - } else { - var margin = (entrySpacing*lineSpacing).toString()+"em 0 0 0"; - } - output += '<p style="margin:'+margin+'">'+string+span+"</p>\r\n"; - } - } else { - if(this.class == "note" && isCitation) { - if(format == "RTF") { - index++; - output += index+". "; - } else if(format == "Text") { - index++; - output += index+". "; - } - } - output += string+returnChars; - } - } - - if(format == "HTML") { - if(this.class == "note" && isCitation) { - output += '</ol>'; - } else if(secondFieldAlign) { - output += '</table>'; - } else { - output += '</div>'; - } - } else if(format == "RTF" || format == "Integration") { - if(secondFieldAlign) { - // this is a really sticky issue. the below works for first fields - // that look like "[1]" and "1." otherwise, i have no idea. luckily, - // this will be good enough 99% of the time. - var alignAt = 24+maxFirstFieldLength*120; - - if(secondFieldAlign == "margin") { - firstLineIndent -= alignAt; - tabStop = 0; - } else { - indent += alignAt; - firstLineIndent = -indent; - tabStop = indent; - } - } - - preamble += "\\li"+indent+" \\fi"+firstLineIndent+" "; - if(format == "Integration") { - preamble += "\\sl"+lineSpacing+" "; - } else if(format == "RTF" && lineSpacing != 1) { - preamble += "\\sl"+(240*lineSpacing)+" \\slmult1 "; - } - - if(tabStop !== null) { - preamble += "\\tx"+tabStop+" "; - } - preamble += "\r\n"; - - // drop last returns - output = output.substr(0, output.length-returnChars.length); - - // add bracket for RTF - if(format == "RTF") output += "\\par }"; - } - - return preamble+output; -} - -/* - * gets a term, in singular or plural form - */ -Zotero.CSL.prototype._getTerm = function(term, plural, form, includePeriod) { - if(!form) { - form = "long"; - } - - if(!this._terms[form] || !this._terms[form][term]) { - if(form == "verb-short") { - return this._getTerm(term, plural, "verb"); - } else if(form == "symbol") { - return this._getTerm(term, plural, "short"); - } else if(form != "long") { - return this._getTerm(term, plural, "long"); - } else { - Zotero.debug("CSL: WARNING: could not find term \""+term+'"'); - return ""; - } - } - - var term; - if(typeof(this._terms[form][term]) == "object") { // singular and plural forms - // are available - if(plural) { - term = this._terms[form][term][1]; - } else { - term = this._terms[form][term][0]; - } - } else { - term = this._terms[form][term]; - } - - if((form == "short" || form == "verb-short") && includePeriod) { - term += "."; - } - - return term; -} - -/* - * process creator objects; if someone had a creator model that handled - * non-Western names better than ours, this would be the function to change - */ -Zotero.CSL.prototype._processNames = function(item, element, formattedString, context, citationItem, variables) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); - - var children = element.children(); - if(!children.length()) return false; - var variableSucceeded = false; - - for(var j=0; j<variables.length; j++) { - var success = false; - var newString = formattedString.clone(); - - if(formattedString.format != "Sort" && variables[j] == "author" && context - && context.option.(@name == "subsequent-author-substitute").length() - && item.getProperty("subsequent-author-substitute") - && context.localName() == "bibliography") { - newString.append(context.option.(@name == "subsequent-author-substitute").@value.toString()); - success = true; - } else { - var creators = item.getNames(variables[j]); - - if(creators && creators.length) { - var maxCreators = creators.length; - - for each(var child in children) { - if(child.namespace() != Zotero.CSL.Global.ns) continue; - - var name = child.localName(); - if(name == "name") { - var useEtAl = false; - - if(context) { - // figure out if we need to use "et al" - var etAlMin = context.option.(@name == "et-al-min").@value.toString(); - var etAlUseFirst = context.option.(@name == "et-al-use-first").@value.toString(); - - if(citationItem && citationItem.position - && citationItem.position >= Zotero.CSL.POSITION_SUBSEQUENT) { - if(context.option.(@name == "et-al-subsequent-min").length()) { - etAlMin = context.option.(@name == "et-al-subsequent-min").@value.toString(); - } - if(context.option.(@name == "et-al-subsequent-use-first").length()) { - etAlUseFirst = context.option.(@name == "et-al-subsequent-use-first").@value.toString(); - } - } - - if(etAlMin && etAlUseFirst && maxCreators >= parseInt(etAlMin, 10)) { - etAlUseFirst = parseInt(etAlUseFirst, 10); - if(etAlUseFirst != maxCreators) { - maxCreators = etAlUseFirst; - useEtAl = true; - } - } - - // add additional names to disambiguate - if(variables[j] == "author" && useEtAl) { - var disambigNames = item.getProperty("disambiguate-add-names"); - if(disambigNames != "") { - maxCreators = disambigNames; - if(disambigNames == creators.length) useEtAl = false; - } - } - - if(child.@form == "short") { - var fullNames = item.getProperty("disambiguate-add-givenname").split(","); - } - } - - var authorStrings = []; - var firstName, lastName; - // parse authors into strings - for(var i=0; i<maxCreators; i++) { - if(formattedString.format == "Sort") { - // for sort, we use the plain names - var name = creators[i].getNameVariable("lastName"); - - // cut off lowercase parts of otherwise capitalized names (e.g., "de") - var lastNameParts = name.split(" "); - if(lastNameParts.length > 1 && lastNameParts[0] !== "" && lastNameParts[0].length <= 4 - && lastNameParts[0][0].toLowerCase() == lastNameParts[0][0] - && lastNameParts[lastNameParts.length-1][0].toUpperCase() == lastNameParts[lastNameParts.length-1][0]) { - name = ""; - for(var k=1; k<lastNameParts.length; k++) { - if(lastNameParts[k][0].toUpperCase() == lastNameParts[k][0]) { - name += " "+lastNameParts[k]; - } - } - name = name.substr(1); - } - - var firstName = creators[i].getNameVariable("firstName"); - if(name && firstName) name += ", "; - name += firstName; - - newString.append(name); - } else { - var firstName = ""; - - if(child.@form != "short" || (fullNames && fullNames[i])) { - if(child["@initialize-with"].length() && (!fullNames || - fullNames[i] != Zotero.CSL.NAME_USE_FULL)) { - // even if initialize-with is simply an empty string, use - // initials - - // use first initials - var firstNames = creators[i].getNameVariable("firstName").split(" "); - for(var k in firstNames) { - if(firstNames[k]) { - // get first initial, put in upper case, add initializeWith string - firstName += firstNames[k][0].toUpperCase()+child["@initialize-with"].toString(); - } - } - - if(firstName[firstName.length-1] == " ") { - firstName = firstName.substr(0, firstName.length-1); - } - } else { - firstName = creators[i].getNameVariable("firstName"); - } - } - lastName = creators[i].getNameVariable("lastName"); - - if(child["@name-as-sort-order"].length() - && ((i == 0 && child["@name-as-sort-order"] == "first") - || child["@name-as-sort-order"] == "all") - && child["@sort-separator"].length()) { - // if this is the first author and name-as-sort="first" - // or if this is a subsequent author and name-as-sort="all" - // then the name gets inverted - authorStrings.push(lastName+(firstName ? child["@sort-separator"].toString()+firstName : "")); - } else { - authorStrings.push((firstName ? firstName+" " : "")+lastName); - } - } - } - - if(formattedString.format != "Sort") { - // figure out if we need an "and" or an "et al" - var joinString = (child["@delimiter"].length() ? child["@delimiter"].toString() : ", "); - if(creators.length > 1) { - if(useEtAl) { // multiple creators and need et al - authorStrings.push(this._getTerm("et-al")); - } else { // multiple creators but no et al - // add and to last creator - if(child["@and"].length()) { - if(child["@and"] == "symbol") { - var and = "&" - } else if(child["@and"] == "text") { - var and = this._getTerm("and"); - } - - authorStrings[maxCreators-1] = and+" "+authorStrings[maxCreators-1]; - } - } - - // check whether to use a serial comma - if((authorStrings.length == 2 && (child["@delimiter-precedes-last"] != "always" || useEtAl)) || - (authorStrings.length > 2 && child["@delimiter-precedes-last"] == "never")) { - var lastString = authorStrings.pop(); - authorStrings[authorStrings.length-1] = authorStrings[authorStrings.length-1]+" "+lastString; - } - } - newString.append(authorStrings.join(joinString), child); - } - } else if(formattedString.format != "Sort" && - name == "label" && variables[j] != "author") { - newString.append(this._getTerm(variables[j], (maxCreators != 1), child["@form"].toString(), child["@include-period"] == "true"), child); - } - } - success = true; - } - } - - if(success) { - variableSucceeded = true; - formattedString.concat(newString); - } - } - - return variableSucceeded; -} - -/* - * processes an element from a (pre-processed) item into text - */ -Zotero.CSL.prototype._processElements = function(item, element, formattedString, - context, citationItem, ignore, isSingle) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); - - if(!ignore) { - ignore = [[], []]; - // ignore[0] is for variables; ignore[1] is for macros - } - - var dataAppended = false; - - if(isSingle) { - // handle single elements - var numberOfChildren = 1; - var children = [element]; - } else { - // accept groups of elements by default - var children = element.children(); - var numberOfChildren = children.length(); - var lastChild = children.length()-1; - } - - for(var i=0; i<numberOfChildren; i++) { - var child = children[i]; - if(child.namespace() != Zotero.CSL.Global.ns) continue; - var name = child.localName(); - - if(name == "text") { - if(child["@term"].length()) { - var term = this._getTerm(child["@term"].toString(), child.@plural == "true", child.@form.toString(), child["@include-period"] == "true"); - if(term) { - formattedString.append(term, child); - } - } else if(child.@variable.length()) { - var form = child.@form.toString(); - var variables = child["@variable"].toString().split(" "); - var newString = formattedString.clone(child.@delimiter.toString()); - var success = false; - - for(var j=0; j<variables.length; j++) { - if(ignore[0][variables[j]]) continue; - - if(variables[j] == "locator") { - // special case for locator - var text = citationItem && citationItem.locator ? citationItem.locator : ""; - } else if(citationItem && citationItem._csl && citationItem._csl[variables[j]]) { - // override if requested - var text = citationItem._csl[variables[j]]; - } else if(variables[j] == "citation-number") { - // special case for citation-number - var text = item.getProperty("citation-number"); - } else { - var text = item.getVariable(variables[j], form); - } - - if(text) { - newString.append(text); - success = true; - } - } - - if(success) { - formattedString.concat(newString, child); - dataAppended = true; - } - } else if(child.@macro.length()) { - var macro = this._csl.macro.(@name == child.@macro); - if(!macro.length()) throw "CSL: style references undefined macro " + child.@macro; - - // If not ignored (bc already used as a substitution) - if(!ignore[1][child.@macro.toString()]) { - var newString = formattedString.clone(child.@delimiter.toString()); - var success = this._processElements(item, macro, newString, - context, citationItem, ignore); - if(success) dataAppended = true; - formattedString.concat(newString, child); - } - } else if(child.@value.length()) { - formattedString.append(child.@value.toString(), child); - } - } else if(name == "number") { - if(child.@variable.length()) { - var form = child.@form.toString(); - var variables = child["@variable"].toString().split(" "); - var newString = formattedString.clone(child.@delimiter.toString()); - var success = false; - - for(var j=0; j<variables.length; j++) { - if(ignore[0][variables[j]]) continue; - - var text = item.getNumericVariable(variables[j], form); - if(text) { - newString.append(text); - success = true; - } - } - - if(success) { - formattedString.concat(newString, child); - dataAppended = true; - } - } - } else if(name == "label") { - var form = child.@form.toString(); - var variables = child["@variable"].toString().split(" "); - var newString = formattedString.clone(child.@delimiter.toString()); - var success = false; - - for(var j=0; j<variables.length; j++) { - if(ignore[0][variables[j]]) continue; - - if(variables[j] == "locator") { - // special case for locator - var term = (citationItem && citationItem.locatorType) ? citationItem.locatorType : "page"; - // if "other" specified as the term, don't do anything - if(term == "other") term = false; - var value = citationItem && citationItem.locator ? citationItem.locator : false; - } else { - var term = variables[j]; - var value = item.getVariable(variables[j]).toString(); - } - - if(term !== false && value) { - if (child["@pluralize"] == "always") { - var isPlural = true; - } - else if (child["@pluralize"] == "never") { - var isPlural = false; - } - else { // contextual - var isPlural = value.indexOf("-") != -1 || value.indexOf(",") != -1 || value.indexOf("\u2013") != -1; - } - var text = this._getTerm(term, isPlural, child.@form.toString(), child["@include-period"] == "true"); - - if(text) { - newString.append(text); - success = true; - } - } - } - - if(success) { - formattedString.concat(newString, child); - } - } else if(name == "names") { - var variables = child["@variable"].toString().split(" "); - var newString = formattedString.clone(child.@delimiter.toString()); - - // remove variables that aren't supposed to be there - for(var j=0; j<variables.length; j++) { - if(ignore[0][variables[j]]) { - variables.splice(j, 1); - } - } - if(!variables.length) continue; - - var success = this._processNames(item, child, newString, context, - citationItem, variables); - - if(!success && child.substitute.length()) { - for each(var newChild in child.substitute.children()) { - if(newChild.namespace() != Zotero.CSL.Global.ns) continue; - - if(newChild.localName() == "names" && newChild.children.length() == 0) { - // apply same rules to substitute names - // with no children - var variable = newChild.@variable.toString(); - variables = variable.split(" "); - success = this._processNames(item, child, newString, - context, citationItem, variables); - - ignore[0][newChild.@variable.toString()] = true; - - if(success) break; - } else { - if(!newChild.@suffix.length()) newChild.@suffix = element.@suffix; - if(!newChild.@prefix.length()) newChild.@prefix = element.@prefix; - - success = this._processElements(item, - newChild, newString, context, citationItem, ignore, true); - - // ignore if used as substitution - if(newChild.@variable.length()) { - ignore[0][newChild.@variable.toString()] = true; - } else if(newChild.@macro.length()) { - ignore[1][newChild.@macro.toString()] = true; - } - - // if substitution was successful, stop - if(success) break; - } - } - } - - if(success) { - formattedString.concat(newString, child); - dataAppended = true; - } - } else if(name == "date") { - var variables = child["@variable"].toString().split(" "); - var newString = formattedString.clone(child.@delimiter.toString()); - var success = false; - - for(var j=0; j<variables.length; j++) { - if(ignore[0][variables[j]]) continue; - - var date = item.getDate(variables[j]); - if(!date) continue; - - var variableString = formattedString.clone(); - success = true; - - if(formattedString.format == "Sort") { - variableString.append(date.getDateVariable("sort")); - } else { - for each(var newChild in child.children()) { - if(newChild.namespace() != Zotero.CSL.Global.ns) continue; - var newName = newChild.localName(); - var newForm = newChild.@form.toString(); - - if(newName == "date-part") { - var part = newChild.@name.toString(); - - if(citationItem && citationItem._csl && citationItem._csl[variables[j]] && citationItem._csl[variables[j]][part]) { - // date is in citationItem - var string = citationItem._csl[variables[j]][part]; - } else { - var string = date.getDateVariable(part); - if(string === "") continue; - - if(part == "year") { - string = string.toString(); - - // if 4 digits and no B.C., use short form - if(newForm == "short" && string.length == 4 && !isNaN(string*1)) { - string = string.substr(2, 2); - } - - var disambiguate = item.getProperty("disambiguate-add-year-suffix"); - if(disambiguate && variables[j] == "issued") { - string += disambiguate; - } - } else if(part == "month") { - // if month is a numeric month, format as such - if(!isNaN(string*1)) { - if(newForm == "numeric-leading-zeros") { - string = (string+1).toString(); - if(string.length == 1) { - string = "0" + string; - } - } else if(newForm == "short") { - string = this._terms["short"]["_months"][string]; - } else if(newForm == "numeric") { - string = (1+string).toString(); - } else { - string = this._terms["long"]["_months"][string]; - } - } else if(newForm == "numeric") { - string = ""; - } - } else if(part == "day") { - string = string.toString(); - if(form == "numeric-leading-zeros" - && string.length() == 1) { - string = "0" + string; - } else if (newForm == "ordinal") { - var ind = parseInt(string); - var daySuffixes = Zotero.getString("date.daySuffixes").replace(/, ?/g, "|").split("|"); - string += (parseInt(ind/10)%10) == 1 ? daySuffixes[3] : (ind % 10 == 1) ? daySuffixes[0] : (ind % 10 == 2) ? daySuffixes[1] : (ind % 10 == 3) ? daySuffixes[2] : daySuffixes[3]; - } - } - } - } - - variableString.append(string, newChild); - } - - newString.concat(variableString); - } - - formattedString.concat(newString, child); - } - - if(success) { - dataAppended = true; - } - } else if(name == "group") { - var newString = formattedString.clone(child.@delimiter.toString()); - var success = this._processElements(item, - child, newString, context, citationItem, - ignore); - - // concat only if true data (not text element) was appended - if(success) { - formattedString.concat(newString, child); - dataAppended = true; - } - } else if(name == "choose") { - for each(var newChild in child.children()) { - if(newChild.namespace() != Zotero.CSL.Global.ns) continue; - - var truthValue; - - if(newChild.localName() == "else") { - // always true, if we got to this point in the loop - truthValue = true; - } else if(newChild.localName() == "if" - || newChild.localName() == "else-if") { - - var matchAny = newChild.@match == "any"; - var matchNone = newChild.@match == "none"; - if(matchAny) { - // if matching any, begin with false, then set to true - // if a condition is true - truthValue = false; - } else { - // if matching all, begin with true, then set to false - // if a condition is false - truthValue = true; - } - - // inspect variables - var done = false; - var attributes = ["variable", "is-date", "is-numeric", "is-plural", "type", "disambiguate", "locator", "position"]; - for(var k=0; !done && k<attributes.length; k++) { - var attribute = attributes[k]; - - if(newChild["@"+attribute].length()) { - var variables = newChild["@"+attribute].toString().split(" "); - for(var j=0; !done && j<variables.length; j++) { - var exists = false; - if(attribute == "variable") { - if(variables[j] == "locator") { - // special case for locator - exists = citationItem && citationItem.locator && citationItem.locator.length > 0 - } - else if(Zotero.CSL._dateVariables[variables[j]]) { - // getDate not false/undefined - exists = !!item.getDate(variables[j]); - } else if(Zotero.CSL._namesVariables[variables[j]]) { - // getNames not false/undefined, not empty - exists = item.getNames(variables[j]); - if(exists) exists = !!exists.length; - } else { - exists = item.getVariable(variables[j]); - if (exists) exists = !!exists.length; - } - } else if (attribute == "is-numeric") { - exists = item.getNumericVariable(variables[j]); - } else if (attribute == "is-date") { // XXX - this needs improving - if (Zotero.CSL._dateVariables[variables[j]]) { - exists = !!item.getDate(variables[j]); - } - } else if(attribute == "is-plural") { - if(Zotero.CSL._namesVariables[variables[j]]) { - exists = item.getNames(variables[j]); - if(exists) exists = exists.length > 1; - } else if(variables[j] == "page" || variables[j] == "locator") { - if(variables[j] == "page") { - var value = item.getVariable("page"); - } else { - var value = citationItem && citationItem.locator ? citationItem.locator : ""; - } - exists = value.indexOf("-") != -1 || value.indexOf(",") != -1 || value.indexOf("\u2013") != -1; - } - } else if(attribute == "type") { - exists = item.isType(variables[j]); - } else if(attribute == "disambiguate") { - exists = (variables[j] == "true" && item.getProperty("disambiguate-condition")) - || (variables[j] == "false" && !item.getProperty("disambiguate-condition")); - } else if(attribute == "locator") { - exists = citationItem && citationItem.locator && - (citationItem.locatorType == variables[j] - || (!citationItem.locatorType && variables[j] == "page")); - } else { // attribute == "position" - if(variables[j] == "first") { - exists = !citationItem - || !citationItem.position - || citationItem.position == Zotero.CSL.POSITION_FIRST; - } else if(variables[j] == "subsequent") { - exists = citationItem && citationItem.position >= Zotero.CSL.POSITION_SUBSEQUENT; - } else if(variables[j] == "ibid") { - exists = citationItem && citationItem.position >= Zotero.CSL.POSITION_IBID; - } else if(variables[j] == "ibid-with-locator") { - exists = citationItem && citationItem.position == Zotero.CSL.POSITION_IBID_WITH_LOCATOR; - } - } - - if(matchAny) { - if(exists) { - truthValue = true; - done = true; - } - } else if(matchNone) { - if(exists) { - truthValue = false; - done = true; - } - } else if(!exists) { - truthValue = false; - done = true; - } - } - } - } - } - - if(truthValue) { - // if true, process - var newString = formattedString.clone(newChild.@delimiter.toString()); - var success = this._processElements(item, newChild, - newString, context, citationItem, ignore); - if(success) dataAppended = true; - formattedString.concat(newString, child); - - // then break - break; - } - } - } else { - Zotero.debug("CSL: WARNING: could not add element "+name); - } - } - - return dataAppended; -} - -/* - * Compares two items, in order to sort the reference list - * Returns -1 if A comes before B, 1 if B comes before A, or 0 if they are equal - */ -Zotero.CSL.prototype._compareItem = function(a, b, context, cache) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); - - var sortA = []; - var sortB = []; - - var aID = a.id; - var bID = b.id; - - // author - if(context.sort.key.length()) { - var keyA, keyB; - for each(var key in context.sort.key) { - if(key.@macro.length()) { - var aCacheKey = aID+"-macro-"+key.@macro; - var bCacheKey = bID+"-macro-"+key.@macro; - - if(cache[aCacheKey]) { - keyA = cache[aCacheKey]; - } else { - keyA = new Zotero.CSL.SortString(); - this._processElements(a, this._csl.macro.(@name == key.@macro), keyA); - cache[aCacheKey] = keyA; - } - - if(cache[bCacheKey]) { - keyB = cache[bCacheKey]; - } else { - keyB = new Zotero.CSL.SortString(); - this._processElements(b, this._csl.macro.(@name == key.@macro), keyB); - cache[bCacheKey] = keyB; - } - } else if(key.@variable.length()) { - var variable = key.@variable.toString(); - var keyA = new Zotero.CSL.SortString(); - var keyB = new Zotero.CSL.SortString(); - - if(Zotero.CSL._dateVariables[variable]) { // date - var date = a.getDate(variable); - if(date) keyA.append(date.getDateVariable("sort")); - date = b.getDate(variable); - if(date) keyB.append(date.getDateVariable("sort")); - } else if(Zotero.CSL._namesVariables[key.@variable]) { // names - var element = <names><name/></names>; - element.setNamespace(Zotero.CSL.Global.ns); - - this._processNames(a, element, keyA, context, null, [variable]); - this._processNames(b, element, keyB, context, null, [variable]); - } else { // text - if(variable == "citation-number") { - keyA.append(a.getProperty(variable)); - keyB.append(b.getProperty(variable)); - } else { - keyA.append(a.getVariable(variable)); - keyB.append(b.getVariable(variable)); - } - } - } - - var compare = keyA.compare(keyB); - if(key.@sort == "descending") { // the compare method sorts ascending - // so we sort descending by reversing it - if(compare < 1) return 1; - if(compare > 1) return -1; - } else if(compare != 0) { - return compare; - } - } - } - - // sort by index in document - var aIndex = a.getProperty("index"); - var bIndex = b.getProperty("index"); - if(aIndex !== "" && (bIndex === "" || aIndex < bIndex)) { - return -1; - } else if(aIndex != bIndex) { - return 1; - } - - // sort by old index (to make this a stable sort) - var aOldIndex = a.getProperty("oldIndex"); - var bOldIndex = b.getProperty("oldIndex"); - if(aOldIndex < bOldIndex) { - return -1; - } else if(aOldIndex != bOldIndex) { - return 1; - } - - return 0; -} - - -/** - * Sorts a list of items, keeping a cache of processed keys - **/ -Zotero.CSL.prototype.cachedSort = function(items, context, field) { - var me = this; - var cache = new Object(); - - for(var i=0; i<items.length; i++) { - if(items[i].setProperty) items[i].setProperty("oldIndex", i); - } - - if(field) { - var newItems = items.sort(function(a, b) { - return me._compareItem(a[field], b[field], context, cache); - }); - } else { - var newItems = items.sort(function(a, b) { - return me._compareItem(a, b, context, cache); - }); - } - - delete cache; - return newItems; -} - -Zotero.CSL.prototype.getEqualCitations = function(items) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); - - var citationsEqual = []; - - if(items) { - var context = this._csl.citation; - - var string = new Zotero.CSL.FormattedString(context.options, "Text"); - this._processElements(items[0], context.layout, string, - context, "subsequent"); - var lastString = string.get(); - - for(var i=1; i<items.length; i++) { - string = new Zotero.CSL.FormattedString(context.option, "Text"); - this._processElements(items[i], context.layout, string, - context, "subsequent"); - string = string.get(); - - citationsEqual[i] = string == lastString; - lastString = string; - } - } - - return citationsEqual; -} - -/* - * Compares two citations; returns true if they are different, false if they are equal - */ -Zotero.CSL.prototype.compareCitations = function(a, b, context) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); - - if((!a && b) || (a && !b)) { - return true; - } else if(!a && !b) { - return false; - } - - var option = (context ? context.option : null); - var aString = new Zotero.CSL.FormattedString(option, "Text"); - this._processElements(a, this._csl.citation.layout, aString, - context, "subsequent"); - - var bString = new Zotero.CSL.FormattedString(option, "Text"); - this._processElements(b, this._csl.citation.layout, bString, - context, "subsequent"); - - return !(aString.get() == bString.get()); -} - -Zotero.CSL.Global = new function() { - this.init = init; - this.getMonthStrings = getMonthStrings; - this.getLocatorStrings = getLocatorStrings; - this.cleanXML = cleanXML; - this.parseLocales = parseLocales; - - default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); - this.ns = "http://purl.org/net/xbiblio/csl"; - - this.__defineGetter__("locale", function() { - Zotero.CSL.Global.init() - return Zotero.CSL.Global._xmlLang; - }); - this.collation = Components.classes["@mozilla.org/intl/collation-factory;1"] - .getService(Components.interfaces.nsICollationFactory) - .CreateCollation(Components.classes["@mozilla.org/intl/nslocaleservice;1"] - .getService(Components.interfaces.nsILocaleService) - .getApplicationLocale()); - - var locatorTypeTerms = ["page", "book", "chapter", "column", "figure", "folio", - "issue", "line", "note", "opus", "paragraph", "part", "section", - "volume", "verse"]; - - /* - * initializes CSL interpreter - */ - function init() { - if(!Zotero.CSL.Global._xmlLang) { - var prefix = "chrome://zotero/content/locale/csl/locales-"; - var ext = ".xml"; - - // If explicit bib locale, try to use that - var bibLocale = Zotero.Prefs.get('export.bibliographyLocale'); - if (bibLocale) { - var loc = bibLocale; - var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]. - createInstance(); - req.open("GET", prefix + loc + ext, false); - req.overrideMimeType("text/plain"); - var fail = false; - try { - req.send(null); - } - catch (e) { - fail = true; - } - - if (!fail) { - Zotero.CSL.Global._xmlLang = loc; - var xml = req.responseText; - } - } - - // If no or invalid bib locale, try Firefox locale - if (!xml) { - var loc = Zotero.locale; - var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]. - createInstance(); - req.open("GET", prefix + loc + ext, false); - req.overrideMimeType("text/plain"); - var fail = false; - try { - req.send(null); - } - catch (e) { - fail = true; - } - - if (!fail) { - Zotero.CSL.Global._xmlLang = loc; - var xml = req.responseText; - } - } - - // Fall back to en-US if no locales.xml - if (!xml) { - var loc = 'en-US'; - var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]. - createInstance(); - req.open("GET", prefix + loc + ext, false); - req.overrideMimeType("text/plain"); - req.send(null); - - Zotero.CSL.Global._xmlLang = loc; - var xml = req.responseText; - } - - Zotero.debug('CSL: Using ' + loc + ' as bibliography locale'); - - // get default terms - var locales = new XML(Zotero.CSL.Global.cleanXML(xml)); - Zotero.CSL.Global._defaultTerms = Zotero.CSL.Global.parseLocales(locales, true); - } - } - - /* - * returns an array of short or long month strings - */ - function getMonthStrings(form) { - Zotero.CSL.Global.init(); - return Zotero.CSL.Global._defaultTerms[form]["_months"]; - } - - /* - * returns an array of short or long locator strings - */ - function getLocatorStrings(form) { - if(!form) form = "long"; - - Zotero.CSL.Global.init(); - var locatorStrings = new Object(); - for(var i=0; i<locatorTypeTerms.length; i++) { - var term = locatorTypeTerms[i]; - var termKey = term; - if(term == "page") termKey = ""; - locatorStrings[termKey] = Zotero.CSL.Global._defaultTerms[form][term]; - - if(!locatorStrings[termKey] && form == "symbol") { - locatorStrings[termKey] = Zotero.CSL.Global._defaultTerms["short"][term]; - } - if(!locatorStrings[termKey]) { - locatorStrings[termKey] = Zotero.CSL.Global._defaultTerms["long"][term]; - } - - // use singular form - if(typeof(locatorStrings[termKey]) == "object") locatorStrings[termKey] = locatorStrings[termKey][0]; - } - return locatorStrings; - } - - /* - * removes parse instructions from XML - */ - function cleanXML(xml) { - return xml.replace(/<\?[^>]*\?>/g, ""); - } - - /* - * parses locale strings into an array; - */ - function parseLocales(termXML, ignoreLang) { - // return defaults if there are no terms - if(!termXML.length()) { - return (Zotero.CSL.Global._defaultTerms ? Zotero.CSL.Global._defaultTerms : {}); - } - - var xml = new Namespace("http://www.w3.org/XML/1998/namespace"); - - if(ignoreLang) { - // ignore lang if loaded from chrome - locale = termXML.locale[0]; - } else { - // get proper locale - var locale = termXML.locale.(@xml::lang == Zotero.CSL.Global._xmlLang); - if(!locale.length()) { - var xmlLang = Zotero.CSL.Global._xmlLang.substr(0, 2); - locale = termXML.locale.(@xml::lang == xmlLang); - } - if(!locale.length()) { - // return defaults if there are no locales - return (Zotero.CSL.Global._defaultTerms ? Zotero.CSL.Global._defaultTerms : {}); - } - } - - var termArray = new Object(); - termArray["default"] = new Object(); - - if(Zotero.CSL.Global._defaultTerms) { - // ugh. copy default array. javascript dumb. - for(var i in Zotero.CSL.Global._defaultTerms) { - termArray[i] = new Object(); - for(var j in Zotero.CSL.Global._defaultTerms[i]) { - if(typeof(Zotero.CSL.Global._defaultTerms[i]) == "object") { - termArray[i][j] = [Zotero.CSL.Global._defaultTerms[i][j][0], - Zotero.CSL.Global._defaultTerms[i][j][1]]; - } else { - termArray[i][j] = Zotero.CSL.Global_defaultTerms[i][j]; - } - } - } - } - - // loop through terms - for each(var term in locale.term) { - var name = term.@name.toString(); - if(!name) { - throw("CSL: citations cannot be generated: no name defined on term in locales.xml"); - } - // unless otherwise specified, assume "long" form - var form = term.@form.toString(); - if(!form) { - var form = "long"; - } - if(!termArray[form]) { - termArray[form] = new Object(); - } - - var single = term.single.text().toString(); - var multiple = term.multiple.text().toString(); - if(single || multiple) { - if((single && multiple) // if there's both elements or - || !termArray[form][name]) { // no previously defined value - termArray[form][name] = [single, multiple]; - } else { - if(typeof(termArray[name]) != "object") { - // if old object was just a single value, make it two copies - termArray[form][name] = [termArray[form][name], termArray[form][name]]; - } - - // redefine either single or multiple - if(single) { - termArray[form][name][0] = single; - } else { - termArray[form][name][1] = multiple; - } - } - } else { - if(name.substr(0, 6) == "month-") { - // place months into separate array - if(!termArray[form]["_months"]) { - termArray[form]["_months"] = new Array(); - } - var monthIndex = parseInt(name.substr(6),10)-1; - var term = term.text().toString(); - termArray[form]["_months"][monthIndex] = term[0].toUpperCase()+term.substr(1).toLowerCase(); - } else { - termArray[form][name] = term.text().toString(); - } - } - } - - // ensure parity between long and short months - var longMonths = termArray["long"]["_months"]; - var shortMonths = termArray["short"]["_months"]; - for(var i=0; i<longMonths.length; i++) { - if(!shortMonths[i]) { - shortMonths[i] = longMonths[i]; - } - } - - return termArray; - } -} - -/* - * the CitationItem object represents an individual source within a citation. - * - * PROPERTIES: - * prefix - * suffix - * locatorType - * locator - * suppressAuthor - * item - * itemID - */ -Zotero.CSL.CitationItem = function(item) { - if(item) { - this.item = item; - this.itemID = item.id; - } -} - -/* - * the Citation object represents a citation. - */ -Zotero.CSL.Citation = function(citationItems, csl) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); - - if(csl) { - this._csl = csl; - this._citation = csl._csl.citation; - this.sortable = this._citation.sort.key.length(); - } else { - this.sortable = false; - } - - this.citationItems = []; - if(citationItems) this.add(citationItems); - - // reserved for application use - this.properties = {}; -} - -/* - * sorts a citation - */ -Zotero.CSL.Citation.prototype.sort = function() { - if(this.sortable) { - this.citationItems = this._csl.cachedSort(this.citationItems, this._citation, "item"); - } -} - -/* - * adds a citationItem to a citation - */ -Zotero.CSL.Citation.prototype.add = function(citationItems) { - for(var i=0; i<citationItems.length; i++) { - var citationItem = citationItems[i]; - - if(citationItem instanceof Zotero.CSL.Item - || citationItem instanceof Zotero.Item) { - this.citationItems.push(new Zotero.CSL.CitationItem(citationItem)); - } else { - this.citationItems.push(citationItem); - } - } -} - -/* - * removes a citationItem from a citation - */ -Zotero.CSL.Citation.prototype.remove = function(citationItems) { - for each(var citationItem in citationItems){ - var index = this.citationItems.indexOf(citationItem); - if(index == -1) throw "Zotero.CSL.Citation: tried to remove an item not in citation"; - this.citationItems.splice(index, 1); - } -} - -/* - * copies a citation - */ -Zotero.CSL.Citation.prototype.clone = function() { - var clone = new Zotero.CSL.Citation(); - - // copy items - for(var i=0; i<this.citationItems.length; i++) { - var oldCitationItem = this.citationItems[i]; - var newCitationItem = new Zotero.CSL.CitationItem(); - for(var key in oldCitationItem) { - newCitationItem[key] = oldCitationItem[key]; - } - clone.citationItems.push(newCitationItem); - } - - // copy properties - for(var key in this.properties) { - clone.properties[key] = this.properties[key]; - } - - return clone; -} - -/* - * This is an item wrapper class for Zotero items. If converting this code to - * work with another application, this is what needs changing. Potentially, this - * function could accept an ID or an XML data structure instead of an actual - * item, provided it implements the same public interfaces (those not beginning - * with "_") are implemented. - */ -Zotero.CSL.Item = function(item) { - if(item instanceof Zotero.Item) { - this.zoteroItem = item; - } else if(parseInt(item, 10) == item) { - // is an item ID - this.zoteroItem = Zotero.Items.get(item); - } - - if(!this.zoteroItem) { - throw "Zotero.CSL.Item called to wrap a non-item"; - } - - this.id = this.zoteroItem.id; - this.key = this.zoteroItem.key; - - // don't return URL or accessed information for journal articles if a - // pages field exists - var itemType = Zotero.ItemTypes.getName(this.zoteroItem.itemTypeID); - if(!Zotero.Prefs.get("export.citePaperJournalArticleURL") - && ["journalArticle", "newspaperArticle", "magazineArticle"].indexOf(itemType) !== -1 - && this.zoteroItem.getField("pages")) { - this._ignoreURL = true; - } - - this._properties = {}; - this._refreshItem(); -} - - -/** - * Returns some identifier for the item. Used to create citations. In Zotero, - * this is the item ID - * - * @deprecated - **/ -Zotero.CSL.Item.prototype.getID = function() { - Zotero.debug("Zotero.CSL.Item.getID() deprecated; use Zotero.CSL.Item.id"); - return this.zoteroItem.id; -} - -/** - * Refreshes item if it has been modified - */ -Zotero.CSL.Item.prototype._refreshItem = function() { - var previousChanged = this._lastChanged; - this._lastChanged = this.zoteroItem.getField("dateModified", false, true); - - if(this._lastChanged != previousChanged) { - this._names = undefined; - this._dates = {}; - } -} - -/* - * Mappings for names - */ -Zotero.CSL.Item._zoteroNameMap = { - "collection-editor":"seriesEditor" -} - -/* - * Gets an array of Item.Name objects for a variable. - */ -Zotero.CSL.Item.prototype.getNames = function(variable) { - var field = Zotero.CSL.Item._zoteroNameMap[variable]; - if (field) variable = field; - this._refreshItem(); - if(!this._names) { - this._separateNames(); - } - - if(this._names[variable]) { - return this._names[variable]; - } - return []; -} - -/* - * Gets an Item.Date object for a specific type. - */ -Zotero.CSL.Item.prototype.getDate = function(variable) { - // ignore accessed date - if(this._ignoreURL && variable == "accessed") return false; - - // load date variable if possible - this._refreshItem(); - if(this._dates[variable] == undefined) { - this._createDate(variable); - } - - if(this._dates[variable]) return this._dates[variable]; - return false; -} - -Zotero.CSL.Item._zoteroFieldMap = { - "long":{ - "title":"title", - "container-title":["publicationTitle", "reporter", "code"], /* reporter and code should move to SQL mapping tables */ - "collection-title":["seriesTitle", "series"], - "collection-number":"seriesNumber", - "publisher":["publisher", "distributor"], /* distributor should move to SQL mapping tables */ - "publisher-place":"place", - "page":"pages", - "volume":"volume", - "issue":"issue", - "number-of-volumes":"numberOfVolumes", - "edition":"edition", - "version":"version", - "section":"section", - "genre":["type", "artworkSize"], /* artworkSize should move to SQL mapping tables, or added as a CSL variable */ - "medium":"medium", - "archive":"repository", - "archive_location":"archiveLocation", - "event":["meetingName", "conferenceName"], /* these should be mapped to the same base field in SQL mapping tables */ - "event-place":"place", - "abstract":"abstractNote", - "URL":"url", - "DOI":"DOI", - "ISBN" : "ISBN", - "call-number":"callNumber", - "note":"extra", - "number":"number", - "references":"history" - }, - "short":{ - "title":["shortTitle", "title"], - "container-title":"journalAbbreviation", - "genre":["shortTitle", "type"] /* needed for subsequent citations of items with no title */ - } -} - -/* - * Gets a text object for a specific type. - */ -Zotero.CSL.Item.prototype.getVariable = function(variable, form) { - if(!Zotero.CSL.Item._zoteroFieldMap["long"][variable]) return ""; - - // ignore URL - if(this._ignoreURL && variable == "URL") return "" - - var zoteroFields = []; - var field; - - if(form == "short" && Zotero.CSL.Item._zoteroFieldMap["short"][variable]) { - field = Zotero.CSL.Item._zoteroFieldMap["short"][variable]; - if(typeof field == "string") { - zoteroFields.push(field); - } else { - zoteroFields = zoteroFields.concat(field); - } - } - - field = Zotero.CSL.Item._zoteroFieldMap["long"][variable]; - if(typeof field == "string") { - zoteroFields.push(field); - } else { - zoteroFields = zoteroFields.concat(field); - } - - for each(var zoteroField in zoteroFields) { - var value = this.zoteroItem.getField(zoteroField, false, true); - if(value != "") return value; - } - - return ""; -} - -/* - * convert a number into a ordinal number 1st, 2nd, 3rd etc. - */ -Zotero.CSL.Item.prototype.makeOrdinal = function(value) { - var ind = parseInt(value); - var daySuffixes = Zotero.getString("date.daySuffixes").replace(/, ?/g, "|").split("|"); - value += (parseInt(ind/10)%10) == 1 ? daySuffixes[3] : (ind % 10 == 1) ? daySuffixes[0] : (ind % 10 == 2) ? daySuffixes[1] : (ind % 10 == 3) ? daySuffixes[2] : daySuffixes[3]; - return value; -} - - -Zotero.CSL.Item._zoteroRomanNumerals = { - "0" : [ "", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix" ], - "1" : [ "", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc" ], - "2" : [ "", "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm" ], - "3" : [ "", "m", "mm", "mmm", "mmmm", "mmmmm"], - } - -/* - * Convert a number into a roman numeral. - */ -Zotero.CSL.Item.prototype.makeRoman = function(value) { - - var number = parseInt(value); - var result = ""; - if (number > 5000) return ""; - var thousands = parseInt(number/1000); - if (thousands > 0) { - result += Zotero.CSL.Item._zoteroRomanNumerals[3][thousands]; - } - number = number % 1000; - var hundreds = parseInt(number/100); - if (hundreds > 0) { - result += Zotero.CSL.Item._zoteroRomanNumerals[2][hundreds]; - } - number = number % 100; - var tens = parseInt(number/10); - if (tens > 0) { - result += Zotero.CSL.Item._zoteroRomanNumerals[1][tens]; - } - number = number % 10; - if (number > 0) { - result += Zotero.CSL.Item._zoteroRomanNumerals[0][number]; - } - return result; -} - -Zotero.CSL.Item._zoteroNumberFieldMap = { - "volume":"volume", - "issue":"issue", - "number-of-volumes":"numberOfVolumes", - "edition":"edition", - "number":"number" -} -/* - * Gets a numeric object for a specific type. <number variable="edition" form="roman"/> - */ -Zotero.CSL.Item.prototype.getNumericVariable = function(variable, form) { - - if(!Zotero.CSL.Item._zoteroNumberFieldMap[variable]) return ""; - - var zoteroFields = []; - var field; - - field = Zotero.CSL.Item._zoteroNumberFieldMap[variable]; - if(typeof field == "string") { - zoteroFields.push(field); - } else { - zoteroFields = zoteroFields.concat(field); - } - - var matches; - for each(var zoteroField in zoteroFields) { - var value = this.zoteroItem.getField(zoteroField, false, true); - var matches; - if(value != "" && (matches = value.toString().match(Zotero.CSL._numberRegexp)) ) { - value = matches[0]; - if (form == "ordinal") { - return this.makeOrdinal(value); - } - else if (form == "roman") { - return this.makeRoman(value); - } - else - return value; - } - } - return ""; -} - -/* - * Sets an item-specific property to a given value. - */ -Zotero.CSL.Item.prototype.setProperty = function(property, value) { - this._properties[property] = value; -} - -/* - * Sets an item-specific property to a given value. - */ -Zotero.CSL.Item.prototype.getProperty = function(property, value) { - return (this._properties[property] !== undefined ? this._properties[property] : ""); -} - -Zotero.CSL.Item._optionalTypeMap = { - journalArticle:"article-journal", - magazineArticle:"article-magazine", - newspaperArticle:"article-newspaper", - thesis:"thesis", - conferencePaper:"paper-conference", - letter:"personal_communication", - manuscript:"manuscript", - interview:"interview", - film:"motion_picture", - artwork:"graphic", - webpage:"webpage", - report:"report", - bill:"bill", - case:"legal_case", - hearing:"bill", // ?? - patent:"patent", - statute:"bill", // ?? - email:"personal_communication", - map:"map", - blogPost:"webpage", - instantMessage:"personal_communication", - forumPost:"webpage", - audioRecording:"song", // ?? - presentation:"speech", - videoRecording:"motion_picture", - tvBroadcast:"broadcast", - radioBroadcast:"broadcast", - podcast:"song", // ?? - computerProgram:"book" // ?? -}; - -// TODO: check with Elena/APA/MLA on this -Zotero.CSL.Item._fallbackTypeMap = { - book:"book", - bookSection:"chapter", - journalArticle:"article", - magazineArticle:"article", - newspaperArticle:"article", - thesis:"article", - encyclopediaArticle:"chapter", - dictionaryEntry:"chapter", - conferencePaper:"chapter", - letter:"article", - manuscript:"article", - interview:"article", - film:"book", - artwork:"book", - webpage:"article", - report:"book", - bill:"book", - case:"book", - hearing:"book", - patent:"article", - statute:"book", - email:"article", - map:"article", - blogPost:"article", - instantMessage:"article", - forumPost:"article", - audioRecording:"book", - presentation:"article", - videoRecording:"book", - tvBroadcast:"article", - radioBroadcast:"article", - podcast:"article", - computerProgram:"book" -}; - -/* - * Determines whether this item is of a given type - */ -Zotero.CSL.Item.prototype.isType = function(type) { - var zoteroType = Zotero.ItemTypes.getName(this.zoteroItem.itemTypeID); - - return (Zotero.CSL.Item._optionalTypeMap[zoteroType] - && Zotero.CSL.Item._optionalTypeMap[zoteroType] == type) - || (Zotero.CSL.Item._fallbackTypeMap[zoteroType] ? Zotero.CSL.Item._fallbackTypeMap[zoteroType] : "article") == type; -} - -/* - * Separates names into different types. - */ -Zotero.CSL.Item.prototype._separateNames = function() { - this._names = []; - - var authorID = Zotero.CreatorTypes.getPrimaryIDForType(this.zoteroItem.itemTypeID); - - var creators = this.zoteroItem.getCreators(); - for each(var creator in creators) { - if(creator.creatorTypeID == authorID) { - var variable = "author"; - } else { - var variable = Zotero.CreatorTypes.getName(creator.creatorTypeID); - } - - var name = new Zotero.CSL.Item.Name(creator); - - if(!this._names[variable]) { - this._names[variable] = [name]; - } else { - this._names[variable].push(name); - } - } -} - -/* - * Generates an date object for a given variable (currently supported: issued - * and accessed) - */ -Zotero.CSL.Item.prototype._createDate = function(variable) { - // first, figure out what date variable to use. - if(variable == "issued") { - var date = this.zoteroItem.getField("date", false, true); - var sort = this.zoteroItem.getField("date", true, true); - } else if(variable == "accessed") { - var date = this.zoteroItem.getField("accessDate", false, true); - var sort = this.zoteroItem.getField("accessDate", true, true); - } - - if(date) { - this._dates[variable] = new Zotero.CSL.Item.Date(date, sort); - } else { - this._dates[variable] = false; - } -} - -/* - * Date class - */ -Zotero.CSL.Item.Date = function(date, sort) { - this.date = date; - this.sort = sort; -} - -/* - * Should accept the following variables: - * - * year - returns a year (optionally, with attached B.C.) - * month - returns a month (numeric from 0, or, if numeric is not available, long) - * day - returns a day (numeric) - * sort - a date that can be used for sorting purposes - */ -Zotero.CSL.Item.Date.prototype.getDateVariable = function(variable) { - if(this.date) { - if(variable == "sort") { - return this.sort; - } - - if(!this.dateArray) { - this.dateArray = Zotero.Date.strToDate(this.date); - } - - if(this.dateArray[variable] !== undefined && this.dateArray[variable] !== false) { - return this.dateArray[variable]; - } else if(variable == "month") { - if(this.dateArray.part) { - return this.dateArray.part; - } - } - } - - return ""; -} - -/* - * Name class - */ -Zotero.CSL.Item.Name = function(zoteroCreator) { - this._zoteroCreator = zoteroCreator; -} - -/* - * Should accept the following variables: - * - * firstName - first name - * lastName - last name - */ -Zotero.CSL.Item.Name.prototype.getNameVariable = function(variable) { - return this._zoteroCreator.ref[variable] ? this._zoteroCreator.ref[variable] : ""; -} - -/* - * When an array of items are passed to create a new item set, each is wrapped - * in an item wrapper. - */ -Zotero.CSL.ItemSet = function(items, csl) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); - - this.csl = csl; - - this.citation = csl._csl.citation; - this.bibliography = csl._csl.bibliography; - - // collect options - this.options = new Object(); - var options = this.citation.option.(@name.substr(0, 12) == "disambiguate") - + this.bibliography.option.(@name == "subsequent-author-substitute"); - for each(var option in options) { - this.options[option.@name.toString()] = option.@value.toString(); - } - - // check for disambiguate condition - for each(var thisIf in csl._csl..if) { - if(thisIf.@disambiguate.length()) { - this.options["disambiguate-condition"] = true; - break; - } - } - - // check for citation number - for each(var thisText in csl._csl..text) { - if(thisText.@variable == "citation-number") { - this.options["citation-number"] = true; - break; - } - } - - // set sortable - this.sortable = !!this.bibliography.sort.key.length(); - - this.items = []; - this.itemsById = {}; - this.itemsByKey = {}; - - // add items - this.add(items); - - // check which disambiguation options are enabled - this._citationChangingOptions = new Array(); - this._disambiguate = false; - for(var option in this.options) { - if(option.substr(0, 12) == "disambiguate" && this.options[option]) { - this._citationChangingOptions.push(option); - this._disambiguate = true; - } else if(option == "citation-number" && this.options[option]) { - this._citationChangingOptions.push(option); - } - } - - if(!items) { - return; - } - - this.resort(); -} - - -/** - * Gets CSL.Item objects from an item set using their ids - * - * @param {Array} ids An array of ids - * @return {Array} items An array whose indexes correspond to those of ids, whose values are either - * the CSL.Item objects or false - **/ -Zotero.CSL.ItemSet.prototype.getItemsByIds = function(ids) { - var items = []; - for each(var id in ids) { - if(this.itemsById[id] != undefined) { - items.push(this.itemsById[id]); - } else { - items.push(false); - } - } - return items; -} - -/** - * Gets CSL.Item objects from an item set using their keys - * - * @param {Array} keys An array of keys - * @return {Array} items An array whose indexes correspond to those of keys, whose values are either - * the CSL.Item objects or false - **/ -Zotero.CSL.ItemSet.prototype.getItemsByKeys = function(keys) { - var items = []; - for each(var key in keys) { - if(this.itemsByKey[key] != undefined) { - items.push(this.itemsByKey[key]); - } else { - items.push(false); - } - } - return items; -} - -/* - * Adds items to the given item set; must be passed either CSL.Item - * objects or objects that may be wrapped as CSL.Item objects - */ -Zotero.CSL.ItemSet.prototype.add = function(items) { - var newItems = new Array(); - - for(var i in items) { - if(items[i] instanceof Zotero.CSL.Item) { - var newItem = items[i]; - } else { - var newItem = new Zotero.CSL.Item(items[i]); - } - - newItem.setProperty("index", this.items.length); - - this.itemsById[newItem.id] = newItem; - this.itemsByKey[newItem.key] = newItem; - this.items.push(newItem); - newItems.push(newItem); - } - - return newItems; -} - -/* - * Removes items from the item set; must be passed either CSL.Item objects - * or item IDs - */ -Zotero.CSL.ItemSet.prototype.remove = function(items) { - for(var i in items) { - if(!items[i]) continue; - if(items[i] instanceof Zotero.CSL.Item) { - var item = items[i]; - } else { - var item = this.itemsById[items[i]]; - } - this.itemsById[item.id] = undefined; - this.itemsByKey[item.key] = undefined; - this.items.splice(this.items.indexOf(item), 1); - } -} - -/* - * Sorts the item set, also running postprocessing and returning items whose - * citations have changed - */ -Zotero.CSL.ItemSet.prototype.resort = function() { - default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); - - // sort - this.items = this.csl.cachedSort(this.items, this.bibliography); - - // first loop through to collect disambiguation data by item, so we can - // see if any items have changed; also collect last names - var oldOptions = new Array(); - for(var i in this._citationChangingOptions) { - oldOptions[i] = new Array(); - for(var j in this.items) { - if(this.items[j] == undefined) continue; - oldOptions[i][j] = this.items[j].getProperty(this._citationChangingOptions[i]); - this.items[j].setProperty(this._citationChangingOptions[i], ""); - } - } - - var namesByItem = new Object(); - for(var i=0; i<this.items.length; i++) { - var names = this.items[i].getNames("author"); - if(!names) names = this.items[i].getNames("editor"); - if(!names) names = this.items[i].getNames("translator"); - if(!names) names = this.items[i].getNames("recipient"); - if(!names) names = this.items[i].getNames("interviewer"); - if(!names) names = this.items[i].getNames("collection-editor"); - if(!names) continue; - namesByItem[i] = names; - } - - // check where last names are the same but given names are different - if(this.options["disambiguate-add-givenname"]) { - var firstNamesByItem = new Object(); - var allNames = new Object(); - var nameType = new Object(); - - for(var i=0; i<this.items.length; i++) { - var names = namesByItem[i]; - var firstNames = []; - for(var j=0; j<names.length; j++) { - // get firstName and lastName - var m = Zotero.CSL._firstNameRegexp.exec(names[j].getNameVariable("firstName")); - var firstName = m[0].toLowerCase(); - firstNames.push(firstName); - if(!firstName) continue; - var lastName = names[j].getNameVariable("lastName"); - - // add last name - if(!allNames[lastName]) { - allNames[lastName] = [firstName]; - } else if(allNames[lastName].indexOf(firstName) == -1) { - allNames[lastName].push(firstName); - } - } - - firstNamesByItem[i] = firstNames; - } - - // loop through last names - for(var i=0; i<this.items.length; i++) { - if(!namesByItem[i]) continue; - - var nameFormat = new Array(); - for(var j=0; j<namesByItem[i].length; j++) { - var lastName = namesByItem[i][j].getNameVariable("lastName"); - if(nameType[lastName] === undefined) { - // determine how to format name - var theNames = allNames[lastName]; - if(theNames && theNames.length > 1) { - // have two items with identical last names but different - // first names - nameType[lastName] = Zotero.CSL.NAME_USE_INITIAL; - - // check initials to see if any match - var initials = new Object(); - for(var k=0; k<theNames.length; k++) { - if(initials[theNames[k][0]]) { - nameType[lastName] = Zotero.CSL.NAME_USE_FULL; - break; - } - initials[theNames[k][0]] = true; - } - } - } - - nameFormat[j] = nameType[lastName]; - } - - if(nameFormat.length) { - // if some names have special formatting, save - this.items[i].setProperty("disambiguate-add-givenname", nameFormat.join(",")); - } - } - } - - // loop through once to determine where items equal the previous item - if(this._disambiguate && this.items.length) { - var citationsEqual = this.csl.getEqualCitations(this.items, this.citation); - } - - var allNames = {}; - - var lastItem = false; - var lastNames = false; - var lastYear = false; - var citationNumber = 1; - - for(var i=0; i<this.items.length; i++) { - var item = this.items[i]; - if(item == undefined) continue; - - var year = item.getDate("issued"); - if(year) year = year.getDateVariable("year"); - var names = namesByItem[i]; - var disambiguated = false; - - if(this._disambiguate && i != 0 && citationsEqual[i] == true) { - // some options can only be applied if there are actual authors - if(names && lastNames && this.options["disambiguate-add-names"]) { - // try adding names to disambiguate - var oldAddNames = lastItem.getProperty("disambiguate-add-names"); - - // if a different number of names, disambiguation is - // easy, although we should still see if there is a - // smaller number of names that works - var numberOfNames = names.length; - if(numberOfNames > lastNames.length) { - numberOfNames = lastNames.length; - item.setProperty("disambiguate-add-names", numberOfNames+1); - - // have to check old property - if(!oldAddNames || oldAddNames < numberOfNames) { - lastItem.setProperty("disambiguate-add-names", numberOfNames); - } - - disambiguated = true; - } else if(numberOfNames != lastNames.length) { - item.setProperty("disambiguate-add-names", numberOfNames); - - // have to check old property - if(!oldAddNames || oldAddNames < numberOfNames+1) { - lastItem.setProperty("disambiguate-add-names", numberOfNames+1); - } - - disambiguated = true; - } - } - - // now, loop through and see whether there's a - // dissimilarity before the end - var namesDiffer = false; - for(var j=0; j<numberOfNames; j++) { - namesDiffer = (names[j].getNameVariable("lastName") != lastNames[j].getNameVariable("lastName") - || (firstNamesByItem && firstNamesByItem[i][j] != firstNamesByItem[i-1][j])); - if(this.options["disambiguate-add-names"] && namesDiffer) { - item.setProperty("disambiguate-add-names", j+1); - - if(!oldAddNames || oldAddNames < j+1) { - lastItem.setProperty("disambiguate-add-names", j+1); - } - - disambiguated = true; - } - - if(namesDiffer) { - break; - } - } - - // add a year suffix, if the above didn't work - if(!disambiguated && year && !namesDiffer && this.options["disambiguate-add-year-suffix"]) { - var lastDisambiguate = lastItem.getProperty("disambiguate-add-year-suffix"); - if(!lastDisambiguate) { - lastItem.setProperty("disambiguate-add-year-suffix", "a"); - item.setProperty("disambiguate-add-year-suffix", "b"); - } else { - var newDisambiguate = ""; - if(lastDisambiguate.length > 1) { - newDisambiguate = oldLetter.substr(0, lastDisambiguate.length-1); - } - - var charCode = lastDisambiguate.charCodeAt(lastDisambiguate.length-1); - if(charCode == 122) { - // item is z; add another letter - newDisambiguate += "a"; - } else { - // next lowercase letter - newDisambiguate += String.fromCharCode(charCode+1); - } - - item.setProperty("disambiguate-add-year-suffix", newDisambiguate); - } - - disambiguated = true; - } - - // use disambiguate condition if above didn't work - if(!disambiguated && this.options["disambiguate-condition"]) { - var oldCondition = lastItem.getProperty("disambiguate-condition"); - lastItem.setProperty("disambiguate-condition", true); - item.setProperty("disambiguate-condition", true); - - // if we cannot disambiguate with the conditional, revert - if(this.csl.compareCitations(lastItem, item) == 0) { - if(!oldCondition) { - lastItem.setProperty("disambiguate-condition", undefined); - } - item.setProperty("disambiguate-condition", undefined); - } - } - } - - if(this.options["subsequent-author-substitute"] - && lastNames && names.length && lastNames.length == names.length) { - var namesDiffer = false; - for(var j=0; j<names.length; j++) { - namesDiffer = (names[j].getNameVariable("lastName") != lastNames[j].getNameVariable("lastName") - || (names[j].getNameVariable("firstName") != lastNames[j].getNameVariable("firstName"))); - if(namesDiffer) break; - } - - if(!namesDiffer) item.setProperty("subsequent-author-substitute", true); - } - - item.setProperty("citation-number", citationNumber++); - - lastItem = item; - lastNames = names; - lastYear = year; - } - - // find changed citations - var changedCitations = new Array(); - for(var j in this.items) { - if(this.items[j] == undefined) continue; - for(var i in this._citationChangingOptions) { - if(this.items[j].getProperty(this._citationChangingOptions[i]) != oldOptions[i][j]) { - changedCitations.push(this.items[j]); - } - } - } - - return changedCitations; -} - -/* - * Copies disambiguation settings (with the exception of disambiguate-add-year-suffix) - * from one item to another - */ -Zotero.CSL.ItemSet.prototype._copyDisambiguation = function(fromItem, toItem) { - for each(var option in ["disambiguate-add-givenname", "disambiguate-add-names", - "disambiguate-add-title"]) { - var value = fromItem.getProperty(option); - if(value) { - toItem.setProperty(option, value); - } - } -} - -Zotero.CSL.FormattedString = function(context, format, delimiter, subsequent) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); - - this.context = context; - this.option = context ? context.option : new XMLList(); - this.format = format; - this.delimiter = delimiter; - this.string = ""; - this.closePunctuation = ""; - this.closeFormatting = ""; - this.useBritishStyleQuotes = false; - - // insert tab iff second-field-align is on - this.insertTabAfterField = (!subsequent && this.option.(@name == "second-field-align").@value.toString()); - this.insertTabBeforeField = false; - // append line before next - this.prependLine = false; - - if(format == "RTF") { - this._openQuote = "\\uc0\\u8220 "; - this._closeQuote = "\\uc0\\u8221 "; - } else { - this._openQuote = "\u201c"; - this._closeQuote = "\u201d"; - } -} - -Zotero.CSL.FormattedString._punctuation = "!.,?:"; - -/* - * attaches another formatted string to the end of the current one - */ -Zotero.CSL.FormattedString.prototype.concat = function(formattedString, element) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); - - if(!formattedString || !formattedString.string) { - return false; - } - - if(formattedString.format != this.format) { - throw "CSL: cannot concatenate formatted strings: formats do not match"; - } - - var haveAppended = false; - var suffix = false; - if(formattedString.string !== "") { - // first, append the actual string without its suffix - if(element && element.@suffix.length()) { - // don't edit original element - element = element.copy(); - // let us decide to add the suffix - suffix = element.@suffix.toString(); - element.@suffix = []; - } - haveAppended = this.append(formattedString.string, element, false, true); - } - - // if there's close punctuation to append, that also counts - if(formattedString.closePunctuation || formattedString.closeFormatting) { - haveAppended = true; - // add the new close punctuation - this.closeFormatting += formattedString.closeFormatting; - this.closePunctuation += formattedString.closePunctuation; - } - - // append suffix, if we didn't before - if(haveAppended && suffix !== false) this.append(suffix, null, true); - - return haveAppended; -} - -Zotero.CSL.FormattedString._rtfEscapeFunction = function(aChar) { - return "{\\uc0\\u"+aChar.charCodeAt(0).toString()+"}" -} - -/* - * appends a string (with format parameters) to the current one - */ -Zotero.CSL.FormattedString.prototype.append = function(string, element, dontDelimit, dontEscape) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); - - if(!string && string !== 0) return false; - - if(typeof(string) != "string") { - string = string.toString(); - } - - // get prefix - var prefix = ""; - if(element && element.@prefix.length()) { - var prefix = element.@prefix.toString(); - } - - // append tab before if necessary - if(!dontDelimit && this.insertTabBeforeField) { - // replace any space preceding tab - this.string = this.string.replace(/\s+$/, ""); - - if(this.format == "HTML") { - this.string += '</td><td style="padding-left:4pt;">'; - } else if(this.format == "RTF") { - this.string += "\\tab "; - } else if(this.format == "Integration") { - this.string += "\t"; - } else { - this.string += " "; - } - - this.insertTabBeforeField = false; - if(prefix !== "") { - prefix = prefix.replace(/^\s+/, ""); - } else { - string = string.replace(/^\s+/, ""); - } - } - - // append delimiter if necessary - if(this.delimiter && this.string && !dontDelimit) { - this.append(this.delimiter, null, true); - } - - // append prefix before closing punctuation - if(prefix !== "") { - this.append(prefix, null, true); - } - - var addBefore = ""; - var addAfter = ""; - - // prepend line before if display="block" - if(element && (element["@display"] == "block" || this.prependLine)) { - if(this.format == "HTML") { - if(this.option.(@name == "hanging-indent").@value == "true") { - addBefore += '<div style="text-indent:0.5in;">' - } else { - addBefore += '<div>'; - } - addAfter = '</div>'; - } else { - if(this.format == "RTF") { - addBefore += "\r\n\\line "; - } else if(this.format == "Integration") { - addBefore += "\x0B"; - } else { - addBefore += (Zotero.isWin ? "\r\n" : "\n"); - } - this.prependLine = element["@display"] == "block"; - } - } - - // close quotes, etc. using punctuation - if(this.closePunctuation) { - if(Zotero.CSL.FormattedString._punctuation.indexOf(string[0]) != -1) { - this.string += string[0]; - string = string.substr(1); - } - this.string += this.closePunctuation; - this.closePunctuation = ""; - } - - // clean up - if(string.length && string[0] == "." && - Zotero.CSL.FormattedString._punctuation.indexOf(this.string[this.string.length-1]) != -1) { - // if string already ends in punctuation, preserve the existing stuff - // and don't add a period - string = string.substr(1); - } else if(this.string[this.string.length-1] == "(" && string[0] == " ") { - string = string.substr(1); - } else if(this.string[this.string.length-1] == " " && string[0] == ")") { - this.string = this.string.substr(0, this.string.length-1); - } - - // close previous formatting - this.string += this.closeFormatting; - this.closeFormatting = ""; - - // handling of "text-transform" attribute (now obsolete) - if(element && element["@text-transform"].length() && !element["@text-case"].length()) { - var mapping = {"lowercase":"lowercase", "uppercase":"uppercase", "capitalize":"capitalize-first"}; - element["@text-case"] = mapping[element["@text-transform"].toString()]; - } - - // handle text case - if(element) { - if(element["@text-case"].length()) { - if(element["@text-case"] == "lowercase") { - // all lowercase - string = string.toLowerCase(); - } else if(element["@text-case"] == "uppercase") { - // all uppercase - string = string.toUpperCase(); - } else if(element["@text-case"] == "sentence") { - // for now capitalizes only the first letter, the rest are lowercase - string = string[0].toUpperCase()+string.substr(1).toLowerCase(); - } else if(element["@text-case"] == "capitalize-first") { - // capitalize first - string = string[0].toUpperCase()+string.substr(1); - } else if(element["@text-case"] == "capitalize-all") { - // capitalize first - var strings = string.split(" "); - for(var i=0; i<strings.length; i++) { - if(strings[i].length > 1) { - strings[i] = strings[i][0].toUpperCase()+strings[i].substr(1).toLowerCase(); - } else if(strings[i].length == 1) { - strings[i] = strings[i].toUpperCase(); - } - } - string = strings.join(" "); - } else if(element["@text-case"] == "title") { - string = Zotero.Text.titleCase(string); - } - } - - // style attributes - if(this.format == "HTML") { - var style = ""; - - var cssAttributes = ["font-family", "font-style", "font-variant", - "font-weight", "vertical-align", "display", - "text-decoration" ]; - for(var j in cssAttributes) { - var value = element["@"+cssAttributes[j]].toString(); - if(value && value.indexOf('"') == -1) { - style += cssAttributes[j]+":"+value+";"; - } - } - - if(style) { - addBefore += '<span style="'+style+'">'; - addAfter = '</span>'+addAfter; - } - } else { - if(this.format == "RTF" || this.format == "Integration") { - var rtfAttributes = { - "font-style":{"oblique":"i", "italic":"i"}, - "font-variant":{"small-caps":"scaps"}, - "font-weight":{"bold":"b"}, - "text-decoration":{"underline":"ul"}, - "vertical-align":{"sup":"super", "sub":"sub"} - } - - for(var j in rtfAttributes) { - for(var k in rtfAttributes[j]) { - if(element["@"+j] == k) { - addBefore += "\\"+rtfAttributes[j][k]+" "; - addAfter = "\\"+rtfAttributes[j][k]+"0 "+addAfter; - } - } - } - } - } - - // add quotes if necessary - if(element.@quotes == "true") { - this.string += this._openQuote; - - if(this.useBritishStyleQuotes) { - string += this._closeQuote; - } else { - this.closePunctuation = this._closeQuote; - } - } - } - - if(!dontEscape) { - if(this.format == "HTML") { - string = string.replace("&", "&amp;", "g") - .replace("<", "&lt;", "g") - .replace(">", "&gt;", "g") - .replace(/(\r\n|\r|\n)/g, "<br />") - .replace(/[\x00-\x1F]/g, ""); - } else if(this.format == "RTF" || this.format == "Integration") { - string = string.replace("\\", "\\\\", "g") - .replace(/(\r\n|\r|\n)/g, "\\line "); - if(string.substr(string.length-6) == "\\line ") { - string = string.substr(0, string.length-6); - addAfter = "\\line "+addAfter; - } - - if(this.format == "RTF") { - string = string.replace(/[\x7F-\uFFFF]/g, Zotero.CSL.FormattedString._rtfEscapeFunction) - .replace("\t", "\\tab ", "g"); - - if(string.substr(string.length-5) == "\\tab ") { - string = string.substr(0, string.length-5); - addAfter = "\\tab "+addAfter; - } - } - } else { - string = string.replace(/(\r\n|\r|\n)/g, (Zotero.isWin ? "\r\n" : "\n")); - } - } - - this.string += addBefore+string; - - if(element && element.@suffix.length()) { - this.append(element.@suffix.toString(), null, true); - } - - // save for second-field-align - if(!dontDelimit && this.insertTabAfterField) { - this.insertTabAfterField = false; - this.insertTabBeforeField = true; - } - - this.closeFormatting = addAfter; - - return true; -} - -/* - * gets the formatted string - */ -Zotero.CSL.FormattedString.prototype.get = function() { - return this.string+this.closeFormatting+this.closePunctuation; -} - -/* - * creates a new formatted string with the same formatting parameters as this one - */ -Zotero.CSL.FormattedString.prototype.clone = function(delimiter) { - return new Zotero.CSL.FormattedString(this.context, this.format, delimiter, true); -} - -/* - * Implementation of FormattedString for sort purposes. - */ -Zotero.CSL.SortString = function() { - default xml namespace = "http://purl.org/net/xbiblio/csl"; - - this.format = "Sort"; - this.string = []; -} - -Zotero.CSL.SortString.prototype.concat = function(newString) { - if(newString.string.length == 0) { - return; - } else if(newString.string.length == 1) { - this.string.push(newString.string[0]); - } else { - this.string.push(newString.string); - } -} - -Zotero.CSL.SortString.prototype.append = function(newString) { - this.string.push(newString); -} - -Zotero.CSL.SortString.prototype.compare = function(b, a) { - // by default, a is this string - if(a == undefined) { - a = this.string; - b = b.string; - } - - var aIsString = typeof(a) != "object"; - var bIsString = typeof(b) != "object"; - if(aIsString && bIsString) { - if(a == b) { - return 0; - } else if(!isNaN(a % 1) && !isNaN(b % 1)) { - // both numeric - if(b > a) return -1; - return 1; // already know they're not equal - } else { - var cmp = Zotero.CSL.Global.collation.compareString(Zotero.CSL.Global.collation.kCollationCaseInSensitive, a, b); - if(cmp == 0) { - // for some reason collation service returned 0; the collation - // service sucks! they can't be equal! - if(b > a) { - return -1; - } else { - return 1; - } - } - return cmp; - } - } else if(aIsString && !bIsString) { - var cmp = this.compare(b[0], a); - if(cmp == 0) { - return -1; // a before b - } - return cmp; - } else if(bIsString && !aIsString) { - var cmp = this.compare(b, a[0]); - if(cmp == 0) { - return 1; // b before a - } - return cmp; - } - - var maxLength = Math.min(b.length, a.length); - for(var i = 0; i < maxLength; i++) { - var cmp = this.compare(b[i], a[i]); - if(cmp != 0) { - return cmp; - } - } - - if(b.length > a.length) { - return -1; // a before b - } else if(b.length < a.length) { - return 1; // b before a - } - - return 0; -} - - -Zotero.CSL.SortString.prototype.clone = function() { - return new Zotero.CSL.SortString(); -} -\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/csl.js b/chrome/content/zotero/xpcom/csl.js @@ -0,0 +1,2941 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +*/ + +/* + * CSL: a class for creating bibliographies from CSL files + * this is abstracted as a separate class for the benefit of anyone who doesn't + * want to use the Scholar data model, but does want to use CSL in JavaScript + */ +Zotero.CSL = function(csl) { + // "with ({});" needed to fix default namespace scope issue + // See https://bugzilla.mozilla.org/show_bug.cgi?id=330572 + default xml namespace = "http://purl.org/net/xbiblio/csl"; with ({}); + + if(typeof csl != "XML") { + this._csl = new XML(Zotero.CSL.Global.cleanXML(csl)); + } else { + this._csl = CSL; + } + + // initialize CSL + Zotero.CSL.Global.init(); + + // load localizations + this._terms = Zotero.CSL.Global.parseLocales(this._csl.terms); + + // load class and styleID + this.styleID = this._csl.info.id.toString(); + this.class = this._csl["@class"].toString(); + Zotero.debug("CSL: style class is "+this.class); + + this.hasBibliography = (this._csl.bibliography.length() ? 1 : 0); +} + +/* + * Constants for citation positions + */ +Zotero.CSL.POSITION_FIRST = 0; +Zotero.CSL.POSITION_SUBSEQUENT = 1; +Zotero.CSL.POSITION_IBID = 2; +Zotero.CSL.POSITION_IBID_WITH_LOCATOR = 3; + + +Zotero.CSL._dateVariables = { + "issued":true, + "accessDate":true +} + +Zotero.CSL._namesVariables = { + "editor":true, + "translator":true, + "recipient":true, + "interviewer":true, + "collection-editor":true, + "author":true +} + +/* + * Constants for name (used for disambiguate-add-givenname) + */ +Zotero.CSL.NAME_USE_INITIAL = 1; +Zotero.CSL.NAME_USE_FULL = 2; + +/* + * generate an item set + */ +Zotero.CSL.prototype.createItemSet = function(items) { + return new Zotero.CSL.ItemSet(items, this); +} + +/* + * generate a citation object + */ +Zotero.CSL.prototype.createCitation = function(citationItems) { + return new Zotero.CSL.Citation(citationItems, this); +} + +/* + * create a citation (in-text or footnote) + */ +Zotero.CSL._firstNameRegexp = /^[^\s]*/; +Zotero.CSL._textCharRegexp = /[a-zA-Z0-9]/; +Zotero.CSL._numberRegexp = /\d+/; +Zotero.CSL.prototype.formatCitation = function(citation, format) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + var context = this._csl.citation; + if(!context) { + throw "CSL: formatCitation called on style with no citation context"; + } + if(!citation.citationItems.length) { + throw "CSL: formatCitation called with empty citation"; + } + + // clone citationItems, so as not to disturb the citation + var citationItems = citation.citationItems; + + // handle collapse + var cslAdded = []; + + var collapse = context.option.(@name == "collapse").@value.toString(); + if(collapse) { + // clone citationItems, so as not to disturb the citation + citationItems = new Array(); + + if(collapse == "citation-number") { + // loop through, collecting citation numbers + var citationNumbers = new Object(); + for(var i=0; i<citation.citationItems.length; i++) { + citationNumbers[citation.citationItems[i].item.getProperty("citation-number")] = i; + } + // add -1 at the end so that the last span gets added (loop below + // must be run once more) + citationNumbers[-1] = false; + + var previousI = -1; + var span = []; + // loop through citation numbers and collect ranges in span + for(var i in citationNumbers) { + if(i != -1 && !citation.citationItems[citationNumbers[i]].prefix + && !citation.citationItems[citationNumbers[i]].suffix + && i == parseInt(previousI, 10)+1) { + // could be part of a range including the previous number + span.push(citationNumbers[i]); + } else { // not part of a range + if(span.length) citationItems[span[0]] = citation.citationItems[span[0]]; + if(span.length > 2) { + // if previous set of citations was a range, collapse them + var firstNumber = citationItems[span[0]].item.getProperty("citation-number"); + citationItems[span[0]]._csl = {"citation-number":(firstNumber+"-"+(parseInt(firstNumber, 10)+span.length-1))}; + cslAdded.push(span[0]); + } else if(span.length == 2) { + citationItems[span[1]] = citation.citationItems[span[1]]; + } + + span = [citationNumbers[i]]; + } + previousI = i; + } + } else if(collapse.substr(0, 4) == "year") { + // loop through, collecting citations (sans date) in an array + var lastNames = {}; + for(var i=0; i<citation.citationItems.length; i++) { + var citationString = new Zotero.CSL.FormattedString(context, format); + this._processElements(citation.citationItems[i].item, context.layout, citationString, + context, null, [{"issued":true}, {}]); + var cite = citationString.get(); + + // put into lastNames array + if(!lastNames[cite]) { + lastNames[cite] = [i]; + } else { + lastNames[cite].push(i); + } + } + + for(var i in lastNames) { + var itemsSharingName = lastNames[i]; + if(itemsSharingName.length == 1) { + // if only one, don't worry about grouping + citationItems[itemsSharingName[0]] = citation.citationItems[itemsSharingName[0]]; + } else { + var years = []; + // if grouping by year-suffix, we need to do more (to pull + // together various letters) + if(collapse == "year-suffix" && context.option.(@name == "disambiguate-add-year-suffix").@value == "true") { + var yearsArray = new Object(); + for(var j=0; j<itemsSharingName.length; j++) { + var year = citation.citationItems[itemsSharingName[j]].item.getDate("issued"); + if(year) { + year = year.getDateVariable("year"); + if(year) { + // add to years + if(!yearsArray[year]) { + yearsArray[year] = [itemsSharingName[j]]; + } else { + yearsArray[year].push(itemsSharingName[j]); + } + } + } + + if(!year) { + // if no year, just copy + years.push(""); + } + } + + // loop through all years + for(var j in yearsArray) { + var citationItem = citation.citationItems[yearsArray[j][0]]; + + // push first year with any suffix + var year = j; + var suffix = citationItem.item.getProperty("disambiguate-add-year-suffix"); + if(suffix) year += suffix; + years.push(year); + + // also push subsequent years + if(yearsArray[j].length > 1) { + for(k=1; k<yearsArray[j].length; k++) { + var suffix = citation.citationItems[yearsArray[j][k]].item.getProperty("disambiguate-add-year-suffix"); + if(suffix) years.push(suffix); + } + } + } + } else { + // just add years + for(var j=0; j<itemsSharingName.length; j++) { + var item = citation.citationItems[itemsSharingName[j]].item; + var year = item.getDate("issued"); + if(year) { + years[j] = year.getDateVariable("year"); + var suffix = item.getProperty("disambiguate-add-year-suffix"); + if(suffix) years[j] += suffix; + } + } + } + citation.citationItems[itemsSharingName[0]]._csl = {"issued":{"year":years.join(", ")}}; + citationItems[itemsSharingName[0]] = citation.citationItems[itemsSharingName[0]]; + cslAdded.push(itemsSharingName[0]); + } + } + } + } + + var string = new Zotero.CSL.FormattedString(context, format, context.layout.@delimiter.toString()); + for(var i=0; i<citationItems.length; i++) { + var citationItem = citationItems[i]; + if(!citationItem) continue; + + var citationString = string.clone(); + + // suppress author if requested + var ignore = citationItem.suppressAuthor ? [{"author":true}, {}] : undefined; + + // add prefix + if(citationItem.prefix) { + var prefix = citationItem.prefix; + + // add space to prefix if last char is alphanumeric + if(Zotero.CSL._textCharRegexp.test(prefix[prefix.length-1])) prefix += " "; + + citationString.append(prefix); + } + + this._processElements(citationItem.item, context.layout, citationString, + context, citationItem, ignore); + + // add suffix + if(citationItem.suffix) { + var suffix = citationItem.suffix; + + // add space to suffix if last char is alphanumeric + if(Zotero.CSL._textCharRegexp.test(suffix[0])) suffix = " "+suffix; + + citationString.append(suffix); + } + + string.concat(citationString); + } + + var returnString = string.clone(); + returnString.concat(string, context.layout); + var returnString = returnString.get(); + + // loop through to remove _csl property + for(var i=0; i<cslAdded.length; i++) { + citationItems[cslAdded[i]]._csl = undefined; + } + + return returnString; +} + +/* + * create a bibliography + */ +Zotero.CSL.prototype.formatBibliography = function(itemSet, format) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + var context = this._csl.bibliography; + if(!context.length()) { + context = this._csl.citation; + var isCitation = true; + } + if(!context) { + throw "CSL: formatBibliography called on style with no bibliography context"; + } + + if(!itemSet.items.length) return ""; + + var hangingIndent = !!(context.option.(@name == "hanging-indent").@value == "true"); + var secondFieldAlign = context.option.(@name == "second-field-align").@value.toString(); + var lineSpacing = context.option.(@name == "line-spacing").@value.toString(); + lineSpacing = (lineSpacing === "" ? 1 : parseInt(lineSpacing, 10)); + if(lineSpacing == NaN) throw "Invalid line spacing"; + var entrySpacing = context.option.(@name == "entry-spacing").@value.toString(); + entrySpacing = (entrySpacing === "" ? 1 : parseInt(entrySpacing, 10)); + if(entrySpacing == NaN) throw "Invalid entry spacing"; + + var index = 0; + var output = ""; + var preamble = ""; + if(format == "HTML") { + if(this.class == "note" && isCitation) { + // note citations are formatted as an ordered list + preamble = '<ol>\r\n'; + secondFieldAlign = false; + } else { + // needed bc HTML doesn't force lines to be at least as big as the + // tallest character + if(lineSpacing <= 1.1) lineSpacing = 1.1; + + // add style + var style = 'line-height:'+lineSpacing+'em;' + if(hangingIndent) { + style += 'margin-left:0.5in;text-indent:-0.5in;'; + } + + if(secondFieldAlign) { + preamble += '<table style="border-collapse:collapse;'+style+'">\r\n'; + } else { + preamble += '<div style="'+style+'">\r\n'; + } + } + } else { + if(format == "RTF" || format == "Integration") { + if(format == "RTF") { + preamble = "{\\rtf\\ansi{\\fonttbl\\f0\\froman Times New Roman;}{\\colortbl;\\red255\\green255\\blue255;}\\pard\\f0\r\n"; + } + + var tabStop = null; + if(hangingIndent) { + var indent = 720; // 720 twips = 0.5 in + var firstLineIndent = -720; // -720 twips = -0.5 in + } else { + var indent = 0; + var firstLineIndent = 0; + } + } + + var returnChars = ""; + for(j=0; j<=entrySpacing; j++) { + if(format == "RTF") { + returnChars += "\\\r\n"; + } else if(Zotero.isWin) { + returnChars += "\r\n"; + } else { + returnChars += "\n"; + } + } + } + + var maxFirstFieldLength = 0; + for(var i in itemSet.items) { + var item = itemSet.items[i]; + if(item == undefined) continue; + + // try to get custom bibliography + var string = item.getProperty("bibliography-"+(format == "Integration" ? "RTF" : format)); + if(!string) { + string = new Zotero.CSL.FormattedString(context, format); + this._processElements(item, context.layout, string, context); + if(!string) { + continue; + } + + // add format + string.string = context.layout.@prefix.toString() + string.string; + if(context.layout.@suffix.length()) { + string.append(context.layout.@suffix.toString()); + } + + string = string.get(); + } + + if(secondFieldAlign && (format == "RTF" || format == "Integration")) { + if(format == "RTF") { + var tab = string.indexOf("\\tab "); + } else { + var tab = string.indexOf("\t"); + } + if(tab > maxFirstFieldLength) { + maxFirstFieldLength = tab; + } + } + + // add line feeds + if(format == "HTML") { + var coins = Zotero.OpenURL.createContextObject(item.zoteroItem, "1.0"); + + var span = (coins ? ' <span class="Z3988" title="'+coins.replace("&", "&amp;", "g")+'">&nbsp;</span>' : ''); + + if(this.class == "note" && isCitation) { + output += "<li>"+string+span+"</li>\r\n"; + } else if(secondFieldAlign) { + output += '<tr style="vertical-align:top;"><td>'+string+span+"</td></tr>\r\n"; + for(var j=0; j<entrySpacing; j++) { + output += '<tr><td colspan="2">&nbsp;</td></tr>\r\n'; + } + } else { + if(i == 0) { + // first p has no margins + var margin = "0"; + } else { + var margin = (entrySpacing*lineSpacing).toString()+"em 0 0 0"; + } + output += '<p style="margin:'+margin+'">'+string+span+"</p>\r\n"; + } + } else { + if(this.class == "note" && isCitation) { + if(format == "RTF") { + index++; + output += index+". "; + } else if(format == "Text") { + index++; + output += index+". "; + } + } + output += string+returnChars; + } + } + + if(format == "HTML") { + if(this.class == "note" && isCitation) { + output += '</ol>'; + } else if(secondFieldAlign) { + output += '</table>'; + } else { + output += '</div>'; + } + } else if(format == "RTF" || format == "Integration") { + if(secondFieldAlign) { + // this is a really sticky issue. the below works for first fields + // that look like "[1]" and "1." otherwise, i have no idea. luckily, + // this will be good enough 99% of the time. + var alignAt = 24+maxFirstFieldLength*120; + + if(secondFieldAlign == "margin") { + firstLineIndent -= alignAt; + tabStop = 0; + } else { + indent += alignAt; + firstLineIndent = -indent; + tabStop = indent; + } + } + + preamble += "\\li"+indent+" \\fi"+firstLineIndent+" "; + if(format == "Integration") { + preamble += "\\sl"+lineSpacing+" "; + } else if(format == "RTF" && lineSpacing != 1) { + preamble += "\\sl"+(240*lineSpacing)+" \\slmult1 "; + } + + if(tabStop !== null) { + preamble += "\\tx"+tabStop+" "; + } + preamble += "\r\n"; + + // drop last returns + output = output.substr(0, output.length-returnChars.length); + + // add bracket for RTF + if(format == "RTF") output += "\\par }"; + } + + return preamble+output; +} + +/* + * gets a term, in singular or plural form + */ +Zotero.CSL.prototype._getTerm = function(term, plural, form, includePeriod) { + if(!form) { + form = "long"; + } + + if(!this._terms[form] || !this._terms[form][term]) { + if(form == "verb-short") { + return this._getTerm(term, plural, "verb"); + } else if(form == "symbol") { + return this._getTerm(term, plural, "short"); + } else if(form != "long") { + return this._getTerm(term, plural, "long"); + } else { + Zotero.debug("CSL: WARNING: could not find term \""+term+'"'); + return ""; + } + } + + var term; + if(typeof(this._terms[form][term]) == "object") { // singular and plural forms + // are available + if(plural) { + term = this._terms[form][term][1]; + } else { + term = this._terms[form][term][0]; + } + } else { + term = this._terms[form][term]; + } + + if((form == "short" || form == "verb-short") && includePeriod) { + term += "."; + } + + return term; +} + +/* + * process creator objects; if someone had a creator model that handled + * non-Western names better than ours, this would be the function to change + */ +Zotero.CSL.prototype._processNames = function(item, element, formattedString, context, citationItem, variables) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + var children = element.children(); + if(!children.length()) return false; + var variableSucceeded = false; + + for(var j=0; j<variables.length; j++) { + var success = false; + var newString = formattedString.clone(); + + if(formattedString.format != "Sort" && variables[j] == "author" && context + && context.option.(@name == "subsequent-author-substitute").length() + && item.getProperty("subsequent-author-substitute") + && context.localName() == "bibliography") { + newString.append(context.option.(@name == "subsequent-author-substitute").@value.toString()); + success = true; + } else { + var creators = item.getNames(variables[j]); + + if(creators && creators.length) { + var maxCreators = creators.length; + + for each(var child in children) { + if(child.namespace() != Zotero.CSL.Global.ns) continue; + + var name = child.localName(); + if(name == "name") { + var useEtAl = false; + + if(context) { + // figure out if we need to use "et al" + var etAlMin = context.option.(@name == "et-al-min").@value.toString(); + var etAlUseFirst = context.option.(@name == "et-al-use-first").@value.toString(); + + if(citationItem && citationItem.position + && citationItem.position >= Zotero.CSL.POSITION_SUBSEQUENT) { + if(context.option.(@name == "et-al-subsequent-min").length()) { + etAlMin = context.option.(@name == "et-al-subsequent-min").@value.toString(); + } + if(context.option.(@name == "et-al-subsequent-use-first").length()) { + etAlUseFirst = context.option.(@name == "et-al-subsequent-use-first").@value.toString(); + } + } + + if(etAlMin && etAlUseFirst && maxCreators >= parseInt(etAlMin, 10)) { + etAlUseFirst = parseInt(etAlUseFirst, 10); + if(etAlUseFirst != maxCreators) { + maxCreators = etAlUseFirst; + useEtAl = true; + } + } + + // add additional names to disambiguate + if(variables[j] == "author" && useEtAl) { + var disambigNames = item.getProperty("disambiguate-add-names"); + if(disambigNames != "") { + maxCreators = disambigNames; + if(disambigNames == creators.length) useEtAl = false; + } + } + + if(child.@form == "short") { + var fullNames = item.getProperty("disambiguate-add-givenname").split(","); + } + } + + var authorStrings = []; + var firstName, lastName; + // parse authors into strings + for(var i=0; i<maxCreators; i++) { + if(formattedString.format == "Sort") { + // for sort, we use the plain names + var name = creators[i].getNameVariable("lastName"); + + // cut off lowercase parts of otherwise capitalized names (e.g., "de") + var lastNameParts = name.split(" "); + if(lastNameParts.length > 1 && lastNameParts[0] !== "" && lastNameParts[0].length <= 4 + && lastNameParts[0][0].toLowerCase() == lastNameParts[0][0] + && lastNameParts[lastNameParts.length-1][0].toUpperCase() == lastNameParts[lastNameParts.length-1][0]) { + name = ""; + for(var k=1; k<lastNameParts.length; k++) { + if(lastNameParts[k][0].toUpperCase() == lastNameParts[k][0]) { + name += " "+lastNameParts[k]; + } + } + name = name.substr(1); + } + + var firstName = creators[i].getNameVariable("firstName"); + if(name && firstName) name += ", "; + name += firstName; + + newString.append(name); + } else { + var firstName = ""; + + if(child.@form != "short" || (fullNames && fullNames[i])) { + if(child["@initialize-with"].length() && (!fullNames || + fullNames[i] != Zotero.CSL.NAME_USE_FULL)) { + // even if initialize-with is simply an empty string, use + // initials + + // use first initials + var firstNames = creators[i].getNameVariable("firstName").split(" "); + for(var k in firstNames) { + if(firstNames[k]) { + // get first initial, put in upper case, add initializeWith string + firstName += firstNames[k][0].toUpperCase()+child["@initialize-with"].toString(); + } + } + + if(firstName[firstName.length-1] == " ") { + firstName = firstName.substr(0, firstName.length-1); + } + } else { + firstName = creators[i].getNameVariable("firstName"); + } + } + lastName = creators[i].getNameVariable("lastName"); + + if(child["@name-as-sort-order"].length() + && ((i == 0 && child["@name-as-sort-order"] == "first") + || child["@name-as-sort-order"] == "all") + && child["@sort-separator"].length()) { + // if this is the first author and name-as-sort="first" + // or if this is a subsequent author and name-as-sort="all" + // then the name gets inverted + authorStrings.push(lastName+(firstName ? child["@sort-separator"].toString()+firstName : "")); + } else { + authorStrings.push((firstName ? firstName+" " : "")+lastName); + } + } + } + + if(formattedString.format != "Sort") { + // figure out if we need an "and" or an "et al" + var joinString = (child["@delimiter"].length() ? child["@delimiter"].toString() : ", "); + if(creators.length > 1) { + if(useEtAl) { // multiple creators and need et al + authorStrings.push(this._getTerm("et-al")); + } else { // multiple creators but no et al + // add and to last creator + if(child["@and"].length()) { + if(child["@and"] == "symbol") { + var and = "&" + } else if(child["@and"] == "text") { + var and = this._getTerm("and"); + } + + authorStrings[maxCreators-1] = and+" "+authorStrings[maxCreators-1]; + } + } + + // check whether to use a serial comma + if((authorStrings.length == 2 && (child["@delimiter-precedes-last"] != "always" || useEtAl)) || + (authorStrings.length > 2 && child["@delimiter-precedes-last"] == "never")) { + var lastString = authorStrings.pop(); + authorStrings[authorStrings.length-1] = authorStrings[authorStrings.length-1]+" "+lastString; + } + } + newString.append(authorStrings.join(joinString), child); + } + } else if(formattedString.format != "Sort" && + name == "label" && variables[j] != "author") { + newString.append(this._getTerm(variables[j], (maxCreators != 1), child["@form"].toString(), child["@include-period"] == "true"), child); + } + } + success = true; + } + } + + if(success) { + variableSucceeded = true; + formattedString.concat(newString); + } + } + + return variableSucceeded; +} + +/* + * processes an element from a (pre-processed) item into text + */ +Zotero.CSL.prototype._processElements = function(item, element, formattedString, + context, citationItem, ignore, isSingle) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + if(!ignore) { + ignore = [[], []]; + // ignore[0] is for variables; ignore[1] is for macros + } + + var dataAppended = false; + + if(isSingle) { + // handle single elements + var numberOfChildren = 1; + var children = [element]; + } else { + // accept groups of elements by default + var children = element.children(); + var numberOfChildren = children.length(); + var lastChild = children.length()-1; + } + + for(var i=0; i<numberOfChildren; i++) { + var child = children[i]; + if(child.namespace() != Zotero.CSL.Global.ns) continue; + var name = child.localName(); + + if(name == "text") { + if(child["@term"].length()) { + var term = this._getTerm(child["@term"].toString(), child.@plural == "true", child.@form.toString(), child["@include-period"] == "true"); + if(term) { + formattedString.append(term, child); + } + } else if(child.@variable.length()) { + var form = child.@form.toString(); + var variables = child["@variable"].toString().split(" "); + var newString = formattedString.clone(child.@delimiter.toString()); + var success = false; + + for(var j=0; j<variables.length; j++) { + if(ignore[0][variables[j]]) continue; + + if(variables[j] == "locator") { + // special case for locator + var text = citationItem && citationItem.locator ? citationItem.locator : ""; + } else if(citationItem && citationItem._csl && citationItem._csl[variables[j]]) { + // override if requested + var text = citationItem._csl[variables[j]]; + } else if(variables[j] == "citation-number") { + // special case for citation-number + var text = item.getProperty("citation-number"); + } else { + var text = item.getVariable(variables[j], form); + } + + if(text) { + newString.append(text); + success = true; + } + } + + if(success) { + formattedString.concat(newString, child); + dataAppended = true; + } + } else if(child.@macro.length()) { + var macro = this._csl.macro.(@name == child.@macro); + if(!macro.length()) throw "CSL: style references undefined macro " + child.@macro; + + // If not ignored (bc already used as a substitution) + if(!ignore[1][child.@macro.toString()]) { + var newString = formattedString.clone(child.@delimiter.toString()); + var success = this._processElements(item, macro, newString, + context, citationItem, ignore); + if(success) dataAppended = true; + formattedString.concat(newString, child); + } + } else if(child.@value.length()) { + formattedString.append(child.@value.toString(), child); + } + } else if(name == "number") { + if(child.@variable.length()) { + var form = child.@form.toString(); + var variables = child["@variable"].toString().split(" "); + var newString = formattedString.clone(child.@delimiter.toString()); + var success = false; + + for(var j=0; j<variables.length; j++) { + if(ignore[0][variables[j]]) continue; + + var text = item.getNumericVariable(variables[j], form); + if(text) { + newString.append(text); + success = true; + } + } + + if(success) { + formattedString.concat(newString, child); + dataAppended = true; + } + } + } else if(name == "label") { + var form = child.@form.toString(); + var variables = child["@variable"].toString().split(" "); + var newString = formattedString.clone(child.@delimiter.toString()); + var success = false; + + for(var j=0; j<variables.length; j++) { + if(ignore[0][variables[j]]) continue; + + if(variables[j] == "locator") { + // special case for locator + var term = (citationItem && citationItem.locatorType) ? citationItem.locatorType : "page"; + // if "other" specified as the term, don't do anything + if(term == "other") term = false; + var value = citationItem && citationItem.locator ? citationItem.locator : false; + } else { + var term = variables[j]; + var value = item.getVariable(variables[j]).toString(); + } + + if(term !== false && value) { + if (child["@pluralize"] == "always") { + var isPlural = true; + } + else if (child["@pluralize"] == "never") { + var isPlural = false; + } + else { // contextual + var isPlural = value.indexOf("-") != -1 || value.indexOf(",") != -1 || value.indexOf("\u2013") != -1; + } + var text = this._getTerm(term, isPlural, child.@form.toString(), child["@include-period"] == "true"); + + if(text) { + newString.append(text); + success = true; + } + } + } + + if(success) { + formattedString.concat(newString, child); + } + } else if(name == "names") { + var variables = child["@variable"].toString().split(" "); + var newString = formattedString.clone(child.@delimiter.toString()); + + // remove variables that aren't supposed to be there + for(var j=0; j<variables.length; j++) { + if(ignore[0][variables[j]]) { + variables.splice(j, 1); + } + } + if(!variables.length) continue; + + var success = this._processNames(item, child, newString, context, + citationItem, variables); + + if(!success && child.substitute.length()) { + for each(var newChild in child.substitute.children()) { + if(newChild.namespace() != Zotero.CSL.Global.ns) continue; + + if(newChild.localName() == "names" && newChild.children.length() == 0) { + // apply same rules to substitute names + // with no children + var variable = newChild.@variable.toString(); + variables = variable.split(" "); + success = this._processNames(item, child, newString, + context, citationItem, variables); + + ignore[0][newChild.@variable.toString()] = true; + + if(success) break; + } else { + if(!newChild.@suffix.length()) newChild.@suffix = element.@suffix; + if(!newChild.@prefix.length()) newChild.@prefix = element.@prefix; + + success = this._processElements(item, + newChild, newString, context, citationItem, ignore, true); + + // ignore if used as substitution + if(newChild.@variable.length()) { + ignore[0][newChild.@variable.toString()] = true; + } else if(newChild.@macro.length()) { + ignore[1][newChild.@macro.toString()] = true; + } + + // if substitution was successful, stop + if(success) break; + } + } + } + + if(success) { + formattedString.concat(newString, child); + dataAppended = true; + } + } else if(name == "date") { + var variables = child["@variable"].toString().split(" "); + var newString = formattedString.clone(child.@delimiter.toString()); + var success = false; + + for(var j=0; j<variables.length; j++) { + if(ignore[0][variables[j]]) continue; + + var date = item.getDate(variables[j]); + if(!date) continue; + + var variableString = formattedString.clone(); + success = true; + + if(formattedString.format == "Sort") { + variableString.append(date.getDateVariable("sort")); + } else { + for each(var newChild in child.children()) { + if(newChild.namespace() != Zotero.CSL.Global.ns) continue; + var newName = newChild.localName(); + var newForm = newChild.@form.toString(); + + if(newName == "date-part") { + var part = newChild.@name.toString(); + + if(citationItem && citationItem._csl && citationItem._csl[variables[j]] && citationItem._csl[variables[j]][part]) { + // date is in citationItem + var string = citationItem._csl[variables[j]][part]; + } else { + var string = date.getDateVariable(part); + if(string === "") continue; + + if(part == "year") { + string = string.toString(); + + // if 4 digits and no B.C., use short form + if(newForm == "short" && string.length == 4 && !isNaN(string*1)) { + string = string.substr(2, 2); + } + + var disambiguate = item.getProperty("disambiguate-add-year-suffix"); + if(disambiguate && variables[j] == "issued") { + string += disambiguate; + } + } else if(part == "month") { + // if month is a numeric month, format as such + if(!isNaN(string*1)) { + if(newForm == "numeric-leading-zeros") { + string = (string+1).toString(); + if(string.length == 1) { + string = "0" + string; + } + } else if(newForm == "short") { + string = this._terms["short"]["_months"][string]; + } else if(newForm == "numeric") { + string = (1+string).toString(); + } else { + string = this._terms["long"]["_months"][string]; + } + } else if(newForm == "numeric") { + string = ""; + } + } else if(part == "day") { + string = string.toString(); + if(form == "numeric-leading-zeros" + && string.length() == 1) { + string = "0" + string; + } else if (newForm == "ordinal") { + var ind = parseInt(string); + var daySuffixes = Zotero.getString("date.daySuffixes").replace(/, ?/g, "|").split("|"); + string += (parseInt(ind/10)%10) == 1 ? daySuffixes[3] : (ind % 10 == 1) ? daySuffixes[0] : (ind % 10 == 2) ? daySuffixes[1] : (ind % 10 == 3) ? daySuffixes[2] : daySuffixes[3]; + } + } + } + } + + variableString.append(string, newChild); + } + + newString.concat(variableString); + } + + formattedString.concat(newString, child); + } + + if(success) { + dataAppended = true; + } + } else if(name == "group") { + var newString = formattedString.clone(child.@delimiter.toString()); + var success = this._processElements(item, + child, newString, context, citationItem, + ignore); + + // concat only if true data (not text element) was appended + if(success) { + formattedString.concat(newString, child); + dataAppended = true; + } + } else if(name == "choose") { + for each(var newChild in child.children()) { + if(newChild.namespace() != Zotero.CSL.Global.ns) continue; + + var truthValue; + + if(newChild.localName() == "else") { + // always true, if we got to this point in the loop + truthValue = true; + } else if(newChild.localName() == "if" + || newChild.localName() == "else-if") { + + var matchAny = newChild.@match == "any"; + var matchNone = newChild.@match == "none"; + if(matchAny) { + // if matching any, begin with false, then set to true + // if a condition is true + truthValue = false; + } else { + // if matching all, begin with true, then set to false + // if a condition is false + truthValue = true; + } + + // inspect variables + var done = false; + var attributes = ["variable", "is-date", "is-numeric", "is-plural", "type", "disambiguate", "locator", "position"]; + for(var k=0; !done && k<attributes.length; k++) { + var attribute = attributes[k]; + + if(newChild["@"+attribute].length()) { + var variables = newChild["@"+attribute].toString().split(" "); + for(var j=0; !done && j<variables.length; j++) { + var exists = false; + if(attribute == "variable") { + if(variables[j] == "locator") { + // special case for locator + exists = citationItem && citationItem.locator && citationItem.locator.length > 0 + } + else if(Zotero.CSL._dateVariables[variables[j]]) { + // getDate not false/undefined + exists = !!item.getDate(variables[j]); + } else if(Zotero.CSL._namesVariables[variables[j]]) { + // getNames not false/undefined, not empty + exists = item.getNames(variables[j]); + if(exists) exists = !!exists.length; + } else { + exists = item.getVariable(variables[j]); + if (exists) exists = !!exists.length; + } + } else if (attribute == "is-numeric") { + exists = item.getNumericVariable(variables[j]); + } else if (attribute == "is-date") { // XXX - this needs improving + if (Zotero.CSL._dateVariables[variables[j]]) { + exists = !!item.getDate(variables[j]); + } + } else if(attribute == "is-plural") { + if(Zotero.CSL._namesVariables[variables[j]]) { + exists = item.getNames(variables[j]); + if(exists) exists = exists.length > 1; + } else if(variables[j] == "page" || variables[j] == "locator") { + if(variables[j] == "page") { + var value = item.getVariable("page"); + } else { + var value = citationItem && citationItem.locator ? citationItem.locator : ""; + } + exists = value.indexOf("-") != -1 || value.indexOf(",") != -1 || value.indexOf("\u2013") != -1; + } + } else if(attribute == "type") { + exists = item.isType(variables[j]); + } else if(attribute == "disambiguate") { + exists = (variables[j] == "true" && item.getProperty("disambiguate-condition")) + || (variables[j] == "false" && !item.getProperty("disambiguate-condition")); + } else if(attribute == "locator") { + exists = citationItem && citationItem.locator && + (citationItem.locatorType == variables[j] + || (!citationItem.locatorType && variables[j] == "page")); + } else { // attribute == "position" + if(variables[j] == "first") { + exists = !citationItem + || !citationItem.position + || citationItem.position == Zotero.CSL.POSITION_FIRST; + } else if(variables[j] == "subsequent") { + exists = citationItem && citationItem.position >= Zotero.CSL.POSITION_SUBSEQUENT; + } else if(variables[j] == "ibid") { + exists = citationItem && citationItem.position >= Zotero.CSL.POSITION_IBID; + } else if(variables[j] == "ibid-with-locator") { + exists = citationItem && citationItem.position == Zotero.CSL.POSITION_IBID_WITH_LOCATOR; + } + } + + if(matchAny) { + if(exists) { + truthValue = true; + done = true; + } + } else if(matchNone) { + if(exists) { + truthValue = false; + done = true; + } + } else if(!exists) { + truthValue = false; + done = true; + } + } + } + } + } + + if(truthValue) { + // if true, process + var newString = formattedString.clone(newChild.@delimiter.toString()); + var success = this._processElements(item, newChild, + newString, context, citationItem, ignore); + if(success) dataAppended = true; + formattedString.concat(newString, child); + + // then break + break; + } + } + } else { + Zotero.debug("CSL: WARNING: could not add element "+name); + } + } + + return dataAppended; +} + +/* + * Compares two items, in order to sort the reference list + * Returns -1 if A comes before B, 1 if B comes before A, or 0 if they are equal + */ +Zotero.CSL.prototype._compareItem = function(a, b, context, cache) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + var sortA = []; + var sortB = []; + + var aID = a.id; + var bID = b.id; + + // author + if(context.sort.key.length()) { + var keyA, keyB; + for each(var key in context.sort.key) { + if(key.@macro.length()) { + var aCacheKey = aID+"-macro-"+key.@macro; + var bCacheKey = bID+"-macro-"+key.@macro; + + if(cache[aCacheKey]) { + keyA = cache[aCacheKey]; + } else { + keyA = new Zotero.CSL.SortString(); + this._processElements(a, this._csl.macro.(@name == key.@macro), keyA); + cache[aCacheKey] = keyA; + } + + if(cache[bCacheKey]) { + keyB = cache[bCacheKey]; + } else { + keyB = new Zotero.CSL.SortString(); + this._processElements(b, this._csl.macro.(@name == key.@macro), keyB); + cache[bCacheKey] = keyB; + } + } else if(key.@variable.length()) { + var variable = key.@variable.toString(); + var keyA = new Zotero.CSL.SortString(); + var keyB = new Zotero.CSL.SortString(); + + if(Zotero.CSL._dateVariables[variable]) { // date + var date = a.getDate(variable); + if(date) keyA.append(date.getDateVariable("sort")); + date = b.getDate(variable); + if(date) keyB.append(date.getDateVariable("sort")); + } else if(Zotero.CSL._namesVariables[key.@variable]) { // names + var element = <names><name/></names>; + element.setNamespace(Zotero.CSL.Global.ns); + + this._processNames(a, element, keyA, context, null, [variable]); + this._processNames(b, element, keyB, context, null, [variable]); + } else { // text + if(variable == "citation-number") { + keyA.append(a.getProperty(variable)); + keyB.append(b.getProperty(variable)); + } else { + keyA.append(a.getVariable(variable)); + keyB.append(b.getVariable(variable)); + } + } + } + + var compare = keyA.compare(keyB); + if(key.@sort == "descending") { // the compare method sorts ascending + // so we sort descending by reversing it + if(compare < 1) return 1; + if(compare > 1) return -1; + } else if(compare != 0) { + return compare; + } + } + } + + // sort by index in document + var aIndex = a.getProperty("index"); + var bIndex = b.getProperty("index"); + if(aIndex !== "" && (bIndex === "" || aIndex < bIndex)) { + return -1; + } else if(aIndex != bIndex) { + return 1; + } + + // sort by old index (to make this a stable sort) + var aOldIndex = a.getProperty("oldIndex"); + var bOldIndex = b.getProperty("oldIndex"); + if(aOldIndex < bOldIndex) { + return -1; + } else if(aOldIndex != bOldIndex) { + return 1; + } + + return 0; +} + + +/** + * Sorts a list of items, keeping a cache of processed keys + **/ +Zotero.CSL.prototype.cachedSort = function(items, context, field) { + var me = this; + var cache = new Object(); + + for(var i=0; i<items.length; i++) { + if(items[i].setProperty) items[i].setProperty("oldIndex", i); + } + + if(field) { + var newItems = items.sort(function(a, b) { + return me._compareItem(a[field], b[field], context, cache); + }); + } else { + var newItems = items.sort(function(a, b) { + return me._compareItem(a, b, context, cache); + }); + } + + delete cache; + return newItems; +} + +Zotero.CSL.prototype.getEqualCitations = function(items) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + var citationsEqual = []; + + if(items) { + var context = this._csl.citation; + + var string = new Zotero.CSL.FormattedString(context.options, "Text"); + this._processElements(items[0], context.layout, string, + context, "subsequent"); + var lastString = string.get(); + + for(var i=1; i<items.length; i++) { + string = new Zotero.CSL.FormattedString(context.option, "Text"); + this._processElements(items[i], context.layout, string, + context, "subsequent"); + string = string.get(); + + citationsEqual[i] = string == lastString; + lastString = string; + } + } + + return citationsEqual; +} + +/* + * Compares two citations; returns true if they are different, false if they are equal + */ +Zotero.CSL.prototype.compareCitations = function(a, b, context) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + if((!a && b) || (a && !b)) { + return true; + } else if(!a && !b) { + return false; + } + + var option = (context ? context.option : null); + var aString = new Zotero.CSL.FormattedString(option, "Text"); + this._processElements(a, this._csl.citation.layout, aString, + context, "subsequent"); + + var bString = new Zotero.CSL.FormattedString(option, "Text"); + this._processElements(b, this._csl.citation.layout, bString, + context, "subsequent"); + + return !(aString.get() == bString.get()); +} + +Zotero.CSL.Global = new function() { + this.init = init; + this.getMonthStrings = getMonthStrings; + this.getLocatorStrings = getLocatorStrings; + this.cleanXML = cleanXML; + this.parseLocales = parseLocales; + + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + this.ns = "http://purl.org/net/xbiblio/csl"; + + this.__defineGetter__("locale", function() { + Zotero.CSL.Global.init() + return Zotero.CSL.Global._xmlLang; + }); + this.collation = Components.classes["@mozilla.org/intl/collation-factory;1"] + .getService(Components.interfaces.nsICollationFactory) + .CreateCollation(Components.classes["@mozilla.org/intl/nslocaleservice;1"] + .getService(Components.interfaces.nsILocaleService) + .getApplicationLocale()); + + var locatorTypeTerms = ["page", "book", "chapter", "column", "figure", "folio", + "issue", "line", "note", "opus", "paragraph", "part", "section", + "volume", "verse"]; + + /* + * initializes CSL interpreter + */ + function init() { + if(!Zotero.CSL.Global._xmlLang) { + var prefix = "chrome://zotero/content/locale/csl/locales-"; + var ext = ".xml"; + + // If explicit bib locale, try to use that + var bibLocale = Zotero.Prefs.get('export.bibliographyLocale'); + if (bibLocale) { + var loc = bibLocale; + var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(); + req.open("GET", prefix + loc + ext, false); + req.overrideMimeType("text/plain"); + var fail = false; + try { + req.send(null); + } + catch (e) { + fail = true; + } + + if (!fail) { + Zotero.CSL.Global._xmlLang = loc; + var xml = req.responseText; + } + } + + // If no or invalid bib locale, try Firefox locale + if (!xml) { + var loc = Zotero.locale; + var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(); + req.open("GET", prefix + loc + ext, false); + req.overrideMimeType("text/plain"); + var fail = false; + try { + req.send(null); + } + catch (e) { + fail = true; + } + + if (!fail) { + Zotero.CSL.Global._xmlLang = loc; + var xml = req.responseText; + } + } + + // Fall back to en-US if no locales.xml + if (!xml) { + var loc = 'en-US'; + var req = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]. + createInstance(); + req.open("GET", prefix + loc + ext, false); + req.overrideMimeType("text/plain"); + req.send(null); + + Zotero.CSL.Global._xmlLang = loc; + var xml = req.responseText; + } + + Zotero.debug('CSL: Using ' + loc + ' as bibliography locale'); + + // get default terms + var locales = new XML(Zotero.CSL.Global.cleanXML(xml)); + Zotero.CSL.Global._defaultTerms = Zotero.CSL.Global.parseLocales(locales, true); + } + } + + /* + * returns an array of short or long month strings + */ + function getMonthStrings(form) { + Zotero.CSL.Global.init(); + return Zotero.CSL.Global._defaultTerms[form]["_months"]; + } + + /* + * returns an array of short or long locator strings + */ + function getLocatorStrings(form) { + if(!form) form = "long"; + + Zotero.CSL.Global.init(); + var locatorStrings = new Object(); + for(var i=0; i<locatorTypeTerms.length; i++) { + var term = locatorTypeTerms[i]; + var termKey = term; + if(term == "page") termKey = ""; + locatorStrings[termKey] = Zotero.CSL.Global._defaultTerms[form][term]; + + if(!locatorStrings[termKey] && form == "symbol") { + locatorStrings[termKey] = Zotero.CSL.Global._defaultTerms["short"][term]; + } + if(!locatorStrings[termKey]) { + locatorStrings[termKey] = Zotero.CSL.Global._defaultTerms["long"][term]; + } + + // use singular form + if(typeof(locatorStrings[termKey]) == "object") locatorStrings[termKey] = locatorStrings[termKey][0]; + } + return locatorStrings; + } + + /* + * removes parse instructions from XML + */ + function cleanXML(xml) { + return xml.replace(/<\?[^>]*\?>/g, ""); + } + + /* + * parses locale strings into an array; + */ + function parseLocales(termXML, ignoreLang) { + // return defaults if there are no terms + if(!termXML.length()) { + return (Zotero.CSL.Global._defaultTerms ? Zotero.CSL.Global._defaultTerms : {}); + } + + var xml = new Namespace("http://www.w3.org/XML/1998/namespace"); + + if(ignoreLang) { + // ignore lang if loaded from chrome + locale = termXML.locale[0]; + } else { + // get proper locale + var locale = termXML.locale.(@xml::lang == Zotero.CSL.Global._xmlLang); + if(!locale.length()) { + var xmlLang = Zotero.CSL.Global._xmlLang.substr(0, 2); + locale = termXML.locale.(@xml::lang == xmlLang); + } + if(!locale.length()) { + // return defaults if there are no locales + return (Zotero.CSL.Global._defaultTerms ? Zotero.CSL.Global._defaultTerms : {}); + } + } + + var termArray = new Object(); + termArray["default"] = new Object(); + + if(Zotero.CSL.Global._defaultTerms) { + // ugh. copy default array. javascript dumb. + for(var i in Zotero.CSL.Global._defaultTerms) { + termArray[i] = new Object(); + for(var j in Zotero.CSL.Global._defaultTerms[i]) { + if(typeof(Zotero.CSL.Global._defaultTerms[i]) == "object") { + termArray[i][j] = [Zotero.CSL.Global._defaultTerms[i][j][0], + Zotero.CSL.Global._defaultTerms[i][j][1]]; + } else { + termArray[i][j] = Zotero.CSL.Global_defaultTerms[i][j]; + } + } + } + } + + // loop through terms + for each(var term in locale.term) { + var name = term.@name.toString(); + if(!name) { + throw("CSL: citations cannot be generated: no name defined on term in locales.xml"); + } + // unless otherwise specified, assume "long" form + var form = term.@form.toString(); + if(!form) { + var form = "long"; + } + if(!termArray[form]) { + termArray[form] = new Object(); + } + + var single = term.single.text().toString(); + var multiple = term.multiple.text().toString(); + if(single || multiple) { + if((single && multiple) // if there's both elements or + || !termArray[form][name]) { // no previously defined value + termArray[form][name] = [single, multiple]; + } else { + if(typeof(termArray[name]) != "object") { + // if old object was just a single value, make it two copies + termArray[form][name] = [termArray[form][name], termArray[form][name]]; + } + + // redefine either single or multiple + if(single) { + termArray[form][name][0] = single; + } else { + termArray[form][name][1] = multiple; + } + } + } else { + if(name.substr(0, 6) == "month-") { + // place months into separate array + if(!termArray[form]["_months"]) { + termArray[form]["_months"] = new Array(); + } + var monthIndex = parseInt(name.substr(6),10)-1; + var term = term.text().toString(); + termArray[form]["_months"][monthIndex] = term[0].toUpperCase()+term.substr(1).toLowerCase(); + } else { + termArray[form][name] = term.text().toString(); + } + } + } + + // ensure parity between long and short months + var longMonths = termArray["long"]["_months"]; + var shortMonths = termArray["short"]["_months"]; + for(var i=0; i<longMonths.length; i++) { + if(!shortMonths[i]) { + shortMonths[i] = longMonths[i]; + } + } + + return termArray; + } +} + +/* + * the CitationItem object represents an individual source within a citation. + * + * PROPERTIES: + * prefix + * suffix + * locatorType + * locator + * suppressAuthor + * item + * itemID + */ +Zotero.CSL.CitationItem = function(item) { + if(item) { + this.item = item; + this.itemID = item.id; + } +} + +/* + * the Citation object represents a citation. + */ +Zotero.CSL.Citation = function(citationItems, csl) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + if(csl) { + this._csl = csl; + this._citation = csl._csl.citation; + this.sortable = this._citation.sort.key.length(); + } else { + this.sortable = false; + } + + this.citationItems = []; + if(citationItems) this.add(citationItems); + + // reserved for application use + this.properties = {}; +} + +/* + * sorts a citation + */ +Zotero.CSL.Citation.prototype.sort = function() { + if(this.sortable) { + this.citationItems = this._csl.cachedSort(this.citationItems, this._citation, "item"); + } +} + +/* + * adds a citationItem to a citation + */ +Zotero.CSL.Citation.prototype.add = function(citationItems) { + for(var i=0; i<citationItems.length; i++) { + var citationItem = citationItems[i]; + + if(citationItem instanceof Zotero.CSL.Item + || citationItem instanceof Zotero.Item) { + this.citationItems.push(new Zotero.CSL.CitationItem(citationItem)); + } else { + this.citationItems.push(citationItem); + } + } +} + +/* + * removes a citationItem from a citation + */ +Zotero.CSL.Citation.prototype.remove = function(citationItems) { + for each(var citationItem in citationItems){ + var index = this.citationItems.indexOf(citationItem); + if(index == -1) throw "Zotero.CSL.Citation: tried to remove an item not in citation"; + this.citationItems.splice(index, 1); + } +} + +/* + * copies a citation + */ +Zotero.CSL.Citation.prototype.clone = function() { + var clone = new Zotero.CSL.Citation(); + + // copy items + for(var i=0; i<this.citationItems.length; i++) { + var oldCitationItem = this.citationItems[i]; + var newCitationItem = new Zotero.CSL.CitationItem(); + for(var key in oldCitationItem) { + newCitationItem[key] = oldCitationItem[key]; + } + clone.citationItems.push(newCitationItem); + } + + // copy properties + for(var key in this.properties) { + clone.properties[key] = this.properties[key]; + } + + return clone; +} + +/* + * This is an item wrapper class for Zotero items. If converting this code to + * work with another application, this is what needs changing. Potentially, this + * function could accept an ID or an XML data structure instead of an actual + * item, provided it implements the same public interfaces (those not beginning + * with "_") are implemented. + */ +Zotero.CSL.Item = function(item) { + if(item instanceof Zotero.Item) { + this.zoteroItem = item; + } else if(parseInt(item, 10) == item) { + // is an item ID + this.zoteroItem = Zotero.Items.get(item); + } + + if(!this.zoteroItem) { + throw "Zotero.CSL.Item called to wrap a non-item"; + } + + this.id = this.zoteroItem.id; + this.key = this.zoteroItem.key; + + // don't return URL or accessed information for journal articles if a + // pages field exists + var itemType = Zotero.ItemTypes.getName(this.zoteroItem.itemTypeID); + if(!Zotero.Prefs.get("export.citePaperJournalArticleURL") + && ["journalArticle", "newspaperArticle", "magazineArticle"].indexOf(itemType) !== -1 + && this.zoteroItem.getField("pages")) { + this._ignoreURL = true; + } + + this._properties = {}; + this._refreshItem(); +} + + +/** + * Returns some identifier for the item. Used to create citations. In Zotero, + * this is the item ID + * + * @deprecated + **/ +Zotero.CSL.Item.prototype.getID = function() { + Zotero.debug("Zotero.CSL.Item.getID() deprecated; use Zotero.CSL.Item.id"); + return this.zoteroItem.id; +} + +/** + * Refreshes item if it has been modified + */ +Zotero.CSL.Item.prototype._refreshItem = function() { + var previousChanged = this._lastChanged; + this._lastChanged = this.zoteroItem.getField("dateModified", false, true); + + if(this._lastChanged != previousChanged) { + this._names = undefined; + this._dates = {}; + } +} + +/* + * Mappings for names + */ +Zotero.CSL.Item._zoteroNameMap = { + "collection-editor":"seriesEditor" +} + +/* + * Gets an array of Item.Name objects for a variable. + */ +Zotero.CSL.Item.prototype.getNames = function(variable) { + var field = Zotero.CSL.Item._zoteroNameMap[variable]; + if (field) variable = field; + this._refreshItem(); + if(!this._names) { + this._separateNames(); + } + + if(this._names[variable]) { + return this._names[variable]; + } + return []; +} + +/* + * Gets an Item.Date object for a specific type. + */ +Zotero.CSL.Item.prototype.getDate = function(variable) { + // ignore accessed date + if(this._ignoreURL && variable == "accessed") return false; + + // load date variable if possible + this._refreshItem(); + if(this._dates[variable] == undefined) { + this._createDate(variable); + } + + if(this._dates[variable]) return this._dates[variable]; + return false; +} + +Zotero.CSL.Item._zoteroFieldMap = { + "long":{ + "title":"title", + "container-title":["publicationTitle", "reporter", "code"], /* reporter and code should move to SQL mapping tables */ + "collection-title":["seriesTitle", "series"], + "collection-number":"seriesNumber", + "publisher":["publisher", "distributor"], /* distributor should move to SQL mapping tables */ + "publisher-place":"place", + "page":"pages", + "volume":"volume", + "issue":"issue", + "number-of-volumes":"numberOfVolumes", + "edition":"edition", + "version":"version", + "section":"section", + "genre":["type", "artworkSize"], /* artworkSize should move to SQL mapping tables, or added as a CSL variable */ + "medium":"medium", + "archive":"repository", + "archive_location":"archiveLocation", + "event":["meetingName", "conferenceName"], /* these should be mapped to the same base field in SQL mapping tables */ + "event-place":"place", + "abstract":"abstractNote", + "URL":"url", + "DOI":"DOI", + "ISBN" : "ISBN", + "call-number":"callNumber", + "note":"extra", + "number":"number", + "references":"history" + }, + "short":{ + "title":["shortTitle", "title"], + "container-title":"journalAbbreviation", + "genre":["shortTitle", "type"] /* needed for subsequent citations of items with no title */ + } +} + +/* + * Gets a text object for a specific type. + */ +Zotero.CSL.Item.prototype.getVariable = function(variable, form) { + if(!Zotero.CSL.Item._zoteroFieldMap["long"][variable]) return ""; + + // ignore URL + if(this._ignoreURL && variable == "URL") return "" + + var zoteroFields = []; + var field; + + if(form == "short" && Zotero.CSL.Item._zoteroFieldMap["short"][variable]) { + field = Zotero.CSL.Item._zoteroFieldMap["short"][variable]; + if(typeof field == "string") { + zoteroFields.push(field); + } else { + zoteroFields = zoteroFields.concat(field); + } + } + + field = Zotero.CSL.Item._zoteroFieldMap["long"][variable]; + if(typeof field == "string") { + zoteroFields.push(field); + } else { + zoteroFields = zoteroFields.concat(field); + } + + for each(var zoteroField in zoteroFields) { + var value = this.zoteroItem.getField(zoteroField, false, true); + if(value != "") return value; + } + + return ""; +} + +/* + * convert a number into a ordinal number 1st, 2nd, 3rd etc. + */ +Zotero.CSL.Item.prototype.makeOrdinal = function(value) { + var ind = parseInt(value); + var daySuffixes = Zotero.getString("date.daySuffixes").replace(/, ?/g, "|").split("|"); + value += (parseInt(ind/10)%10) == 1 ? daySuffixes[3] : (ind % 10 == 1) ? daySuffixes[0] : (ind % 10 == 2) ? daySuffixes[1] : (ind % 10 == 3) ? daySuffixes[2] : daySuffixes[3]; + return value; +} + + +Zotero.CSL.Item._zoteroRomanNumerals = { + "0" : [ "", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix" ], + "1" : [ "", "x", "xx", "xxx", "xl", "l", "lx", "lxx", "lxxx", "xc" ], + "2" : [ "", "c", "cc", "ccc", "cd", "d", "dc", "dcc", "dccc", "cm" ], + "3" : [ "", "m", "mm", "mmm", "mmmm", "mmmmm"], + } + +/* + * Convert a number into a roman numeral. + */ +Zotero.CSL.Item.prototype.makeRoman = function(value) { + + var number = parseInt(value); + var result = ""; + if (number > 5000) return ""; + var thousands = parseInt(number/1000); + if (thousands > 0) { + result += Zotero.CSL.Item._zoteroRomanNumerals[3][thousands]; + } + number = number % 1000; + var hundreds = parseInt(number/100); + if (hundreds > 0) { + result += Zotero.CSL.Item._zoteroRomanNumerals[2][hundreds]; + } + number = number % 100; + var tens = parseInt(number/10); + if (tens > 0) { + result += Zotero.CSL.Item._zoteroRomanNumerals[1][tens]; + } + number = number % 10; + if (number > 0) { + result += Zotero.CSL.Item._zoteroRomanNumerals[0][number]; + } + return result; +} + +Zotero.CSL.Item._zoteroNumberFieldMap = { + "volume":"volume", + "issue":"issue", + "number-of-volumes":"numberOfVolumes", + "edition":"edition", + "number":"number" +} +/* + * Gets a numeric object for a specific type. <number variable="edition" form="roman"/> + */ +Zotero.CSL.Item.prototype.getNumericVariable = function(variable, form) { + + if(!Zotero.CSL.Item._zoteroNumberFieldMap[variable]) return ""; + + var zoteroFields = []; + var field; + + field = Zotero.CSL.Item._zoteroNumberFieldMap[variable]; + if(typeof field == "string") { + zoteroFields.push(field); + } else { + zoteroFields = zoteroFields.concat(field); + } + + var matches; + for each(var zoteroField in zoteroFields) { + var value = this.zoteroItem.getField(zoteroField, false, true); + var matches; + if(value != "" && (matches = value.toString().match(Zotero.CSL._numberRegexp)) ) { + value = matches[0]; + if (form == "ordinal") { + return this.makeOrdinal(value); + } + else if (form == "roman") { + return this.makeRoman(value); + } + else + return value; + } + } + return ""; +} + +/* + * Sets an item-specific property to a given value. + */ +Zotero.CSL.Item.prototype.setProperty = function(property, value) { + this._properties[property] = value; +} + +/* + * Sets an item-specific property to a given value. + */ +Zotero.CSL.Item.prototype.getProperty = function(property, value) { + return (this._properties[property] !== undefined ? this._properties[property] : ""); +} + +Zotero.CSL.Item._optionalTypeMap = { + journalArticle:"article-journal", + magazineArticle:"article-magazine", + newspaperArticle:"article-newspaper", + thesis:"thesis", + conferencePaper:"paper-conference", + letter:"personal_communication", + manuscript:"manuscript", + interview:"interview", + film:"motion_picture", + artwork:"graphic", + webpage:"webpage", + report:"report", + bill:"bill", + case:"legal_case", + hearing:"bill", // ?? + patent:"patent", + statute:"bill", // ?? + email:"personal_communication", + map:"map", + blogPost:"webpage", + instantMessage:"personal_communication", + forumPost:"webpage", + audioRecording:"song", // ?? + presentation:"speech", + videoRecording:"motion_picture", + tvBroadcast:"broadcast", + radioBroadcast:"broadcast", + podcast:"song", // ?? + computerProgram:"book" // ?? +}; + +// TODO: check with Elena/APA/MLA on this +Zotero.CSL.Item._fallbackTypeMap = { + book:"book", + bookSection:"chapter", + journalArticle:"article", + magazineArticle:"article", + newspaperArticle:"article", + thesis:"article", + encyclopediaArticle:"chapter", + dictionaryEntry:"chapter", + conferencePaper:"chapter", + letter:"article", + manuscript:"article", + interview:"article", + film:"book", + artwork:"book", + webpage:"article", + report:"book", + bill:"book", + case:"book", + hearing:"book", + patent:"article", + statute:"book", + email:"article", + map:"article", + blogPost:"article", + instantMessage:"article", + forumPost:"article", + audioRecording:"book", + presentation:"article", + videoRecording:"book", + tvBroadcast:"article", + radioBroadcast:"article", + podcast:"article", + computerProgram:"book" +}; + +/* + * Determines whether this item is of a given type + */ +Zotero.CSL.Item.prototype.isType = function(type) { + var zoteroType = Zotero.ItemTypes.getName(this.zoteroItem.itemTypeID); + + return (Zotero.CSL.Item._optionalTypeMap[zoteroType] + && Zotero.CSL.Item._optionalTypeMap[zoteroType] == type) + || (Zotero.CSL.Item._fallbackTypeMap[zoteroType] ? Zotero.CSL.Item._fallbackTypeMap[zoteroType] : "article") == type; +} + +/* + * Separates names into different types. + */ +Zotero.CSL.Item.prototype._separateNames = function() { + this._names = []; + + var authorID = Zotero.CreatorTypes.getPrimaryIDForType(this.zoteroItem.itemTypeID); + + var creators = this.zoteroItem.getCreators(); + for each(var creator in creators) { + if(creator.creatorTypeID == authorID) { + var variable = "author"; + } else { + var variable = Zotero.CreatorTypes.getName(creator.creatorTypeID); + } + + var name = new Zotero.CSL.Item.Name(creator); + + if(!this._names[variable]) { + this._names[variable] = [name]; + } else { + this._names[variable].push(name); + } + } +} + +/* + * Generates an date object for a given variable (currently supported: issued + * and accessed) + */ +Zotero.CSL.Item.prototype._createDate = function(variable) { + // first, figure out what date variable to use. + if(variable == "issued") { + var date = this.zoteroItem.getField("date", false, true); + var sort = this.zoteroItem.getField("date", true, true); + } else if(variable == "accessed") { + var date = this.zoteroItem.getField("accessDate", false, true); + var sort = this.zoteroItem.getField("accessDate", true, true); + } + + if(date) { + this._dates[variable] = new Zotero.CSL.Item.Date(date, sort); + } else { + this._dates[variable] = false; + } +} + +/* + * Date class + */ +Zotero.CSL.Item.Date = function(date, sort) { + this.date = date; + this.sort = sort; +} + +/* + * Should accept the following variables: + * + * year - returns a year (optionally, with attached B.C.) + * month - returns a month (numeric from 0, or, if numeric is not available, long) + * day - returns a day (numeric) + * sort - a date that can be used for sorting purposes + */ +Zotero.CSL.Item.Date.prototype.getDateVariable = function(variable) { + if(this.date) { + if(variable == "sort") { + return this.sort; + } + + if(!this.dateArray) { + this.dateArray = Zotero.Date.strToDate(this.date); + } + + if(this.dateArray[variable] !== undefined && this.dateArray[variable] !== false) { + return this.dateArray[variable]; + } else if(variable == "month") { + if(this.dateArray.part) { + return this.dateArray.part; + } + } + } + + return ""; +} + +/* + * Name class + */ +Zotero.CSL.Item.Name = function(zoteroCreator) { + this._zoteroCreator = zoteroCreator; +} + +/* + * Should accept the following variables: + * + * firstName - first name + * lastName - last name + */ +Zotero.CSL.Item.Name.prototype.getNameVariable = function(variable) { + return this._zoteroCreator.ref[variable] ? this._zoteroCreator.ref[variable] : ""; +} + +/* + * When an array of items are passed to create a new item set, each is wrapped + * in an item wrapper. + */ +Zotero.CSL.ItemSet = function(items, csl) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + this.csl = csl; + + this.citation = csl._csl.citation; + this.bibliography = csl._csl.bibliography; + + // collect options + this.options = new Object(); + var options = this.citation.option.(@name.substr(0, 12) == "disambiguate") + + this.bibliography.option.(@name == "subsequent-author-substitute"); + for each(var option in options) { + this.options[option.@name.toString()] = option.@value.toString(); + } + + // check for disambiguate condition + for each(var thisIf in csl._csl..if) { + if(thisIf.@disambiguate.length()) { + this.options["disambiguate-condition"] = true; + break; + } + } + + // check for citation number + for each(var thisText in csl._csl..text) { + if(thisText.@variable == "citation-number") { + this.options["citation-number"] = true; + break; + } + } + + // set sortable + this.sortable = !!this.bibliography.sort.key.length(); + + this.items = []; + this.itemsById = {}; + this.itemsByKey = {}; + + // add items + this.add(items); + + // check which disambiguation options are enabled + this._citationChangingOptions = new Array(); + this._disambiguate = false; + for(var option in this.options) { + if(option.substr(0, 12) == "disambiguate" && this.options[option]) { + this._citationChangingOptions.push(option); + this._disambiguate = true; + } else if(option == "citation-number" && this.options[option]) { + this._citationChangingOptions.push(option); + } + } + + if(!items) { + return; + } + + this.resort(); +} + + +/** + * Gets CSL.Item objects from an item set using their ids + * + * @param {Array} ids An array of ids + * @return {Array} items An array whose indexes correspond to those of ids, whose values are either + * the CSL.Item objects or false + **/ +Zotero.CSL.ItemSet.prototype.getItemsByIds = function(ids) { + var items = []; + for each(var id in ids) { + if(this.itemsById[id] != undefined) { + items.push(this.itemsById[id]); + } else { + items.push(false); + } + } + return items; +} + +/** + * Gets CSL.Item objects from an item set using their keys + * + * @param {Array} keys An array of keys + * @return {Array} items An array whose indexes correspond to those of keys, whose values are either + * the CSL.Item objects or false + **/ +Zotero.CSL.ItemSet.prototype.getItemsByKeys = function(keys) { + var items = []; + for each(var key in keys) { + if(this.itemsByKey[key] != undefined) { + items.push(this.itemsByKey[key]); + } else { + items.push(false); + } + } + return items; +} + +/* + * Adds items to the given item set; must be passed either CSL.Item + * objects or objects that may be wrapped as CSL.Item objects + */ +Zotero.CSL.ItemSet.prototype.add = function(items) { + var newItems = new Array(); + + for(var i in items) { + if(items[i] instanceof Zotero.CSL.Item) { + var newItem = items[i]; + } else { + var newItem = new Zotero.CSL.Item(items[i]); + } + + newItem.setProperty("index", this.items.length); + + this.itemsById[newItem.id] = newItem; + this.itemsByKey[newItem.key] = newItem; + this.items.push(newItem); + newItems.push(newItem); + } + + return newItems; +} + +/* + * Removes items from the item set; must be passed either CSL.Item objects + * or item IDs + */ +Zotero.CSL.ItemSet.prototype.remove = function(items) { + for(var i in items) { + if(!items[i]) continue; + if(items[i] instanceof Zotero.CSL.Item) { + var item = items[i]; + } else { + var item = this.itemsById[items[i]]; + } + this.itemsById[item.id] = undefined; + this.itemsByKey[item.key] = undefined; + this.items.splice(this.items.indexOf(item), 1); + } +} + +/* + * Sorts the item set, also running postprocessing and returning items whose + * citations have changed + */ +Zotero.CSL.ItemSet.prototype.resort = function() { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + // sort + this.items = this.csl.cachedSort(this.items, this.bibliography); + + // first loop through to collect disambiguation data by item, so we can + // see if any items have changed; also collect last names + var oldOptions = new Array(); + for(var i in this._citationChangingOptions) { + oldOptions[i] = new Array(); + for(var j in this.items) { + if(this.items[j] == undefined) continue; + oldOptions[i][j] = this.items[j].getProperty(this._citationChangingOptions[i]); + this.items[j].setProperty(this._citationChangingOptions[i], ""); + } + } + + var namesByItem = new Object(); + for(var i=0; i<this.items.length; i++) { + var names = this.items[i].getNames("author"); + if(!names) names = this.items[i].getNames("editor"); + if(!names) names = this.items[i].getNames("translator"); + if(!names) names = this.items[i].getNames("recipient"); + if(!names) names = this.items[i].getNames("interviewer"); + if(!names) names = this.items[i].getNames("collection-editor"); + if(!names) continue; + namesByItem[i] = names; + } + + // check where last names are the same but given names are different + if(this.options["disambiguate-add-givenname"]) { + var firstNamesByItem = new Object(); + var allNames = new Object(); + var nameType = new Object(); + + for(var i=0; i<this.items.length; i++) { + var names = namesByItem[i]; + var firstNames = []; + for(var j=0; j<names.length; j++) { + // get firstName and lastName + var m = Zotero.CSL._firstNameRegexp.exec(names[j].getNameVariable("firstName")); + var firstName = m[0].toLowerCase(); + firstNames.push(firstName); + if(!firstName) continue; + var lastName = names[j].getNameVariable("lastName"); + + // add last name + if(!allNames[lastName]) { + allNames[lastName] = [firstName]; + } else if(allNames[lastName].indexOf(firstName) == -1) { + allNames[lastName].push(firstName); + } + } + + firstNamesByItem[i] = firstNames; + } + + // loop through last names + for(var i=0; i<this.items.length; i++) { + if(!namesByItem[i]) continue; + + var nameFormat = new Array(); + for(var j=0; j<namesByItem[i].length; j++) { + var lastName = namesByItem[i][j].getNameVariable("lastName"); + if(nameType[lastName] === undefined) { + // determine how to format name + var theNames = allNames[lastName]; + if(theNames && theNames.length > 1) { + // have two items with identical last names but different + // first names + nameType[lastName] = Zotero.CSL.NAME_USE_INITIAL; + + // check initials to see if any match + var initials = new Object(); + for(var k=0; k<theNames.length; k++) { + if(initials[theNames[k][0]]) { + nameType[lastName] = Zotero.CSL.NAME_USE_FULL; + break; + } + initials[theNames[k][0]] = true; + } + } + } + + nameFormat[j] = nameType[lastName]; + } + + if(nameFormat.length) { + // if some names have special formatting, save + this.items[i].setProperty("disambiguate-add-givenname", nameFormat.join(",")); + } + } + } + + // loop through once to determine where items equal the previous item + if(this._disambiguate && this.items.length) { + var citationsEqual = this.csl.getEqualCitations(this.items, this.citation); + } + + var allNames = {}; + + var lastItem = false; + var lastNames = false; + var lastYear = false; + var citationNumber = 1; + + for(var i=0; i<this.items.length; i++) { + var item = this.items[i]; + if(item == undefined) continue; + + var year = item.getDate("issued"); + if(year) year = year.getDateVariable("year"); + var names = namesByItem[i]; + var disambiguated = false; + + if(this._disambiguate && i != 0 && citationsEqual[i] == true) { + // some options can only be applied if there are actual authors + if(names && lastNames && this.options["disambiguate-add-names"]) { + // try adding names to disambiguate + var oldAddNames = lastItem.getProperty("disambiguate-add-names"); + + // if a different number of names, disambiguation is + // easy, although we should still see if there is a + // smaller number of names that works + var numberOfNames = names.length; + if(numberOfNames > lastNames.length) { + numberOfNames = lastNames.length; + item.setProperty("disambiguate-add-names", numberOfNames+1); + + // have to check old property + if(!oldAddNames || oldAddNames < numberOfNames) { + lastItem.setProperty("disambiguate-add-names", numberOfNames); + } + + disambiguated = true; + } else if(numberOfNames != lastNames.length) { + item.setProperty("disambiguate-add-names", numberOfNames); + + // have to check old property + if(!oldAddNames || oldAddNames < numberOfNames+1) { + lastItem.setProperty("disambiguate-add-names", numberOfNames+1); + } + + disambiguated = true; + } + } + + // now, loop through and see whether there's a + // dissimilarity before the end + var namesDiffer = false; + for(var j=0; j<numberOfNames; j++) { + namesDiffer = (names[j].getNameVariable("lastName") != lastNames[j].getNameVariable("lastName") + || (firstNamesByItem && firstNamesByItem[i][j] != firstNamesByItem[i-1][j])); + if(this.options["disambiguate-add-names"] && namesDiffer) { + item.setProperty("disambiguate-add-names", j+1); + + if(!oldAddNames || oldAddNames < j+1) { + lastItem.setProperty("disambiguate-add-names", j+1); + } + + disambiguated = true; + } + + if(namesDiffer) { + break; + } + } + + // add a year suffix, if the above didn't work + if(!disambiguated && year && !namesDiffer && this.options["disambiguate-add-year-suffix"]) { + var lastDisambiguate = lastItem.getProperty("disambiguate-add-year-suffix"); + if(!lastDisambiguate) { + lastItem.setProperty("disambiguate-add-year-suffix", "a"); + item.setProperty("disambiguate-add-year-suffix", "b"); + } else { + var newDisambiguate = ""; + if(lastDisambiguate.length > 1) { + newDisambiguate = oldLetter.substr(0, lastDisambiguate.length-1); + } + + var charCode = lastDisambiguate.charCodeAt(lastDisambiguate.length-1); + if(charCode == 122) { + // item is z; add another letter + newDisambiguate += "a"; + } else { + // next lowercase letter + newDisambiguate += String.fromCharCode(charCode+1); + } + + item.setProperty("disambiguate-add-year-suffix", newDisambiguate); + } + + disambiguated = true; + } + + // use disambiguate condition if above didn't work + if(!disambiguated && this.options["disambiguate-condition"]) { + var oldCondition = lastItem.getProperty("disambiguate-condition"); + lastItem.setProperty("disambiguate-condition", true); + item.setProperty("disambiguate-condition", true); + + // if we cannot disambiguate with the conditional, revert + if(this.csl.compareCitations(lastItem, item) == 0) { + if(!oldCondition) { + lastItem.setProperty("disambiguate-condition", undefined); + } + item.setProperty("disambiguate-condition", undefined); + } + } + } + + if(this.options["subsequent-author-substitute"] + && lastNames && names.length && lastNames.length == names.length) { + var namesDiffer = false; + for(var j=0; j<names.length; j++) { + namesDiffer = (names[j].getNameVariable("lastName") != lastNames[j].getNameVariable("lastName") + || (names[j].getNameVariable("firstName") != lastNames[j].getNameVariable("firstName"))); + if(namesDiffer) break; + } + + if(!namesDiffer) item.setProperty("subsequent-author-substitute", true); + } + + item.setProperty("citation-number", citationNumber++); + + lastItem = item; + lastNames = names; + lastYear = year; + } + + // find changed citations + var changedCitations = new Array(); + for(var j in this.items) { + if(this.items[j] == undefined) continue; + for(var i in this._citationChangingOptions) { + if(this.items[j].getProperty(this._citationChangingOptions[i]) != oldOptions[i][j]) { + changedCitations.push(this.items[j]); + } + } + } + + return changedCitations; +} + +/* + * Copies disambiguation settings (with the exception of disambiguate-add-year-suffix) + * from one item to another + */ +Zotero.CSL.ItemSet.prototype._copyDisambiguation = function(fromItem, toItem) { + for each(var option in ["disambiguate-add-givenname", "disambiguate-add-names", + "disambiguate-add-title"]) { + var value = fromItem.getProperty(option); + if(value) { + toItem.setProperty(option, value); + } + } +} + +Zotero.CSL.FormattedString = function(context, format, delimiter, subsequent) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + this.context = context; + this.option = context ? context.option : new XMLList(); + this.format = format; + this.delimiter = delimiter; + this.string = ""; + this.closePunctuation = ""; + this.closeFormatting = ""; + this.useBritishStyleQuotes = false; + + // insert tab iff second-field-align is on + this.insertTabAfterField = (!subsequent && this.option.(@name == "second-field-align").@value.toString()); + this.insertTabBeforeField = false; + // append line before next + this.prependLine = false; + + if(format == "RTF") { + this._openQuote = "\\uc0\\u8220 "; + this._closeQuote = "\\uc0\\u8221 "; + } else { + this._openQuote = "\u201c"; + this._closeQuote = "\u201d"; + } +} + +Zotero.CSL.FormattedString._punctuation = "!.,?:"; + +/* + * attaches another formatted string to the end of the current one + */ +Zotero.CSL.FormattedString.prototype.concat = function(formattedString, element) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + if(!formattedString || !formattedString.string) { + return false; + } + + if(formattedString.format != this.format) { + throw "CSL: cannot concatenate formatted strings: formats do not match"; + } + + var haveAppended = false; + var suffix = false; + if(formattedString.string !== "") { + // first, append the actual string without its suffix + if(element && element.@suffix.length()) { + // don't edit original element + element = element.copy(); + // let us decide to add the suffix + suffix = element.@suffix.toString(); + element.@suffix = []; + } + haveAppended = this.append(formattedString.string, element, false, true); + } + + // if there's close punctuation to append, that also counts + if(formattedString.closePunctuation || formattedString.closeFormatting) { + haveAppended = true; + // add the new close punctuation + this.closeFormatting += formattedString.closeFormatting; + this.closePunctuation += formattedString.closePunctuation; + } + + // append suffix, if we didn't before + if(haveAppended && suffix !== false) this.append(suffix, null, true); + + return haveAppended; +} + +Zotero.CSL.FormattedString._rtfEscapeFunction = function(aChar) { + return "{\\uc0\\u"+aChar.charCodeAt(0).toString()+"}" +} + +/* + * appends a string (with format parameters) to the current one + */ +Zotero.CSL.FormattedString.prototype.append = function(string, element, dontDelimit, dontEscape) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + if(!string && string !== 0) return false; + + if(typeof(string) != "string") { + string = string.toString(); + } + + // get prefix + var prefix = ""; + if(element && element.@prefix.length()) { + var prefix = element.@prefix.toString(); + } + + // append tab before if necessary + if(!dontDelimit && this.insertTabBeforeField) { + // replace any space preceding tab + this.string = this.string.replace(/\s+$/, ""); + + if(this.format == "HTML") { + this.string += '</td><td style="padding-left:4pt;">'; + } else if(this.format == "RTF") { + this.string += "\\tab "; + } else if(this.format == "Integration") { + this.string += "\t"; + } else { + this.string += " "; + } + + this.insertTabBeforeField = false; + if(prefix !== "") { + prefix = prefix.replace(/^\s+/, ""); + } else { + string = string.replace(/^\s+/, ""); + } + } + + // append delimiter if necessary + if(this.delimiter && this.string && !dontDelimit) { + this.append(this.delimiter, null, true); + } + + // append prefix before closing punctuation + if(prefix !== "") { + this.append(prefix, null, true); + } + + var addBefore = ""; + var addAfter = ""; + + // prepend line before if display="block" + if(element && (element["@display"] == "block" || this.prependLine)) { + if(this.format == "HTML") { + if(this.option.(@name == "hanging-indent").@value == "true") { + addBefore += '<div style="text-indent:0.5in;">' + } else { + addBefore += '<div>'; + } + addAfter = '</div>'; + } else { + if(this.format == "RTF") { + addBefore += "\r\n\\line "; + } else if(this.format == "Integration") { + addBefore += "\x0B"; + } else { + addBefore += (Zotero.isWin ? "\r\n" : "\n"); + } + this.prependLine = element["@display"] == "block"; + } + } + + // close quotes, etc. using punctuation + if(this.closePunctuation) { + if(Zotero.CSL.FormattedString._punctuation.indexOf(string[0]) != -1) { + this.string += string[0]; + string = string.substr(1); + } + this.string += this.closePunctuation; + this.closePunctuation = ""; + } + + // clean up + if(string.length && string[0] == "." && + Zotero.CSL.FormattedString._punctuation.indexOf(this.string[this.string.length-1]) != -1) { + // if string already ends in punctuation, preserve the existing stuff + // and don't add a period + string = string.substr(1); + } else if(this.string[this.string.length-1] == "(" && string[0] == " ") { + string = string.substr(1); + } else if(this.string[this.string.length-1] == " " && string[0] == ")") { + this.string = this.string.substr(0, this.string.length-1); + } + + // close previous formatting + this.string += this.closeFormatting; + this.closeFormatting = ""; + + // handling of "text-transform" attribute (now obsolete) + if(element && element["@text-transform"].length() && !element["@text-case"].length()) { + var mapping = {"lowercase":"lowercase", "uppercase":"uppercase", "capitalize":"capitalize-first"}; + element["@text-case"] = mapping[element["@text-transform"].toString()]; + } + + // handle text case + if(element) { + if(element["@text-case"].length()) { + if(element["@text-case"] == "lowercase") { + // all lowercase + string = string.toLowerCase(); + } else if(element["@text-case"] == "uppercase") { + // all uppercase + string = string.toUpperCase(); + } else if(element["@text-case"] == "sentence") { + // for now capitalizes only the first letter, the rest are lowercase + string = string[0].toUpperCase()+string.substr(1).toLowerCase(); + } else if(element["@text-case"] == "capitalize-first") { + // capitalize first + string = string[0].toUpperCase()+string.substr(1); + } else if(element["@text-case"] == "capitalize-all") { + // capitalize first + var strings = string.split(" "); + for(var i=0; i<strings.length; i++) { + if(strings[i].length > 1) { + strings[i] = strings[i][0].toUpperCase()+strings[i].substr(1).toLowerCase(); + } else if(strings[i].length == 1) { + strings[i] = strings[i].toUpperCase(); + } + } + string = strings.join(" "); + } else if(element["@text-case"] == "title") { + string = Zotero.Text.titleCase(string); + } + } + + // style attributes + if(this.format == "HTML") { + var style = ""; + + var cssAttributes = ["font-family", "font-style", "font-variant", + "font-weight", "vertical-align", "display", + "text-decoration" ]; + for(var j in cssAttributes) { + var value = element["@"+cssAttributes[j]].toString(); + if(value && value.indexOf('"') == -1) { + style += cssAttributes[j]+":"+value+";"; + } + } + + if(style) { + addBefore += '<span style="'+style+'">'; + addAfter = '</span>'+addAfter; + } + } else { + if(this.format == "RTF" || this.format == "Integration") { + var rtfAttributes = { + "font-style":{"oblique":"i", "italic":"i"}, + "font-variant":{"small-caps":"scaps"}, + "font-weight":{"bold":"b"}, + "text-decoration":{"underline":"ul"}, + "vertical-align":{"sup":"super", "sub":"sub"} + } + + for(var j in rtfAttributes) { + for(var k in rtfAttributes[j]) { + if(element["@"+j] == k) { + addBefore += "\\"+rtfAttributes[j][k]+" "; + addAfter = "\\"+rtfAttributes[j][k]+"0 "+addAfter; + } + } + } + } + } + + // add quotes if necessary + if(element.@quotes == "true") { + this.string += this._openQuote; + + if(this.useBritishStyleQuotes) { + string += this._closeQuote; + } else { + this.closePunctuation = this._closeQuote; + } + } + } + + if(!dontEscape) { + if(this.format == "HTML") { + string = string.replace("&", "&amp;", "g") + .replace("<", "&lt;", "g") + .replace(">", "&gt;", "g") + .replace(/(\r\n|\r|\n)/g, "<br />") + .replace(/[\x00-\x1F]/g, ""); + } else if(this.format == "RTF" || this.format == "Integration") { + string = string.replace("\\", "\\\\", "g") + .replace(/(\r\n|\r|\n)/g, "\\line "); + if(string.substr(string.length-6) == "\\line ") { + string = string.substr(0, string.length-6); + addAfter = "\\line "+addAfter; + } + + if(this.format == "RTF") { + string = string.replace(/[\x7F-\uFFFF]/g, Zotero.CSL.FormattedString._rtfEscapeFunction) + .replace("\t", "\\tab ", "g"); + + if(string.substr(string.length-5) == "\\tab ") { + string = string.substr(0, string.length-5); + addAfter = "\\tab "+addAfter; + } + } + } else { + string = string.replace(/(\r\n|\r|\n)/g, (Zotero.isWin ? "\r\n" : "\n")); + } + } + + this.string += addBefore+string; + + if(element && element.@suffix.length()) { + this.append(element.@suffix.toString(), null, true); + } + + // save for second-field-align + if(!dontDelimit && this.insertTabAfterField) { + this.insertTabAfterField = false; + this.insertTabBeforeField = true; + } + + this.closeFormatting = addAfter; + + return true; +} + +/* + * gets the formatted string + */ +Zotero.CSL.FormattedString.prototype.get = function() { + return this.string+this.closeFormatting+this.closePunctuation; +} + +/* + * creates a new formatted string with the same formatting parameters as this one + */ +Zotero.CSL.FormattedString.prototype.clone = function(delimiter) { + return new Zotero.CSL.FormattedString(this.context, this.format, delimiter, true); +} + +/* + * Implementation of FormattedString for sort purposes. + */ +Zotero.CSL.SortString = function() { + default xml namespace = "http://purl.org/net/xbiblio/csl"; + + this.format = "Sort"; + this.string = []; +} + +Zotero.CSL.SortString.prototype.concat = function(newString) { + if(newString.string.length == 0) { + return; + } else if(newString.string.length == 1) { + this.string.push(newString.string[0]); + } else { + this.string.push(newString.string); + } +} + +Zotero.CSL.SortString.prototype.append = function(newString) { + this.string.push(newString); +} + +Zotero.CSL.SortString.prototype.compare = function(b, a) { + // by default, a is this string + if(a == undefined) { + a = this.string; + b = b.string; + } + + var aIsString = typeof(a) != "object"; + var bIsString = typeof(b) != "object"; + if(aIsString && bIsString) { + if(a == b) { + return 0; + } else if(!isNaN(a % 1) && !isNaN(b % 1)) { + // both numeric + if(b > a) return -1; + return 1; // already know they're not equal + } else { + var cmp = Zotero.CSL.Global.collation.compareString(Zotero.CSL.Global.collation.kCollationCaseInSensitive, a, b); + if(cmp == 0) { + // for some reason collation service returned 0; the collation + // service sucks! they can't be equal! + if(b > a) { + return -1; + } else { + return 1; + } + } + return cmp; + } + } else if(aIsString && !bIsString) { + var cmp = this.compare(b[0], a); + if(cmp == 0) { + return -1; // a before b + } + return cmp; + } else if(bIsString && !aIsString) { + var cmp = this.compare(b, a[0]); + if(cmp == 0) { + return 1; // b before a + } + return cmp; + } + + var maxLength = Math.min(b.length, a.length); + for(var i = 0; i < maxLength; i++) { + var cmp = this.compare(b[i], a[i]); + if(cmp != 0) { + return cmp; + } + } + + if(b.length > a.length) { + return -1; // a before b + } else if(b.length < a.length) { + return 1; // b before a + } + + return 0; +} + + +Zotero.CSL.SortString.prototype.clone = function() { + return new Zotero.CSL.SortString(); +} +\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/file.js b/chrome/content/zotero/xpcom/file.js @@ -78,6 +78,22 @@ Zotero.File = new function(){ } + /** + * Get contents of a binary file + */ + this.getBinaryContents = function(file) { + var iStream = Components.classes["@mozilla.org/network/file-input-stream;1"] + .createInstance(Components.interfaces.nsIFileInputStream); + iStream.init(file, 0x01, 0664, 0); + var bStream = Components.classes["@mozilla.org/binaryinputstream;1"] + .createInstance(Components.interfaces.nsIBinaryInputStream); + bStream.setInputStream(iStream); + var string = bStream.readBytes(file.fileSize); + iStream.close(); + return string; + } + + function getContents(file, charset, maxLength){ var fis = Components.classes["@mozilla.org/network/file-input-stream;1"]. createInstance(Components.interfaces.nsIFileInputStream); diff --git a/chrome/content/zotero/xpcom/ingester.js b/chrome/content/zotero/xpcom/ingester.js @@ -17,14 +17,40 @@ See the License for the specific language governing permissions and limitations under the License. - - Utilities based in part on code taken from Piggy Bank 2.1.1 (BSD-licensed) - - ***** END LICENSE BLOCK ***** */ -Zotero.Ingester = new Object(); +Zotero.Ingester = new function() { + this.importHandler = function(string, uri) { + // attempt to import through Zotero.Translate + var translation = new Zotero.Translate("import"); + translation.setLocation(uri); + translation.setString(string); + + var frontWindow = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Components.interfaces.nsIWindowWatcher).activeWindow; + + frontWindow.Zotero_Browser.progress.show(); + var saveLocation = null; + try { + saveLocation = frontWindow.ZoteroPane.getSelectedCollection(); + } catch(e) {} + translation.setHandler("itemDone", function(obj, item) { frontWindow.Zotero_Browser.itemDone(obj, item, saveLocation) }); + translation.setHandler("done", function(obj, item) { frontWindow.Zotero_Browser.finishScraping(obj, item, saveLocation) }); + + // attempt to retrieve translators + var translators = translation.getTranslators(); + if(!translators.length) { + // we lied. we can't really translate this file. + frontWindow.Zotero_Browser.progress.close(); + throw "No translator found for handled RIS or Refer file" + } + + // translate using first available + translation.setTranslator(translators[0]); + translation.translate(); + } +} Zotero.OpenURL = new function() { this.resolve = resolve; @@ -457,176 +483,4 @@ Zotero.OpenURL = new function() { return ""; } } -} - -Zotero.Ingester.MIMEHandler = new function() { - var on = false; - - this.init = init; - - /* - * registers URIContentListener to handle MIME types - */ - function init() { - var prefStatus = Zotero.Prefs.get("parseEndNoteMIMETypes"); - if(!on && prefStatus) { - Zotero.debug("Registering URIContentListener for RIS/Refer"); - var uriLoader = Components.classes["@mozilla.org/uriloader;1"]. - getService(Components.interfaces.nsIURILoader); - uriLoader.registerContentListener(Zotero.Ingester.MIMEHandler.URIContentListener); - on = true; - } else if(on && !prefStatus) { - Zotero.debug("Unregistering URIContentListener for RIS/Refer"); - var uriLoader = Components.classes["@mozilla.org/uriloader;1"]. - getService(Components.interfaces.nsIURILoader); - uriLoader.unRegisterContentListener(Zotero.Ingester.MIMEHandler.URIContentListener); - on = false; - } - } -} - -/* - * Zotero.Ingester.MIMEHandler.URIContentListener: implements - * nsIURIContentListener interface to grab MIME types - */ -Zotero.Ingester.MIMEHandler.URIContentListener = new function() { - // list of content types to capture - // NOTE: must be from shortest to longest length - this.desiredContentTypes = ["application/x-endnote-refer", - "application/x-research-info-systems"]; - - this.QueryInterface = QueryInterface; - this.canHandleContent = canHandleContent; - this.doContent = doContent; - this.isPreferred = isPreferred; - this.onStartURIOpen = onStartURIOpen; - - function QueryInterface(iid) { - if(iid.equals(Components.interfaces.nsISupports) - || iid.equals(Components.interfaces.nsISupportsWeakReference) - || iid.equals(Components.interfaces.nsIURIContentListener)) { - return this; - } - throw Components.results.NS_ERROR_NO_INTERFACE; - } - - function canHandleContent(contentType, isContentPreferred, desiredContentType) { - if(Zotero.inArray(contentType, this.desiredContentTypes)) { - return true; - } - return false; - } - - function doContent(contentType, isContentPreferred, request, contentHandler) { - Zotero.debug("doing content for "+request.name); - contentHandler.value = new Zotero.Ingester.MIMEHandler.StreamListener(request, contentType); - return false; - } - - function isPreferred(contentType, desiredContentType) { - if(Zotero.inArray(contentType, this.desiredContentTypes)) { - return true; - } - return false; - } - - function onStartURIOpen(URI) { - return true; - } -} - -/* - * Zotero.Ingester.MIMEHandler.StreamListener: implements nsIStreamListener and - * nsIRequestObserver interfaces to download MIME types we've grabbed - */ -Zotero.Ingester.MIMEHandler.StreamListener = function(request, contentType) { - this._request = request; - this._contentType = contentType - this._readString = ""; - this._scriptableStream = null; - this._scriptableStreamInput = null - - // get front window - var windowWatcher = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]. - getService(Components.interfaces.nsIWindowWatcher); - this._frontWindow = windowWatcher.activeWindow; - this._frontWindow.Zotero_Browser.progress.show(); - - Zotero.debug("EndNote prepared to grab content type "+contentType); -} - -Zotero.Ingester.MIMEHandler.StreamListener.prototype.QueryInterface = function(iid) { - if(iid.equals(Components.interfaces.nsISupports) - || iid.equals(Components.interfaces.nsIRequestObserver) - || iid.equals(Components.interfaces.nsIStreamListener)) { - return this; - } - throw Components.results.NS_ERROR_NO_INTERFACE; -} - -Zotero.Ingester.MIMEHandler.StreamListener.prototype.onStartRequest = function(channel, context) {} - -/* - * called when there's data available; basicallly, we just want to collect this data - */ -Zotero.Ingester.MIMEHandler.StreamListener.prototype.onDataAvailable = function(request, context, inputStream, offset, count) { - Zotero.debug(count+" bytes available"); - - if(inputStream != this._scriptableStreamInput) { // get storage stream - // if there's not one - this._scriptableStream = Components.classes["@mozilla.org/scriptableinputstream;1"]. - createInstance(Components.interfaces.nsIScriptableInputStream); - this._scriptableStream.init(inputStream); - this._scriptableStreamInput = inputStream; - } - this._readString += this._scriptableStream.read(count); -} - -/* - * called when the request is done - */ -Zotero.Ingester.MIMEHandler.StreamListener.prototype.onStopRequest = function(channel, context, status) { - Zotero.debug("request finished"); - var externalHelperAppService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"]. - getService(Components.interfaces.nsIExternalHelperAppService); - - // attempt to import through Zotero.Translate - var translation = new Zotero.Translate("import"); - translation.setLocation(this._request.name); - translation.setString(this._readString); - - // use front window's save functions and folder - var frontWindow = this._frontWindow; - - var saveLocation = null; - try { - saveLocation = frontWindow.ZoteroPane.getSelectedCollection(); - } catch(e) {} - translation.setHandler("itemDone", function(obj, item) { frontWindow.Zotero_Browser.itemDone(obj, item, saveLocation) }); - translation.setHandler("done", function(obj, item) { frontWindow.Zotero_Browser.finishScraping(obj, item, saveLocation) }); - - // attempt to retrieve translators - var translators = translation.getTranslators(); - if(!translators.length) { - // we lied. we can't really translate this file. call - // nsIExternalHelperAppService with the data - frontWindow.Zotero_Browser.progress.close(); - - var streamListener; - if(streamListener = externalHelperAppService.doContent(this._contentType, this._request, frontWindow)) { - // create a string input stream - var inputStream = Components.classes["@mozilla.org/io/string-input-stream;1"]. - createInstance(Components.interfaces.nsIStringInputStream); - inputStream.setData(this._readString, this._readString.length); - - streamListener.onStartRequest(channel, context); - streamListener.onDataAvailable(this._request, context, inputStream, 0, this._readString.length); - streamListener.onStopRequest(channel, context, status); - } - return false; - } - - // translate using first available - translation.setTranslator(translators[0]); - translation.translate(); } \ No newline at end of file diff --git a/chrome/content/zotero/xpcom/mimeTypeHandler.js b/chrome/content/zotero/xpcom/mimeTypeHandler.js @@ -0,0 +1,220 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +*/ + +Zotero.MIMETypeHandler = new function () { + var _typeHandlers, _ignoreContentDispositionTypes, _observers; + + /** + * Registers URIContentListener to handle MIME types + */ + this.init = function() { + Zotero.debug("Registering URIContentListener"); + // register our nsIURIContentListener and nsIObserver + Components.classes["@mozilla.org/uriloader;1"]. + getService(Components.interfaces.nsIURILoader). + registerContentListener(_URIContentListener); + Components.classes["@mozilla.org/observer-service;1"]. + getService(Components.interfaces.nsIObserverService). + addObserver(_Observer, "http-on-examine-response", false); + this.initializeHandlers(); + } + + /** + * Initializes handlers for MIME types + */ + this.initializeHandlers = function() { + _typeHandlers = {}; + _ignoreContentDispositionTypes = []; + _observers = []; + + if(Zotero.Prefs.get("parseEndNoteMIMETypes")) { + this.addHandler("application/x-endnote-refer", Zotero.Ingester.importHandler, true); + this.addHandler("application/x-research-info-systems", Zotero.Ingester.importHandler, true); + } + this.addHandler("text/x-csl", function(a1, a2, a3) { Zotero.Styles.install(a1, a2, a3) }); + } + + /** + * Adds a handler to handle a specific MIME type + * @param {String} type MIME type to handle + * @param {Function} fn Function to call to handle type + * @param {Boolean} ignoreContentDisposition If true, ignores the Content-Disposition header, + * which is often used to force a file to download rather than let it be handled by the web + * browser + */ + this.addHandler = function(type, fn, ignoreContentDisposition) { + _typeHandlers[type] = fn; + _ignoreContentDispositionTypes.push(type); + } + + /** + * Adds an observer to inspect and possibly modify page headers + */ + this.addObserver = function(fn) { + _observers.push(fn); + } + + /** + * Called to observe a page load + */ + var _Observer = new function() { + this.observe = function(channel) { + channel.QueryInterface(Components.interfaces.nsIRequest); + if(channel.loadFlags & Components.interfaces.nsIHttpChannel.LOAD_DOCUMENT_URI) { + channel.QueryInterface(Components.interfaces.nsIHttpChannel); + try { + // remove content-disposition headers for EndNote, etc. + var contentType = channel.getResponseHeader("Content-Type").toLowerCase(); + for each(var handledType in _ignoreContentDispositionTypes) { + if(contentType.length < handledType.length) { + break; + } else { + if(contentType.substr(0, handledType.length) == handledType) { + channel.setResponseHeader("Content-Disposition", "", false); + break; + } + } + } + } catch(e) {} + + for each(var observer in _observers) { + observer(channel); + } + } + } + } + + var _URIContentListener = new function() { + /** + * Standard QI definiton + */ + this.QueryInterface = function(iid) { + if (iid.equals(Components.interfaces.nsISupports) + || iid.equals(Components.interfaces.nsISupportsWeakReference) + || iid.equals(Components.interfaces.nsIURIContentListener)) { + return this; + } + throw Components.results.NS_ERROR_NO_INTERFACE; + } + + /** + * Called to see if we can handle a content type + */ + this.canHandleContent = this.isPreferred = function(contentType, isContentPreferred, desiredContentType) { + return !!_typeHandlers[contentType.toLowerCase()]; + } + + /** + * Called to begin handling a content type + */ + this.doContent = function(contentType, isContentPreferred, request, contentHandler) { + Zotero.debug("MIMETypeHandler: handling "+contentType+" from " + request.name); + contentHandler.value = new _StreamListener(request, contentType.toLowerCase()); + return false; + } + + /** + * Called so that we could stop a load before it happened if we wanted to + */ + this.onStartURIOpen = function(URI) { + return true; + } + } + + /** + * @class Implements nsIStreamListener and nsIRequestObserver interfaces to download MIME types + * we've registered ourself as the handler for + * @param {nsIRequest} request The request to handle + * @param {String} contenType The content type being handled + */ + var _StreamListener = function(request, contentType) { + this._request = request; + this._contentType = contentType + this._readString = ""; + this._scriptableStream = null; + this._scriptableStreamInput = null; + } + + /** + * Standard QI definiton + */ + _StreamListener.prototype.QueryInterface = function(iid) { + if (iid.equals(Components.interfaces.nsISupports) + || iid.equals(Components.interfaces.nsIRequestObserver) + || iid.equals(Components.interfaces.nsIStreamListener)) { + return this; + } + throw Components.results.NS_ERROR_NO_INTERFACE; + } + + /** + * Called when the request is started; we ignore this + */ + _StreamListener.prototype.onStartRequest = function(channel, context) {} + + + /** + * Called when there's data available; we collect this data and keep it until the request is + * done + */ + _StreamListener.prototype.onDataAvailable = function(request, context, inputStream, offset, count) { + if (inputStream != this._scriptableStreamInput) { + this._scriptableStream = Components.classes["@mozilla.org/scriptableinputstream;1"] + .createInstance(Components.interfaces.nsIScriptableInputStream); + this._scriptableStream.init(inputStream); + this._scriptableStreamInput = inputStream; + } + this._readString += this._scriptableStream.read(count); + } + + /** + * Called when the request is done + */ + _StreamListener.prototype.onStopRequest = function(channel, context, status) { + try { + _typeHandlers[this._contentType](this._readString, (this._request.name ? this._request.name : null), + this._contentType); + } catch(e) { + // if there was an error, handle using nsIExternalHelperAppService + var externalHelperAppService = Components.classes["@mozilla.org/uriloader/external-helper-app-service;1"]. + getService(Components.interfaces.nsIExternalHelperAppService); + var frontWindow = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]. + getService(Components.interfaces.nsIWindowWatcher).activeWindow; + + var newStreamListener = externalHelperAppService.doContent(this._contentType, + this._request, frontWindow, false); + if(newStreamListener) { + // create a string input stream + var inputStream = Components.classes["@mozilla.org/io/string-input-stream;1"]. + createInstance(Components.interfaces.nsIStringInputStream); + inputStream.setData(this._readString, this._readString.length); + + newStreamListener.onStartRequest(channel, context); + newStreamListener.onDataAvailable(this._request, context, inputStream, 0, this._readString.length); + newStreamListener.onStopRequest(channel, context, status); + } + + // then throw our error + throw e; + } + } +} +\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/proxy.js b/chrome/content/zotero/xpcom/proxy.js @@ -44,12 +44,11 @@ Zotero.Proxies = new function() { */ this.init = function() { if(!on) { - var observerService = Components.classes["@mozilla.org/observer-service;1"] - .getService(Components.interfaces.nsIObserverService); - observerService.addObserver(this, "http-on-examine-response", false); + var me = this; + Zotero.MIMETypeHandler.addObserver(function(ch) { me.observe(ch) }); this.get(); + on = true; } - on = true; autoRecognize = Zotero.Prefs.get("proxies.autoRecognize"); transparent = Zotero.Prefs.get("proxies.transparent"); @@ -63,94 +62,73 @@ Zotero.Proxies = new function() { * @param {nsIChannel} channel */ this.observe = function(channel) { + // try to detect a proxy channel.QueryInterface(Components.interfaces.nsIHttpChannel); - try { - // remove content-disposition headers for endnote, etc. - var contentType = channel.getResponseHeader("Content-Type").toLowerCase(); - for each(var desiredContentType in Zotero.Ingester.MIMEHandler.URIContentListener.desiredContentTypes) { - if(contentType.length < desiredContentType.length) { - break; - } else { - if(contentType.substr(0, desiredContentType.length) == desiredContentType) { - channel.setResponseHeader("Content-Disposition", "", false); - break; - } - } + var url = channel.URI.spec; + + // see if there is a proxy we already know + var m = false; + var proxy; + for each(proxy in proxies) { + if(proxy.regexp && proxy.multiHost) { + m = proxy.regexp.exec(url); + if(m) break; } - } catch(e) {} + } - // try to detect a proxy - channel.QueryInterface(Components.interfaces.nsIRequest); - if(channel.loadFlags & Components.interfaces.nsIHttpChannel.LOAD_DOCUMENT_URI) { - channel.QueryInterface(Components.interfaces.nsIHttpChannel); - var url = channel.URI.spec; - - // see if there is a proxy we already know - var m = false; - var proxy; - for each(proxy in proxies) { - if(proxy.regexp && proxy.multiHost) { - m = proxy.regexp.exec(url); - if(m) break; + if(m) { + // add this host if we know a proxy + if(proxy.autoAssociate) { + var host = m[proxy.parameters.indexOf("%h")+1]; + if(proxy.hosts.indexOf(host) == -1) { + proxy.hosts.push(host); + proxy.save(); } } - - if(m) { - // add this host if we know a proxy - if(proxy.autoAssociate) { - var host = m[proxy.parameters.indexOf("%h")+1]; - if(proxy.hosts.indexOf(host) == -1) { - proxy.hosts.push(host); - proxy.save(); - } + } else if(autoRecognize) { + // otherwise, try to detect a proxy + var proxy = false; + for each(var detector in Zotero.Proxies.Detectors) { + try { + proxy = detector(channel); + } catch(e) { + Components.utils.reportError(e); } - } else if(autoRecognize) { - // otherwise, try to detect a proxy - var proxy = false; - for each(var detector in Zotero.Proxies.Detectors) { - try { - proxy = detector(channel); - } catch(e) { - Components.utils.reportError(e); - } + + if(!transparent) { + // if transparent is turned off, just save the proxy + proxy.save(); + } else if(proxy) { + // otherwise, make sure we want it + var io = {site:proxy.hosts[0], proxy:channel.URI.hostPort}; + var window = Components.classes["@mozilla.org/appshell/window-mediator;1"] + .getService(Components.interfaces.nsIWindowMediator) + .getMostRecentWindow("navigator:browser"); + window.openDialog('chrome://zotero/content/proxy.xul', '', 'chrome,modal', io); - if(!transparent) { - // if transparent is turned off, just save the proxy - proxy.save(); - } else if(proxy) { - // otherwise, make sure we want it - var io = {site:proxy.hosts[0], proxy:channel.URI.hostPort}; - var window = Components.classes["@mozilla.org/appshell/window-mediator;1"] - .getService(Components.interfaces.nsIWindowMediator) - .getMostRecentWindow("navigator:browser"); - window.openDialog('chrome://zotero/content/proxy.xul', '', 'chrome,modal', io); - - if(io.add) proxy.save(); - if(io.disable) { - transparent = false; - Zotero.Prefs.set("proxies.transparent", false); - } - - break; + if(io.add) proxy.save(); + if(io.disable) { + transparent = false; + Zotero.Prefs.set("proxies.transparent", false); } + + break; } } + } + + // try to get an applicable proxy + if(transparent) { + var webNav = null; + try { + webNav = channel.notificationCallbacks.QueryInterface(Components.interfaces.nsIWebNavigation); + } catch(e) {} - // try to get an applicable proxy - if(transparent) { - var webNav = null; - try { - webNav = channel.notificationCallbacks.QueryInterface(Components.interfaces.nsIWebNavigation); - } catch(e) {} - - if(webNav) { - var proxied = this.properToProxy(url, true); - if(proxied) webNav.loadURI(proxied, 0, channel.URI, null, null); - } + if(webNav) { + var proxied = this.properToProxy(url, true); + if(proxied) webNav.loadURI(proxied, 0, channel.URI, null, null); } } - - delete channel; } /** diff --git a/chrome/content/zotero/xpcom/style.js b/chrome/content/zotero/xpcom/style.js @@ -0,0 +1,473 @@ +/* + ***** BEGIN LICENSE BLOCK ***** + + Copyright (c) 2006 Center for History and New Media + George Mason University, Fairfax, Virginia, USA + http://chnm.gmu.edu + + Licensed under the Educational Community License, Version 1.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.opensource.org/licenses/ecl1.php + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + ***** END LICENSE BLOCK ***** +*/ + +/** + * @property {Boolean} cacheTranslatorData Whether translator data should be cached or reloaded + * every time a translator is accessed + * @property {Zotero.CSL} lastCSL + */ +Zotero.Styles = new function() { + var _initialized = false; + var _styles, _visibleStyles; + + this.ios = Components.classes["@mozilla.org/network/io-service;1"]. + getService(Components.interfaces.nsIIOService); + + /** + * Initializes styles cache, loading metadata for styles into memory + */ + this.init = function() { + _initialized = true; + + var start = (new Date()).getTime() + + _styles = {}; + _visibleStyles = []; + this.cacheTranslatorData = Zotero.Prefs.get("cacheTranslatorData"); + this.lastCSL = null; + + // main dir + var dir = Zotero.getStylesDirectory(); + var i = _readStylesFromDirectory(dir, false); + + // hidden dir + dir.append("hidden"); + if(dir.exists()) i += _readStylesFromDirectory(dir, true); + + Zotero.debug("Cached "+i+" styles in "+((new Date()).getTime() - start)+" ms"); + } + + /** + * Reads all styles from a given directory and caches their metadata + * @privates + */ + function _readStylesFromDirectory(dir, hidden) { + var i = 0; + var contents = dir.directoryEntries; + while(contents.hasMoreElements()) { + var file = contents.getNext().QueryInterface(Components.interfaces.nsIFile); + if(!file.leafName || file.leafName[0] == "." || file.isDirectory()) continue; + + var style = new Zotero.Style(file); + if(style.styleID) { + if(_styles[style.styleID]) { + // same style is already cached + Zotero.log('Style with ID '+style.styleID+' already loaded from "'+ + _styles[style.styleID].file.leafName+'"', "error", + Zotero.Styles.ios.newFileURI(style.file).spec); + } else { + // add to cache + _styles[style.styleID] = style; + _styles[style.styleID].hidden = hidden; + if(!hidden) _visibleStyles.push(style); + } + } + i++; + } + return i; + } + + /** + * Gets a style with a given ID + * @param {String} id + */ + this.get = function(id) { + if(!_initialized) this.init(); + return _styles[id] ? _styles[id] : false; + } + + /** + * Gets all visible styles + * @return {Zotero.Style[]} An array of Zotero.Style objects + */ + this.getVisible = function() { + if(!_initialized || !this.cacheTranslatorData) this.init(); + return _visibleStyles.slice(0); + } + + /** + * Gets all styles + * @return {Object} An object whose keys are style IDs, and whose values are Zotero.Style objects + */ + this.getAll = function() { + if(!_initialized || !this.cacheTranslatorData) this.init(); + return _styles; + } + + /** + * Installs a style file + * @param {String|nsIFile} style An nsIFile representing a style on disk, or a string containing + * the style data + * @param {String} loadURI The URI this style file was loaded from + * @param {Boolean} hidden Whether style is to be hidden. If this parameter is true, UI alerts + * are silenced as well + */ + this.install = function(style, loadURI, hidden) { + // "with ({});" needed to fix default namespace scope issue + // See https://bugzilla.mozilla.org/show_bug.cgi?id=330572 + default xml namespace = "http://purl.org/net/xbiblio/csl"; with ({}); + const pathRe = /[^\/]+$/; + + if(!_initialized || !this.cacheTranslatorData) this.init(); + + var type = "csl"; + + // handle nsIFiles + var styleFile = null; + if(style instanceof Components.interfaces.nsIFile) { + styleFile = style; + loadURI = style.leafName; + if(loadURI.substr(-4) == ".ens") { + type = "ens"; + style = Zotero.File.getBinaryContents(styleFile); + } else { + style = Zotero.File.getContents(styleFile); + } + } + + var error = false; + try { + if(type == "ens") { + // EN style + var type = "ens"; + var enConverter = new Zotero.ENConverter(style); + var xml = enConverter.parse(); + } else { + // CSL + var xml = new XML(Zotero.CSL.Global.cleanXML(style)); + } + } catch(e) { + error = e; + } + + if(!xml || error) { + if(!hidden) alert(Zotero.getString('styles.installErrorURI', loadURI)); + if(error) throw error; + return false; + } + + var source = null; + var styleID = xml.info.id.toString(); + if(type == "ens") { + var title = styleFile.leafName.substr(0, styleFile.leafName.length-4); + var fileName = styleFile.leafName; + } else { + // get file name from URL + var m = pathRe.exec(styleID); + var fileName = Zotero.File.getValidFileName(m ? m[0] : styleID); + var title = xml.info.title.toString(); + + // look for a parent + for each(var link in xml.info.link) { + if(link.@rel == "source") { + source = link.@href.toString(); + if(source == styleID) { + if(!hidden) alert(Zotero.getString('styles.installErrorURI', loadURI)); + throw "Style with ID "+this.styleID+" references itself as source"; + } + break; + } + } + } + + // ensure csl or ens extension + if(fileName.substr(-4).toLowerCase() != "."+type) fileName += "."+type; + + var destFile = Zotero.getStylesDirectory(); + var destFileHidden = destFile.clone(); + destFile.append(fileName); + destFileHidden.append("hidden"); + if(hidden) Zotero.File.createDirectoryIfMissing(destFileHidden); + destFileHidden.append(fileName); + + // look for an existing style with the same styleID or filename + var existingFile = null; + var existingTitle = null; + if(_styles[styleID]) { + existingFile = _styles[styleID].file; + existingTitle = _styles[styleID].title; + } else { + if(destFile.exists()) { + existingFile = destFile; + } else if(destFileHidden.exists()) { + existingFile = destFileHidden; + } + + if(existingFile) { + // find associated style + for each(var existingStyle in this._styles) { + if(destFile.equals(existingStyle.file)) { + existingTitle = existingStyle.title; + break; + } + } + } + } + + // display a dialog to tell the user we're about to install the style + if(hidden) { + destFile = destFileHidden; + } else { + var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"] + .getService(Components.interfaces.nsIPromptService); + + if(existingTitle) { + var text = Zotero.getString('styles.updateStyleURI', [existingTitle, title, loadURI]); + } else { + var text = Zotero.getString('styles.installStyleURI', [title, loadURI]); + } + + var index = ps.confirmEx(null, '', + text, + ((ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING) + + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL)), + Zotero.getString('general.install'), null, null, null, {} + ); + } + + if(hidden || index == 0) { + // user wants to install/update + if(source && !_styles[source]) { + // need to fetch source + if(source.substr(0, 7) == "http://" || source.substr(0, 8) == "https://") { + Zotero.Utilities.HTTP.doGet(source, function(xmlhttp) { + var success = false; + var error = null; + try { + var success = Zotero.Styles.install(xmlhttp.responseText, loadURI, true); + } catch(e) { + error = e; + } + + if(success) { + _completeInstall(style, styleID, destFile, existingFile, styleFile); + } else { + if(!hidden) alert(Zotero.getString('styles.installSourceErrorURI', [loadURI, source])); + throw error; + } + }); + } else { + if(!hidden) alert(Zotero.getString('styles.installSourceErrorURI', [loadURI, source])); + throw "Source CSL URI is invalid"; + } + } else { + _completeInstall(style, styleID, destFile, existingFile, styleFile); + } + return styleID; + } + + return false; + } + + /** + * Finishes installing a style, copying the file, reloading the style cache, and refreshing the + * styles list in any open windows + * @param {String} style The style string + * @param {String} styleID The style ID + * @param {nsIFile} destFile The destination for the style + * @param {nsIFile} [existingFile] The existing file to delete before copying this one + * @param {nsIFile} [styleFile] The file that contains the style to be installed + * @private + */ + function _completeInstall(style, styleID, destFile, existingFile, styleFile) { + // remove any existing file with a different name + if(existingFile) existingFile.remove(false); + + if(styleFile) { + styleFile.copyToFollowingLinks(destFile.parent, destFile.leafName); + } else { + Zotero.File.putContents(destFile, style); + } + + // recache + Zotero.Styles.init(); + + // refresh preferences windows + var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]. + getService(Components.interfaces.nsIWindowMediator); + var enumerator = wm.getEnumerator("zotero:pref"); + while(enumerator.hasMoreElements()) { + var win = enumerator.getNext(); + win.refreshStylesList(styleID); + } + } +} + +/** + * @class Represents a style file and its metadata + * @property {nsIFile} file The path to the style file + * @property {String} styleID + * @property {String} type "csl" for CSL styles, "ens" for legacy styles + * @property {String} title + * @property {String} updated SQL-style date updated + * @property {String} class "in-text" or "note" + * @property {String} source The CSL that contains the formatting information for this one, or null + * if this CSL contains formatting information + * @property {Zotero.CSL} csl The Zotero.CSL object used to format using this style + * @property {Boolean} hidden True if this style is hidden in style selection dialogs, false if it + * is not + */ +Zotero.Style = function(file) { + this.file = file; + + var extension = file.leafName.substr(-4).toLowerCase(); + if(extension == ".ens") { + this.type = "ens"; + + this.styleID = Zotero.Styles.ios.newFileURI(this.file).spec; + this.title = file.leafName.substr(0, file.leafName.length-4); + this.updated = Zotero.Date.dateToSQL(new Date(file.lastModifiedTime)); + } else if(extension == ".csl") { + // "with ({});" needed to fix default namespace scope issue + // See https://bugzilla.mozilla.org/show_bug.cgi?id=330572 + default xml namespace = "http://purl.org/net/xbiblio/csl"; with ({}); + + this.type = "csl"; + + var xml = Zotero.CSL.Global.cleanXML(Zotero.File.getContents(file)); + try { + xml = new XML(xml); + } catch(e) { + Zotero.log(e.toString(), "error", + Zotero.Styles.ios.newFileURI(this.file).spec, xml.split(/\r?\n/)[e.lineNumber-1], + e.lineNumber); + return; + } + + this.styleID = xml.info.id.toString(); + this.title = xml.info.title.toString(); + this.updated = xml.info.updated.toString().replace(/(.+)T([^\+]+)\+?.*/, "$1 $2"); + this._class = xml.@class.toString(); + + this.source = null; + for each(var link in xml.info.link) { + if(link.@rel == "source") { + this.source = link.@href.toString(); + if(this.source == this.styleID) { + throw "Style with ID "+this.styleID+" references itself as source"; + this.source = null; + } + break; + } + } + } +} + +Zotero.Style.prototype.__defineGetter__("csl", +/** + * Retrieves the Zotero.CSL object for this style + * @type Zotero.CSL + */ +function() { + // cache last style + if(Zotero.Styles.cacheTranslatorData && Zotero.Styles.lastCSL && + Zotero.Styles.lastCSL.styleID == this.styleID) { + return Zotero.Styles.lastCSL; + } + + if(this.type == "ens") { + // EN style + var string = Zotero.File.getBinaryContents(this.file); + var enConverter = new Zotero.ENConverter(string, null, this.title); + var xml = enConverter.parse(); + } else { + // "with ({});" needed to fix default namespace scope issue + // See https://bugzilla.mozilla.org/show_bug.cgi?id=330572 + default xml namespace = "http://purl.org/net/xbiblio/csl"; with ({}); + + if(this.source) { + // parent/child + var formatCSL = Zotero.Styles.get(this.source); + if(!formatCSL) { + throw(new Error('Style references '+this.source+', but this style is not installed', + Zotero.Styles.ios.newFileURI(this.file).spec, null)); + } + var file = formatCSL.file; + } else { + var file = this.file; + } + + var cslString = Zotero.File.getContents(file); + var xml = new XML(Zotero.CSL.Global.cleanXML(cslString)); + } + + return (Zotero.Styles.lastCSL = new Zotero.CSL(xml)); +}); + +Zotero.Style.prototype.__defineGetter__("class", +/** + * Retrieves the style class, either from the metadata that's already loaded or by loading the file + * @type String + */ +function() { + if(this._class) return this._class; + return (this._class = this.csl.class); +}); + +/** + * Deletes a style + */ +Zotero.Style.prototype.delete = function() { + // make sure no styles depend on this one + var dependentStyles = false; + var styles = Zotero.Styles.getAll(); + for each(var style in styles) { + if(style.source == this.styleID) { + dependentStyles = true; + break; + } + } + + if(dependentStyles) { + // copy dependent styles to hidden directory + var hiddenDir = Zotero.getStylesDirectory(); + hiddenDir.append("hidden"); + Zotero.File.createDirectoryIfMissing(hiddenDir); + this.file.moveTo(hiddenDir, null); + } else { + // remove defunct files + this.file.remove(false); + } + + // check to see if this style depended on a hidden one + if(this.source) { + var source = Zotero.Styles.get(this.source); + if(source && source.hidden) { + var deleteSource = true; + + // check to see if any other styles depend on the hidden one + for each(var style in Zotero.Styles.getAll()) { + if(style.source == this.source && style.styleID != this.styleID) { + deleteSource = false; + break; + } + } + + // if it was only this style with the dependency, delete the source + if(deleteSource) { + source.delete(); + } + } + } + + Zotero.Styles.init(); +} +\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js @@ -275,9 +275,8 @@ var Zotero = new function(){ Zotero.Sync.Runner.init(); Zotero.Sync.Storage.init(); + Zotero.MIMETypeHandler.init(); Zotero.Proxies.init(); - Zotero.Ingester.MIMEHandler.init(); - Zotero.Styles.MIMEHandler.init(); this.initialized = true; Zotero.debug("Initialized in "+((new Date()).getTime() - start)+" ms"); diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties @@ -495,12 +495,12 @@ integration.deleteCitedItem.title = Are you sure you want to remove this referen integration.deleteCitedItem.body = This reference is cited in the text of your document. Deleting it will remove all citations. styles.installStyleURI = Install style "%1$S" from %2$S? -styles.installStyle = Install style "%1$S"? styles.updateStyleURI = Update existing style "%1$S" with "%2$S" from %3$S? -styles.updateStyle = Update existing style "%1$S" with "%2$S"? styles.installed = The style "%S" was installed successfully. -styles.installError = %S does not appear to be a valid CSL file. +styles.installErrorURI = %S does not appear to be a valid CSL file. +styles.installSourceErrorURI = %1$S references an invalid or non-existent CSL file at %2$S as its source. styles.deleteStyle = Are you sure you want to delete the style "%1$S"? +styles.deleteStyles = Are you sure you want to delete the selected styles? sync.storage.kbRemaining = %SKB remaining sync.storage.none = None diff --git a/chrome/skin/default/zotero/preferences.css b/chrome/skin/default/zotero/preferences.css @@ -164,6 +164,11 @@ grid row hbox:first-child -moz-box-align: center; } +#styleManager-csl +{ + width: 36px; +} + #zotero-prefpane-keys textbox { margin-left: -1px; diff --git a/components/zotero-service.js b/components/zotero-service.js @@ -18,8 +18,8 @@ var xpcomFiles = [ 'zotero', 'annotate', 'attachments', - 'cite', 'collectionTreeView', + 'csl', 'dataServer', 'data_access', 'data/dataObjects', @@ -43,6 +43,7 @@ var xpcomFiles = [ 'integration', 'itemTreeView', 'mime', + 'mimeTypeHandler', 'notifier', 'progressWindow', 'proxy', @@ -50,6 +51,7 @@ var xpcomFiles = [ 'report', 'schema', 'search', + 'style', 'sync', 'storage', 'timeline',