diff --git a/chrome/content/zotero/xpcom/cite.js b/chrome/content/zotero/xpcom/cite.js index dac7c6386..912c94f42 100644 --- a/chrome/content/zotero/xpcom/cite.js +++ b/chrome/content/zotero/xpcom/cite.js @@ -1,442 +1,481 @@ -Zotero.Cite = function(){} -Zotero.Cite.System = function(){}; - -Zotero.Cite.System._quotedRegexp = /^".+"$/; - -// TODO: Clear this cache from time to time -Zotero.Cite.System._cache = new Object(); - -Zotero.Cite.System.retrieveItem = function(item) { - var zoteroItem, slashIndex; - if(item instanceof Zotero.Item) { - //if(this._cache[item.id]) return this._cache[item.id]; - zoteroItem = item; - } else { - var type = typeof item; - if(type === "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; +/** + * 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 {Array} itemsToRemove Array 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[bib[0].entry_ids[i][j]]) { + removeItems.push(i); + break; } } - } else { - // is an item ID - //if(this._cache[item]) return this._cache[item]; - zoteroItem = Zotero.Items.get(item); } - } + for(let i=removeItems.length-1; i>=0; i--) { + bib[0].entry_ids.splice(removeItems[i], 1); + bib[1].splice(removeItems[i], 1); + } + }, - if(!zoteroItem) { - throw "Zotero.Cite.getCSLItem called to wrap a non-item "+item; - } - - // don't return URL or accessed information for journal articles if a - // pages field exists - var itemType = Zotero.ItemTypes.getName(zoteroItem.itemTypeID); - var cslType = CSL_TYPE_MAPPINGS[itemType]; - if(!cslType) cslType = "article"; - var ignoreURL = ((zoteroItem.getField("accessDate", true, true) || zoteroItem.getField("url", true, true)) && - ["journalArticle", "newspaperArticle", "magazineArticle"].indexOf(itemType) !== -1 - && zoteroItem.getField("pages") - && !Zotero.Prefs.get("export.citePaperJournalArticleURL")); - - var cslItem = { - 'id':zoteroItem.id, - 'type':cslType - }; - - // get all text variables (there must be a better way) - // TODO: does citeproc-js permit short forms? - for(var variable in CSL_TEXT_MAPPINGS) { - var fields = CSL_TEXT_MAPPINGS[variable]; - if(variable == "URL" && ignoreURL) continue; - for each(var field in fields) { - var value = zoteroItem.getField(field, false, true).toString(); - if(value != "") { - // Strip enclosing quotes - if(value.match(Zotero.Cite.System._quotedRegexp)) { - value = value.substr(1, value.length-2); - } - cslItem[variable] = value; - break; + /** + * 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]; } } - } - - // separate name variables - var authorID = Zotero.CreatorTypes.getPrimaryIDForType(zoteroItem.itemTypeID); - var creators = zoteroItem.getCreators(); - for each(var creator in creators) { - if(creator.creatorTypeID == authorID) { - var creatorType = "author"; + + 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(style, items, format) { + var cslEngine = style.csl; + cslEngine.setOutputFormat(format); + cslEngine.updateItems([item.id for each(item in items)]); + + var bibliography = Zotero.Cite.makeFormattedBibliography(cslEngine, format); + if(bibliography) return bibliography; + + var styleClass = style.class; + var citations = [cslEngine.appendCitationCluster({"citationItems":[{"id":item.id}], "properties":{}}, true)[0][1] + for each(item in items)]; + + if(styleClass == "note") { + if(format == "html") { + return "
    \n\t
  1. "+citations.join("
  2. \n\t
  3. ")+"
  4. \n
"; + } else if(format == "text") { + var output = []; + for(var i=0; i"); + } else if(format == "text") { + return citations.join("\r\n"); + } else if(format == "rtf") { + return "<\\rtf \n"+citations.join("\\\n")+"\n}"; + } } - - var creatorType = CSL_NAMES_MAPPINGS[creatorType]; - if(!creatorType) continue; - - var nameObj = {'family':creator.ref.lastName, 'given':creator.ref.firstName}; - - if(cslItem[creatorType]) { - cslItem[creatorType].push(nameObj); - } else { - cslItem[creatorType] = [nameObj]; - } - } + }, - // get date variables - for(var variable in CSL_DATE_MAPPINGS) { - var date = zoteroItem.getField(CSL_DATE_MAPPINGS[variable], false, true); - if(date) { - var dateObj = Zotero.Date.strToDate(date); - // otherwise, use date-parts - var dateParts = []; - if(dateObj.year) { - // add year, month, and day, if they exist - dateParts.push(dateObj.year); - if(dateObj.month !== undefined) { - dateParts.push(dateObj.month+1); - if(dateObj.day) { - dateParts.push(dateObj.day); + /** + * 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) { + cslEngine.setOutputFormat(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 each(var itemID in bib[0].entry_ids[i]) { + try { + var co = Zotero.OpenURL.createContextObject(Zotero.Items.get(itemID), "1.0"); + if(!co) continue; + output.push(' ", ">", "g")+ + '"/>\n'); + } catch(e) { + Zotero.logError(e); } } - cslItem[variable] = {"date-parts":[dateParts]}; + } + 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 = parseInt(bib[0].hangingindent); + var secondFieldAlign = bib[0]["second-field-align"]; + + // Validate input + if(maxOffset == NaN) throw "Invalid maxoffset"; + if(entrySpacing == NaN) throw "Invalid entryspacing"; + if(lineSpacing == NaN) throw "Invalid linespacing"; + + var str; + try { + var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] + .createInstance(Components.interfaces.nsIDOMParser), + doc = parser.parseFromString(html, "application/xml"); - // if no month, use season as month - if(dateObj.part && !dateObj.month) { - cslItem[variable].season = dateObj.part; + var 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 = doc.documentElement.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 += "padding-left: " + hangingIndent + "em; text-indent:-" + hangingIndent + "em;"; + } } - } else { - // if no year, pass date literally - cslItem[variable] = {"literal":date}; + + if(style) doc.documentElement.setAttribute("style", style); + + // csl-entry + var divs = Zotero.Utilities.xpath(doc, '//div[@class="csl-entry"]'); + for(var i=0, n=divs.length; i=0; i--) { - bib[0].entry_ids.splice(removeItems[i], 1); - bib[1].splice(removeItems[i], 1); - } -} - -Zotero.Cite.getBibliographyFormatParameters = function(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 - */ -Zotero.Cite.makeFormattedBibliographyOrCitationList = function(style, items, format) { - var cslEngine = style.csl; - cslEngine.setOutputFormat(format); - cslEngine.updateItems([item.id for each(item in items)]); - - var bibliography = Zotero.Cite.makeFormattedBibliography(cslEngine, format); - if(bibliography) return bibliography; - - var styleClass = style.class; - var citations = [cslEngine.appendCitationCluster({"citationItems":[{"id":item.id}], "properties":{}}, true)[0][1] - for each(item in items)]; - - if(styleClass == "note") { - if(format == "html") { - return "
    \n\t
  1. "+citations.join("
  2. \n\t
  3. ")+"
  4. \n
"; + + return str; } else if(format == "text") { - var output = []; - for(var i=0; i"); - } else if(format == "text") { - return citations.join("\r\n"); - } else if(format == "rtf") { - return "<\\rtf \n"+citations.join("\\\n")+"\n}"; + }, + + /** + * 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 [Zotero.Cite.getItem(anId) for each(anId in id)]; + } 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); } } -} +}; /** - * Makes a formatted bibliography - * @param {Zotero.Style} style The style - * @param {Zotero.Item[]} items An array of items + * citeproc-js system object + * @namespace */ -Zotero.Cite.makeFormattedBibliography = function(cslEngine, format) { - cslEngine.setOutputFormat(format); - var bib = cslEngine.makeBibliography(); - if(!bib) return false; +Zotero.Cite.System = { + /** + * citeproc-js system function for getting items + * See http://gsl-nagoya-u.net/http/pub/citeproc-doc.html#retrieveitem + * @param {String|Integer} Item ID, or string item for embedded citations + * @return {Object} citeproc-js item + */ + "retrieveItem":function retrieveItem(item) { + var zoteroItem, slashIndex; + if(item instanceof Zotero.Item) { + //if(this._cache[item.id]) return this._cache[item.id]; + zoteroItem = item; + } else { + var type = typeof item; + if(type === "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]; + zoteroItem = Zotero.Items.get(item); + } + } - if(format == "html") { - var output = [bib[0].bibstart]; - for(var i in bib[1]) { - output.push(bib[1][i]); - - // add COinS - for each(var itemID in bib[0].entry_ids[i]) { - try { - var co = Zotero.OpenURL.createContextObject(Zotero.Items.get(itemID), "1.0"); - if(!co) continue; - output.push(' ", ">", "g")+ - '"/>\n'); - } catch(e) { - Zotero.logError(e); + if(!zoteroItem) { + throw "Zotero.Cite.getCSLItem called to wrap a non-item "+item; + } + + // don't return URL or accessed information for journal articles if a + // pages field exists + var itemType = Zotero.ItemTypes.getName(zoteroItem.itemTypeID); + var cslType = CSL_TYPE_MAPPINGS[itemType]; + if(!cslType) cslType = "article"; + var ignoreURL = ((zoteroItem.getField("accessDate", true, true) || zoteroItem.getField("url", true, true)) && + ["journalArticle", "newspaperArticle", "magazineArticle"].indexOf(itemType) !== -1 + && zoteroItem.getField("pages") + && !Zotero.Prefs.get("export.citePaperJournalArticleURL")); + + var cslItem = { + 'id':zoteroItem.id, + 'type':cslType + }; + + // get all text variables (there must be a better way) + // TODO: does citeproc-js permit short forms? + for(var variable in CSL_TEXT_MAPPINGS) { + var fields = CSL_TEXT_MAPPINGS[variable]; + if(variable == "URL" && ignoreURL) continue; + for each(var field in fields) { + var value = zoteroItem.getField(field, false, true).toString(); + if(value != "") { + // Strip enclosing quotes + if(value.match(/^".+"$/)) { + value = value.substr(1, value.length-2); + } + cslItem[variable] = value; + break; } } } - output.push(bib[0].bibend); - var html = output.join(""); - var inlineCSS = true; - if (!inlineCSS) { - return html; + // separate name variables + var authorID = Zotero.CreatorTypes.getPrimaryIDForType(zoteroItem.itemTypeID); + var creators = zoteroItem.getCreators(); + for each(var creator in creators) { + if(creator.creatorTypeID == authorID) { + var creatorType = "author"; + } else { + var creatorType = Zotero.CreatorTypes.getName(creator.creatorTypeID); + } + + var creatorType = CSL_NAMES_MAPPINGS[creatorType]; + if(!creatorType) continue; + + var nameObj = {'family':creator.ref.lastName, 'given':creator.ref.firstName}; + + if(cslItem[creatorType]) { + cslItem[creatorType].push(nameObj); + } else { + cslItem[creatorType] = [nameObj]; + } } - //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"]); + // get date variables + for(var variable in CSL_DATE_MAPPINGS) { + var date = zoteroItem.getField(CSL_DATE_MAPPINGS[variable], false, true); + if(date) { + var dateObj = Zotero.Date.strToDate(date); + // otherwise, use date-parts + var dateParts = []; + if(dateObj.year) { + // add year, month, and day, if they exist + dateParts.push(dateObj.year); + if(dateObj.month !== undefined) { + dateParts.push(dateObj.month+1); + if(dateObj.day) { + dateParts.push(dateObj.day); + } + } + cslItem[variable] = {"date-parts":[dateParts]}; + + // if no month, use season as month + if(dateObj.part && !dateObj.month) { + cslItem[variable].season = dateObj.part; + } + } else { + // if no year, pass date literally + cslItem[variable] = {"literal":date}; + } + } + } - var maxOffset = parseInt(bib[0].maxoffset); - var entrySpacing = parseInt(bib[0].entryspacing); - var lineSpacing = parseInt(bib[0].linespacing); - var hangingIndent = parseInt(bib[0].hangingindent); - var secondFieldAlign = bib[0]["second-field-align"]; - - // Validate input - if(maxOffset == NaN) throw "Invalid maxoffset"; - if(entrySpacing == NaN) throw "Invalid entryspacing"; - if(lineSpacing == NaN) throw "Invalid linespacing"; - - var str; + //this._cache[zoteroItem.id] = cslItem; + 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) { + var protHandler = Components.classes["@mozilla.org/network/protocol;1?name=chrome"] + .createInstance(Components.interfaces.nsIProtocolHandler); try { - var parser = Components.classes["@mozilla.org/xmlextras/domparser;1"] - .createInstance(Components.interfaces.nsIDOMParser), - doc = parser.parseFromString(html, "application/xml"); - - var 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 = doc.documentElement.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 += "padding-left: " + hangingIndent + "em; text-indent:-" + hangingIndent + "em;"; - } - } - - if(style) doc.documentElement.setAttribute("style", style); - - // csl-entry - var divs = Zotero.Utilities.xpath(doc, '//div[@class="csl-entry"]'); - for(var i=0, n=divs.length; i