www

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

commit d8868cd9cbfbf5bc3f21fe4db1173b946efa31c3
parent d65e75fbc9863a2fe95a8f7df3cb7de02e6f4390
Author: Simon Kornblith <simon@simonster.com>
Date:   Fri, 20 Jun 2008 06:40:05 +0000

fixes #1030, CSL broken on trunk due to XML namespace change
closes #704, EndNote to Zotero style converter (won't actually convert styles due to copyright concerns, but will load them into the DB)
also adds CSL style manager


Diffstat:
Mchrome/content/zotero/preferences/preferences.js | 131+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/preferences/preferences.xul | 23+++++++++++++++++++++++
Mchrome/content/zotero/xpcom/cite.js | 76+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------
Achrome/content/zotero/xpcom/enstyle.js | 1375+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mchrome/content/zotero/xpcom/zotero.js | 55+++++++++++++++++++++++++++++++++----------------------
Mchrome/locale/en-US/zotero/preferences.dtd | 4++++
Mchrome/locale/en-US/zotero/zotero.properties | 7++++++-
Mchrome/skin/default/zotero/preferences.css | 8+++++++-
Achrome/skin/default/zotero/prefs-styles.png | 0
Mcomponents/zotero-service.js | 4++--
10 files changed, 1644 insertions(+), 39 deletions(-)

diff --git a/chrome/content/zotero/preferences/preferences.js b/chrome/content/zotero/preferences/preferences.js @@ -31,6 +31,7 @@ function init() rows[i].firstChild.nextSibling.value = Zotero.isMac ? 'Cmd+Shift+' : 'Ctrl+Alt+'; } + refreshStylesList(); populateQuickCopyList(); updateQuickCopyInstructions(); initSearchPane(); @@ -912,4 +913,134 @@ function onOpenURLSelected() function onOpenURLCustomized() { document.getElementById('openURLMenu').value = "custom"; +} + +/** STYLES **/ + +/** + * Refreshes the list of styles in the styles pane + **/ +function refreshStylesList(cslID) { + var treechildren = document.getElementById('styleManager-rows'); + while (treechildren.hasChildNodes()) { + treechildren.removeChild(treechildren.firstChild); + } + + var sql = "SELECT cslID, title, updated FROM csl ORDER BY title"; + var styleData = Zotero.DB.query(sql); + if (!styleData) return; + + Zotero.debug("ASKED FOR "+cslID); + + var selectIndex = false; + for (var i=0; i<styleData.length; i++) { + var treeitem = document.createElement('treeitem'); + var treerow = document.createElement('treerow'); + var titleCell = document.createElement('treecell'); + var updatedCell = document.createElement('treecell'); + + var updatedDate = Zotero.Date.formatDate(Zotero.Date.strToDate(styleData[i].updated), true); + + treeitem.setAttribute('id', 'zotero-csl-'+styleData[i].cslID); + titleCell.setAttribute('label', styleData[i].title); + updatedCell.setAttribute('label', updatedDate); + + treerow.appendChild(titleCell); + treerow.appendChild(updatedCell); + treeitem.appendChild(treerow); + treechildren.appendChild(treeitem); + + if(cslID == styleData[i].cslID) { + document.getElementById('styleManager').view.selection.select(i); + } + } +} + +/** + * Adds a new style to the style pane + **/ +function addStyle() { + const nsIFilePicker = Components.interfaces.nsIFilePicker; + var fp = Components.classes["@mozilla.org/filepicker;1"] + .createInstance(nsIFilePicker); + fp.init(window, Zotero.getString("zotero.preferences.styles.addStyle"), nsIFilePicker.modeOpen); + + fp.appendFilter("CSL Style", "*.csl"); + fp.appendFilter("EndNote Style", "*.ens"); + + 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 fallback name and modification date + var fallbackName = file.leafName; + fallbackName = fallbackName.replace(/\.ens$/i, ""); + var date = new Date(file.lastModifiedTime); + Zotero.debug(file.lastModifiedTime); + Zotero.debug(date); + + try { + var enConverter = new Zotero.ENConverter(read, date, fallbackName); + var xml = enConverter.parse(); + } catch(e) { + styleImportError(); + throw e; + } + var cslID = Zotero.Cite.installStyle(xml.toXMLString()); + } 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.Cite.installStyle(req.responseText); + } + } + + if(cslID !== false) this.refreshStylesList(cslID); +} + +/** + * Deletes a style from the style pane + **/ +function deleteStyle() { + var tree = document.getElementById('styleManager'); + var treeitem = tree.lastChild.childNodes[tree.currentIndex]; + Zotero.debug(treeitem.getAttribute('id')); + var cslID = treeitem.getAttribute('id').substr(11); + + Zotero.Cite.deleteStyle(cslID); + this.refreshStylesList(); +} + +/** + * Shows an error if import fails + **/ +function styleImportError() { + alert(Zotero.getString('styles.installError', "This")); } \ No newline at end of file diff --git a/chrome/content/zotero/preferences/preferences.xul b/chrome/content/zotero/preferences/preferences.xul @@ -323,6 +323,29 @@ To add a new preference: <separator/> <label id="quickCopy-macWarning" hidden="true" value="&zotero.preferences.quickCopy.macWarning;"/> + </groupbox> + </prefpane> + + + <prefpane id="zotero-prefpane-styles" + label="&zotero.preferences.prefpane.styles;" + image="chrome://zotero/skin/prefs-styles.png"> + <groupbox flex="1"> + <caption label="&zotero.preferences.styles.styleManager;"/> + + <tree flex="1" id="styleManager" hidecolumnpicker="true" rows="6" + onkeypress="if (event.keyCode == event.DOM_VK_DELETE) { deleteSelectedStyle(); }"> + <treecols> + <treecol id="styleManager-title" label="&zotero.preferences.styles.styleManager.title;" flex="3"/> + <treecol id="styleManager-updated" label="&zotero.preferences.styles.styleManager.updated;" flex="1"/> + </treecols> + <treechildren id="styleManager-rows"/> + </tree> + <separator class="thin"/> + <hbox pack="end"> + <button label="-" onclick="deleteStyle()"/> + <button label="+" onclick="addStyle()"/> + </hbox> <separator/> <label class="text-link" href="http://www.zotero.org/styles/" value="&zotero.preferences.export.getAdditionalStyles;"/> </groupbox> diff --git a/chrome/content/zotero/xpcom/cite.js b/chrome/content/zotero/xpcom/cite.js @@ -25,9 +25,9 @@ * this class handles pulling the CSL file and item data out of the database, * while CSL, below, handles the actual generation of the bibliography */ +default xml namespace = "http://purl.org/net/xbiblio/csl"; + Zotero.Cite = new function() { - default xml namespace = "http://purl.org/net/xbiblio/csl"; - var _lastCSL = null; var _lastStyle = null; @@ -35,6 +35,7 @@ Zotero.Cite = new function() { this.getStyleClass = getStyleClass; this.getStyle = getStyle; this.installStyle = installStyle; + this.deleteStyle = deleteStyle; /* * returns an associative array of cslID => styleName pairs @@ -105,8 +106,8 @@ Zotero.Cite = new function() { } if (!xml || error) { - alert(Zotero.getString('styles.installError', loadURI)); - return; + alert(Zotero.getString('styles.installError', (loadURI ? loadURI : "This"))); + return false; } var uri = xml.info.id.toString(); @@ -123,10 +124,18 @@ Zotero.Cite = new function() { + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_CANCEL); if (existingTitle) { - var text = Zotero.getString('styles.updateStyle', [existingTitle, title, loadURI]); + if(loadURI) { + var text = Zotero.getString('styles.updateStyleURI', [existingTitle, title, loadURI]); + } else { + var text = Zotero.getString('styles.updateStyle', [existingTitle, title]); + } } else { - var text = Zotero.getString('styles.installStyle', [title, loadURI]); + if(loadURI) { + var text = Zotero.getString('styles.installStyleURI', [title, loadURI]); + } else { + var text = Zotero.getString('styles.installStyle', [title]); + } } var acceptButton = Zotero.getString('general.install'); @@ -142,6 +151,27 @@ Zotero.Cite = new function() { var sql = "REPLACE INTO csl VALUES (?,?,?,?)"; Zotero.DB.query(sql, [uri, updated, title, cslString]); alert(Zotero.getString('styles.installed', title)); + return uri; + } + } + + /** + * deletes a style + **/ + function deleteStyle(uri) { + var sql = "SELECT title FROM csl WHERE cslID=?"; + var title = Zotero.DB.valueQuery(sql, uri); + + if(!title) throw "Cite: style to delete does not exist!" + + 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)) { + var sql = "DELETE FROM csl WHERE cslID=?"; + Zotero.DB.query(sql, uri); } } } @@ -277,7 +307,7 @@ Zotero.Cite.MIMEHandler.StreamListener.prototype.onStopRequest = function(channe * want to use the Scholar data model, but does want to use CSL in JavaScript */ Zotero.CSL = function(csl) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; + default xml namespace = "http://purl.org/net/xbiblio/csl"; with ({}); this._csl = new XML(Zotero.CSL.Global.cleanXML(csl)); @@ -292,6 +322,7 @@ Zotero.CSL = function(csl) { Zotero.debug("CSL: style class is "+this.class); this.hasBibliography = (this._csl.bibliography.length() ? 1 : 0); + Zotero.debug("hasBibliography "+this.hasBibliography); } /* @@ -344,6 +375,8 @@ 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"; @@ -535,6 +568,8 @@ Zotero.CSL.prototype.formatCitation = function(citation, format) { * 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; @@ -770,6 +805,8 @@ Zotero.CSL.prototype._getTerm = function(term, plural, form, includePeriod) { * 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; @@ -953,6 +990,8 @@ Zotero.CSL.prototype._processNames = function(item, element, formattedString, co */ 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 @@ -1381,6 +1420,8 @@ Zotero.CSL.prototype._processElements = function(item, element, formattedString, * 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 = []; @@ -1496,6 +1537,8 @@ Zotero.CSL.prototype.cachedSort = function(items, context, field) { } Zotero.CSL.prototype.getEqualCitations = function(items) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + var citationsEqual = []; if(items) { @@ -1524,6 +1567,8 @@ Zotero.CSL.prototype.getEqualCitations = function(items) { * 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) { @@ -1549,7 +1594,7 @@ Zotero.CSL.Global = new function() { this.cleanXML = cleanXML; this.parseLocales = parseLocales; - default xml namespace = "http://purl.org/net/xbiblio/csl"; + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); this.ns = "http://purl.org/net/xbiblio/csl"; this.__defineGetter__("locale", function() { @@ -1810,6 +1855,8 @@ Zotero.CSL.CitationItem = function(item) { * 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; @@ -1893,8 +1940,6 @@ Zotero.CSL.Citation.prototype.clone = function() { * with "_") are implemented. */ Zotero.CSL.Item = function(item) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; - if(item instanceof Zotero.Item) { this.zoteroItem = item; } else if(parseInt(item, 10) == item) { @@ -2337,7 +2382,7 @@ Zotero.CSL.Item.Name = function(zoteroCreator) { * lastName - last name */ Zotero.CSL.Item.Name.prototype.getNameVariable = function(variable) { - return this._zoteroCreator[variable] ? this._zoteroCreator[variable] : ""; + return this._zoteroCreator.ref[variable] ? this._zoteroCreator.ref[variable] : ""; } /* @@ -2345,7 +2390,7 @@ Zotero.CSL.Item.Name.prototype.getNameVariable = function(variable) { * in an item wrapper. */ Zotero.CSL.ItemSet = function(items, csl) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); this.csl = csl; @@ -2465,6 +2510,8 @@ Zotero.CSL.ItemSet.prototype.remove = function(items) { * 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); @@ -2722,7 +2769,7 @@ Zotero.CSL.ItemSet.prototype._copyDisambiguation = function(fromItem, toItem) { } Zotero.CSL.FormattedString = function(context, format, delimiter, subsequent) { - default xml namespace = "http://purl.org/net/xbiblio/csl"; + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); this.context = context; this.option = context ? context.option : new XMLList(); @@ -2754,6 +2801,8 @@ 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; } @@ -2798,6 +2847,7 @@ Zotero.CSL.FormattedString._rtfEscapeFunction = function(aChar) { * 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; diff --git a/chrome/content/zotero/xpcom/enstyle.js b/chrome/content/zotero/xpcom/enstyle.js @@ -0,0 +1,1374 @@ +/* + ***** 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 ***** +*/ + +/** + * Constructor for EndNote converter + * @constructor + **/ +Zotero.ENConverter = function(styleData, date, fileTitle) { + this.data = styleData; + if(date) this.date = date; + if(fileTitle) this.fileTitle = fileTitle; + + var formatCode = this.data.substr(8, 8); + if(formatCode != "RSFTSTYL" && formatCode != "ENDNENFT") { + throw "Zotero.ENConverter: File not recognized. Is this a style file?"; + } +} + +/** + * Mappings for item types + * + * Those in lower case are not in the mapping list because they are implemented + * elsewhere in the code. + * Those in upper case are not implemented. + **/ +Zotero.ENConverter.typeMappings = { + "\x00":"article-journal", + "\x01":"book", + "\x02":"thesis", +// \x03 - CONFERENCE PROCEEDINGS + "\x04":"personal_communication", + "\x05":"article-newspaper", +// \x06 - PROGRAM + "\x07":"chapter", + "\x08":"article-magazine", +// \x09 - Edited Book + "\x0A":"report", + "\x0B":"map", +// \x0C - AUDIOVISUAL MATERIAL + "\x0D":"graphic", +// \x0E - UNUSED 1 + "\x0F":"patent", + "\x10":"webpage", + "\x11":"bill", + "\x12":"legal_case", +// \x13 - HEARING + "\x14":"manuscript", + "\x15":"motion_picture", +// \x16 - STATUTE +// \x17 - UNUSED 2 +// \x18 - UNUSED 3 + "\x19":"figure", +// \x1A - CHART OR TABLE +// \x1B - EQUATION +// \x1C - Electronic Article +// \x1D - Electronic Book +// \x1E - ONLINE DATABASE +// \x1F - Generic +// \x20 - GOVERNMENT DOCUMENT + "\x21":"paper-conference", +// \x22 - ONLINE MULTIMEDIA +// \x23 - CLASSICAL WORK + "\x24":"legislation", +// \x25 - UNPUBLISHED WORK +// \x26 - ANCIENT TEXT + "\x27":"entry-dictionary", + "\x28":"entry-encyclopedia" +// \x29 - GRANT +}; + +/** + * List of CSL fallback types + **/ +Zotero.ENConverter.fallbackTypes = { + "book":true, + "chapter":true, + "article":true +} + +/** + * List of types that we should parse, but not map directly + **/ +Zotero.ENConverter.supplementalTypes = { + "\x09":true, // Edited Book (use conditional on book) + "\x1C":true, // Electronic Article (use conditional on article) + "\x1D":true, // Electronic Book (use conditional on book) + "\x1F":true // Generic (use for all unspecified types) +}; + +/** + * Mappings for text variables + * + * Those in lower case are not in the mapping list because they are implemented + * elsewhere in the code. + * Those in upper case are not implemented. + **/ +Zotero.ENConverter.variableMappings = { +// \x01 - TYPE OF REFERENCE +// \x02 - Author/Editor +// \x03 - Year + "\x05":"page", +// \x06 - Secondary/Series/Publication Title/Bill Code + "\x07":"volume", + "\x08":"issue", + "\x09":"number-of-volumes", +// \x0A - Secondary/Series Author + "\x0B":"publisher-place", + "\x0C":"publisher", +// \x0D - Translator/Subsidiary Author +// \x0F - KEYWORD + "\x10":"genre", +// \x11 - Date + "\x12":"abstract", + "\x13":"citation-label", + "\x14":"URL", +// \x15 - TERTIARY TITLE +// \x16 - TERTIARY AUTHOR + "\x17":"note", + "\x18":"ISBN", +// \x19 - CUSTOM 1 +// \x1A - CUSTOM 2 +// \x1B - CUSTOM 3 +// \x1C - CUSTOM 4 +// \x1D - ABBREVIATION +// \x1E - ACCESSION NUMBER +// \x1F - CALL NUMBER +// \x21 - CUSTOM 5 +// \x22 - CUSTOM 6 +// \x23 - SECTION/PAGES CITED/Chapter Title +// \x24 - ORIGINAL PUBLICATION +// \x25 - REPRINT EDITION +// \x26 - REVIEWED ITEM +// \x27 - AUTHOR ADDRESS +// \x28 - IMAGE +// \x29 - CAPTION +// \x2A - CUSTOM 7 +// \x2B - ELECTRONIC RESOURCE NUMBER +// \x2C - LINK TO PDF +// \x2D - TRANSLATED AUTHOR +// \x2E - TRANSLATED TITLE +// \x2F - NAME OF DATABASE +// \x30 - DATABASE PROVIDER +// \x31 - RESEARCH NOTES +// \x32 - LANGUAGE +// \x33 - accessed +// \x34 - LAST MODIFIED +// \x00\x40 - citation + "\x01\x40":"citation-number", +// \x02\x40 - RECORD NUMBER + "\x06\x40":"locator" +} + +/** + * List of types that we should map to macros. + **/ +Zotero.ENConverter.macroVariables = { + "\x02":["author-citation", "author-bibliography"], // Author + "\x03":["year-citation", "year-bibliography"], // Year + "\x04":["title-citation", "title-bibliography"], // Title/Book Title + "\x06":"secondary_title", // Series/Publication Title + "\x0A":["secondary_author-citation", "secondary_author-bibliography"], // Editor/Series Editor + "\x0D":["translator-citation", "translator-bibliography"], // Translator + "\x0E":"edition", // Edition + "\x11":"date", // Date + "\x15":"tertiary_title", + "\x16":["tertiary_author-citation", "tertiary_author-bibliography"], // Chapter Series Editor + "\x20":"short-title", // Short Title/Short Book Title + "\x23":"chapter_title", // Chapter Title + "\x33":"accessed", + "\x00\x40":"citation" // Citation +}; + +/** + * List of types where \x06 maps to collection-title (rather than + * container-title) + **/ +Zotero.ENConverter.seriesTypes = "book report map motion_picture"; +Zotero.ENConverter.seriesCodes = ["\x01", "\x09", "\x0A", "\x0B", "\x15", "\x1D"]; + +/** + * Generates the <info> element for the style + **/ +Zotero.ENConverter.prototype.parseInfo = function() { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + var guid = ""; + for(var i=0; i<16; i++) { + var bite = Math.floor(Math.random() * 255); + + if(i == 4 || i == 6 || i == 8 || i == 10) { + guid += "-"; + + // version + if(i == 6) bite = bite & 0x0f | 0x40; + // variant + if(i == 8) bite = bite & 0x3f | 0x80; + } + var str = bite.toString(16); + guid += str.length == 1 ? '0' + str : str; + } + + this.xml.info.id = "urn:uuid:"+guid; + if(this.fileTitle) { + var title = this.fileTitle; + } else { + var title = this.convertFromUTF16(this.findField(this.fields, "\x10")[0].data.replace("\xFB", "", "g")); + if(!title) title = "Untitled Style"; + } + this.xml.info.title = title; + + if(this.date) { + var date = this.date; + } else { + var date = new Date(); + } + + var y = date.getFullYear().toString(); + var m = (date.getUTCMonth()+1).toString(); + var d = date.getUTCDay().toString(); + var h = date.getUTCHours().toString(); + var n = date.getUTCMinutes().toString(); + var s = date.getUTCSeconds().toString(); + if(m.length == 1) m = "0"+m; + if(d.length == 1) d = "0"+d; + if(h.length == 1) h = "0"+h; + if(n.length == 1) n = "0"+n; + if(s.length == 1) s = "0"+s; + this.xml.info.updated = y+"-"+m+"-"+d+"T"+h+":"+n+":"+s+"+00:00"; +} + +/** + * Converts a little endian binary representation of an integer into a JS integer + * + * @param {String} binaryData The binary representation of the integer + * @returns The JS integer + * @type Integer + **/ +Zotero.ENConverter.prototype.parseInt = function(binaryData) { + if(binaryData.length == 4) { + // since this is so common, avoid overhead + return binaryData.charCodeAt(0)+binaryData.charCodeAt(1)*0x100 + +binaryData.charCodeAt(2)*0x10000+binaryData.charCodeAt(3)*0x1000000; + } else { + var integer = 0; + for(var i=0; i<binaryData.length; i++) { + integer += binaryData.charCodeAt(i)*Math.pow(0x100, i); + } + return integer; + } +} + +/** + * Parses a commonly used string format found in these EndNote files, which + * is padded by 8 bytes at the beginning and contains an \xFB\xFB + * + * @param {String} string The unparsed string + * @param {Boolean} richOutput Whether to return XML <text> tags instead of + * plain text + * + * @returns Plain text, or the XML representation as <text> tags, depending on + * the richOutput parameter + **/ +Zotero.ENConverter.prototype.parseFormattedString = function(string, richOutput) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + var stringLength = this.parseInt(string.substr(4, 4))-8; + if(stringLength == 0) return richOutput ? false : ""; + + var newString = this.convertFromUTF16(string.substr(8, stringLength)); + + // strip out chars that shouldn't be there + newString = newString.replace(/[\x00-\x08\x0E-\x1F]/g, ""); + + if(richOutput) { + var formattedXML = new XML(); + var lastOffset = 0; + var style = false; + + var i = stringLength + 8; + while(i < string.length) { + var length = this.parseInt(string.substr(i+4, 4)); + var data = string.substr(i+8, length-8); + if(length == 0) break; + var startOffset = this.parseInt(data.substr(4, 4)); + + // add term containing text from last element to new element + var text = newString.substring(lastOffset, startOffset); + if(lastOffset != startOffset) { + var textElement = <text value={text}/>; + if(style) this.applyFormattingAttributes(textElement, style); + formattedXML += textElement; + } + + var style = data.substr(data.indexOf("&")+4, 4); + lastOffset = startOffset; + + i += length + length % 4; + } + + if(lastOffset != newString.length) { + var text = newString.substring(lastOffset); + var textElement = <text value={text}/>; + if(style) this.applyFormattingAttributes(textElement, style); + formattedXML += textElement; + } + + return formattedXML; + } else { + return newString; + } +} + + +/** + * Applies the formatting attributes specified by a binary string to a given + * text element + * + * @param {XML} element The element to which the formatting attributes will be + * applied + * @param {String} formatCode The binary format string + **/ +Zotero.ENConverter.prototype.applyFormattingAttributes = function(element, formatCode) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + if(!formatCode) return; + + var binaryDigits = this.parseInt(formatCode).toString(2); + // pad to 7 digits + while(binaryDigits.length < 7) binaryDigits = "0"+binaryDigits; + + if(binaryDigits[6] == "1") { // italics + element["@font-weight"] = "bold"; + } + if(binaryDigits[5] == "1") { // bold + element["@font-style"] = "italic"; + } + if(binaryDigits[4] == "1") { // underline + element["@text-decoration"] = "underline"; + } + // what are bits 3 and 2? + if(binaryDigits[1] == "1") { // subscript + element["@vertical-align"] = "sup"; + } else if(binaryDigits[0] == "1") { // superscript + element["@vertical-align"] = "sub"; + } +} + +/** + * Parses format to create a hierarchical data structure + * + * @param {String} styleData The EndNote style file, as binary data + * @returns An array of objects representing the structure of the EndNote file + * @type Array + **/ +Zotero.ENConverter.prototype.parseFormat = function(styleData) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + var fields = []; + + var offset1 = styleData.indexOf("\x10"); + var offset2 = styleData.indexOf("\x11"); + if(offset1 != -1 && (offset2 == -1 || offset1 < offset2)) { // \x10 comes first + var offset = offset1; + } else if(offset2 != -1) { // \x11 comes first + var offset = offset2; + } else { // both must be -1; no tags here + return [styleData.replace("\xFB", "", "g")]; + } + // want to start with tag before + offset--; + // for the \x10\x10 case + if(offset === -1) offset = 0; + + if(offset !== 0) { + fields.push(styleData.substr(0, offset).replace("\xFB", "", "g")); + } + + while(offset < styleData.length) { + if((styleData[offset] == "\x10" || styleData[offset] == "\x11" || styleData[offset] == "\x12") + && styleData[offset+1] == "\x00" && styleData[offset+2] == "\x03") { + // 10 00 02 style short tag (use unknown) + + fields.push({dle:styleData[offset], + type:"\x00", + flags:styleData[offset+3], + data:styleData.substr(offset+4, 4)}); + + offset += 8; + } else if(styleData[offset+2] == "\x01" || styleData[offset+2] == "\x02") { + // standard data-bearing tag + + // size is little endian + var size = this.parseInt(styleData.substr(offset+4, 4)); + size += size % 4; + + // set field data + var field = {code:styleData[offset], + dle:styleData[offset+1], + type:styleData[offset+2], + flags:styleData[offset+3], + size:size, + data:styleData.substr(offset+8, size-8)}; + field.subfields = this.parseFormat(field.data); + fields.push(field); + + offset += size; + } else if(styleData[offset+2] == "\x03" || styleData[offset+2] == "\x00") { + // 10 03 style short tag (use unknown) + + fields.push({code:styleData[offset], + dle:styleData[offset+1], + type:"\x03", + flags:styleData[offset+3], + data:styleData.substr(offset+4, 4)}); + + offset += 8; + } else if(styleData[offset+1] != "\x10" && styleData[offset+1] != "\x11") { + // unknown tag; error out + throw("Zotero.ENConverter: Unexpected end of file at "+offset.toString(16)+"; "+varDump(styleData.substr(offset, offset+4))); + } + } + + return fields; +} + +/** + * Finds all instances of a given field at a given level of the hierarchy + * @param {Array} fields The array of fields + * @param {String} code The field code + **/ +Zotero.ENConverter.prototype.findField = function(fields, code) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + var output = []; + + while(code.length) { + if(typeof code != "object") { + var searchCode = code; + code = []; + } else { + var searchCode = code.shift(); + } + + for(var i=0; i<fields.length; i++) { + if(fields[i].code && fields[i].code == searchCode) { + if(!code.length) { + output.push(fields[i]); + } else { + fields = fields[i].subfields; + break; + } + } + } + } + + return output; +} + +/** + * Parses various options pertaining to a name to generate a <names/> tag + * + * @param {Object} options A binary field object + * @returns A <names/> tag + * @type XML + **/ +Zotero.ENConverter.prototype.parseName = function(options) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + var authorXML = <name/>; + + // "Author Lists" preferences + var delimiterOptions = this.findField(options.subfields, ["t", "x"]); + if(this.parseInt(this.findField(delimiterOptions[0].subfields, ["z"])[0].data) == 2) { + // this probably means delimiter-precedes-last shouldn't be set, because + // this is probably configured such that, from 1-2, we use no delimiter + // before the last, but for all other numbers of authors we do + var delimiter = this.parseFormattedString(this.findField(delimiterOptions[1].subfields, "{")[0].data); + var lastDelimiter = this.parseFormattedString(this.findField(delimiterOptions[1].subfields, "|")[0].data); + } else { + var delimiter = this.parseFormattedString(this.findField(delimiterOptions[0].subfields, "{")[0].data); + var lastDelimiter = this.parseFormattedString(this.findField(delimiterOptions[0].subfields, "|")[0].data); + // delimiter-precedes-last should probably be "always" or "never" + // ignore second set of list separator options + if(lastDelimiter.length >= delimiter.length + && lastDelimiter.substr(0, delimiter.length) == delimiter) { + // there is a delimiter preceding the last + authorXML["@delimiter-precedes-last"] = "always"; + } else { + // there is no delimiter + authorXML["@delimiter-precedes-last"] = "never"; + } + } + if(delimiter !== "") authorXML["@delimiter"] = delimiter; + // TODO: alter locale "and" if neither text nor symbol + if(lastDelimiter.indexOf("and") !== -1) { + // text and + authorXML["@and"] = "text"; + } else if(lastDelimiter.indexOf("&") !== -1) { + // symbol and + authorXML["@and"] = "symbol"; + } + + // "Author Name" preferences + var firstAuthorPref = this.findField(options.subfields, "q")[0].data[0]; + var subsequentAuthorPref = this.findField(options.subfields, "r")[0].data[0]; + if(firstAuthorPref != "\x00") { + if(subsequentAuthorPref != "\x00") { + authorXML["@name-as-sort-order"] = "all"; + } else { + authorXML["@name-as-sort-order"] = "first"; + } + } + if(firstAuthorPref == "\x01") { + authorXML["@sort-separator"] = ", "; + } else if(firstAuthorPref == "\x02") { + authorXML["@sort-separator"] = " "; + } + + var capitalizationPref = this.findField(options.subfields, "s")[0].data[0]; + if(capitalizationPref == "\x02") { + authorXML["@text-case"] = "uppercase"; + } else if(capitalizationPref == "\x03") { + authorXML["@font-variant"] = "small-caps"; + } + + var initialPref = this.findField(options.subfields, "p")[0].data[0]; + if(initialPref == "\x01") { // B. C. + authorXML["@initialize-with"] = ". "; + } else if(initialPref == "\x02") { // B.C. + authorXML["@initialize-with"] = "."; + } else if(initialPref == "\x03") { // B C + authorXML["@initialize-with"] = " "; + } else if(initialPref == "\x04") { // BC + authorXML["@initialize-with"] = ""; + } else if(initialPref == "\x05") { // just last name + authorXML["@form"] = "short"; + } + + return authorXML; +} + +/** + * Parses options pertaining to subsequent author handling + * + * @param {Object} etAlOptions A binary field object + * @param {XML} context The CSL context (<bibliography> or <citation> tag) + * @param {Boolean} subsequent Whether this set of fields controls the behavior + * of subsequent references. + **/ +Zotero.ENConverter.prototype.parseEtAl = function(etAlOptions, context, subsequent) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + if(this.findField(etAlOptions.subfields, "}")[0].data[0] == "\x01") { + // if author list abbreviation is on + + // et-al-min + var value = this.parseInt(this.findField(etAlOptions.subfields, "~")[0].data); + var optionName = subsequent ? "et-al-subsequent-min" : "et-al-min"; + context.prependChild(<option name={optionName} value={value}/>); + // et-al-use-first + var value = this.parseInt(this.findField(etAlOptions.subfields, "\x7F")[0].data); + var optionName = subsequent ? "et-al-subsequent-use-first" : "et-al-use-first"; + context.prependChild(<option name={optionName} value={value}/>); + // TODO: presumably, codes \x81 and \x80 contain the et-al term and + // whether to italicize the et-al, respectively. + } +} + +/** + * Parses sorting data + * + * @param {Object} sort A binary field object + **/ +Zotero.ENConverter.prototype.parseSort = function(sort) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + var sortXML = false; + var numberOfSortFields = this.parseInt(this.findField(sort.subfields, "\xB2")[0].data); + if(numberOfSortFields) { + sortXML = <sort/>; + var sortFields = this.findField(sort.subfields, ["\xB3", "\xB4"]); + for(var i=0; i<numberOfSortFields; i++) { + var variable = this.findField(sortFields[i].subfields, "\xB5")[0].data; + while(variable[variable.length-1] == "\x00") { + variable = variable.substr(0, variable.length-1); + } + + // add appropriate sort key + if(variable == "\x11") { // date + sortXML.key += <key variable="date"/>; + } else if(variable == "\x33") { // accessed + sortXML.key += <key variable="date"/>; + } else { + var macro = this.getMacro(variable, true); + if(macro) { + sortXML.key += <key macro={macro}/>; + } else if(Zotero.ENConverter.variableMappings[variable]) { + sortXML.key += <key variable={Zotero.ENConverter.variableMappings[variable]}/>; + } + } + } + } + return sortXML; +} + +/** + * Parses citation and bibliography options, adding them to the style as necessary + **/ +Zotero.ENConverter.prototype.parseOptions = function() { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + /** Bibliography Options **/ + var bibliography = this.findField(this.fields, "\x02")[0]; + var bibliographyOptions = this.findField(bibliography.subfields, "!")[0]; + + var etAlOptions = this.findField(bibliographyOptions.subfields, "u")[0]; + this.parseEtAl(etAlOptions, this.xml.bibliography); + + var performSubstitute = this.findField(bibliographyOptions.subfields, "v"); + if(performSubstitute[0].data[0] == "\x01") { + // subsequent author substitute should be skipped + this.xml.bibliography.option.prependChild(<option name="subsequent-author-substitute" value=""/>); + } else if(performSubstitute[0].data[0] == "\x02") { + // subsequent author substitute exists + var subsequentAuthorSubstitute = this.parseFormattedString(this.findField(bibliographyOptions.subfields, "w")[0].data); + this.xml.bibliography.prependChild(<option name="subsequent-author-substitute" value={subsequentAuthorSubstitute}/>); + } + + var hangingIndent = this.findField(bibliography.subfields, ["&", "e"])[0].data[0]; + if(hangingIndent == "\x01" || hangingIndent == "\x02") { + this.xml.bibliography.prependChild(<option name="hanging-indent" value="true"/>); + } + // TODO: support second paragraph only and all paragraphs but first + + // get author name tag + this.bibliographyAuthor = this.parseName(bibliographyOptions); + // get editor name tag + this.bibliographyEditor = this.parseName(this.findField(bibliography.subfields, '"')[0]); + + // text case + var textCase = this.findField(bibliography.subfields, " ")[0].data[0]; + this.bibliographyCase = false; + if(textCase == "\x01") { + this.bibliographyCase = "title"; + } else if(textCase == "\x02") { + this.bibliographyCase = "sentence"; + } + + // sort order + var bibliographySort = this.parseSort(this.findField(bibliography.subfields, ["'", "\xB1"])[0]); + if(bibliographySort) this.xml.bibliography.insertChildBefore(this.xml.bibliography.layout[0], bibliographySort); + + /** Citation Options **/ + var citation = this.findField(this.fields, "\x03")[0]; + var footnote = this.findField(this.fields, "\x04")[0]; + this.isFootnote = this.findField(footnote.subfields, "Q")[0].data[0] == "\x02"; + if(this.isFootnote) { // special footnote style specified + this.xml.@class = "note"; + + // get author name tag + var author = this.findField(footnote.subfields, "S")[0]; + this.citationAuthor = this.parseName(this.findField(footnote.subfields, "S")[0]); + + // get editor name tag + var editor = this.findField(footnote.subfields, "T")[0] + this.citationEditor = this.parseName(this.findField(footnote.subfields, "T")[0]); + + // get et al rules + var etAlOptions = this.findField(author.subfields, "u")[0]; + this.parseEtAl(etAlOptions, this.xml.citation); + + // text case + var textCase = this.findField(footnote.subfields, "R")[0].data[0]; + this.citationCase = false; + if(textCase == "\x01") { + this.citationCase = "title"; + } else if(textCase == "\x02") { + this.citationCase = "sentence"; + } + } else { + this.citationCase = this.bibliographyCase; + this.xml.@class = "in-text"; + + // parse name + var citationOptions = this.findField(citation.subfields, "4")[0]; + var name = this.parseName(citationOptions); + + // this is strange, because there is one preference called "include the + // author initials or full name in citation" to disambiguate and another + // called "use initials only for primary authors with the same last name." + // at the moment, we ignore the latter, since in the included APA style, + // it's checked, but no initials are specified. + var initializeWith = this.findField(citation.subfields, ">")[0].data[0]; + if(initializeWith != "\x00") { + this.xml.citation.prependChild(<option name="disambiguate-add-givenname" value="true"/>); + } + if(initializeWith == "A") { // B. C. + name["@initialize-with"] = ". "; + } else if(initializeWith == "B") { // B.C. + name["@initialize-with"] = "."; + } else if(initializeWith == "C") { // B C + name["@initialize-with"] = " "; + } else if(initializeWith == "D") { // BC + name["@initialize-with"] = ""; + } + + this.citationAuthor = this.citationEditor = name; + + var addNames = this.findField(citation.subfields, "?")[0].data[0]; + if(addNames != "\x00") { + this.xml.citation.prependChild(<option name="disambiguate-add-names" value="true"/>); + } + + var addYearSuffix = this.findField(citation.subfields, "=")[0].data[0]; + if(addYearSuffix == "A") { + this.xml.citation.prependChild(<option name="disambiguate-add-year-suffix" value="true"/>); + this.xml.citation.prependChild(<option name="collapse" value="year-suffix"/>); + } else if(addYearSuffix == "B") { + this.xml.citation.prependChild(<option name="disambiguate-add-year-suffix" value="true"/>); + + // TODO: There are more options here than are currently implemented in + // CSL. Specifically, one can choose the delimiter here, and one can + // choose not to omit the authors from citations with suffixes. + var collapse = this.findField(citation.subfields, "9")[0].data[0]; + if(collapse != "\x00") { + this.xml.citation.prependChild(<option name="collapse" value="year"/>); + } + } + + var etAlOptions = this.findField(citationOptions.subfields, "u")[0]; + this.parseEtAl(etAlOptions, this.xml.citation); + var etAlSubsequentOptions = this.findField(citation.subfields, "8")[0]; + this.parseEtAl(etAlSubsequentOptions, this.xml.citation, true); + + // can't determine whether to add this until we know whether the citation + // has a number + var numberCollapse = this.findField(citation.subfields, ["6", "\xD0"])[0].data[0]; + this.numberCollapse = numberCollapse == "\x00" ? false : true; + + // sort order + var sort = this.findField(citation.subfields, "5")[0]; + var useBibliographySort = this.findField(sort.subfields, "\xB0")[0].data[0]; + if(useBibliographySort == "\x02") { + if(bibliographySort) this.xml.citation.insertChildBefore(this.xml.citation.layout[0], bibliographySort); + } else { + var citationSort = this.parseSort(this.findField(sort.subfields, "\xB1")[0]); + if(citationSort) this.xml.citation.insertChildBefore(this.xml.citation.layout[0], citationSort); + } + + // TODO: CSL needs support for one number for references always cited + // together + + // 2 digit year format and ability to add title for different works by the + // same author are included in getMacro + } +} + +/** + * Converts text from UTF-16 + * + * @param {String} text The UTF-16 text + * @returns The text as a JS string + * @type String + **/ +Zotero.ENConverter.prototype.convertFromUTF16 = function(text) { + // convert from UTF-16 + return text.replace("\x00", "", "g"); // this won't work for non-ASCII + + var dataStream = Components.classes["@mozilla.org/io/string-input-stream;1"] + .createInstance(Components.interfaces.nsIStringInputStream); + dataStream.setData(text, text.length); + + var textStream = Components.classes["@mozilla.org/intl/converter-input-stream;1"] + .createInstance(Components.interfaces.nsIConverterInputStream); + textStream.init(dataStream, "UTF-16LE", 4096, "?"); + + var newText = ""; + var string = {}; + while(textStream.readString(text.length, string)) { + newText += string.value; + } + + return newText; +} + +/** + * Handles plural terms inside of <text/> fields (indicated by a caret, as in + * the EndNote editor) + * + * @param {XML} The <text/> fields + * @type {String} The type, as a string with the nulls trimmed + * @variable {String} The variable, as a string with the nulls trimmed + **/ +Zotero.ENConverter.prototype.pluralize = function(fields, type, variable) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + if(!fields) return; + var pluralVar = false; + switch(variable) { + case "\x02": // author + // \x09 - edited book + pluralVar = (type == "\x09" ? "editor" : "author"); + break; + + case "\x0A": // secondary author + if(Zotero.ENConverter.seriesCodes.indexOf(type) !== -1) { + pluralVar = "series-editor"; + } else if(type == "\x04") { // personal_communication + pluralVar = "recipient"; + } else if(type == "\x07") { // chapter + pluralVar = "editor"; + } + break; + + case "\x16": // tertiary author + if(type == "\x07") { // chapter + pluralVar = "series-editor"; + } + break; + + case "\x0D": // subsidiary author + pluralVar = "translator"; + break; + + case "\x05": // pages + pluralVar = "page"; + break; + + case "\x06\x40": // locator + pluralVar = "locator"; + break; + } + + for(var i=0; i<fields.length(); i++) { + var text = fields[i]; + if(text.@value.indexOf("^") !== -1) { + var textString = text.@value.toString(); + var singular = textString.replace(/([^\^\s]*)\^[^\^\s]*/g, "$1"); + if(pluralVar) { + var singularTerm = text.copy(); + var pluralTerm = text.copy(); + singularTerm.@value = singular; + pluralTerm.@value = textString.toString().replace(/[^\^\s]*\^([^\^\s]*)/g, "$1"); + + fields[i] = <choose> + <if is-plural={pluralVar}>{pluralTerm}</if> + <else>{singularTerm}</else> + </choose>; + } else { + text.@value = singular; + } + } + } +} + +/** + * Gets the name of the macro for a given variable and adds that macro to + * the style. + * + * @param {String} code The binary variable, with nulls trimmed. + * @param {Boolean} isBibliography Whether this variable occurs in the bibliography + * + * @returns A string macro name, or false if no macro is available for the given + * variable. + **/ +Zotero.ENConverter.prototype.getMacro = function(code, isBibliography) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + var name = Zotero.ENConverter.macroVariables[code]; + if(!name) return false; + if(typeof name == "object") name = Zotero.ENConverter.macroVariables[code][isBibliography ? 1 : 0]; + if(this.xml.macro.(@name == name).length()) return name; + + var macroXML = false; + switch(code) { + case "\x02": // author/editor + // if edited book, use editor; otherwise, use author + var editor = (isBibliography ? this.bibliographyEditor : this.citationEditor); + var author = (isBibliography ? this.bibliographyAuthor : this.citationAuthor); + + macroXML = <choose> + <if type="book" variable="editor"> + <names variable="editor">{editor}</names> + </if> + <else> + <names variable="author">{author}</names> + </else> + </choose>; + + // determine whether to add title to disambiguate + if(!isBibliography) { + var addTitle = this.findField(this.fields, ["\x03", "@"])[0].data[0]; + var titleElement = false; + if(addTitle == "A") { // long title + titleElement = <text variable="title"/>; + } else if(addTitle == "B") { // short title + titleElement = <text variable="title" form="short"/>; + } + + if(titleElement) { + macroXML += <choose> + <if disambiguate="true">{titleElement}</if> + </choose>; + } + } + break; + + case "\x03": // year + macroXML = <date variable="issued"> + <date-part name="year"/> + </date>; + + if(!isBibliography && !this.isFootnote) { + // check to see whether to use 2-digit year + var twoDigitYear = this.findField(this.fields, ["\x03", "2"])[0].data[0]; + if(twoDigitYear != "\x00") { + macroXML["date-part"]["@form"] = "short"; + } + } + break; + + case "\x04": // book title/title + var title = <text variable="title"/>; + var containerTitle = <text variable="container-title"/>; + + // determine whether to capitalize title + var textCase = isBibliography ? this.bibliographyCase : this.citationCase; + if(textCase) title["@text-case"] = containerTitle["@text-case"] = textCase; + + macroXML = <choose> + <if type="chapter"> + {containerTitle} + </if> + <else> + {title} + </else> + </choose>; + break; + + case "\x06": // secondary title + macroXML = <choose> + <if type={Zotero.ENConverter.seriesTypes} match="any"> + <text variable="collection-title"/> + </if> + <else> + <text variable="container-title"/> + </else> + </choose>; + break; + + case "\x0A": // secondary author + var nameElement = (isBibliography ? this.bibliographyEditor : this.citationEditor); + + macroXML = <choose> + <if type={Zotero.ENConverter.seriesTypes} match="any"> + <names variable="series-editor">{nameElement}</names> + </if> + <else-if type="personal_communication"> + <names variable="recipient">{nameElement}</names> + </else-if> + <else-if type="chapter"> + <names variable="editor">{nameElement}</names> + </else-if> + </choose>; + break; + + case "\x0D": // translator + var nameElement = (isBibliography ? this.bibliographyEditor : this.citationEditor); + + macroXML = <names variable="translator">{nameElement}</names> + break; + + case "\x0E": // edition + macroXML = <number variable="edition" form="ordinal"/>; + break; + + case "\x11": // date + macroXML = <date variable="issued"> + <date-part name="month" suffix=" "/> + <date-part name="day" suffix=", "/> + <date-part name="year"/> + </date>; + break; + + case "\x15": // tertiary title + macroXML = <choose> + <if type="chapter"> + <text variable="collection-title"/> + </if> + </choose>; + break; + + case "\x16": // tertiary author + var nameElement = (isBibliography ? this.bibliographyEditor : this.citationEditor); + + macroXML = <choose> + <if type="chapter"> + <names variable="series-editor">{nameElement}</names> + </if> + </choose>; + break; + + case "\x20": // short title + macroXML = <text variable="title" form="short"/> + + var textCase = isBibliography ? this.bibliographyCase : this.citationCase; + if(textCase) macroXML["@text-case"] = textCase; + + break; + + case "\x23": // chapter title + macroXML = <choose> + <if type="chapter"> + <text variable="title"/> + </if> + </choose>; + break; + + case "\x33": // accessed + macroXML = <date variable="accessed"> + <date-part name="month" suffix=" "/> + <date-part name="day" suffix=", "/> + <date-part name="year"/> + </date> + break; + } + + this.xml.info += <macro name={name}>{macroXML}</macro>; + return name; +} + +/** + * Parses a set of fields + * + * @param {Array} fields An array of binary objects + * @param {String} type The binary reference type, with nulls trimmed + * @param {Boolean} isBibliography Whether these fields are in the bibliography + **/ +Zotero.ENConverter.prototype.parseFields = function(fields, type, isBibliography) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + var referenceXML = new XML(); + for each(var field in fields) { + var prefix = this.parseFormattedString(this.findField(field.subfields, "\x94")[0].data, true); + var suffix = this.parseFormattedString(this.findField(field.subfields, "\x95")[0].data, true); + + // get only non-null characters of variable + var variable = this.findField(field.subfields, "\x91")[0].data; + while(variable[variable.length-1] == "\x00") { + variable = variable.substr(0, variable.length-1); + } + + var macro = this.getMacro(variable, isBibliography); + + if(macro || Zotero.ENConverter.variableMappings[variable]) { + if(macro) { + var text = <text macro={macro}/>; + } else { + // use mappings + var text = <text variable={Zotero.ENConverter.variableMappings[variable]}/>; + } + + // caret allows pluralization of terms + this.pluralize(prefix, type, variable); + this.pluralize(suffix, type, variable); + + var fieldFormat = this.findField(field.subfields, "\x92")[0].subfields[2].data; + + this.applyFormattingAttributes(text, fieldFormat); + + if(text) { + if(prefix || suffix) { + text = <group>{text}</group>; + if(prefix) text.prependChild(prefix); + if(suffix) text.appendChild(suffix); + } + referenceXML += text; + } + } else if(variable == "") { + // this is a value + if(prefix) referenceXML += prefix; + if(suffix) referenceXML += suffix; + } else if(variable == "\x05\x40") { // newline + if(prefix) referenceXML += prefix; + referenceXML += <text value="&#xA;"/>; + if(suffix) referenceXML += suffix; + } else { + /*var variableString = "EN-"; + for(var i=0; i<variable.length; i++) { + variableString += showCode(variable[i]) + } + referenceXML += <text variable={variableString}/>;*/ + } + } + return referenceXML; +} + +/** + * Parses separate descriptions for multiple references into one <choose> tag + * + * @param {Array} referenceDescriptions An array of binary objects + * @param {Boolean} isBibliography Whether these references are in the bibliography + **/ +Zotero.ENConverter.prototype.parseReferences = function(referenceDescriptions, isBibliography) { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + var types = []; + var descriptions = []; + var supplementalTypes = {}; + + for each(var description in referenceDescriptions) { + // map appropriate type + var type = this.findField(description.subfields, "\xA1")[0].data[0]; + var cslType = Zotero.ENConverter.typeMappings[type]; + if(!cslType && !Zotero.ENConverter.supplementalTypes[type]) continue; + + // parse references into XML + var referenceXML = this.parseFields(this.findField(description.subfields, ["\xA2", "\x90"]), type, isBibliography); + + // need to make sure more specific types get tested before less specific + // types + if(Zotero.ENConverter.fallbackTypes[cslType]) { + types.push(cslType); + descriptions.push(referenceXML); + } else if(cslType) { + types.unshift(cslType); + descriptions.unshift(referenceXML); + } else { + supplementalTypes[type] = referenceXML; + } + } + + // determine whether we have multiple types + var haveTypes = !!types.length; + if(!haveTypes) { + for(var i in supplementalTypes) { + if(i != "\x1F") { + haveTypes = true; + break; + } + } + } + + // build XML + if(haveTypes) { + // first tag must be an <if>; subsequent are else-ifs + var tagName = "if"; + + var choose = new XML(); + + // put in supplemental types first, to make sure that they override + for(var i in supplementalTypes) { + var ifTag = <{tagName}>{supplementalTypes[i]}</{tagName}>; + if(i == "\x09") { + ifTag.@type = "book"; + ifTag.@variable = "editor"; + } else if(i == "\x1C") { + ifTag.@type = "article"; + ifTag.@variable = "URL"; + } else if(i == "\x1D") { + ifTag.@type = "book"; + ifTag.@variable = "URL"; + } else { + continue; + } + + choose += ifTag; + tagName = "else-if"; + } + + // add mapped types + for(var i in types) { + choose += <{tagName} type={types[i]}>{descriptions[i]}</{tagName}>; + tagName = "else-if"; + } + + // if not all fallback types are specified, use an <else> with the + // generic record + if(supplementalTypes["\x1F"]) { + choose += <else>{supplementalTypes["\x1F"]}</else>; + } + + return <choose>{choose}</choose>; + } else if(supplementalTypes["\x1F"]) { + // only a generic record; use for everything + return supplementalTypes["\x1F"]; + } else { + return new XML(); + } +} + +Zotero.ENConverter.positions = ["ibid-with-locator", "ibid", "subsequent"]; +/** + * Converts the citation to CSL + * Requires that parseOptions has already been run + **/ +Zotero.ENConverter.prototype.parseCitation = function() { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + if(this.isFootnote) { + var footnote = this.findField(this.fields, "\x04")[0]; + + var repeatRules = this.findField(footnote.subfields, "Y")[0]; + var positions = new Object(); + positions.else = this.parseReferences(this.findField(footnote.subfields, ["U", "\xA0"])); + + // determine whether to use short form + if(this.findField(repeatRules.subfields, "\xC0")[0].data[0] == "\x01") { + // determine whether to include title + // TODO: better title handling (quotes, italics, etc.) + positions.subsequent = new XML(); + + if(this.findField(repeatRules.subfields, "\xC1")[0].data[0] == "\x01") { + // include title + var author = this.getMacro("\x02"); + var shortTitle = this.getMacro("\x20"); + positions.subsequent += <group delimiter=", "> + <text macro={author}/> + <text macro={shortTitle}/> + </group>; + } else { + positions.subsequent += <text macro={author}/> + } + positions.subsequent += <text prefix=", " variable="locator"/> + } + + // determine whether to use ibid + var useIbid = this.findField(repeatRules.subfields, "\xC2")[0].data[0]; + if(useIbid == "\x00") { + // replace repeated citations with ibid data + var ibidTerm = this.parseFormattedString(this.findField(repeatRules.subfields, "\xC4")[0].data); + positions.ibid = <text value={ibidTerm}/>; + } + // TODO: EndNote has an "omit repeated data" feature that doesn't + // seem to make any sense, since this just suggests there isn't + // supposed to be a footnote. Need to figure out what this means + + var useIbidWithLocator = this.findField(repeatRules.subfields, "\xC3")[0].data[0]; + var ibidWithLocatorTerm = this.parseFormattedString(this.findField(repeatRules.subfields, "\xC5")[0].data); + if(useIbidWithLocator == "\x00") { + // replace citations with different locators with ibid + positions["ibid-with-locator"] = new XML(); + positions["ibid-with-locator"] += <text value={ibidWithLocatorTerm}/>; + positions["ibid-with-locator"] += <text variable="locator" prefix=", "/>; + } else if(useIbidWithLocator == "\x01") { + positions["ibid-with-locator"] = positions.else.copy(); + for each(var position in positions["ibid-with-locator"]..text) { + if(position.@macro == "secondary_title") { + delete position.@macro; + position.@value = ibidWithLocatorTerm; + } + } + } + + // else find out whether we have any non-else positions + var elseOnly = true; + for(var i=0; i<Zotero.ENConverter.positions.length; i++) { + if(positions[Zotero.ENConverter.positions[i]]) { + elseOnly = false; + } + } + + if(elseOnly) { + // if only first, don't use choose + var citationXML = positions.else; + } else { + var citationXML = <choose/>; + var isFirst = true; + for(var i=0; i<Zotero.ENConverter.positions.length; i++) { + if(positions[Zotero.ENConverter.positions[i]]) { + var ifType = isFirst ? "if" : "else-if"; + isFirst = false; + citationXML.appendChild(<{ifType} position={Zotero.ENConverter.positions[i]}> + {positions[Zotero.ENConverter.positions[i]]} + </{ifType}>); + } + } + citationXML.appendChild(<else>{positions.else}</else>); + } + } else { + var citation = this.findField(this.fields, "\x03")[0]; + + // put citation in a macro so that it can potentially be used as a field + var citationFields = this.findField(citation.subfields, ["3", "\xA0", "\xA2", "\x90"]); + var citationXML = this.parseFields(citationFields); + + var delimiter = this.parseFormattedString(this.findField(citation.subfields, "0")[0].data); + if(delimiter !== "") this.xml.citation.layout.@delimiter = delimiter; + + // determine whether to collapse numbers, since we need to know whether + // there's a number first (because EndNote stupidly allows both collapse + // on author and on number, which seems impossible) + if(this.numberCollapse && citationXML..variable.(@text == "citation-number").length()) { + if(this.xml.citation.option.(@name == "collapse").length()) { + this.xml.citation.option.(@name == "collapse").value = "citation-number"; + } else { + this.xml.citation.prependChild(<option name="collapse" value="citation-number"/>); + } + } + } + + this.xml.info += <macro name="citation">{citationXML}</macro>; +} + +/** + * Converts the bibliography to CSL + * Requires that parseOptions has already been run + **/ +Zotero.ENConverter.prototype.parseBibliography = function() { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + var references = this.findField(this.fields, "\x02")[0]; + + // add prefix + var prefixFields = this.findField(references.subfields, ["$", "\x90"]); + this.xml.bibliography.layout.appendChild(this.parseFields(prefixFields, true)); + + // contains a record for each type within EndNote + var referenceDescriptions = this.findField(references.subfields, ["#", "\xA0"]); + this.xml.bibliography.layout.appendChild(this.parseReferences(referenceDescriptions, true)); + + // add suffix + var prefixFields = this.findField(references.subfields, ["%", "\x90"]); + this.xml.bibliography.layout.appendChild(this.parseFields(prefixFields, true)); +} + +/** + * Parses the EN file format, returning CSL in the format of an E4X XML object + **/ +Zotero.ENConverter.prototype.parse = function() { + default xml namespace = "http://purl.org/net/xbiblio/csl"; with({}); + + this.fields = this.parseFormat(this.data.substr(0x20)); + + // create XML skeleton + this.xml = <style xml:lang="en" xmlns="http://purl.org/net/xbiblio/csl"> + <info/> + <citation> + <layout> + <text macro="citation"/> + </layout> + </citation> + <bibliography> + <layout/> + </bibliography> + </style>; + + this.parseInfo(); + this.parseOptions(); + this.parseCitation(); + this.parseBibliography(); + + Zotero.debug(this.xml); + return this.xml; +} +\ No newline at end of file diff --git a/chrome/content/zotero/xpcom/zotero.js b/chrome/content/zotero/xpcom/zotero.js @@ -1450,31 +1450,42 @@ Zotero.Date = new function(){ return date; } - /* + /** * does pretty formatting of a date object returned by strToDate() * - * |date| is *not* a JS Date object - */ - function formatDate(date) { - var string = ""; - - if(date.part) { - string += date.part+" "; - } - - var months = Zotero.CSL.Global.getMonthStrings("long"); - if(date.month != undefined && months[date.month]) { - // get short month strings from CSL interpreter - string += months[date.month]; - if(date.day) { - string += " "+date.day+", "; - } else { - string += " "; + * @param {Object} date A date object, as returned from strToDate() + * @param {Boolean} shortFormat Whether to return a short (12/1/95) date + * @return A formatted date string + * @type String + **/ + function formatDate(date, shortFormat) { + if(shortFormat) { + var localeDateOrder = getLocaleDateOrder(); + var string = localeDateOrder[0]+"/"+localeDateOrder[1]+"/"+localeDateOrder[2]; + return string.replace("y", (date.year !== undefined ? date.year : "00")) + .replace("m", (date.month !== undefined ? 1+date.month : "0")) + .replace("d", (date.day !== undefined ? date.day : "0")); + } else { + var string = ""; + + if(date.part) { + string += date.part+" "; + } + + var months = Zotero.CSL.Global.getMonthStrings("long"); + if(date.month != undefined && months[date.month]) { + // get short month strings from CSL interpreter + string += months[date.month]; + if(date.day) { + string += " "+date.day+", "; + } else { + string += " "; + } + } + + if(date.year) { + string += date.year; } - } - - if(date.year) { - string += date.year; } return string; diff --git a/chrome/locale/en-US/zotero/preferences.dtd b/chrome/locale/en-US/zotero/preferences.dtd @@ -60,6 +60,10 @@ <!ENTITY zotero.preferences.quickCopy.siteEditor.domainPath.example "(e.g. wikipedia.org)"> <!ENTITY zotero.preferences.quickCopy.siteEditor.outputFormat "Output Format"> +<!ENTITY zotero.preferences.prefpane.styles "Styles"> +<!ENTITY zotero.preferences.styles.styleManager "Style Manager"> +<!ENTITY zotero.preferences.styles.styleManager.title "Title"> +<!ENTITY zotero.preferences.styles.styleManager.updated "Updated"> <!ENTITY zotero.preferences.export.getAdditionalStyles "Get additional styles..."> <!ENTITY zotero.preferences.prefpane.keys "Shortcut Keys"> diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties @@ -380,6 +380,7 @@ zotero.preferences.search.pdf.tryAgainOrViewManualInstructions = Please try aga zotero.preferences.export.quickCopy.bibStyles = Bibliographic Styles zotero.preferences.export.quickCopy.exportFormats = Export Formats zotero.preferences.export.quickCopy.instructions = Quick Copy allows you to copy selected references to the clipboard by pressing a shortcut key (%S) or dragging items into a text box on a web page. +zotero.preferences.styles.addStyle = Add Style zotero.preferences.advanced.resetTranslatorsAndStyles = Reset Translators and Styles zotero.preferences.advanced.resetTranslatorsAndStyles.changesLost = Any new or modified translators or styles will be lost. @@ -489,6 +490,10 @@ integration.regenerate.saveBehavior = Always follow this selection. integration.deleteCitedItem.title = Are you sure you want to remove this reference? integration.deleteCitedItem.body = This reference is cited in the text of your document. Deleting it will remove all citations. -styles.installStyle = Install style "%1$S" from %2$S? +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.deleteStyle = Are you sure you want to delete the style "%1$S"? diff --git a/chrome/skin/default/zotero/preferences.css b/chrome/skin/default/zotero/preferences.css @@ -113,6 +113,11 @@ grid row hbox:first-child min-height: 1.5em; /* Fix collapse on Windows */ } +/* Styles pane */ +#styleManager +{ + height: 250px; +} /* Shortcut Keys pane */ #zotero-prefpane-keys row @@ -128,4 +133,4 @@ grid row hbox:first-child #zotero-prefpane-keys checkbox { margin: .75em 0; -} +} +\ No newline at end of file diff --git a/chrome/skin/default/zotero/prefs-styles.png b/chrome/skin/default/zotero/prefs-styles.png Binary files differ. diff --git a/components/zotero-service.js b/components/zotero-service.js @@ -18,8 +18,8 @@ var xpcomFiles = ['zotero', 'annotate', 'attachments', 'cite', 'cite_compat', 'collectionTreeView', 'dataServer', 'data_access', 'data/item', 'data/items', 'data/collection', 'data/collections', 'data/cachedTypes', 'data/creator', 'data/creators', - 'data/itemFields', 'data/notes', 'data/tag', 'data/tags', 'db', 'file', - 'fulltext', 'id', 'ingester', 'integration', 'itemTreeView', 'mime', + 'data/itemFields', 'data/notes', 'data/tag', 'data/tags', 'db', 'enstyle', + 'file', 'fulltext', 'id', 'ingester', 'integration', 'itemTreeView', 'mime', 'notifier', 'progressWindow', 'quickCopy', 'report', 'schema', 'search', 'sync', 'timeline', 'translate', 'utilities', 'zeroconf'];