diff --git a/csl/core/engine.lua b/csl/core/engine.lua index fd8218563..aa7239240 100644 --- a/csl/core/engine.lua +++ b/csl/core/engine.lua @@ -7,6 +7,15 @@ -- - CslEngine:cite(entries) -> string -- - CslEngine:reference(entries) -> string -- +-- The expected internal representation of a CSL entry is similar to CSL-JSON +-- but with some differences: +-- Date fields are structured tables (not an array of numbers as in CSL-JSON). +-- citation-number (mandatory) is supposed to have been added by the citation processor. +-- locator (optional, also possibly added by the citation processor) is a table with label and value fields. +-- names are parsed, +-- as personal names (ex. `{ given = "George", family = "Smith" ... }`), +-- or are literal strings (ex. `{ literal = "T.C.B.S" }`). +-- -- 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 -- that they are all valid. @@ -14,7 +23,7 @@ -- THINGS NOT DONE -- - disambiguation logic (not done at all) -- - collapse logic in citations (not done at all) --- - other FIXME/TODOs in the code on specific features +-- - other FIXME in the code on quite specific features -- -- luacheck: no unused args @@ -92,6 +101,17 @@ function CslEngine:_init (style, locale, extras) self.style.bibliography and self.style.bibliography.options or {} ), } + + self.subsequentAuthorSubstitute = self.inheritable["bibliography"]["subsequent-author-substitute"] + local _, count = luautf8.gsub(self.subsequentAuthorSubstitute, "[%-_–—]", "") -- naive count + if count > 0 then + -- With many fonts, a sequence of dashes is not looking that great. + -- So replace them with a command, and let the typesetter decide for a better rendering. + -- NOTE: Avoid (quoted) attributes and dashes in tags, as some global + -- substitutions might affect quotes...So we use a simple "wrapper" command. + local trail = luautf8.gsub(self.subsequentAuthorSubstitute, "^[%-–—_]+", "") + self.subsequentAuthorSubstitute = "" .. count .. "" .. trail + end end function CslEngine:_prerender () @@ -101,6 +121,13 @@ function CslEngine:_prerender () -- Track first name for name-as-sort-order self.firstName = true + + -- Track first rendered cs:names for subsequent-author-substitute + self.doAuthorSubstitute = self.mode == "bibliography" and self.subsequentAuthorSubstitute + self.hasRenderedNames = false + -- Track authors for subsequent-author-substitute + self.precAuthors = self.currentAuthors + self.currentAuthors = {} end function CslEngine:_merge_locales (locale1, locale2) @@ -307,8 +334,8 @@ function CslEngine:_render_formatting (t, options) t = "" .. t .. "" end if options["font-variant"] == "small-caps" then - -- To avoid (quoted) attributes in the output, as some global - -- substitutions might affect quotes, we use a simple "wrapper" command. + -- NOTE: Avoid (quoted) attributes and dashes in tags, as some global + -- substitutions might affect quotes...So we use a simple "wrapper" command. t = "" .. t .. "" end if options["font-weight"] == "bold" then -- FIXME: also light, normal, and how nesting is supposed to work? @@ -340,11 +367,12 @@ function CslEngine:_render_display (t, options) if not t then return end - -- if options.display then - -- FIXME Add rationale for not supporting it... - -- Keep silent: it's not a critical feature yet - -- SU.warn("CSL display not implemented") - -- end + -- FIXME NOT IMPLEMENTED: + -- If set, options.display can be "block", "left-margin", "right-inline", "indent" + -- Usual styles such as Chicago, MLA, ACS etc. do not use it. + if options.display then + SU.warn("CSL display attribute not implemented: output will likely be incorrect") + end return t end @@ -375,6 +403,8 @@ function CslEngine:_render_link (t, link) if t and link and not self.sorting then -- We'll let the processor implement CSL 1.0.2 link handling. -- (appendix VI) + -- NOTE: Avoid (quoted) attributes and dashes in tags, as some global + -- substitutions might affect quotes...So we use a simple "wrapper" command. t = "" .. t .. "" end return t @@ -438,7 +468,7 @@ function CslEngine:_text (options, content, entry) t = self.page_range_replace(t) end - -- FIXME NOT IMPLEMENTED SPEC: + -- FIXME NOT IMPLEMENTED: -- "May be accompanied by the form attribute to select the “long” -- (default) or “short” form of a variable (e.g. the full or short -- title). If the “short” form is selected but unavailable, the @@ -595,8 +625,8 @@ end function CslEngine:_date_part (options, content, date) local name = SU.required(options, "name", "cs:date-part") - -- FIXME TODO full date range are not implemented properly - -- But we need to decide how to encode them in the pseudo CSL-JSON... + -- FIXME TODO + -- Full date range are not implemented properly local t local callback = "_a_date_" .. name if self[callback] then @@ -748,6 +778,9 @@ function CslEngine:_name_et_al (options) end function CslEngine:_a_name (options, content, entry) + if entry.literal then -- pass through literal names + return entry.literal + end if not entry.family then -- There's one element in a name we can't do without. SU.error("Name without family: what do you expect me to do with it?") @@ -927,30 +960,53 @@ function CslEngine:_names_with_resolved_opts (options, substitute_node, entry) local needEtAl = false local names = type(entry[var]) == "table" and entry[var] or { entry[var] } local l = {} - for i, name in ipairs(names) do - if #names >= et_al_min and i > et_al_use_first then - needEtAl = true - break - end - local t = self:_a_name(name_node.options, name_node, name) + + -- FIXME EXPLAIN + if not self.hasRenderedNames then + pl.tablex.insertvalues(self.currentAuthors, names) + end + if + self.doAuthorSubstitute + and not self.sorting + and not self.hasRenderedNames + and self.precAuthors + and pl.tablex.deepcompare(names, self.precAuthors) + then + -- FIXME NOT IMPLEMENTED + -- subsequent-author-substitute-rule (default "complete-all" is assumed here) + -- NOTE: Avoid (quoted) attributes and dashes in tags, as some global + -- substitutions might affect quotes... + -- So we use a simple "wrapper" command. + table.insert(l, self.subsequentAuthorSubstitute) self.firstName = false - table.insert(l, t) + else + for i, name in ipairs(names) do + if #names >= et_al_min and i > et_al_use_first then + needEtAl = true + break + end + local t = self:_a_name(name_node.options, name_node, name) + self.firstName = false + table.insert(l, t) + end end + local joined if needEtAl then - -- TODO THINGS TO SUPPORT THAT MIGHT REQUIRE A REFACTOR - -- They are not needed in Chicago style, so let's keep it simple for now. + -- FIXME NOT IMPLEMENTED + -- They are not needed in Chicago style, so let's keep it simple for now: -- delimiter-precedes-et-al ("contextual" by default = hard-coded) -- et-al-use-last (default false, if true, the last is rendered as ", ... Name) instead of using et-al. local rendered_et_all = self:_name_et_al(et_al_opts) - local sep_et_al = #l > 1 and name_delimiter + local sep_et_al = #l > 1 and name_delimiter or " " joined = table.concat(l, name_delimiter) .. sep_et_al .. rendered_et_all elseif #l == 1 then joined = l[1] else - -- TODO THINGS TO SUPPORT THAT MIGHT REQUIRE A REFACTOR - -- delimiter-precedes-last ("contextual" by default) + -- FIXME NOT IMPLEMENTED FULLY + -- Likewise, not need in many styles, so we headed towards a shortcut: -- Minimal support for "contextual" and "always" for Chicago style. + -- delimiter-precedes-last ("contextual" by default) local sep_delim if delimiter_precedes_last == "always" then sep_delim = name_delimiter @@ -1013,8 +1069,10 @@ function CslEngine:_names (options, content, entry) local and_opt = name_node.options["and"] or "text" local and_word = and_opt == "symbol" and "&" or self:_render_term("and") -- text by default local name_delimiter = name_node.options.delimiter or inherited_opts["names-delimiter"] or ", " - -- local delimiter_precedes_et_al = name_node.options["delimiter-precedes-et-al"] -- TODO NOT IMPLEMENTED - local delimiter_precedes_last = name_node.options["delimiter-precedes-last"] or inherited_opts["delimiter-precedes-last"] or "contextual" + -- local delimiter_precedes_et_al = name_node.options["delimiter-precedes-et-al"] -- FIXME NOT IMPLEMENTED + local delimiter_precedes_last = name_node.options["delimiter-precedes-last"] + or inherited_opts["delimiter-precedes-last"] + or "contextual" if name_delimiter and not self.cache[name_delimiter] then name_delimiter = self:_xmlEscape(name_delimiter) @@ -1036,7 +1094,11 @@ function CslEngine:_names (options, content, entry) } resolved = pl.tablex.union(options, resolved) - return self:_names_with_resolved_opts(resolved, substitute, entry) + local rendered = self:_names_with_resolved_opts(resolved, substitute, entry) + if rendered and not self.hasRenderedNames then + self.hasRenderedNames = true + end + return rendered end function CslEngine:_label (options, content, entry) @@ -1113,7 +1175,7 @@ function CslEngine:_if (options, content, entry) end if options["is-numeric"] then for _, var in ipairs(pl.stringx.split(options["is-numeric"], " ")) do - -- TODO FIXME NOT IMPLEMENTED FULLY + -- FIXME NOT IMPLEMENTED FULLY -- Content is considered numeric if it solely consists of numbers. -- Numbers may have prefixes and suffixes (“D2”, “2b”, “L2d”), and may -- be separated by a comma, hyphen, or ampersand, with or without @@ -1136,10 +1198,10 @@ function CslEngine:_if (options, content, entry) table.insert(conds, cond) end end - -- FIXME TODO other conditions: position, disambiguate + -- FIXME NOT IMPLEMENTED other conditions: "position", "disambiguate" for _, v in ipairs({ "position", "disambiguate" }) do if options[v] then - SU.warn("CSL if condition " .. v .. " not implemented yet") + SU.warn("CSL if condition '" .. v .. "' not implemented yet") table.insert(conds, false) end end @@ -1198,7 +1260,7 @@ end function CslEngine:_key (options, content, entry) -- Attribute 'sort' is managed at a higher level - -- NOT IMPLEMENTED: + -- FIXME NOT IMPLEMENTED: -- Attributes 'names-min', 'names-use-first', and 'names-use-last' -- (overrides for the 'et-al-xxx' attributes) if options.macro then @@ -1222,8 +1284,10 @@ function CslEngine:_key (options, content, entry) 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. + -- FIXME NOT IMPLEMENTED + -- Names need a special rule here. + -- Many styles (e.g. Chicago) use a macro here (for substitutes, etc.) + -- so this case is not yet implemented. SU.error("CSL variable not yet usable for sorting: " .. options.variable) end return value diff --git a/packages/bibtex/init.lua b/packages/bibtex/init.lua index c8f557a43..f46f13d56 100644 --- a/packages/bibtex/init.lua +++ b/packages/bibtex/init.lua @@ -408,7 +408,7 @@ function package:registerCommands () -- NEW CSL IMPLEMENTATION - -- Internal commands for CSL processing + -- Hooks for CSL processing self:registerCommand("bibSmallCaps", function (_, content) -- To avoid attributes in the CSL-processed content @@ -462,6 +462,14 @@ function package:registerCommands () SILE.call("bibLink", { src = link }, content) end) + self:registerCommand("bibRule", function (_, content) + local n = content[1] and tonumber(content[1]) or 3 + local width = n .. "em" + SILE.call("raise", { height = "0.4ex" }, function () + SILE.call("hrule", { height = "0.4pt", width = width }) + end) + end) + -- Style and locale loading self:registerCommand("bibliographystyle", function (options, _)