Skip to content

Commit 808c6bb

Browse files
Omikhleiaalerque
authored andcommitted
feat(packages): Use experimental CSL renderer for BibTeX
1 parent 6b12365 commit 808c6bb

File tree

3 files changed

+459
-3
lines changed

3 files changed

+459
-3
lines changed

packages/bibtex/init.lua

Lines changed: 258 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,47 @@
11
local base = require("packages.base")
22

3+
local loadkit = require("loadkit")
4+
local cslStyleLoader = loadkit.make_loader("csl")
5+
local cslLocaleLoader = loadkit.make_loader("xml")
6+
7+
local CslLocale = require("csl.core.locale").CslLocale
8+
local CslStyle = require("csl.core.style").CslStyle
9+
local CslEngine = require("csl.core.engine").CslEngine
10+
11+
local function loadCslLocale (name)
12+
local filename = SILE.resolveFile("csl/locales/locales-" .. name .. ".xml")
13+
or cslLocaleLoader("csl.locales.locales-" .. name)
14+
if not filename then
15+
SU.error("Could not find CSL locale '" .. name .. "'")
16+
end
17+
local locale, err = CslLocale.read(filename)
18+
if not locale then
19+
SU.error("Could not open CSL locale '" .. name .. "'': " .. err)
20+
return
21+
end
22+
return locale
23+
end
24+
local function loadCslStyle (name)
25+
local filename = SILE.resolveFile("csl/styles/" .. name .. ".csl") or cslStyleLoader("csl.styles." .. name)
26+
if not filename then
27+
SU.error("Could not find CSL style '" .. name .. "'")
28+
end
29+
local style, err = CslStyle.read(filename)
30+
if not style then
31+
SU.error("Could not open CSL style '" .. name .. "'': " .. err)
32+
return
33+
end
34+
return style
35+
end
36+
337
local package = pl.class(base)
438
package._name = "bibtex"
539

640
local epnf = require("epnf")
741
local nbibtex = require("packages.bibtex.support.nbibtex")
842
local namesplit, parse_name = nbibtex.namesplit, nbibtex.parse_name
943
local isodatetime = require("packages.bibtex.support.isodatetime")
44+
local bib2csl = require("packages.bibtex.support.bib2csl")
1045

1146
local Bibliography
1247

@@ -241,10 +276,36 @@ local function crossrefAndXDataResolve (bib, entry)
241276
end
242277
end
243278

279+
function package:loadOptPackage (pack)
280+
local ok, _ = pcall(function ()
281+
self:loadPackage(pack)
282+
return true
283+
end)
284+
SU.debug("bibtex", "Optional package " .. pack .. (ok and " loaded" or " not loaded"))
285+
return ok
286+
end
287+
244288
function package:_init ()
245289
base._init(self)
246290
SILE.scratch.bibtex = { bib = {} }
247291
Bibliography = require("packages.bibtex.bibliography")
292+
-- For DOI, PMID, PMCID and URL support.
293+
self:loadPackage("url")
294+
-- For underline styling support
295+
self:loadPackage("rules")
296+
-- For TeX-like math support (extension)
297+
self:loadPackage("math")
298+
-- For superscripting support in number formatting
299+
-- Play fair: try to load 3rd-party optional textsubsuper package.
300+
-- If not available, fallback to raiselower to implement textsuperscript
301+
if not self:loadOptPackage("textsubsuper") then
302+
self:loadPackage("raiselower")
303+
self:registerCommand("textsuperscript", function (_, content)
304+
SILE.call("raise", { height = "0.7ex" }, function ()
305+
SILE.call("font", { size = "1.5ex" }, content)
306+
end)
307+
end)
308+
end
248309
end
249310

250311
function package.declareSettings (_)
@@ -262,6 +323,8 @@ function package:registerCommands ()
262323
parseBibtex(file, SILE.scratch.bibtex.bib)
263324
end)
264325

326+
-- LEGACY COMMANDS
327+
265328
self:registerCommand("bibstyle", function (_, _)
266329
SU.deprecated("\\bibstyle", "\\set[parameter=bibtex.style]", "0.13.2", "0.14.0")
267330
end)
@@ -309,6 +372,161 @@ function package:registerCommands ()
309372
end
310373
SILE.processString(("<sile>%s</sile>"):format(cite), "xml")
311374
end)
375+
376+
-- NEW CSL IMPLEMENTATION
377+
378+
-- Internal commands for CSL processing
379+
380+
self:registerCommand("bibSmallCaps", function (_, content)
381+
-- To avoid attributes in the CSL-processed content
382+
SILE.call("font", { features = "+smcp" }, content)
383+
end)
384+
385+
-- CSL 1.0.2 appendix VI
386+
-- "If the bibliography entry for an item renders any of the following
387+
-- identifiers, the identifier should be anchored as a link, with the
388+
-- target of the link as follows:
389+
-- url: output as is
390+
-- doi: prepend with “https://doi.org/”
391+
-- pmid: prepend with “https://www.ncbi.nlm.nih.gov/pubmed/”
392+
-- pmcid: prepend with “https://www.ncbi.nlm.nih.gov/pmc/articles/”
393+
-- NOT IMPLEMENTED:
394+
-- "Citation processors should include an option flag for calling
395+
-- applications to disable bibliography linking behavior."
396+
-- (But users can redefine these commands to their liking...)
397+
self:registerCommand("bibLink", function (options, content)
398+
SILE.call("href", { src = options.src }, {
399+
SU.ast.createCommand("url", {}, { content[1] }),
400+
})
401+
end)
402+
self:registerCommand("bibURL", function (_, content)
403+
local link = content[1]
404+
if not link:match("^https?://") then
405+
-- Play safe
406+
link = "https://" .. link
407+
end
408+
SILE.call("bibLink", { src = link }, content)
409+
end)
410+
self:registerCommand("bibDOI", function (_, content)
411+
local link = content[1]
412+
if not link:match("^https?://") then
413+
link = "https://doi.org/" .. link
414+
end
415+
SILE.call("bibLink", { src = link }, content)
416+
end)
417+
self:registerCommand("bibPMID", function (_, content)
418+
local link = content[1]
419+
if not link:match("^https?://") then
420+
link = "https://www.ncbi.nlm.nih.gov/pubmed/" .. link
421+
end
422+
SILE.call("bibLink", { src = link }, content)
423+
end)
424+
self:registerCommand("bibPMCID", function (_, content)
425+
local link = content[1]
426+
if not link:match("^https?://") then
427+
link = "https://www.ncbi.nlm.nih.gov/pmc/articles/" .. link
428+
end
429+
SILE.call("bibLink", { src = link }, content)
430+
end)
431+
432+
-- Style and locale loading
433+
434+
self:registerCommand("bibliographystyle", function (options, _)
435+
local sty = SU.required(options, "style", "bibliographystyle")
436+
local style = loadCslStyle(sty)
437+
-- FIXME: lang is mandatory until we can map document.lang to a resolved
438+
-- BCP47 with region always present, as this is what CSL locales require.
439+
if not options.lang then
440+
-- Pick the default locale from the style, if any
441+
options.lang = style.globalOptions["default-locale"]
442+
end
443+
local lang = SU.required(options, "lang", "bibliographystyle")
444+
local locale = loadCslLocale(lang)
445+
SILE.scratch.bibtex.engine = CslEngine(style, locale, {
446+
localizedPunctuation = SU.boolean(options.localizedPunctuation, false),
447+
italicExtension = SU.boolean(options.italicExtension, true),
448+
mathExtension = SU.boolean(options.mathExtension, true),
449+
})
450+
end)
451+
452+
self:registerCommand("csl:cite", function (options, content)
453+
-- TODO:
454+
-- - locator support
455+
-- - multiple citation keys
456+
if not SILE.scratch.bibtex.engine then
457+
SILE.call("bibliographystyle", { lang = "en-US", style = "chicago-author-date" })
458+
-- SILE.call("bibliographystyle", { lang = "en-US", style = "chicago-fullnote-bibliography" })
459+
-- SILE.call("bibliographystyle", { lang = "en-US", style = "apa" })
460+
end
461+
local engine = SILE.scratch.bibtex.engine
462+
if not options.key then
463+
options.key = SU.ast.contentToString(content)
464+
end
465+
local entry = SILE.scratch.bibtex.bib[options.key]
466+
if not entry then
467+
SU.warn("Unknown reference in citation " .. options.key)
468+
return
469+
end
470+
if entry.type == "xdata" then
471+
SU.warn("Skipped citation of @xdata entry " .. options.key)
472+
return
473+
end
474+
crossrefAndXDataResolve(SILE.scratch.bibtex.bib, entry)
475+
476+
local csljson = bib2csl(entry)
477+
-- csljson.locator = { -- EXPERIMENTAL
478+
-- label = "page",
479+
-- value = "123-125"
480+
-- }
481+
local cite = engine:cite(csljson)
482+
483+
SILE.processString(("<sile>%s</sile>"):format(cite), "xml")
484+
end)
485+
486+
self:registerCommand("csl:reference", function (options, content)
487+
if not SILE.scratch.bibtex.engine then
488+
SILE.call("bibliographystyle", { lang = "en-US", style = "chicago-author-date" })
489+
-- SILE.call("bibliographystyle", { lang = "en-US", style = "chicago-fullnote-bibliography" })
490+
-- SILE.call("bibliographystyle", { lang = "en-US", style = "apa" })
491+
end
492+
local engine = SILE.scratch.bibtex.engine
493+
if not options.key then
494+
options.key = SU.ast.contentToString(content)
495+
end
496+
local entry = SILE.scratch.bibtex.bib[options.key]
497+
if not entry then
498+
SU.warn("Unknown reference in citation " .. options.key)
499+
return
500+
end
501+
if entry.type == "xdata" then
502+
SU.warn("Skipped citation of @xdata entry " .. options.key)
503+
return
504+
end
505+
crossrefAndXDataResolve(SILE.scratch.bibtex.bib, entry)
506+
507+
local cslentry = bib2csl(entry)
508+
local cite = engine:reference(cslentry)
509+
510+
SILE.processString(("<sile>%s</sile>"):format(cite), "xml")
511+
end)
512+
513+
self:registerCommand("printbibliography", function (_, _)
514+
local bib = SILE.scratch.bibtex.bib
515+
-- TEMP: until we implement proper sorting, let's sort by keys
516+
-- for reproducibility.
517+
local tkeys = {}
518+
for k, _ in pairs(bib) do
519+
table.insert(tkeys, k)
520+
end
521+
table.sort(tkeys)
522+
local count = 0
523+
for _, k in ipairs(tkeys) do
524+
SILE.call("csl:reference", { key = k })
525+
SILE.call("par")
526+
count = count + 1
527+
end
528+
SILE.typesetter:typeset("¤ " .. count .. " references")
529+
end)
312530
end
313531

314532
package.documentation = [[
@@ -321,13 +539,48 @@ This experimental package allows SILE to read and process BibTeX \code{.bib} fil
321539
322540
To load a BibTeX file, issue the command \autodoc:command{\loadbibliography[file=<whatever.bib>]}
323541
542+
\smallskip
543+
\noindent
544+
\em{Producing citations and references (legacy commands)}
545+
\novbreak
546+
547+
\indent
324548
To produce an inline citation, call \autodoc:command{\cite{<key>}}, which will typeset something like “Jones 1982”.
325549
If you want to cite a particular page number, use \autodoc:command{\cite[page=22]{<key>}}.
326550
327551
To produce a full reference, use \autodoc:command{\reference{<key>}}.
328552
329-
Currently, the only supported bibliography style is Chicago referencing, but other styles should be easy to implement.
330-
Adapt \code{packages/bibtex/styles/chicago.lua} as necessary.
553+
Currently, the only supported bibliography style is Chicago referencing.
554+
555+
\smallskip
556+
\noindent
557+
\em{Producing citations and references (CSL implementation)}
558+
\novbreak
559+
560+
\indent
561+
While an experimental work-in-progress, the CSL (Citation Style Language) implementation is more powerful and flexible than the legacy commands.
562+
563+
You must first invoke \autodoc:command{\bibliographystyle[style=<style>, lang=<lang>]}, where \autodoc:parameter{style} is the name of the CSL style file (without the \code{.csl} extension), and \autodoc:parameter{lang} is the language code of the CSL locale to use (e.g., \code{en-US}).
564+
565+
The command accepts a few additional options:
566+
567+
\begin{itemize}
568+
\item{\autodoc:parameter{localizedPunctuation} (default \code{false}): whether to use localized punctuation – this is non-standard but may be useful when using a style that was not designed for the target language;}
569+
\item{\autodoc:parameter{italicExtension} (default \code{true}): whether to convert \code{_text_} to italic text (“à la Markdown”);}
570+
\item{\autodoc:parameter{mathExtension} (default \code{true}): whether to recognize \code{$formula$} as math formulae in (a subset of the) TeX-like syntax.}
571+
\end{itemize}
572+
573+
The locale and styles files are searched in the \code{csl/locales} and \code{csl/styles} directories, respectively, in your project directory, or in the Lua package path.
574+
For convenience and testing, SILE bundles the \code{chicago-author-date} and \code{chicago-author-date-fr} styles, and the \code{en-US} and \code{fr-FR} locales.
575+
If you don’t specify a style or locale, the author-date style and the \code{en-US} locale will be used.
576+
577+
To produce an inline citation, call \autodoc:command{\csl:cite{<key>}}, which will typeset something like “(Jones 1982)”.
578+
579+
To produce a full reference, use \autodoc:command{\csl:reference{<key>}}.
580+
581+
To produce a complete bibliography, use \autodoc:command{\printbibliography}.
582+
As of yet, this command is for testing purposes only.
583+
It does not handle sorting or filtering of the bibliography.
331584
332585
\smallskip
333586
\noindent
@@ -376,7 +629,9 @@ If no such abbreviation is found, the value is considered to be a string literal
376629
377630
String values are assumed to be in the UTF-8 encoding, and shall not contain (La)TeX commands.
378631
Special character sequences from TeX (such as \code{`} assumed to be an opening quote) are not supported.
379-
There are exceptions to this rule. Notably, the \code{~} character can be used to represent a non-breaking space (when not backslash-escaped), and the \code{\\&} sequence is accepted (though this implementation does not mandate escaping ampersands).
632+
There are exceptions to this rule.
633+
Notably, the \code{~} character can be used to represent a non-breaking space (when not backslash-escaped), and the \code{\\&} sequence is accepted (though this implementation does not mandate escaping ampersands).
634+
With the CSL renderer, see also the non-standard extensions above.
380635
381636
Values can also be composed by concatenating strings, using the \code{#} character.
382637

0 commit comments

Comments
 (0)