zotero/chrome/content/zotero/xpcom/cite.js
Adomas Ven 44b6cd0525
citeproc-rs support (#2220)
Disabled under zotero.cite.useCiteprocRs by default
2021-12-15 17:28:41 -05:00

596 lines
18 KiB
JavaScript

"use strict";
/**
* Utility functions for dealing with citations
* @namespace
*/
Zotero.Cite = {
/**
* Locator labels
*/
"labels":["page", "book", "chapter", "column", "figure", "folio",
"issue", "line", "note", "opus", "paragraph", "part", "section", "sub verbo",
"volume", "verse"],
/**
* Remove specified item IDs in-place from a citeproc-js bibliography object returned
* by makeBibliography()
* @param {bib} citeproc-js bibliography object
* @param {Set} itemsToRemove Set of items to remove
*/
"removeFromBibliography":function(bib, itemsToRemove) {
var removeItems = [];
for(let i in bib[0].entry_ids) {
for(let j in bib[0].entry_ids[i]) {
if(itemsToRemove.has(`${bib[0].entry_ids[i][j]}`)) {
removeItems.push(i);
break;
}
}
}
for(let i=removeItems.length-1; i>=0; i--) {
bib[0].entry_ids.splice(removeItems[i], 1);
bib[1].splice(removeItems[i], 1);
}
},
/**
* Convert formatting data from citeproc-js bibliography object into explicit format
* parameters for RTF or word processors
* @param {bib} citeproc-js bibliography object
* @return {Object} Bibliography style parameters.
*/
"getBibliographyFormatParameters":function getBibliographyFormatParameters(bib) {
var bibStyle = {"tabStops":[], "indent":0, "firstLineIndent":0,
"lineSpacing":(240*bib[0].linespacing),
"entrySpacing":(240*bib[0].entryspacing)};
if(bib[0].hangingindent) {
bibStyle.indent = 720; // 720 twips = 0.5 in
bibStyle.firstLineIndent = -720; // -720 twips = -0.5 in
} else if(bib[0]["second-field-align"]) {
// 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+bib[0].maxoffset*120;
bibStyle.firstLineIndent = -alignAt;
if(bib[0]["second-field-align"] == "margin") {
bibStyle.tabStops = [0];
} else {
bibStyle.indent = alignAt;
bibStyle.tabStops = [alignAt];
}
}
return bibStyle;
},
/**
* Makes a formatted bibliography, if the style defines one; otherwise makes a
* formatted list of items
* @param {Zotero.Style} style The style to use
* @param {Zotero.Item[]} items An array of items
* @param {String} format The format of the output (html, text, or rtf)
* @return {String} Bibliography or item list in specified format
*/
"makeFormattedBibliographyOrCitationList":function(cslEngine, items, format, asCitationList) {
cslEngine.setOutputFormat(format);
cslEngine.updateItems(items.map(item => item.id));
if(!asCitationList) {
var bibliography = Zotero.Cite.makeFormattedBibliography(cslEngine, format);
if(bibliography) return bibliography;
}
var styleClass = cslEngine.opt.class;
var citations=[];
for (var i=0, ilen=items.length; i<ilen; i++) {
var item = items[i];
var outList = cslEngine.appendCitationCluster({"citationItems":[{"id":item.id}], "properties":{}}, true);
for (var j=0, jlen=outList.length; j<jlen; j++) {
var citationPos = outList[j][0];
citations[citationPos] = outList[j][1];
}
}
if(styleClass == "note") {
if(format == "html") {
return "<ol>\n\t<li>"+citations.join("</li>\n\t<li>")+"</li>\n</ol>";
} else if(format == "text") {
var output = [];
for(var i=0; i<citations.length; i++) {
output.push((i+1)+". "+citations[i]+"\r\n");
}
return output.join("");
} else if(format == "rtf") {
var output = ["{\\rtf \n{\\*\\listtable{\\list\\listtemplateid1\\listhybrid{\\listlevel"+
"\\levelnfc0\\levelnfcn0\\leveljc0\\leveljcn0\\levelfollow0\\levelstartat1"+
"\\levelspace360\\levelindent0{\\*\\levelmarker \\{decimal\\}.}{\\leveltext"+
"\\leveltemplateid1\\'02\\'00.;}{\\levelnumbers\\'01;}\\fi-360\\li720\\lin720 }"+
"{\\listname ;}\\listid1}}\n{\\*\\listoverridetable{\\listoverride\\listid1"+
"\\listoverridecount0\\ls1}}\n\\tx720\\li720\\fi-480\\ls1\\ilvl0\n"];
for(var i=0; i<citations.length; i++) {
output.push("{\\listtext "+(i+1)+". }"+citations[i]+"\\\n");
}
output.push("}");
return output.join("");
} else {
throw new Error("Unimplemented bibliography format "+format);
}
} else {
if(format == "html") {
return citations.join("<br />");
} else if(format == "text") {
return citations.join("\r\n");
} else if(format == "rtf") {
return "<\\rtf \n"+citations.join("\\\n")+"\n}";
}
}
},
/**
* Makes a formatted bibliography
* @param {Zotero.Style} style The style
* @param {String} format The format of the output (html, text, or rtf)
* @return {String} Bibliography in specified format
*/
"makeFormattedBibliography":function makeFormattedBibliography(cslEngine, format) {
var bib = cslEngine.makeBibliography();
if(!bib) return false;
if(format == "html") {
var output = [bib[0].bibstart];
for(var i in bib[1]) {
output.push(bib[1][i]);
// add COinS
for (let itemID of bib[0].entry_ids[i]) {
try {
var co = Zotero.OpenURL.createContextObject(Zotero.Items.get(itemID), "1.0");
if(!co) continue;
output.push(' <span class="Z3988" title="'+
co.replace("&", "&amp;", "g").replace("<", "&lt;", "g").replace(">", "&gt;", "g")+
'"></span>\n');
} catch(e) {
Zotero.logError(e);
}
}
}
output.push(bib[0].bibend);
var html = output.join("");
var inlineCSS = true;
if (!inlineCSS) {
return html;
}
//Zotero.debug("maxoffset: " + bib[0].maxoffset);
//Zotero.debug("entryspacing: " + bib[0].entryspacing);
//Zotero.debug("linespacing: " + bib[0].linespacing);
//Zotero.debug("hangingindent: " + bib[0].hangingindent);
//Zotero.debug("second-field-align: " + bib[0]["second-field-align"]);
var maxOffset = parseInt(bib[0].maxoffset);
var entrySpacing = parseInt(bib[0].entryspacing);
var lineSpacing = parseInt(bib[0].linespacing);
var hangingIndent = bib[0].hangingindent;
var secondFieldAlign = bib[0]["second-field-align"];
// Validate input
if(maxOffset == NaN) throw new Error("Invalid maxoffset");
if(entrySpacing == NaN) throw new Error("Invalid entryspacing");
if(lineSpacing == NaN) throw new Error("Invalid linespacing");
var str;
var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"]
.createInstance(Components.interfaces.nsIDOMParser),
doc = parser.parseFromString("<!DOCTYPE html><html><body></body></html>", "text/html");
doc.body.insertAdjacentHTML("afterbegin", html);
var div = doc.body.firstChild,
leftMarginDivs = Zotero.Utilities.xpath(doc, '//div[@class="csl-left-margin"]'),
multiField = !!leftMarginDivs.length,
clearEntries = multiField;
// One of the characters is usually a period, so we can adjust this down a bit
maxOffset = Math.max(1, maxOffset - 2);
// Force a minimum line height
if(lineSpacing <= 1.35) lineSpacing = 1.35;
var style = div.getAttribute("style");
if(!style) style = "";
style += "line-height: " + lineSpacing + "; ";
if(hangingIndent) {
if (multiField && !secondFieldAlign) {
throw ("second-field-align=false and hangingindent=true combination is not currently supported");
}
// If only one field, apply hanging indent on root
else if (!multiField) {
style += "margin-left: 2em; text-indent:-2em;";
}
}
if(style) div.setAttribute("style", style);
// csl-entry
var divs = Zotero.Utilities.xpath(doc, '//div[@class="csl-entry"]');
for(var i=0, n=divs.length; i<n; i++) {
var div = divs[i],
divStyle = div.getAttribute("style");
if(!divStyle) divStyle = "";
if (clearEntries) {
divStyle += "clear: left; ";
}
if(entrySpacing && i !== n - 1) {
divStyle += "margin-bottom: " + entrySpacing + "em;";
}
if(divStyle) div.setAttribute("style", divStyle);
}
// Padding on the label column, which we need to include when
// calculating offset of right column
var rightPadding = .5;
// div.csl-left-margin
for (let div of leftMarginDivs) {
var divStyle = div.getAttribute("style");
if(!divStyle) divStyle = "";
divStyle = "float: left; padding-right: " + rightPadding + "em;";
// Right-align the labels if aligning second line, since it looks
// better and we don't need the second line of text to align with
// the left edge of the label
if (secondFieldAlign) {
divStyle += "text-align: right; width: " + maxOffset + "em;";
}
div.setAttribute("style", divStyle);
}
// div.csl-right-inline
for (let div of Zotero.Utilities.xpath(doc, '//div[@class="csl-right-inline"]')) {
var divStyle = div.getAttribute("style");
if(!divStyle) divStyle = "";
divStyle = "margin: 0 .4em 0 " + (secondFieldAlign ? maxOffset + rightPadding : "0") + "em;";
if (hangingIndent) {
divStyle += "padding-left: 2em; text-indent:-2em;";
}
div.setAttribute("style", divStyle);
}
// div.csl-indent
for (let div of Zotero.Utilities.xpath(doc, '//div[@class="csl-indent"]')) {
div.setAttribute("style", "margin: .5em 0 0 2em; padding: 0 0 .2em .5em; border-left: 5px solid #ccc;");
}
return doc.body.innerHTML;
} else if(format == "text") {
return bib[0].bibstart+bib[1].join("")+bib[0].bibend;
} else if(format == "rtf") {
var bibStyle = Zotero.Cite.getBibliographyFormatParameters(bib);
var preamble = (bibStyle.tabStops.length ? "\\tx"+bibStyle.tabStops.join(" \\tx")+" " : "");
preamble += "\\li"+bibStyle.indent+" \\fi"+bibStyle.firstLineIndent+" "
+"\\sl"+bibStyle.lineSpacing+" \\slmult1 "
+"\\sa"+bibStyle.entrySpacing+" ";
return bib[0].bibstart+preamble+bib[1].join("\\\r\n")+"\\\r\n"+bib[0].bibend;
} else {
throw new Error("Unimplemented bibliography format "+format);
}
},
/**
* Get an item by ID, either by retrieving it from the library or looking for the document it
* belongs to.
* @param {String|Number|Array} id
* @return {Zotero.Item} item
*/
"getItem":function getItem(id) {
var slashIndex;
if(id instanceof Array) {
return id.map(anId => Zotero.Cite.getItem(anId));
} else if(typeof id === "string" && (slashIndex = id.indexOf("/")) !== -1) {
var sessionID = id.substr(0, slashIndex),
session = Zotero.Integration.sessions[sessionID],
item;
if (session) {
item = session.embeddedZoteroItems[id.substr(slashIndex+1)];
}
if(!item) {
item = new Zotero.Item("document");
item.setField("title", "Missing Item");
Zotero.log("CSL item "+id+" not found");
}
return item;
} else {
return Zotero.Items.get(id);
}
},
extraToCSL: function (extra) {
Zotero.debug(`Zotero.Cite.extraToCSL() is deprecated -- use Zotero.Utilities.Item.extraToCSL() instead`);
return Zotero.Utilities.Item.extraToCSL(extra);
}
};
/**
* Get a CSL abbreviation in the format expected by citeproc-js
*/
Zotero.Cite.getAbbreviation = new function() {
var abbreviations,
abbreviationCategories;
/**
* Initialize abbreviations database.
*/
function init() {
if(!abbreviations) loadAbbreviations();
}
function loadAbbreviations() {
var file = Zotero.File.pathToFile(Zotero.DataDirectory.dir);
file.append("abbreviations.json");
var json, origin;
if(file.exists()) {
json = Zotero.File.getContents(file);
origin = file.path;
} else {
json = Zotero.File.getContentsFromURL("resource://zotero/schema/abbreviations.json");
origin = "resource://zotero/schema/abbreviations.json";
}
try {
abbreviations = JSON.parse(json);
} catch(e) {
throw new Zotero.Exception.Alert("styles.abbreviations.parseError", origin,
"styles.abbreviations.title", e);
}
if(!abbreviations.info || !abbreviations.info.name || !abbreviations.info.URI) {
throw new Zotero.Exception.Alert("styles.abbreviations.missingInfo", origin,
"styles.abbreviations.title");
}
abbreviationCategories = {};
for(var jurisdiction in abbreviations) {
for(var category in abbreviations[jurisdiction]) {
abbreviationCategories[category] = true;
}
}
}
/**
* Normalizes a key
*/
function normalizeKey(key) {
// Strip periods, normalize spacing, and convert to lowercase
return key.toString().
replace(/(?:\b|^)(?:and|et|y|und|l[ae]|the|[ld]')(?:\b|$)|[\x21-\x2C.\/\x3A-\x40\x5B-\x60\\\x7B-\x7E]/ig, "").
replace(/\s+/g, " ").trim();
}
function lookupKey(key) {
return key.toLowerCase().replace(/\s*\./g, "." );
}
/**
* Replace getAbbreviation on citeproc-js with our own handler.
*/
return function getAbbreviation(listname, obj, jurisdiction, category, key) {
init();
// Short circuit if we know we don't handle this kind of abbreviation
if(!abbreviationCategories[category] && !abbreviationCategories[category+"-word"]) return;
var normalizedKey = normalizeKey(key),
lcNormalizedKey = lookupKey(normalizedKey),
abbreviation;
if(!normalizedKey) return;
var jurisdictions = ["default"];
if(jurisdiction !== "default" && abbreviations[jurisdiction]) {
jurisdictions.unshift(jurisdiction);
}
// Look for full abbreviation
var jur, cat;
for(var i=0; i<jurisdictions.length && !abbreviation; i++) {
if((jur = abbreviations[jurisdictions[i]]) && (cat = jur[category])) {
abbreviation = cat[lcNormalizedKey];
}
}
if(!abbreviation) {
// Abbreviate words individually
var words = normalizedKey.split(/([ \-])/);
if(words.length > 1) {
var lcWords = [];
for(var j=0; j<words.length; j+=2) {
lcWords[j] = lookupKey(words[j]);
}
for(var j=0; j<words.length; j+=2) {
var word = words[j],
lcWord = lcWords[j],
newWord = undefined,
exactMatch = false;
for(var i=0; i<jurisdictions.length && newWord === undefined; i++) {
if(!(jur = abbreviations[jurisdictions[i]])) continue;
if(!(cat = jur[category+"-word"])) continue;
if(cat.hasOwnProperty(lcWord)) {
// Complete match
newWord = cat[lcWord];
exactMatch = true;
} else if(lcWord.charAt(lcWord.length-1) == 's' && cat.hasOwnProperty(lcWord.substr(0, lcWord.length-1))) {
// Try dropping 's'
newWord = cat[lcWord.substr(0, lcWord.length-1)];
exactMatch = true;
} else {
if(j < words.length-2) {
// Two-word match
newWord = cat[lcWord+words[j+1]+lcWords[j+2]];
if(newWord !== undefined) {
words.splice(j+1, 2);
lcWords.splice(j+1, 2);
exactMatch = true;
}
}
if(newWord === undefined) {
// Partial match
for(var k=lcWord.length; k>0 && newWord === undefined; k--) {
newWord = cat[lcWord.substr(0, k)+"-"];
}
}
}
}
// Don't substitute with a longer word
if(newWord && !exactMatch && word.length - newWord.length < 1) {
newWord = word;
}
// Fall back to full word
if(newWord === undefined) newWord = word;
// Don't discard last word (e.g. Climate of the Past => Clim. Past)
if(!newWord && j == words.length-1) newWord = word;
words[j] = newWord.substr(0, 1).toUpperCase() + newWord.substr(1);
}
abbreviation = words.join("").replace(/\s+/g, " ").trim();
} else {
abbreviation = key;
}
}
if(!abbreviation) abbreviation = key; //this should never happen, but just in case
Zotero.debug("Abbreviated "+key+" as "+abbreviation);
// Add to jurisdiction object
if(!obj[jurisdiction]) {
obj[jurisdiction] = new Zotero.CiteProc.CSL.AbbreviationSegments();
}
obj[jurisdiction][category][key] = abbreviation;
}
};
/**
* citeproc-js system object
*
* @class
* @param {Object} options
* @param {Boolean} [options.automaticJournalAbbreviations]
* @param {Boolean} [options.uppercaseSubtitles]
*/
Zotero.Cite.System = function ({ automaticJournalAbbreviations, uppercaseSubtitles }) {
if (automaticJournalAbbreviations) {
this.getAbbreviation = Zotero.Cite.getAbbreviation;
}
if (uppercaseSubtitles) {
this.uppercase_subtitles = true; // eslint-disable-line camelcase
}
};
Zotero.Cite.System.prototype = {
/**
* citeproc-js system function for getting items
* See http://gsl-nagoya-u.net/http/pub/citeproc-doc.html#retrieveitem
* @param {String|Integer} item - Item ID, or string item for embedded citations
* @return {Object} citeproc-js item
*/
"retrieveItem":function retrieveItem(item) {
var zoteroItem, slashIndex;
if(typeof item === "object" && item !== null && item instanceof Zotero.Item) {
//if(this._cache[item.id]) return this._cache[item.id];
zoteroItem = item;
} else if(typeof item === "string" && (slashIndex = item.indexOf("/")) !== -1) {
// is an embedded item
var sessionID = item.substr(0, slashIndex);
var session = Zotero.Integration.sessions[sessionID];
if(session) {
var embeddedCitation = session.embeddedItems[item.substr(slashIndex+1)];
if (embeddedCitation) {
embeddedCitation.id = item;
return embeddedCitation;
}
}
} else {
// is an item ID
//if(this._cache[item]) return this._cache[item];
try {
zoteroItem = Zotero.Items.get(item);
} catch(e) {}
}
if(!zoteroItem) {
throw new Error("Zotero.Cite.System.retrieveItem called on non-item "+item);
}
var cslItem = Zotero.Utilities.Item.itemToCSLJSON(zoteroItem);
// TEMP: citeproc-js currently expects the id property to be the item DB id
cslItem.id = zoteroItem.id;
if (!Zotero.Prefs.get("export.citePaperJournalArticleURL")) {
var itemType = Zotero.ItemTypes.getName(zoteroItem.itemTypeID);
// don't return URL or accessed information for journal articles if a
// pages field exists
if (["journalArticle", "newspaperArticle", "magazineArticle"].indexOf(itemType) !== -1
&& zoteroItem.getField("pages")
) {
delete cslItem.URL;
delete cslItem.accessed;
}
}
return cslItem;
},
/**
* citeproc-js system function for getting locale
* See http://gsl-nagoya-u.net/http/pub/citeproc-doc.html#retrieveLocale
* @param {String} lang Language to look for a locale for
* @return {String|Boolean} The locale as a string if it exists, or false if it doesn't
*/
"retrieveLocale":function retrieveLocale(lang) {
return Zotero.Cite.Locale.get(lang);
}
};
Zotero.Cite.Locale = {
_cache: new Map(),
get: function (locale) {
var str = this._cache.get(locale);
if (str) {
return str;
}
try {
str = Zotero.File.getResource(`chrome://zotero/content/locale/csl/locales-${locale}.xml`);
this._cache.set(locale, str);
return str;
}
catch (e) {
//Zotero.debug(e);
return false;
}
}
};
if (typeof process === 'object' && process + '' === '[object process]'){
module.exports = Zotero.Cite;
}