diff options
Diffstat (limited to 'biblio/citation-style-language/citeproc-node-style.lua')
-rw-r--r-- | biblio/citation-style-language/citeproc-node-style.lua | 573 |
1 files changed, 449 insertions, 124 deletions
diff --git a/biblio/citation-style-language/citeproc-node-style.lua b/biblio/citation-style-language/citeproc-node-style.lua index 2fb10cc79c..5c387bd6c8 100644 --- a/biblio/citation-style-language/citeproc-node-style.lua +++ b/biblio/citation-style-language/citeproc-node-style.lua @@ -4,15 +4,126 @@ -- Repository: https://github.com/zepinglee/citeproc-lua -- -local style = {} +local style_module = {} -local element = require("citeproc-element") +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.Element:new() +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 = { +Style._default_options = { ["initialize-with-hyphen"] = true, ["page-range-format"] = nil, ["demote-non-dropping-particle"] = "display-and-sort", @@ -29,171 +140,385 @@ function Style:set_lang(lang, force_lang) end end -function Style:render_citation (items, context) - self:debug_info(context) - context = self:process_context(context) - context.style = self - local citation = self:get_child("citation") - return citation:render(items, context) -end -function Style:render_biblography (items, context) - self:debug_info(context) - context = self:process_context(context) - context.style = self - local bibliography = self:get_child("bibliography") - return bibliography:render(items, context) -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 -function Style:get_version () - return self:get_attribute("version") + return o end -function Style:get_locales() - if not self.locale_dict then - self.locale_dict = {} - end - local locales = self.locale_dict[self.lang] - if not locales then - locales = self:get_locale_list(self.lang) - self.locale_dict[self.lang] = locales - end - return locales -end -function Style:get_locale_list (lang) - assert(lang ~= nil) - local language = string.sub(lang, 1, 2) - local primary_dialect = util.primary_dialects[language] - if not primary_dialect then - -- util.warning(string.format("Failed to find primary dialect of \"%s\"", language)) - end - local locale_list = {} +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, +}) - -- 1. In-style cs:locale elements - -- i. `xml:lang` set to chosen dialect, “de-AT” - if lang == language then - lang = primary_dialect - end - table.insert(locale_list, self:get_in_style_locale(lang)) +function Citation:from_node(node, style) - -- ii. `xml:lang` set to matching language, “de” (German) - if language and language ~= lang then - table.insert(locale_list, self:get_in_style_locale(language)) - end + local o = Citation:new() + o.children = {} - -- iii. `xml:lang` not set - table.insert(locale_list, self:get_in_style_locale(nil)) + o:process_children_nodes(node) - -- 2. Locale files - -- iv. `xml:lang` set to chosen dialect, “de-AT” - if lang then - table.insert(locale_list, self:get_engine():get_system_locale(lang)) + -- 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 - -- v. `xml:lang` set to matching primary dialect, “de-DE” (Standard German) - -- (only applicable when the chosen locale is a secondary dialect) - if primary_dialect and primary_dialect ~= lang then - table.insert(locale_list, self:get_engine():get_system_locale(primary_dialect)) + -- 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 - -- vi. `xml:lang` set to “en-US” (American English) - if lang ~= "en-US" and primary_dialect ~= "en-US" then - table.insert(locale_list, self:get_engine():get_system_locale("en-US")) + + -- 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 - return locale_list -end + -- Note Distance + o:set_number_attribute(node, "near-note-distance") -function Style:get_in_style_locale (lang) - for _, locale in ipairs(self:query_selector("locale")) do - if locale:get_attribute("xml:lang") == lang then - return locale + 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 - return nil + make_name_inheritance(name_inheritance, node) + o.name_inheritance = name_inheritance + + -- update_mode = "plain" or "numeric" or "position" (or "both"?) + + return o end -function Style:get_term (...) - for _, locale in ipairs(self:get_locales()) do - local res = locale:get_term(...) - if res then - return res - end +function Citation:build_ir(engine, state, context) + if not self.layout then + util.error("Missing citation layout.") end - return nil + return self.layout:build_ir(engine, state, context) end -local Citation = element.Element:new() +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 = {} -function Citation:render (items, context) - self:debug_info(context) - context = self:process_context(context) + o:process_children_nodes(node) - context.mode = "citation" - context.citation = self + -- o.layouts = nil -- CSL-M extension - local sort = self:get_child("sort") - if sort then - sort:sort(items, context) + 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 - local layout = self:get_child("layout") - return layout:render(items, context) + -- 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 -local Bibliography = element.Element:new() + if self.subsequent_author_substitute then + self:substitute_subsequent_authors(engine, ir) + end -Bibliography.default_options = { - ["hanging-indent"] = false, - ["second-field-align"] = nil, - ["line-spacing"] = 1, - ["entry-spacing"] = 1, - ["subsequent-author-substitute"] = nil, - ["subsequent-author-substitute-rule"] = "complete-all", -} + if not ir then + ir = Rendered:new(PlainText:new("[CSL STYLE ERROR: reference with no printed form.]"), self) + end + return ir +end -function Bibliography:render (items, context) - self:debug_info(context) - context = self:process_context(context) - -- util.debug(context) +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 - context.mode = "bibliography" - context.bibliography = self +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 - -- Already sorted in CiteProc:sort_bibliography() +function Bibliography:substitute_subsequent_authors_complete_all(engine, ir) + local bib_names_str = "" - local layout = self:get_child("layout") - local res = layout:render(items, context) + 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 <text variable="title"/> in <substitute> + 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 - local params = res[1] +function Bibliography:substitute_subsequent_authors_complete_each(engine, ir) + local bib_names_str = "" - params.entryspacing = context.options["entry-spacing"] - params.linespacing = context.options["line-spacing"] - params.hangingindent = context.options["hanging-indent"] - params["second-field-align"] = context.options["second-field-align"] - for _, key in ipairs({"bibstart", "bibend"}) do - local value = context.engine.formatter[key] - if type(value) == "function" then - value = value(context) + 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 - params[key] = value + else + -- In case of a <text variable="title"/> in <substitute> + 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 <text variable="title"/> in <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 - params.bibliography_errors = {} - params.entry_ids = {} - for _, item in ipairs(items) do - table.insert(params.entry_ids, item.id) +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 <text variable="title"/> in <substitute> + 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") - return res +function Macro:from_node(node) + local o = Macro:new() + o.children = {} + o:set_attribute(node, "name") + o:process_children_nodes(node) + return o end -style.Style = Style -style.Citation = Citation -style.Bibliography = Bibliography +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 +return style_module |