--
-- Copyright (c) 2021-2022 Zeping Lee
-- Released under the MIT license.
-- Repository: https://github.com/zepinglee/citeproc-lua
--
local style_module = {}
local dom = require("luaxml-domobject")
local Element = require("citeproc-element").Element
local IrNode = require("citeproc-ir-node").IrNode
local Rendered = require("citeproc-ir-node").Rendered
local SeqIr = require("citeproc-ir-node").SeqIr
local PlainText = require("citeproc-output").PlainText
local DisamStringFormat = require("citeproc-output").DisamStringFormat
local util = require("citeproc-util")
local Style = Element:derive("style")
function Style:new()
local o = {
children = {},
macros = {},
locales = {},
initialize_with_hyphen = true,
demote_non_dropping_particle = "display-and-sort",
}
setmetatable(o, self)
self.__index = self
return o
end
function Style:parse(xml_str)
local csl_xml = dom.parse(xml_str)
if not csl_xml then
error("Failed to parse CSL style.")
end
local style_node = csl_xml:get_path("style")[1]
if not csl_xml then
error('Element "style" not found.')
end
return Style:from_node(style_node)
end
local function make_name_inheritance(name, node)
name:set_attribute(node, "and")
name:set_attribute(node, "delimiter-precedes-et-al")
name:set_attribute(node, "delimiter-precedes-last")
name:set_number_attribute(node, "et-al-min")
name:set_number_attribute(node, "et-al-use-first")
name:set_number_attribute(node, "et-al-subsequent-min")
name:set_number_attribute(node, "et-al-subsequent-use-first")
name:set_bool_attribute(node, "et-al-use-last")
name:set_bool_attribute(node, "initialize")
name:set_attribute(node, "initialize-with")
name:set_attribute(node, "name-as-sort-order")
name:set_attribute(node, "sort-separator")
local delimiter = node:get_attribute("name-delimiter")
if delimiter then
name.delimiter = delimiter
end
local form = node:get_attribute("name-form")
if form then
name.form = form
end
local names_delimiter = node:get_attribute("names-delimiter")
if names_delimiter then
name.names_delimiter = names_delimiter
end
end
function Style:from_node(node)
local o = Style:new()
o:set_attribute(node, "class")
o:set_attribute(node, "default-locale")
o:set_attribute(node, "version")
-- Global Options
o.initialize_with_hyphen = true
o:set_bool_attribute(node, "initialize-with-hyphen")
o:set_attribute(node, "page-range-format")
o:set_attribute(node, "demote-non-dropping-particle")
-- Inheritable Name Options
-- https://docs.citationstyles.org/en/stable/specification.html#inheritable-name-options
o.name_inheritance = require("citeproc-node-names").Name:new()
make_name_inheritance(o.name_inheritance, node)
if o.page_range_format == "chicago" then
if o.version < "1.1" then
o.page_range_format = "chicago-15"
else
o.page_range_format = "chicago-16"
end
end
o.macros = {}
o.locales = {}
o.children = {}
o:process_children_nodes(node)
for _, child in ipairs(o.children) do
local element_name = child.element_name
if element_name == "info" then
o.info = child
elseif element_name == "citation" then
o.citation = child
elseif element_name == "bibliography" then
o.bibliography = child
elseif element_name == "macro" then
o.macros[child.name] = child
elseif element_name == "locale" then
local xml_lang = child.xml_lang or "@generic"
o.locales[xml_lang] = child
end
end
return o
end
Style._default_options = {
["initialize-with-hyphen"] = true,
["page-range-format"] = nil,
["demote-non-dropping-particle"] = "display-and-sort",
}
function Style:set_lang(lang, force_lang)
local default_locale = self:get_attribute("default-locale")
if lang then
if default_locale and not force_lang then
self.lang = default_locale
end
else
self.lang = default_locale or "en-US"
end
end
local Info = Element:derive("info")
function Info:from_node(node)
local o = Info:new()
-- o.authors = nil
-- o.contributors = nil
o.categories = {}
o.id = nil
-- o.issn = nil
-- o.eissn = nil
-- o.issnl = nil
o.links = {
independent_parent = nil,
}
-- o.published = nil
-- o.rights = nil
-- o.summary = nil
o.title = nil
-- o.title_short = nil
o.updated = nil
for _, child in ipairs(node:get_children()) do
if child:is_element() then
local element_name = child:get_element_name()
if element_name == "category" then
local citation_format = child:get_attribute("citation-format")
if citation_format then
o.categories.citation_format = citation_format
end
elseif element_name == "id" then
o.id = child:get_text()
elseif element_name == "link" then
local href = child:get_attribute("href")
local rel = child:get_attribute("rel")
if href and rel == "independent-parent" then
o.links.independent_parent = href
end
elseif element_name == "title" then
o.title = child:get_text()
elseif element_name == "updated" then
o.updated = child:get_text()
end
end
end
return o
end
local Citation = Element:derive("citation", {
givenname_disambiguation_rule = "by-cite",
-- https://github.com/citation-style-language/schema/issues/338
-- The cite_group_delimiter may be changed to inherit the delimiter in citaion > layout.
cite_group_delimiter = ", ",
near_note_distance = 5,
})
function Citation:from_node(node, style)
local o = Citation:new()
o.children = {}
o:process_children_nodes(node)
-- o.layouts = nil -- CSL-M extension
for _, child in ipairs(o.children) do
local element_name = child.element_name
if element_name == "layout" then
o.layout = child
elseif element_name == "sort" then
o.sort = child
end
end
-- Disambiguation
o:set_bool_attribute(node, "disambiguate-add-givenname")
o:set_attribute(node, "givenname-disambiguation-rule")
o:set_bool_attribute(node, "disambiguate-add-names")
o:set_bool_attribute(node, "disambiguate-add-year-suffix")
-- Cite Grouping
o:set_attribute(node, "cite-group-delimiter")
-- In the current citeproc-js implementation and test suite,
-- cite grouping is activated by setting the cite-group-delimiter
-- attribute or the collapse attributes on cs:citation.
-- It may be changed to an independent procedure.
-- https://github.com/citation-style-language/schema/issues/338
if node:get_attribute("cite-group-delimiter") then
o.cite_grouping = true
else
o.cite_grouping = false
end
-- Cite Collapsing
o:set_attribute(node, "collapse")
o:set_attribute(node, "year-suffix-delimiter")
if not o.year_suffix_delimiter then
o.year_suffix_delimiter = o.layout.delimiter
end
o:set_attribute(node, "after-collapse-delimiter")
if not o.after_collapse_delimiter then
o.after_collapse_delimiter = o.layout.delimiter
end
-- Note Distance
o:set_number_attribute(node, "near-note-distance")
local name_inheritance = require("citeproc-node-names").Name:new()
for key, value in pairs(style.name_inheritance) do
if value ~= nil then
name_inheritance[key] = value
end
end
make_name_inheritance(name_inheritance, node)
o.name_inheritance = name_inheritance
-- update_mode = "plain" or "numeric" or "position" (or "both"?)
return o
end
function Citation:build_ir(engine, state, context)
if not self.layout then
util.error("Missing citation layout.")
end
return self.layout:build_ir(engine, state, context)
end
local Bibliography = Element:derive("bibliography", {
hanging_indent = false,
line_spacing = 1,
entry_spacing = 1,
subsequent_author_substitute_rule = "complete-all"
})
function Bibliography:from_node(node, style)
local o = Bibliography:new()
o.children = {}
o:process_children_nodes(node)
-- o.layouts = nil -- CSL-M extension
for _, child in ipairs(o.children) do
local element_name = child.element_name
if element_name == "layout" then
o.layout = child
elseif element_name == "sort" then
o.sort = child
end
end
-- Whitespace
o:set_bool_attribute(node, "hanging-indent")
o:set_attribute(node, "second-field-align")
o:set_number_attribute(node, "line-spacing")
o:set_number_attribute(node, "entry-spacing")
-- Reference Grouping
o:set_attribute(node, "subsequent-author-substitute")
o:set_attribute(node, "subsequent-author-substitute-rule")
local name_inheritance = require("citeproc-node-names").Name:new()
for key, value in pairs(style.name_inheritance) do
if value ~= nil then
name_inheritance[key] = value
end
end
make_name_inheritance(name_inheritance, node)
o.name_inheritance = name_inheritance
return o
end
function Bibliography:build_ir(engine, state, context)
if not self.layout then
util.error("Missing bibliography layout.")
end
local ir = self.layout:build_ir(engine, state, context)
-- util.debug(ir)
if self.second_field_align == "flush" and #ir.children >= 2 then
ir.children[1].display = "left-margin"
local right_inline_ir = SeqIr:new(util.slice(ir.children, 2), self)
right_inline_ir.display = "right-inline"
if ir.affixes then
right_inline_ir.affixes = ir.affixes
right_inline_ir.formatting = ir.formatting
ir.affixes = nil
ir.formatting = nil
end
ir.children = {ir.children[1], right_inline_ir}
end
if self.subsequent_author_substitute then
self:substitute_subsequent_authors(engine, ir)
end
if not ir then
ir = Rendered:new(PlainText:new("[CSL STYLE ERROR: reference with no printed form.]"), self)
end
return ir
end
function Bibliography:substitute_subsequent_authors(engine, ir)
ir.first_name_ir = self:find_first_name_ir(ir) -- should be a SeqIr wiht _element = "names"
if not ir.first_name_ir then
engine.previous_bib_names_ir = nil
return
end
if self.subsequent_author_substitute_rule == "complete-all" then
self:substitute_subsequent_authors_complete_all(engine, ir)
elseif self.subsequent_author_substitute_rule == "complete-each" then
self:substitute_subsequent_authors_complete_each(engine, ir)
elseif self.subsequent_author_substitute_rule == "partial-each" then
self:substitute_subsequent_authors_partial_each(engine, ir)
elseif self.subsequent_author_substitute_rule == "partial-first" then
self:substitute_subsequent_authors_partial_first(engine, ir)
end
engine.previous_bib_names_ir = ir.first_name_ir
end
function Bibliography:find_first_name_ir(ir)
if ir._type == "NameIr" then
return ir
elseif ir.children then
for _, child_ir in ipairs(ir.children) do
local first_name_ir = self:find_first_name_ir(child_ir)
if first_name_ir then
return first_name_ir
end
end
end
return nil
end
function Bibliography:substitute_subsequent_authors_complete_all(engine, ir)
local bib_names_str = ""
if #ir.first_name_ir.person_name_irs > 0 then
for _, person_name_ir in ipairs(ir.first_name_ir.person_name_irs) do
if bib_names_str ~= "" then
bib_names_str = bib_names_str .. " "
end
local name_variants = person_name_ir.disam_variants
bib_names_str = bib_names_str .. name_variants[#name_variants]
end
else
-- In case of a in
local disam_format = DisamStringFormat:new()
local inlines = ir.first_name_ir:flatten(disam_format)
bib_names_str = disam_format:output(inlines)
end
ir.first_name_ir.bib_names_str = bib_names_str
if engine.previous_bib_names_ir and
engine.previous_bib_names_ir.bib_names_str == bib_names_str then
local text = self.subsequent_author_substitute
if text == "" then
ir.first_name_ir.children = {}
ir.first_name_ir.group_var = "missing"
else
-- the output of label is not substituted
-- util.debug(ir.first_name_ir)
ir.first_name_ir.children = {Rendered:new({PlainText:new(text)}, self)}
end
end
end
function Bibliography:substitute_subsequent_authors_complete_each(engine, ir)
local bib_names_str = ""
if #ir.first_name_ir.person_name_irs > 0 then
for _, person_name_ir in ipairs(ir.first_name_ir.person_name_irs) do
if bib_names_str ~= "" then
bib_names_str = bib_names_str .. " "
end
local name_variants = person_name_ir.disam_variants
bib_names_str = bib_names_str .. name_variants[#name_variants]
end
else
-- In case of a in
local disam_format = DisamStringFormat:new()
local inlines = ir.first_name_ir:flatten(disam_format)
bib_names_str = disam_format:output(inlines)
end
ir.first_name_ir.bib_names_str = bib_names_str
if engine.previous_bib_names_ir and
engine.previous_bib_names_ir.bib_names_str == bib_names_str then
local text = self.subsequent_author_substitute
if #ir.first_name_ir.person_name_irs > 0 then
for _, person_name_ir in ipairs(ir.first_name_ir.person_name_irs) do
person_name_ir.inlines = {PlainText:new(text)}
end
else
-- In case of a in
if text == "" then
ir.first_name_ir.children = {}
ir.first_name_ir.group_var = "missing"
else
ir.first_name_ir.children = {Rendered:new({PlainText:new(text)}, self)}
end
end
end
end
function Bibliography:substitute_subsequent_authors_partial_each(engine, ir)
local bib_names_str = ""
if #ir.first_name_ir.person_name_irs > 0 then
if engine.previous_bib_names_ir then
for i, person_name_ir in ipairs(ir.first_name_ir.person_name_irs) do
local prev_name_ir = engine.previous_bib_names_ir.person_names[i]
if prev_name_ir then
local prev_name_variants = prev_name_ir.disam_variants
local prev_full_name_str = prev_name_variants[#prev_name_variants]
local name_variants = person_name_ir.disam_variants
local full_name_str = name_variants[#name_variants]
if prev_full_name_str == full_name_str then
local text = self.subsequent_author_substitute
person_name_ir.inlines = {PlainText:new(text)}
else
break
end
end
end
end
else
-- In case of a in
local disam_format = DisamStringFormat:new()
local inlines = ir.first_name_ir:flatten(disam_format)
bib_names_str = disam_format:output(inlines)
ir.first_name_ir.bib_names_str = bib_names_str
if engine.previous_bib_names_ir and
engine.previous_bib_names_ir.bib_names_str == bib_names_str then
local text = self.subsequent_author_substitute
if text == "" then
ir.first_name_ir.children = {}
ir.first_name_ir.group_var = "missing"
else
ir.first_name_ir.children = {Rendered:new({PlainText:new(text)}, self)}
end
end
end
end
function Bibliography:substitute_subsequent_authors_partial_first(engine, ir)
end
local Macro = Element:derive("macro")
function Macro:from_node(node)
local o = Macro:new()
o.children = {}
o:set_attribute(node, "name")
o:process_children_nodes(node)
return o
end
function Macro:build_ir(engine, state, context)
local ir = self:build_group_ir(engine, state, context)
return ir
end
style_module.Style = Style
style_module.Citation = Citation
style_module.Bibliography = Bibliography
return style_module