b4c8dbe700
CSL is stored in a new "csl" table. only metadata relevant to updates and selection (ID, date updated, and title) is stored in columns.
619 lines
No EOL
18 KiB
JavaScript
619 lines
No EOL
18 KiB
JavaScript
/*
|
|
* Scholar.Cite: a class for creating bibliographies from within Scholar
|
|
* this class handles pulling the CSL file and item data out of the database,
|
|
* while Scholar.CSL, below, handles the actual generation of the bibliography
|
|
*/
|
|
Scholar.Cite = new function() {
|
|
this.getBibliography = getBibliography;
|
|
this.getStyles = getStyles;
|
|
|
|
function getStyles() {
|
|
// get styles
|
|
var sql = "SELECT cslID, title FROM csl ORDER BY title";
|
|
var styles = Scholar.DB.query(sql);
|
|
|
|
// convert to associative array
|
|
var stylesObject = new Object();
|
|
for each(var style in styles) {
|
|
stylesObject[style.cslID] = style.title;
|
|
}
|
|
|
|
return stylesObject;
|
|
}
|
|
|
|
function getBibliography(cslID, items) {
|
|
// get style
|
|
var sql = "SELECT csl FROM csl WHERE cslID = ?";
|
|
var style = Scholar.DB.valueQuery(sql, [cslID]);
|
|
|
|
// get item arrays
|
|
var itemArrays = new Array();
|
|
for(var i in items) {
|
|
itemArrays.push(items[i].toArray());
|
|
}
|
|
|
|
// create a Scholar.CSL instance
|
|
var CSL = new Scholar.CSL(style);
|
|
// return bibliography
|
|
return CSL.createBibliography(itemArrays);
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Scholar.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
|
|
*/
|
|
|
|
/*
|
|
* constructor
|
|
*/
|
|
Scholar.CSL = function(csl) {
|
|
default xml namespace = Scholar.CSL.ns;
|
|
this._csl = new XML(csl);
|
|
|
|
// load basic options
|
|
this._parseOptions();
|
|
}
|
|
|
|
|
|
|
|
Scholar.CSL._loc = {
|
|
and:"and",
|
|
etAl:"et al",
|
|
pSingle:"p.",
|
|
pMultiple:"pp.",
|
|
editorVerb:"Edited By",
|
|
editorNounSingle:"Ed.",
|
|
editorNounMultiple:"Eds.",
|
|
translatorVerb:"Translated By",
|
|
translatorNounSingle:"Trans.",
|
|
translatorNounMultiple:"Trans.",
|
|
months:["January", "February", "March", "April", "May", "June", "July",
|
|
"August", "September", "October", "November", "December"],
|
|
monthsAbbreviated:["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug",
|
|
"Sep", "Oct", "Nov", "Dec"],
|
|
pagesShortSingle:"p",
|
|
pagesShortMultiple:"pp",
|
|
pagesLongSingle:"page",
|
|
pagesLongMultiple:"pages"
|
|
}
|
|
|
|
|
|
Scholar.CSL._optionalTypeMappings = {
|
|
journalArticle:"article-journal",
|
|
magazineArticle:"article-magazine",
|
|
newspaperArticle:"article-newspaper",
|
|
thesis:"thesis",
|
|
letter:"personal communication",
|
|
manuscript:"manuscript",
|
|
interview:"interview",
|
|
film:"motion picture",
|
|
artwork:"graphic",
|
|
website:"webpage"
|
|
};
|
|
|
|
// TODO: check with Elena/APA/MLA on this
|
|
Scholar.CSL._fallbackTypeMappings = {
|
|
book:"book",
|
|
bookSection:"chapter",
|
|
journalArticle:"article",
|
|
magazineArticle:"article",
|
|
newspaperArticle:"article",
|
|
thesis:"book",
|
|
letter:"article",
|
|
manuscript:"book",
|
|
interview:"book",
|
|
film:"book",
|
|
artwork:"book",
|
|
website:"article"
|
|
};
|
|
|
|
Scholar.CSL.ns = "http://purl.org/net/xbiblio/csl";
|
|
|
|
/*
|
|
* create a bibliography
|
|
* (items is expected to be an array of items)
|
|
*/
|
|
Scholar.CSL.prototype.createBibliography = function(items) {
|
|
// sort by sort order
|
|
if(this._opt.sortOrder == "author-date") {
|
|
items.sort(function(a, b) {
|
|
// first make sure both have creators at the first index
|
|
if(!a.creators[0] && b.creators[0]) {
|
|
return 1;
|
|
} else if(!b.creators[0] && a.creators[0]) {
|
|
return -1;
|
|
}
|
|
|
|
// now, either both have creators or neither do
|
|
if(a.creators[0]) {
|
|
// sort by last names
|
|
if(b.creators[0].lastName > a.creators[0].lastName) {
|
|
return 1;
|
|
} else if(b.creators[0].lastName < a.creators[0].lastName) {
|
|
return -1;
|
|
}
|
|
// sort by first name
|
|
if(b.creators[0].firstName > a.creators[0].firstName) {
|
|
return 1;
|
|
} else if(b.creators[0].firstName < a.creators[0].firstName) {
|
|
return -1;
|
|
}
|
|
}
|
|
|
|
// now, sort by date
|
|
var date1 = (a.date ? a.date : a.year);
|
|
var date2 = (b.date ? b.date : b.year);
|
|
if(date2 > date1) {
|
|
return 1;
|
|
} else if(date1 > date2) {
|
|
return -1;
|
|
}
|
|
|
|
// finally, give up; they're the same
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
// process items
|
|
var output = "";
|
|
for(var i in items) {
|
|
var item = items[i];
|
|
if(item.itemType == "note") { // skip notes
|
|
continue;
|
|
}
|
|
|
|
// determine mapping
|
|
if(Scholar.CSL._optionalTypeMappings[item.itemType]
|
|
&& this._opt.referenceTypes[Scholar.CSL._optionalTypeMappings[item.itemType]]) {
|
|
if(this._opt.referenceTypes[Scholar.CSL._optionalTypeMappings[item.itemType]] === true) {
|
|
// exists but not yet processed
|
|
this._parseReferenceType(Scholar.CSL._optionalTypeMappings[item.itemType]);
|
|
}
|
|
|
|
var reftype = this._opt.referenceTypes[Scholar.CSL._optionalTypeMappings[item.itemType]];
|
|
} else {
|
|
if(this._opt.referenceTypes[Scholar.CSL._fallbackTypeMappings[item.itemType]] === true) {
|
|
this._parseReferenceType(Scholar.CSL._fallbackTypeMappings[item.itemType]);
|
|
}
|
|
|
|
var reftype = this._opt.referenceTypes[Scholar.CSL._fallbackTypeMappings[item.itemType]];
|
|
}
|
|
|
|
output += "<p>"+this._processElements(reftype, item)+"</p>\n";
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/*
|
|
* process an item
|
|
*/
|
|
Scholar.CSL.prototype._processElements = function(reftype, item) {
|
|
var output = "";
|
|
|
|
// separate item into authors, editors, translators
|
|
var authors = new Array();
|
|
var editors = new Array();
|
|
var translators = new Array();
|
|
for(var j in item.creators) {
|
|
if(item.creators[j].creatorType == "editor") {
|
|
editors.push(item.creators[j]);
|
|
} else if(item.creators[j].creatorType == "translator") {
|
|
translators.push(item.creators[j]);
|
|
} else {
|
|
authors.push(item.creators[j]);
|
|
}
|
|
}
|
|
if(item.date) { // specific date
|
|
var date = this._processDate(item.date);
|
|
} else { // no real date, but might salvage a year
|
|
var date = new Object();
|
|
|
|
if(item.year) {
|
|
date.year = item.year;
|
|
}
|
|
}
|
|
|
|
for(var i in reftype) {
|
|
var element = reftype[i];
|
|
var data = "";
|
|
|
|
if(element.name == "author") {
|
|
if(authors.length) {
|
|
data = this._processCreators("author", authors);
|
|
} else if(element.alternate) { // no authors; use alternate if
|
|
// it exists
|
|
if(element.alternate == "editor") {
|
|
data = this._processCreators("editor", editors);
|
|
editors = new Array();
|
|
} else if(element.alternate == "title") {
|
|
data = item.title;
|
|
item.title = undefined;
|
|
} else if(element.alternate == "container-title") {
|
|
if(item.publication) {
|
|
data = item.publication;
|
|
item.publication = undefined;
|
|
}
|
|
}
|
|
}
|
|
} else if(element.name == "editor") {
|
|
data = this._processCreators("editor", editors);
|
|
} else if(element.name == "translator") {
|
|
data = this._processCreators("translator", translators);
|
|
} else if(element.name == "year") {
|
|
data = date.year;
|
|
} else if(element.name == "month-day") {
|
|
data = date.month+" "+date.day;
|
|
} else if(element.name == "date") {
|
|
data = this._formatDate(date);
|
|
} else if(element.name == "volume") {
|
|
data = item.volume;
|
|
} else if(element.name == "issue") {
|
|
data = item.number;
|
|
} else if(element.name == "pages") {
|
|
if(item.pages) {
|
|
if(this._opt.locators.label) {
|
|
if(item.pages.indexOf(",") != -1 || item.pages.indexOf("-") != -1) {
|
|
var label = this._opt.locators.label[1];
|
|
} else {
|
|
var label = this._opt.locators.label[0];
|
|
}
|
|
if(this._opt.locators.positionBefore) {
|
|
data += label;
|
|
}
|
|
}
|
|
data += item.pages;
|
|
if(this._opt.locators.label && !this._opt.locators.positionBefore) {
|
|
data += label;
|
|
}
|
|
}
|
|
} else if(element.name == "title") {
|
|
if(!element.type) { // standard title
|
|
data = item.title;
|
|
} else if(element.type == "container" && item.publication) {
|
|
data = item.publication;
|
|
} else if(element.type == "series") {
|
|
data = item.series;
|
|
}
|
|
} else if(element.name == "publisher") {
|
|
if(item.publisher) {
|
|
if(item.place) {
|
|
if(this._opt.publishers.publisherFirst) {
|
|
data = item.publisher+this._opt.publishers.separator+item.place;
|
|
} else {
|
|
data = item.place+this._opt.publishers.separator+item.publisher;
|
|
}
|
|
} else {
|
|
data = item.publisher;
|
|
}
|
|
}
|
|
} else if(element.name == "access") {
|
|
var dateAccessed = "";
|
|
if(item.dateAccessed) {
|
|
var dateAccessed = this._formatDate(this._processDate(item.dateAccessed));
|
|
}
|
|
|
|
if(this._opt.access.dateFirst) {
|
|
data = (dateAccessed ? dateAccessed : "");
|
|
} else {
|
|
data = (item.url ? item.url : "");
|
|
}
|
|
if(dateAccessed && item.url) {
|
|
data += this._opt.access.separator;
|
|
}
|
|
if(this._opt.access.dateFirst) {
|
|
data += item.url;
|
|
} else {
|
|
data += dateAccessed;
|
|
}
|
|
} else if(element.name == "group") {
|
|
data = this._processElements(element.elements, item);
|
|
} else {
|
|
data = element.name;
|
|
}
|
|
|
|
style = "";
|
|
var cssAttributes = ["font-family", "font-style", "font-variant",
|
|
"font-weight", "text-transform"];
|
|
for(var j in cssAttributes) {
|
|
if(element[cssAttributes[j]] && element[cssAttributes[j]].indexOf('"') == -1) {
|
|
style += cssAttributes[j]+":"+element[cssAttributes[j]];
|
|
}
|
|
}
|
|
|
|
if(data) {
|
|
var data = data.toString();
|
|
|
|
// add prefix
|
|
if(element.prefix) {
|
|
output += element.prefix;
|
|
}
|
|
|
|
if(style) {
|
|
output += '<span style="'+style+'">';
|
|
}
|
|
output += data;
|
|
if(style) {
|
|
output += '</span>';
|
|
}
|
|
|
|
if(element.suffix) {
|
|
// suffix for this element only
|
|
output += element.suffix;
|
|
} else if(element.name != "group" && this._opt.suffix && data.substr(data.length-this._opt.suffix.length) != this._opt.suffix) {
|
|
// global suffix if no suffix for this element
|
|
output += this._opt.suffix;
|
|
}
|
|
}
|
|
}
|
|
|
|
return output;
|
|
}
|
|
|
|
/*
|
|
* process creator objects; if someone had a creator model that handled
|
|
* non-Western names better than ours, this would be the function to change
|
|
*/
|
|
Scholar.CSL.prototype._processCreators = function(type, creators) {
|
|
var maxCreators = creators.length;
|
|
if(!maxCreators) return;
|
|
|
|
var useEtAl = false;
|
|
|
|
// figure out if we need to use "et al"
|
|
if(this._opt.etAl && maxCreators >= this._opt.etAl.minCreators) {
|
|
maxCreators = this._opt.etAl.useFirst;
|
|
useEtAl = true;
|
|
}
|
|
|
|
// parse authors into strings
|
|
var authorStrings = [];
|
|
var firstName, lastName;
|
|
for(var i=0; i<maxCreators; i++) {
|
|
if(this._opt.names.initializeWith) { // initialize with makes us use first
|
|
// initials, e.g. Doe, J.R.
|
|
var firstName = "";
|
|
var firstNames = creators[i].firstName.split(" ");
|
|
for(var j in firstNames) {
|
|
if(firstNames[j]) {
|
|
// get first initial, put in upper case, add initializeWith string
|
|
firstName += firstNames[j][0].toUpperCase()+this._opt.names.initializeWith;
|
|
}
|
|
}
|
|
} else {
|
|
firstName = creators[i].firstName;
|
|
}
|
|
lastName = creators[i].lastName;
|
|
|
|
if(i == 0 && this._opt.names.firstAuthorInverted || this._opt.names.subsequentAuthorInverted) {
|
|
// if this is the first author and author-as-sort-order="first-author"
|
|
// or if this is a subsequent author and author-as-sort-order="all"
|
|
// then the name gets inverted
|
|
authorStrings.push(lastName+this._opt.names.sortSeparator+firstName);
|
|
} else {
|
|
authorStrings.push(firstName+" "+lastName);
|
|
}
|
|
}
|
|
|
|
// figure out if we need an "and" or an "et al"
|
|
var joinString = ", ";
|
|
if(maxCreators > 1) {
|
|
if(useEtAl) { // multiple creators and need et al
|
|
authorStrings.push(Scholar.CSL._loc.etAl);
|
|
} else { // multiple creators but no et al
|
|
// add and to last creator
|
|
authorStrings[maxCreators-1] = this._opt.names.and+" "+authorStrings[maxCreators-1];
|
|
// skip the comma if there are only two creators and no
|
|
// et al
|
|
if(maxCreators == 2) {
|
|
joinString = " ";
|
|
}
|
|
}
|
|
}
|
|
|
|
var returnString = authorStrings.join(joinString);
|
|
|
|
// add "Edited By" or "Translated By"
|
|
if(this._opt.contributors.label[type]) {
|
|
// figure out whether to use singular or plural representation
|
|
if(maxCreators == 1) {
|
|
var label = this._opt.contributors.label[type][0];
|
|
} else {
|
|
var label = this._opt.contributors.label[type][1];
|
|
}
|
|
// figure out where to add
|
|
if(this._opt.contributors.positionBefore) {
|
|
returnString = label+" "+returnString;
|
|
} else {
|
|
returnString += " ("+label+")";
|
|
}
|
|
}
|
|
|
|
// add to the data
|
|
return returnString;
|
|
}
|
|
|
|
/*
|
|
* process the date "string" into a useful object
|
|
*/
|
|
Scholar.CSL.prototype._processDate = function(string) {
|
|
var date = new Object();
|
|
|
|
var dateRe = /^([0-9]{4})-([0-9]{2})-([0-9]{2})$/;
|
|
var m = dateRe.exec(string);
|
|
if(m) { // sql date
|
|
var jsDate = new Date(m[1], m[2]-1, m[3], false, false, false);
|
|
} else { // not an sql date
|
|
var jsDate = new Date(string);
|
|
}
|
|
|
|
if(isNaN(jsDate.valueOf())) { // couldn't parse
|
|
// get year and say other parts are month
|
|
var yearRe = /^(.*)([^0-9]{4})(.*)$/
|
|
var m = yearRe.exec(string);
|
|
|
|
date.year = m[2];
|
|
date.month = m[1]
|
|
if(m[2] && m[3]) date.month += " ";
|
|
date.month += m[3];
|
|
} else {
|
|
date.year = jsDate.getFullYear();
|
|
date.month = this._opt.dates.months[jsDate.getMonth()];
|
|
date.day = jsDate.getDay();
|
|
}
|
|
|
|
return date;
|
|
}
|
|
|
|
/*
|
|
* format the date according to date processing preference from the date object
|
|
* returned by this._processDate
|
|
*/
|
|
Scholar.CSL.prototype._formatDate = function(date) {
|
|
var data = this._opt.dates.format.replace("year", (date.year ? date.year : ""));
|
|
data = data.replace("month", (date.month ? date.month : ""));
|
|
data = data.replace("day", (date.day ? date.day : ""));
|
|
data = data.replace(/^[^\w]+/, "");
|
|
data = data.replace(/[^\w]+$/, "");
|
|
return data;
|
|
}
|
|
|
|
default xml namespace = Scholar.CSL.ns;
|
|
/*
|
|
* convert options to native structures for speed
|
|
*/
|
|
Scholar.CSL.prototype._parseOptions = function() {
|
|
default xml namespace = Scholar.CSL.ns;
|
|
this._opt = new Object();
|
|
|
|
// names
|
|
this._opt.names = new Object();
|
|
if(this._csl.general.names.@and == "text") {
|
|
this._opt.names.and = Scholar.CSL._loc.and;
|
|
} else if(this._csl.general.names.@and == "symbol") {
|
|
this._opt.names.and = "&";
|
|
} else {
|
|
this._opt.names.and = "";
|
|
}
|
|
this._opt.names.sortSeparator = this._csl.general.names["@sort-separator"].toString();
|
|
this._opt.names.initializeWith = this._csl.general.names["@initialize-with"].toString();
|
|
if(this._csl.bibliography["@author-as-sort-order"] == "all") {
|
|
this._opt.names.firstAuthorInverted = true;
|
|
this._opt.names.subsequentAuthorInverted = true;
|
|
} else if(this._csl.bibliography["@author-as-sort-order"] == "first-author") {
|
|
this._opt.names.firstAuthorInverted = true;
|
|
this._opt.names.subsequentAuthorInverted = false;
|
|
} else {
|
|
this._opt.names.firstAuthorInverted = false;
|
|
this._opt.names.subsequentAuthorInverted = false;
|
|
}
|
|
|
|
// contributors
|
|
this._opt.contributors = new Object();
|
|
if(this._csl.general.contributors.label.length) {
|
|
// contributors
|
|
if(this._csl.general.contributors.label.@position == "before") {
|
|
this._opt.contributors.positionBefore = true;
|
|
}
|
|
if(this._csl.general.contributors.label.@type == "verb") {
|
|
this._opt.contributors.label = {editor:[Scholar.CSL._loc.editorVerb, Scholar.CSL._loc.editorVerb],
|
|
translator:[Scholar.CSL._loc.translatorVerb, Scholar.CSL._loc.translatorVerb]}
|
|
} else {
|
|
this._opt.contributors.label = {editor:[Scholar.CSL._loc.editorNounSingle, Scholar.CSL._loc.editorNounMultiple],
|
|
translator:[Scholar.CSL._loc.translatorNounSingle, Scholar.CSL._loc.translatorNounMultiple]}
|
|
}
|
|
}
|
|
|
|
// locators
|
|
this._opt.locators = new Object();
|
|
if(this._csl.general.Scholar.CSL._locators.label.length) {
|
|
// contributors
|
|
if(this._csl.general.Scholar.CSL._locators.label.@position == "before") {
|
|
this._opt.locators.positionBefore = true;
|
|
}
|
|
if(this._csl.general.Scholar.CSL._locators.label.@form == "short") {
|
|
this._opt.locators.label = [Scholar.CSL._loc.pagesShortSingle, Scholar.CSL._loc.pagesShortMultiple];
|
|
} else {
|
|
this._opt.locators.label = [Scholar.CSL._loc.pagesLongSingle, Scholar.CSL._loc.pagesLongMultiple];
|
|
}
|
|
}
|
|
|
|
// dates
|
|
this._opt.dates = new Object();
|
|
this._opt.dates.format = this._csl.general.dates.@format.toString();
|
|
if(this._csl.general.dates.@month == "abbreviated") {
|
|
this._opt.dates.months = Scholar.CSL._loc.monthsAbbreviated;
|
|
} else {
|
|
this._opt.dates.months = Scholar.CSL._loc.months;
|
|
}
|
|
|
|
// publishers
|
|
this._opt.publishers = new Object();
|
|
if(this._csl.general.publishers.@order == "publisher-address") {
|
|
this._opt.publishers.publisherFirst = true;
|
|
}
|
|
this._opt.publishers.separator = this._csl.general.publishers.@separator.toString();
|
|
|
|
// access
|
|
this._opt.access = new Object();
|
|
if(this._csl.general.access.@order == "date-url") {
|
|
this._opt.access.dateFirst = true;
|
|
}
|
|
this._opt.access.separator = this._csl.general.access.@separator.toString();
|
|
|
|
// et al
|
|
if(this._csl.bibliography['use-et_al'].length()) {
|
|
this._opt.names.etAl = new Object();
|
|
this._opt.names.etAl.minCreators = parseInt(this._csl.bibliography['use-et_al']['@min-authors']);
|
|
this._opt.names.etAl.useFirst = parseInt(this._csl.bibliography['use-et_al']['@use-first']);
|
|
}
|
|
|
|
// sort order
|
|
this._opt.sortOrder = this._csl.bibliography["@sort-order"].toString();
|
|
|
|
// referenceTypes
|
|
this._opt.referenceTypes = new Object();
|
|
for each(var element in this._csl.bibliography['item-layout'].reftype) {
|
|
if(element.namespace() == Scholar.CSL.ns) { // ignore elements in other namespaces
|
|
this._opt.referenceTypes[element.@name.toString()] = true;
|
|
}
|
|
}
|
|
|
|
// global prefix and suffix
|
|
this._opt.suffix = this._csl.bibliography["item-layout"].@suffix.toString();
|
|
this._opt.prefix = this._csl.bibliography["item-layout"].@prefix.toString();
|
|
}
|
|
|
|
/*
|
|
* does the dirty work for parseReferenceTypes - recursively process attributes
|
|
* into an associative array
|
|
*/
|
|
Scholar.CSL.prototype._parseElements = function(ref) {
|
|
var typeDesc = new Array();
|
|
for each(var element in ref) {
|
|
if(element.namespace() == Scholar.CSL.ns) { // ignore elements in other namespaces
|
|
var itemDesc = new Object();
|
|
itemDesc.name = element.localName();
|
|
var attributes = element.attributes();
|
|
for each(var attribute in attributes) {
|
|
itemDesc[attribute.name()] = attribute.toString();
|
|
}
|
|
if(itemDesc.name == "group") { // parse groups recursively
|
|
itemDesc.elements = this._parseElements(element.elements());
|
|
}
|
|
typeDesc.push(itemDesc);
|
|
}
|
|
}
|
|
return typeDesc;
|
|
}
|
|
|
|
/*
|
|
* convert reference types to native structures for speed
|
|
*/
|
|
Scholar.CSL.prototype._parseReferenceType = function(reftype) {
|
|
default xml namespace = Scholar.CSL.ns;
|
|
var ref = this._csl.bibliography['item-layout'].reftype.(@name==reftype).elements();
|
|
this._opt.referenceTypes[reftype] = this._parseElements(ref);
|
|
} |