zotero/chrome/content/zotero/xpcom/cite.js
2007-11-01 04:07:23 +00:00

2518 lines
73 KiB
JavaScript

/*
***** 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.Cite: a class for creating bibliographies from within Scholar
* 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() {
var _lastCSL = null;
var _lastStyle = null;
this.getStyles = getStyles;
this.getStyleClass = getStyleClass;
this.getStyle = getStyle;
/*
* returns an associative array of cslID => styleName pairs
*/
function getStyles() {
// get styles
var sql = "SELECT cslID, title FROM csl ORDER BY title";
var styles = Zotero.DB.query(sql);
// convert to associative array
var stylesObject = new Object();
for each(var style in styles) {
stylesObject[style.cslID] = style.title;
}
return stylesObject;
}
/*
* gets the class of a given style
*/
function getStyleClass(cslID) {
var csl = _getCSL(cslID);
var xml = new XML(Zotero.CSL.Global.cleanXML(csl));
return xml["@class"].toString();
}
/*
* gets CSL from the database, or, if it's the most recently used style,
* from the cache
*/
function getStyle(cslID) {
if(_lastStyle != cslID || Zotero.Prefs.get("cacheTranslatorData") == false) {
// create a CSL instance
var csl = _getCSL(cslID);
// load CSL in compat mode if it is old-style
if(csl.indexOf("<defaults") != -1) {
_lastCSL = new Zotero.CSL.Compat(csl);
} else {
_lastCSL = new Zotero.CSL(csl);
}
_lastStyle = cslID;
}
return _lastCSL;
}
/*
* get CSL for a given style from the database
*/
function _getCSL(cslID) {
var style = Zotero.DB.valueQuery("SELECT csl FROM csl WHERE cslID = ?", [cslID]);
if(!style) throw "Zotero.Cite: invalid CSL ID";
return style;
}
}
/*
* 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) {
this._csl = new XML(Zotero.CSL.Global.cleanXML(csl));
// initialize CSL
Zotero.CSL.Global.init();
// load localizations
this._terms = Zotero.CSL.Global.parseLocales(this._csl.terms);
// load class
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,
"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 = /^[a-zA-Z0-9]*/;
Zotero.CSL._textCharRegexp = /[a-zA-Z0-9]/;
Zotero.CSL.prototype.formatCitation = function(citation, format) {
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 == 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.append(string.get(), context.layout, false, true);
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) {
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 index = 0;
var output = "";
var preamble = "";
if(format == "HTML") {
if(this.class == "note" && isCitation) {
preamble = '<ol>\r\n';
secondFieldAlign = false;
} else {
if(hangingIndent) {
preamble = '<div style="margin-left:0.5in;text-indent:-0.5in;">\r\n';
}
if(secondFieldAlign) {
preamble += '<table style="border-collapse:collapse;">\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";
}
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 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);
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")+'"></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"
+'<tr><td colspan="2">&nbsp;</td></tr>\r\n';
} else {
output += "<p>"+string+span+"</p>\r\n";
}
} else if(format == "RTF") {
if(this.class == "note" && isCitation) {
index++;
output += index+". ";
}
output += string+"\\\r\n\\\r\n";
} else {
if(format == "Text" && this.class == "note" && isCitation) {
index++;
output += index+". ";
}
// attach \n on mac (since both \r and \n count as newlines for
// clipboard purposes)
output += string+(Zotero.isWin ? "\r\n\r\n" : "\n\n");
}
}
if(format == "HTML") {
if(this.class == "note" && isCitation) {
output += '</ol>';
} else {
if(secondFieldAlign) {
output += '</table>';
}
if(hangingIndent) {
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(tabStop !== null) {
preamble += "\\tx"+tabStop+" ";
}
if(format == "RTF") {
// drop last 6 characters of output (last two returns)
output = output.substr(0, output.length-6)+"}";
} else {
// drop last 4 characters (last two returns)
output = output.substr(0, (Zotero.isWin ? output.length-4 : output.length-2));
}
preamble += "\r\n";
}
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) {
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") == "true"
&& 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");
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) {
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";
// 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);
formattedString.concat(newString, child);
dataAppended = true;
}
} else if(child.@value.length()) {
formattedString.append(child.@value.toString(), child);
}
} 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]);
}
if(term !== false && value) {
var isPlural = value.indexOf("-") != -1 || value.indexOf(",") != -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).toString();
if(string == "") continue;
if(part == "year") {
// 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(form == "numeric-leading-zeros") {
if(string.length == 1) {
string = "0" + string;
}
} else if(form == "short") {
string = this._terms["short"]["_months"][string];
} else if(form != "numeric") {
string = this._terms["long"]["_months"][string];
}
} else if(form == "numeric") {
string = "";
}
} else if(part == "day") {
if(form == "numeric-leading-zeros"
&& string.length() == 1) {
string = "0" + string;
}
}
}
}
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", "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++) {
if(attribute == "variable") {
if(Zotero.CSL._dateVariables[variables[j]]) {
// getDate not false/undefined
var exists = !!item.getDate(variables[j]);
} else if(Zotero.CSL._namesVariables[variables[j]]) {
// getNames not false/undefined, not empty
var exists = item.getNames(variables[j]);
if(exists) exists = !!exists.length;
} else {
var exists = item.getVariable(variables[j]) !== "";
}
} else if(attribute == "type") {
var exists = item.isType(variables[j]);
} else if(attribute == "disambiguate") {
var exists = (variables[j] == "true" && item.getProperty("disambiguate-condition"))
|| (variables[j] == "false" && !item.getProperty("disambiguate-condition"));
} else if(attribute == "locator") {
var exists = citationItem && citationItem.locator &&
(citationItem.locatorType == variables[j]
|| (!citation.locatorType && variables[j] == "page"));
} else { // attribute == "position"
if(variables[j] == "first") {
var exists = !citationItem
|| !citationItem.position
|| citationItem.position == Zotero.CSL.POSITION_FIRST;
} else if(variables[j] == "subsequent") {
var exists = citationItem && citationItem.position >= Zotero.CSL.POSITION_SUBSEQUENT;
} else if(variables[j] == "ibid") {
var exists = citationItem && citationItem.position >= Zotero.CSL.POSITION_IBID;
} else if(variables[j] == "ibid-with-locator") {
var 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);
formattedString.concat(newString, child);
dataAppended = true;
// 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) {
var sortA = [];
var sortB = [];
// author
if(context.sort.key.length()) {
for each(var key in context.sort.key) {
var keyA = new Zotero.CSL.SortString();
var keyB = new Zotero.CSL.SortString();
if(key.@macro.length()) {
this._processElements(a, this._csl.macro.(@name == key.@macro), keyA);
this._processElements(b, this._csl.macro.(@name == key.@macro), keyB);
} else if(key.@variable.length()) {
var variable = key.@variable.toString();
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;
}
}
}
return 0;
}
/*
* Compares two citations; returns true if they are different, false if they are equal
*/
Zotero.CSL.prototype._compareCitations = function(a, b, context) {
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;
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");
req.send(null);
if (req.responseText) {
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");
req.send(null);
if (req.responseText) {
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
*/
Zotero.CSL.CitationItem = function(item) {
if(item) {
this.item = item;
this.itemID = item.getID();
}
}
/*
* the Citation object represents a citation.
*/
Zotero.CSL.Citation = function(citationItems, csl) {
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) {
var me = this;
this.citationItems = this.citationItems.sort(function(a, b) {
return me._csl._compareItem(a.item, b.item, me._citation);
});
}
}
/*
* 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._dates = {};
this._properties = {};
}
/*
* Returns some identifier for the item. Used to create citations. In Zotero,
* this is the item ID
*/
Zotero.CSL.Item.prototype.getID = function() {
return this.zoteroItem.getID();
}
/*
* Gets an array of Item.Name objects for a variable.
*/
Zotero.CSL.Item.prototype.getNames = function(variable) {
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) {
// load date variable if possible
if(this._dates[variable] == undefined) {
this._createDate(variable);
}
if(this._dates[variable]) return this._dates[variable];
return false;
}
Zotero.CSL.Item._zoteroFieldMap = {
"title":"title",
"container-title":"publicationTitle",
"collection-title":["seriesTitle", "series"],
"publisher":"publisher",
"publisher-place":"place",
"page":"pages",
"volume":"volume",
"issue":"issue",
"number-of-volumes":"numberOfVolumes",
"edition":"edition",
"genre":"type",
"abstract":"abstract",
"URL":"url",
"DOI":"DOI"
}
/*
* Gets a text object for a specific type.
*/
Zotero.CSL.Item.prototype.getVariable = function(variable, form) {
if(!Zotero.CSL.Item._zoteroFieldMap[variable]) return "";
if(variable == "title" && form == "short") {
var zoteroFields = ["shortTitle", "title"];
} else {
var zoteroFields = Zotero.CSL.Item._zoteroFieldMap[variable];
if(typeof zoteroFields == "string") zoteroFields = [zoteroFields];
}
for each(var zoteroField in zoteroFields) {
var value = this.zoteroItem.getField(zoteroField, false, true);
if(value != "") 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] ? this._properties[property] : "");
}
Zotero.CSL.Item._optionalTypeMap = {
journalArticle:"article-journal",
magazineArticle:"article-magazine",
newspaperArticle:"article-newspaper",
thesis:"thesis",
letter:"personal_communication",
manuscript:"manuscript",
interview:"interview",
film:"motion_picture",
artwork:"graphic",
webpage:"webpage",
report:"paper-conference", // ??
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:"paper-conference",
videoRecording:"motion_picture",
tvBroadcast:"motion_picture",
radioBroadcast:"motion_picture",
podcast:"speech", // ??
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:"book",
letter:"article",
manuscript:"book",
interview:"book",
film:"book",
artwork:"book",
webpage:"article",
report:"book",
bill:"book",
case:"book",
hearing:"book",
patent:"book",
statute:"book",
email:"article",
map:"article",
blogPost:"article",
instantMessage:"article",
forumPost:"article",
audioRecording:"article",
presentation:"article",
videoRecording:"article",
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.getType());
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.getType());
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, 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]) {
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[variable] ? this._zoteroCreator[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) {
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 = {};
// 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
*/
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;
}
/*
* 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]);
}
this.itemsById[newItem.getID()] = 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.getID()] = 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() {
// sort, if necessary
if(this.sortable) {
var me = this;
this.items = this.items.sort(function(a, b) {
return me.csl._compareItem(a, b, me.bibliography);
});
}
// first loop through to collect disambiguation data by item, so we can
// see if any items have changed; also collect last names
if(this._citationChangingOptions.length) {
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) 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) {
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;
}
initials[theNames[k][0]] = true;
break;
}
}
}
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) {
var citationsEqual = [];
for(var i=1; i<this.items.length; i++) {
citationsEqual[i] = this.csl._compareCitations(this.items[i-1], this.items[i], 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] == 0) {
// 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(me.csl._compareCitations(lastItem, item) == 0) {
if(!oldCondition) {
lastItem.setProperty("disambiguate-condition", undefined);
}
item.setProperty("disambiguate-condition", undefined);
}
}
}
if(this.options["subsequent-author-substitute"]) {
var namesDiffer = false;
for(var j=0; j<numberOfNames; 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();
if(this._citationChangingOptions.length) {
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) {
this.context = context;
this.option = context.option;
this.format = format;
this.delimiter = delimiter;
this.string = "";
this.closePunctuation = false;
this.useBritishStyleQuotes = false;
// insert tab iff second-field-align is on
this.insertTabAfterField = (!subsequent && this.option.(@name == "second-field-align").@value.toString());
// whether to remove whitespace from next appended string
this.suppressLeadingWhitespace = false;
// whether to prepend a newline to the next appended string
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) {
if(!formattedString || !formattedString.string) {
return false;
}
if(formattedString.format != this.format) {
throw "CSL: cannot concatenate formatted strings: formats do not match";
}
if(formattedString.string) {
// first, append the actual string
var haveAppended = this.append(formattedString.string, element, false, true);
// if there's close punctuation to append, that also counts
if(formattedString.closePunctuation) {
haveAppended = true;
if(this.closePunctuation) {
// if there's existing close punctuation and punctuation to
// append, we need to append that
this.string += this.closePunctuation;
}
// add the new close punctuation
this.closePunctuation = formattedString.closePunctuation;
}
return haveAppended;
}
return false;
}
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) {
if(!string) return false;
if(typeof(string) != "string") {
string = string.toString();
}
// append delimiter if necessary
if(this.delimiter && this.string && !dontDelimit) {
this.append(this.delimiter, null, true);
}
// append prefix before closing punctuation
if(element && element.@prefix.length()) {
var prefix = element.@prefix.toString();
if(this.suppressLeadingWhitespace) {
var newPrefix = prefix.replace(/^\s+/, "");
if(newPrefix != "" && newPrefix != prefix) {
this.suppressLeadingWhitespace = false;
}
prefix = newPrefix
}
this.append(prefix, null, true);
}
if(this.suppressLeadingWhitespace) {
string = string.replace(/^\s+/, "");
this.suppressLeadingWhitespace = false;
}
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);
}
// 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 = false;
}
// handle text transformation
if(element) {
if(element["@text-transform"].length()) {
if(element["@text-transform"] == "lowercase") {
// all lowercase
string = string.toLowerCase();
} else if(element["@text-transform"] == "uppercase") {
// all uppercase
string = string.toUpperCase();
} else if(element["@text-transform"] == "capitalize") {
// capitalize first
string = string[0].toUpperCase()+string.substr(1);
}
}
}
if(!dontEscape) {
if(this.format == "HTML") {
string = string.replace("<", "&lt;", "g")
.replace(">", "&gt;", "g")
.replace("&", "&amp;", "g")
.replace(/(\r\n|\r|\n)/g, "<br />")
.replace(/[\x00-\x1F]/g, "");
} else if(this.format == "RTF") {
string = string.replace("\\", "\\\\", "g")
.replace(/[\x7F-\uFFFF]/g, Zotero.CSL.FormattedString._rtfEscapeFunction)
.replace("\t", "\\tab ", "g")
.replace(/(\r\n|\r|\n)/g, "\\line ");
} else if(this.format == "Integration") {
string = string.replace(/\\/g, "\\\\")
.replace(/(\r\n|\r|\n)/g, "\\line ");
} else {
string = string.replace(/(\r\n|\r|\n)/g, (Zotero.isWin ? "\r\n" : "\n"));
}
}
if(element) {
// style attributes
if(this.format == "HTML") {
var style = "";
var cssAttributes = ["font-family", "font-style", "font-variant",
"font-weight", "vertical-align", "display"];
for(var j in cssAttributes) {
var value = element["@"+cssAttributes[j]].toString();
if(value && value.indexOf('"') == -1) {
style += cssAttributes[j]+":"+value+";";
}
}
if(element["@display"] == "block") {
if(this.option.(@name == "hanging-indent").@value == "true") {
style += "text-indent:0.5in;"
}
if(style) {
string = '<div style="'+style+'">'+string+'</div>';
} else {
string = '<div>'+string+'</div>';
}
} else if(style) {
string = '<span style="'+style+'">'+string+'</span>';
}
} else {
if(this.format == "RTF" || this.format == "Integration") {
if(element["@font-style"] == "oblique" || element["@font-style"] == "italic") {
string = "\\i "+string+"\\i0 ";
}
if(element["@font-variant"] == "small-caps") {
string = "\\scaps "+string+"\\scaps0 ";
}
if(element["@font-weight"] == "bold") {
string = "\\b "+string+"\\b0 ";
}
if(element["@text-decoration"] == "underline") {
string = "\\ul "+string+"\\ul0 ";
}
if(element["@vertical-align"] == "sup") {
string = "\\super "+string+"\\super0 ";
} else if(element["@vertical-align"] == "sub") {
string = "\\sub "+string+"\\sub0 ";
}
}
if(element["@display"] == "block" || this.appendLine) {
if(this.format == "RTF") {
string = "\r\n\\line "+string;
} else if(this.format == "Integration") {
string = "\x0B"+string;
} else {
string = (Zotero.isWin ? "\r\n" : "\n")+string;
}
this.appendLine = element["@display"] == "block";
}
}
// add quotes if necessary
if(element.@quotes == "true") {
this.string += this._openQuote;
if(this.useBritishStyleQuotes) {
string += this._closeQuote;
} else {
this.closePunctuation = this._closeQuote;
}
}
}
this.string += string;
// special rule: if a variable ends in a punctuation mark, and the suffix
// begins with a period, chop the period off the suffix
var suffix;
if(element && element.@suffix.length()) {
this.append(element.@suffix.toString(), null, true);
}
// save for second-field-align
if(!dontDelimit && this.insertTabAfterField) {
// replace any space following this entry
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.insertTabAfterField = false;
this.suppressLeadingWhitespace = true;
}
return true;
}
/*
* gets the formatted string
*/
Zotero.CSL.FormattedString.prototype.get = function() {
return this.string+(this.closePunctuation ? 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() {
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();
}