diff --git a/csl/core/engine.lua b/csl/core/engine.lua
index be8cdbfdc..8ad4f4736 100644
--- a/csl/core/engine.lua
+++ b/csl/core/engine.lua
@@ -4,8 +4,8 @@
--
-- Public API:
-- - (constructor) CslEngine(style, locale) -> CslEngine
--- - CslEngine:cite(entry) -> string
--- - CslEngine:reference(entry) -> string
+-- - CslEngine:cite(entries) -> string
+-- - CslEngine:reference(entries) -> string
--
-- Important: while some consistency checks are performed, this engine is not
-- intended to handle errors in the locale, style or input data. It is assumed
@@ -13,7 +13,6 @@
--
-- THINGS NOT DONE
-- - disambiguation logic (not done at all)
--- - sorting logic (not done at all)
-- - other FIXME/TODOs in the code on specific features
--
-- luacheck: no unused args
@@ -80,15 +79,11 @@ function CslEngine:_init (style, locale, extras)
}
end
-function CslEngine:_prerender (mode)
+function CslEngine:_prerender ()
-- Stack for processing of cs:group as conditional
self.groupQueue = {}
self.groupState = { variables = {}, count = 0 }
- -- Track mode for processing: "citation" or "bibliography"
- -- Needed to use appropriate inheritable options.
- self.mode = mode
-
-- Track first name for name-as-sort-order
self.firstName = true
end
@@ -216,7 +211,9 @@ function CslEngine:_render_text_specials (value)
-- Typography:
-- Use pseudo-markdown italic extension (_text_) to wrap
-- the text in emphasis.
- s = luautf8.gsub(s, "_([^_]+)_", "%1")
+ -- Skip if sorting, as it's not supposed to affect sorting.
+ local repl = self.sorting and "%1" or "%1"
+ s = luautf8.gsub(s, "_([^_]+)_", repl)
end
table.insert(pieces, s)
else
@@ -278,6 +275,10 @@ function CslEngine:_render_formatting (t, options)
if not t then
return
end
+ if self.sorting then
+ -- Skip all formatting in sorting mode
+ return t
+ end
if options["font-style"] == "italic" then -- FIXME: also normal, oblique, and how nesting is supposed to work?
t = "" .. t .. ""
end
@@ -324,6 +325,13 @@ function CslEngine:_render_display (t, options)
end
function CslEngine:_render_quotes (t, options)
+ if not t then
+ return
+ end
+ if self.sorting then
+ -- Skip all quotes in sorting mode
+ return luautf8.gsub(t, "[“”\"]", "")
+ end
if t and options.quotes then
-- Smart transform curly quotes in the input to localized inner quotes.
t = luautf8.gsub(t, "“", self.punctuation.open_inner_quote)
@@ -340,7 +348,7 @@ function CslEngine:_render_quotes (t, options)
end
function CslEngine:_render_link (t, link)
- if t and link then
+ if t and link and not self.sorting then
-- We'll let the processor implement CSL 1.0.2 link handling.
-- (appendix VI)
t = "" .. t .. ""
@@ -355,19 +363,31 @@ end
-- RENDERING ELEMENTS: layout, text, date, number, names, label, group, choose
-function CslEngine:_layout (options, content, entry)
+function CslEngine:_layout (options, content, entries)
local output = {}
- local entries = type(entry) == "table" and not entry.type and entry or { entry } -- Multiple entries vs. single entry
- for _, ent in ipairs(entries) do
- local elem = self:_render_children(content, ent)
+ for _, entry in ipairs(entries) do
+ self:_prerender()
+ local elem = self:_render_children(content, entry)
+ -- affixes and formatting likely apply on elementary entries
+ -- (The CSL 1.0.2 specification is not very clear on this point.)
+ elem = self:_render_formatting(elem, options)
+ elem = self:_render_affixes(elem, options)
+ elem = self:_postrender(elem)
if elem then
table.insert(output, elem)
end
end
- local t = self:_render_delimiter(output, options.delimiter)
- t = self:_render_formatting(t, options)
- t = self:_render_affixes(t, options)
- return t
+ if options.delimiter then
+ return self:_render_delimiter(output, options.delimiter)
+ end
+ -- (Normally citations have a delimiter options, so we should only reach
+ -- this point for the bibliography)
+ local delim = self.mode == "citation" and "; " or ""
+ -- references all belong to a different paragraph
+ -- FIXME: should account for attributes on the toplevel bibliography element:
+ -- line-spacing
+ -- hanging-indent
+ return table.concat(output, delim)
end
function CslEngine:_text (options, content, entry)
@@ -698,13 +718,22 @@ function CslEngine:_name_et_al (options)
end
function CslEngine:_a_name (options, content, entry)
+ local form = options.form
+ local nameAsSortOrder = options["name-as-sort-order"]
+ if self.sorting then
+ -- Ovveride form and name-as-sort-order in sorting mode
+ form = "long"
+ nameAsSortOrder = "all"
+ end
+
-- TODO FIXME: content can consists in name-part elements for formatting, text-case, affixes
-- Chigaco style does not seem to use them, so we keep it simple for now.
-- TODO FIXME: demote-non-dropping-particle option not implemented, and name particle not implemented at all!
- if options.form == "short" then
+
+ if form == "short" then
return entry.family
end
- if options["name-as-sort-order"] ~= "all" and not self.firstName then
+ if nameAsSortOrder ~= "all" and not self.firstName then
-- Order is: Given Family
return entry.given and (entry.given .. " " .. entry.family) or entry.family
end
@@ -751,7 +780,8 @@ function CslEngine:_names_with_resolved_opts (options, substitute_node, entry)
local skip = editortranslator and var == "translator" -- done via the "editor" field
if not skip and entry[var] then
local label
- if label_opts then
+ if label_opts and not self.sorting then
+ -- (labels in names are skipped in sorting mode)
local v = var == "editor" and editortranslator and "editortranslator" or var
local opts = pl.tablex.union(label_opts, { variable = v })
label = self:_label(opts, nil, entry)
@@ -994,22 +1024,101 @@ function CslEngine:_if (options, content, entry)
end
function CslEngine:_choose (options, content, entry)
- for _, c in ipairs(content) do
- if c.command == "cs:if" or c.command == "cs:else-if" then
- local t, match = self:_if(c.options, c, entry)
+ for _, child in ipairs(content) do
+ if child.command == "cs:if" or child.command == "cs:else-if" then
+ local t, match = self:_if(child.options, child, entry)
if match then
return t
end
- elseif c.command == "cs:else" then
- return self:_render_children(c, entry)
+ elseif child.command == "cs:else" then
+ return self:_render_children(child, entry)
end
end
end
-function CslEngine:_sort (options, content, entry)
- -- FIXME TODO
- -- Silent for now.
- -- SU.warn("CSL sort not implemented yet")
+local function dateToYYMMDD (date)
+ --- Year from BibLaTeX year field may be a literal
+ local y = date.year and date.year:match("%d%d%d%d") or "0000"
+ local m = date.month or "00"
+ local d = date.day or "00"
+ return ("%04d%02d%02d"):format(y, m, d)
+end
+
+function CslEngine:_key (options, content, entry)
+ if options.macro then
+ return self:_render_children(self.macros[options.macro], entry)
+ end
+ if options.variable then
+ local value = entry[options.variable]
+ if type(value) == "table" then
+ if value.range then
+ if value.startdate and value.enddate then
+ return dateToYYMMDD(value.startdate) .. "-" .. dateToYYMMDD(value.enddate)
+ end
+ if value.startdate then
+ return dateToYYMMDD(value.startdate) .. "-"
+ end
+ if value.enddate then
+ return dateToYYMMDD(value.enddate)
+ end
+ return dateToYYMMDD(value.from) .. "-" .. dateToYYMMDD(value.to)
+ end
+ if value.year or value.month or value.day then
+ return dateToYYMMDD(value)
+ end
+ -- FIXME names need a special rule here
+ -- Chicago style use macro here, so not considered for now.
+ SU.error("CSL variable not yet usable for sorting: " .. options.variable)
+ end
+ return value
+ end
+ SU.error("CSL key without variable or macro")
+end
+
+-- FIXME: A bit ugly: When implementing SU.collatedSort, I didn't consider
+-- sorting structured tables, so I need to go low level here.
+-- Moreover, I made icu.compare return a boolean, so we have to pay twice
+-- the comparison cost to check equality...
+local icu = require("justenoughicu")
+
+function CslEngine:_sort (options, content, entries)
+ if not self.sorting then
+ -- Skipped at rendering
+ return
+ end
+ -- FIXME: not implemented: sort direction ascending/descending
+
+ -- Compute the sorting keys for each entry
+ for _, entry in ipairs(entries) do
+ local keys = {}
+ for _, child in ipairs(content) do
+ if child.command == "cs:key" then
+ self:_prerender()
+ -- Deep copy the entry as cs:substitute may remove fields
+ -- And we may need them back in actual rendering
+ local ent = pl.tablex.deepcopy(entry)
+ local key = self:_key(child.options, child, ent)
+ -- No _postrender here, as we don't want to apply punctuation (?)
+ table.insert(keys, key or "")
+ end
+ end
+ entry._keys = keys
+ end
+ -- Perform the sort
+ -- Using the locale language (BCP47).
+ local lang = self.locale.lang
+ local collator = icu.collation_create(lang, {})
+ table.sort(entries, function (a, b)
+ local ak = a._keys
+ local bk = b._keys
+ for i = 1, math.min(#ak, #bk) do
+ if ak[i] ~= bk[i] then -- See comment, ugly inequality check)
+ return icu.compare(collator, ak[i], bk[i])
+ end
+ end
+ return false
+ end)
+ icu.collation_destroy(collator)
end
-- PROCESSING
@@ -1079,33 +1188,44 @@ function CslEngine:_postrender (text)
return text
end
-function CslEngine:_process (entry, mode)
+function CslEngine:_process (entries, mode)
if mode ~= "citation" and mode ~= "bibliography" then
SU.error("CSL processing mode must be 'citation' or 'bibliography'")
end
- self:_prerender(mode)
- -- Deep copy the entry as cs:substitute may remove fields
- entry = pl.tablex.deepcopy(entry)
+ self.mode = mode
+ -- Deep copy the entries as cs:substitute may remove fields
+ entries = pl.tablex.deepcopy(entries)
+
local ast = self[mode]
if not ast then
SU.error("CSL style has no " .. mode .. " definition")
end
- local res = self:_render_children(ast, entry)
- return self:_postrender(res)
+ local sort = SU.ast.findInTree(ast, "cs:sort")
+ if sort then
+ self.sorting = true
+ self:_sort(sort.options, sort, entries)
+ self.sorting = false
+ end
+
+ local res = self:_render_children(ast, entries)
+
+ return res
end
--- Generate a citation string.
--- @tparam table entry TList of CSL-JSON entries
--- @treturn string The citation string
-function CslEngine:cite (entry)
- return self:_process(entry, "citation")
+-- @tparam table entry List of CSL entries
+-- @treturn string The XML citation string
+function CslEngine:cite (entries)
+ entries = type(entries) == "table" and not entries.type and entries or { entries }
+ return self:_process(entries, "citation")
end
--- Generate a reference string.
--- @tparam table entry TList of CSL-JSON entries
--- @treturn string The reference string
-function CslEngine:reference (entry)
- return self:_process(entry, "bibliography")
+-- @tparam table entry List of CSL entries
+-- @treturn string The XML reference string
+function CslEngine:reference (entries)
+ entries = type(entries) == "table" and not entries.type and entries or { entries }
+ return self:_process(entries, "bibliography")
end
return {