summaryrefslogtreecommitdiff
path: root/biblio/citation-style-language/citeproc-node-citation.lua
diff options
context:
space:
mode:
Diffstat (limited to 'biblio/citation-style-language/citeproc-node-citation.lua')
-rw-r--r--biblio/citation-style-language/citeproc-node-citation.lua1417
1 files changed, 1417 insertions, 0 deletions
diff --git a/biblio/citation-style-language/citeproc-node-citation.lua b/biblio/citation-style-language/citeproc-node-citation.lua
new file mode 100644
index 0000000000..6c7f082746
--- /dev/null
+++ b/biblio/citation-style-language/citeproc-node-citation.lua
@@ -0,0 +1,1417 @@
+
+--
+-- Copyright (c) 2021-2022 Zeping Lee
+-- Released under the MIT license.
+-- Repository: https://github.com/zepinglee/citeproc-lua
+--
+
+local citation_module = {}
+
+local dom = require("luaxml-domobject")
+
+local Context = require("citeproc-context").Context
+local IrState = require("citeproc-context").IrState
+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 YearSuffix = require("citeproc-ir-node").YearSuffix
+local Micro = require("citeproc-output").Micro
+local Formatted = require("citeproc-output").Formatted
+local PlainText = require("citeproc-output").PlainText
+local InlineElement = require("citeproc-output").InlineElement
+local DisamStringFormat = require("citeproc-output").DisamStringFormat
+local SortStringFormat = require("citeproc-output").SortStringFormat
+local util = require("citeproc-util")
+
+
+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 = self: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
+ Element.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_citation_str(citation, engine)
+ if engine.registry.requires_sorting then
+ engine:sort_bibliography()
+ end
+
+ local citation_str = self:build_cluster(citation.citationItems, engine, citation.properties)
+ return citation_str
+end
+
+-- Formatting is stripped from the author-only and composite renderings
+-- of the author name
+local function remove_name_formatting(ir)
+ if ir._element == "name" then
+ ir.formatting = nil
+ end
+ if ir.children then
+ for _, child in ipairs(ir.children) do
+ remove_name_formatting(child)
+ end
+ end
+end
+
+function Citation:build_cluster(citation_items, engine, properties)
+ properties = properties or {}
+ local output_format = engine.output_format
+ local irs = {}
+ citation_items = self:sorted_citation_items(citation_items, engine)
+ for _, cite_item in ipairs(citation_items) do
+ local ir = self:build_fully_disambiguated_ir(cite_item, output_format, engine, properties)
+ table.insert(irs, ir)
+ end
+
+ -- Special citation forms
+ -- https://citeproc-js.readthedocs.io/en/latest/running.html#special-citation-forms
+ self:_apply_special_citation_form(irs, properties, output_format, engine)
+
+ if self.cite_grouping then
+ irs = self:group_cites(irs)
+ else
+ local citation_collapse = self.collapse
+ if citation_collapse == "year" or citation_collapse == "year-suffix" or
+ citation_collapse == "year-suffix-ranged" then
+ irs = self:group_cites(irs)
+ end
+ end
+
+ if self.collapse then
+ self:collapse_cites(irs)
+ end
+
+ -- Capitalize first
+ for i, ir in ipairs(irs) do
+ -- local layout_prefix
+ -- local layout_affixes = self.layout.affixes
+ -- if layout_affixes then
+ -- layout_prefix = layout_affixes.prefix
+ -- end
+ local prefix = citation_items[i].prefix
+ if prefix then
+ -- Prefix is inlines
+ local right_most_str = prefix[#prefix]:get_right_most_string()
+ if string.match(right_most_str, "[.!?]%s*$") and InlineElement.has_space(prefix) then
+ ir:capitalize_first_term()
+ end
+ else
+ local delimiter = self.layout.delimiter
+ if i == 1 or not delimiter or string.match(delimiter, "[.!?]%s*$") then
+ ir:capitalize_first_term()
+ end
+ end
+ end
+
+ -- util.debug(irs)
+
+ local citation_delimiter = self.layout.delimiter
+ local citation_stream = {}
+
+ local context = Context:new()
+ context.engine = engine
+ context.style = engine.style
+ context.area = self
+ context.in_bibliography = false
+ context.locale = engine:get_locale(engine.lang)
+ context.name_inheritance = self.name_inheritance
+ context.format = output_format
+
+ local previous_ir
+ for i, ir in ipairs(irs) do
+ local cite_prefix = citation_items[i].prefix
+ local cite_suffix = citation_items[i].suffix
+ if not ir.collapse_suppressed then
+ local ir_inlines = ir:flatten(output_format)
+ if #ir_inlines > 0 then
+ -- Make sure ir_inlines has outputs contents.
+ -- collapse_AuthorCollapseNoDateSorted.txt
+ if previous_ir then
+ if previous_ir.own_delimiter then
+ table.insert(citation_stream, PlainText:new(previous_ir.own_delimiter))
+ else
+ local left_most_str
+ if cite_prefix then
+ left_most_str = cite_prefix[1]:get_left_most_string()
+ end
+ if citation_delimiter and not (cite_prefix and util.startswith(left_most_str, ",")) then
+ table.insert(citation_stream, PlainText:new(citation_delimiter))
+ end
+ end
+ end
+
+ if cite_prefix then
+ table.insert(citation_stream, Micro:new(cite_prefix))
+ end
+
+ -- util.debug(ir)
+ util.extend(citation_stream, ir_inlines)
+ previous_ir = ir
+
+ if cite_suffix then
+ table.insert(citation_stream, Micro:new(cite_suffix))
+ end
+ end
+ end
+ end
+
+ local has_printed_form = true
+ if #citation_items == 0 then
+ -- bugreports_AuthorOnlyFail.txt
+ citation_stream = {PlainText:new("[NO_PRINTED_FORM]")}
+ has_printed_form = false
+ elseif #citation_stream == 0 then
+ -- date_DateNoDateNoTest.txt
+ has_printed_form = false
+ citation_stream = {PlainText:new("[CSL STYLE ERROR: reference with no printed form.]")}
+ elseif #citation_stream == 1 and citation_stream[1].value == "[NO_PRINTED_FORM]" then
+ has_printed_form = false
+ end
+
+ local author_only_mode = (properties.mode == "author-only" or
+ (#citation_items >= 1 and citation_items[1]["author-only"]))
+ if has_printed_form and context.area.layout.affixes and not author_only_mode then
+ local affixes = context.area.layout.affixes
+ if affixes.prefix then
+ table.insert(citation_stream, 1, PlainText:new(affixes.prefix))
+ end
+ if affixes.suffix then
+ table.insert(citation_stream, PlainText:new(affixes.suffix))
+ end
+ end
+
+ if has_printed_form and context.area.layout.formatting then
+ citation_stream = {Formatted:new(citation_stream, context.area.layout.formatting)}
+ end
+
+ if properties.mode == "composite" then
+ local author_ir
+ if irs[1] then
+ author_ir = irs[1].author_ir
+ end
+ if author_ir then
+ local infix = properties.infix
+ if infix then
+ if string.match(infix, "^%w") then
+ -- discretionary_SingleNarrativeCitation.txt
+ infix = " " .. infix
+ end
+ if string.match(infix, "%w$") then
+ infix = infix .. " "
+ end
+ if infix == "" then
+ -- discretionary_AuthorOnlySuppressLocator.txt
+ infix = " "
+ end
+ for i, inline in ipairs(InlineElement:parse(infix, context)) do
+ table.insert(citation_stream, i, inline)
+ end
+ else
+ table.insert(citation_stream, 1, PlainText:new(" "))
+ end
+
+ local author_inlines = author_ir:flatten(output_format)
+ for i, inline in ipairs(author_inlines) do
+ table.insert(citation_stream, i, inline)
+ end
+ end
+ end
+
+ local str = output_format:output(citation_stream, context)
+ str = util.strip(str)
+
+ return str
+end
+
+function Citation:sorted_citation_items(items, engine)
+ local citation_sort = self.sort
+ if not citation_sort then
+ return items
+ end
+
+ local state = IrState:new()
+ local context = Context:new()
+ context.engine = engine
+ context.style = engine.style
+ context.area = self
+ context.in_bibliography = false
+ context.locale = engine:get_locale(engine.lang)
+ context.name_inheritance = self.name_inheritance
+ context.format = SortStringFormat:new()
+ -- context.id = id
+ context.cite = nil
+ -- context.reference = self:get_item(id)
+
+ items = citation_sort:sort(items, state, context)
+ return items
+end
+
+function Citation:build_fully_disambiguated_ir(cite_item, output_format, engine, properties)
+ local cite_ir = self:build_ambiguous_ir(cite_item, output_format, engine)
+ -- util.debug(cite_ir)
+ cite_ir = self:apply_disambiguate_add_givenname(cite_ir, engine)
+ cite_ir = self:apply_disambiguate_add_names(cite_ir, engine)
+ cite_ir = self:apply_disambiguate_conditionals(cite_ir, engine)
+ cite_ir = self:apply_disambiguate_add_year_suffix(cite_ir, engine)
+
+ return cite_ir
+end
+
+function Citation:build_ambiguous_ir(cite_item, output_format, engine)
+ local state = IrState:new(engine.style)
+ local context = Context:new()
+ context.engine = engine
+ context.style = engine.style
+ context.area = self
+ context.locale = engine:get_locale(engine.lang)
+ context.name_inheritance = self.name_inheritance
+ context.format = output_format
+ context.id = cite_item.id
+ context.cite = cite_item
+ -- context.reference = self:get_item(cite_item.id)
+ context.reference = engine.registry.registry[cite_item.id]
+
+ local ir
+ if context.reference then
+ ir = self:build_ir(engine, state, context)
+ else
+ ir = Rendered:new({Formatted:new({PlainText:new(cite_item.id)}, {["font-weight"] = "bold"})}, self)
+ end
+
+ ir.cite_item = cite_item
+ ir.reference = context.reference
+ ir.ir_index = #engine.disam_irs + 1
+ table.insert(engine.disam_irs, ir)
+ ir.is_ambiguous = false
+ ir.disam_level = 0
+
+ -- Formattings like font-style are ignored for disambiguation.
+ local disam_format = DisamStringFormat:new()
+ local inlines = ir:flatten(disam_format)
+ local disam_str = disam_format:output(inlines, context)
+ ir.disam_str = disam_str
+
+ if not engine.cite_irs_by_output[disam_str] then
+ engine.cite_irs_by_output[disam_str] = {}
+ end
+
+ for ir_index, ir_ in pairs(engine.cite_irs_by_output[disam_str]) do
+ if ir_.cite_item.id ~= cite_item.id then
+ ir.is_ambiguous = true
+ break
+ end
+ end
+ engine.cite_irs_by_output[disam_str][ir.ir_index] = ir
+
+ return ir
+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
+
+function Citation:apply_disambiguate_add_givenname(cite_ir, engine)
+ if self.disambiguate_add_givenname then
+ -- util.debug("disambiguate_add_givenname: " .. cite_ir.cite_item.id)
+
+ local gn_disam_rule = self.givenname_disambiguation_rule
+ if gn_disam_rule == "all-names" or gn_disam_rule == "all-names-with-initials" then
+ cite_ir = self:apply_disambiguate_add_givenname_all_names(cite_ir, engine)
+ elseif gn_disam_rule == "primary-name" or gn_disam_rule == "primary-name-with-initials" then
+ cite_ir = self:apply_disambiguate_add_givenname_primary_name(cite_ir, engine)
+ elseif gn_disam_rule == "by-cite" then
+ cite_ir = self:apply_disambiguate_add_givenname_by_cite(cite_ir, engine)
+ end
+ end
+ return cite_ir
+end
+
+-- TODO: reorganize this code
+function Citation:apply_disambiguate_add_givenname_all_names(cite_ir, engine)
+ -- util.debug("disambiguate_add_givenname_all_names: " .. cite_ir.cite_item.id)
+ if not cite_ir.person_name_irs or #cite_ir.person_name_irs == 0 then
+ return cite_ir
+ end
+
+ -- util.debug(cite_ir.disam_str)
+
+ for _, person_name_ir in ipairs(cite_ir.person_name_irs) do
+ local name_output = person_name_ir.name_output
+ -- util.debug(name_output)
+
+ if not person_name_ir.person_name_index then
+ person_name_ir.person_name_index = #engine.person_names + 1
+ table.insert(engine.person_names, person_name_ir)
+ end
+
+ if not engine.person_names_by_output[name_output] then
+ engine.person_names_by_output[name_output] = {}
+ end
+ engine.person_names_by_output[name_output][person_name_ir.person_name_index] = person_name_ir
+
+ local ambiguous_name_irs = {}
+ local ambiguous_same_output_irs = {}
+
+ for _, pn_ir in pairs(engine.person_names_by_output[person_name_ir.name_output]) do
+ if pn_ir.full_name ~= person_name_ir.full_name then
+ table.insert(ambiguous_name_irs, pn_ir)
+ end
+ if pn_ir.name_output == person_name_ir.name_output then
+ table.insert(ambiguous_same_output_irs, pn_ir)
+ end
+ end
+
+ -- util.debug(person_name_ir.name_output)
+ -- util.debug(person_name_ir.full_name)
+ -- util.debug(#ambiguous_name_irs)
+ -- util.debug(person_name_ir.disam_variants_index)
+ -- util.debug(person_name_ir.disam_variants)
+
+ while person_name_ir.disam_variants_index < #person_name_ir.disam_variants do
+ if #ambiguous_name_irs == 0 then
+ break
+ end
+
+ for _, pn_ir in ipairs(ambiguous_same_output_irs) do
+ -- expand one name
+ if pn_ir.disam_variants_index < #pn_ir.disam_variants then
+ pn_ir.disam_variants_index = pn_ir.disam_variants_index + 1
+ pn_ir.name_output = pn_ir.disam_variants[pn_ir.disam_variants_index]
+ pn_ir.inlines = pn_ir.disam_inlines[pn_ir.name_output]
+
+ if not engine.person_names_by_output[pn_ir.name_output] then
+ engine.person_names_by_output[pn_ir.name_output] = {}
+ end
+ engine.person_names_by_output[pn_ir.name_output][pn_ir.person_name_index] = pn_ir
+ end
+ end
+
+ -- util.debug(person_name_ir.name_output)
+
+ -- update ambiguous_name_irs and ambiguous_same_output_irs
+ ambiguous_name_irs = {}
+ ambiguous_same_output_irs = {}
+ for _, pn_ir in pairs(engine.person_names_by_output[person_name_ir.name_output]) do
+ if pn_ir.full_name ~= person_name_ir.full_name then
+ -- util.debug(pn_ir.full_name .. ": " .. pn_ir.name_output)
+ table.insert(ambiguous_name_irs, pn_ir)
+ end
+ if pn_ir.name_output == person_name_ir.name_output then
+ table.insert(ambiguous_same_output_irs, pn_ir)
+ end
+ end
+
+ end
+ end
+
+ -- update cite_ir output
+ local disam_format = DisamStringFormat:new()
+ local inlines = cite_ir:flatten(disam_format)
+ local disam_str = disam_format:output(inlines, nil)
+ cite_ir.disam_str = disam_str
+ if not engine.cite_irs_by_output[disam_str] then
+ engine.cite_irs_by_output[disam_str] = {}
+ end
+ engine.cite_irs_by_output[disam_str][cite_ir.ir_index] = cite_ir
+
+ -- update ambiguous_cite_irs and ambiguous_same_output_irs
+ local ambiguous_cite_irs = {}
+ for ir_index, ir_ in pairs(engine.cite_irs_by_output[cite_ir.disam_str]) do
+ -- util.debug(ir_.cite_item.id)
+ if ir_.cite_item.id ~= cite_ir.cite_item.id then
+ table.insert(ambiguous_cite_irs, ir_)
+ end
+ end
+ if #ambiguous_cite_irs == 0 then
+ cite_ir.is_ambiguous = false
+ end
+
+ return cite_ir
+end
+
+function Citation:apply_disambiguate_add_givenname_primary_name(cite_ir, engine)
+ if not cite_ir.person_name_irs or #cite_ir.person_name_irs == 0 then
+ return cite_ir
+ end
+ local person_name_ir = cite_ir.person_name_irs[1]
+ local name_output = person_name_ir.name_output
+ -- util.debug(name_output)
+
+ if not person_name_ir.person_name_index then
+ person_name_ir.person_name_index = #engine.person_names + 1
+ table.insert(engine.person_names, person_name_ir)
+ end
+ if not engine.person_names_by_output[name_output] then
+ engine.person_names_by_output[name_output] = {}
+ end
+ engine.person_names_by_output[name_output][person_name_ir.person_name_index] = person_name_ir
+
+ local ambiguous_name_irs = {}
+ local ambiguous_same_output_irs = {}
+
+ for _, pn_ir in pairs(engine.person_names_by_output[person_name_ir.name_output]) do
+ if pn_ir.full_name ~= person_name_ir.full_name then
+ table.insert(ambiguous_name_irs, pn_ir)
+ end
+ if pn_ir.name_output == person_name_ir.name_output then
+ table.insert(ambiguous_same_output_irs, pn_ir)
+ end
+ end
+
+ for _, name_variant in ipairs(person_name_ir.disam_variants) do
+ if #ambiguous_name_irs == 0 then
+ break
+ end
+
+ for _, pn_ir in ipairs(ambiguous_same_output_irs) do
+ -- expand one name
+ if pn_ir.disam_variants_index < #pn_ir.disam_variants then
+ pn_ir.disam_variants_index = pn_ir.disam_variants_index + 1
+ pn_ir.name_output = pn_ir.disam_variants[pn_ir.disam_variants_index]
+ pn_ir.inlines = pn_ir.disam_inlines[pn_ir.name_output]
+
+ if not engine.person_names_by_output[pn_ir.name_output] then
+ engine.person_names_by_output[pn_ir.name_output] = {}
+ end
+ engine.person_names_by_output[pn_ir.name_output][person_name_ir.person_name_index] = person_name_ir
+ end
+ end
+
+ -- update ambiguous_name_irs and ambiguous_same_output_irs
+ ambiguous_name_irs = {}
+ ambiguous_same_output_irs = {}
+ for _, pn_ir in pairs(engine.person_names_by_output[person_name_ir.name_output]) do
+ if pn_ir.full_name ~= person_name_ir.full_name then
+ table.insert(ambiguous_name_irs, pn_ir)
+ end
+ if pn_ir.name_output == person_name_ir.name_output then
+ table.insert(ambiguous_same_output_irs, pn_ir)
+ end
+ end
+ end
+
+ return cite_ir
+end
+
+function Citation:apply_disambiguate_add_givenname_by_cite(cite_ir, engine)
+ if not cite_ir.is_ambiguous then
+ return cite_ir
+ end
+ if not cite_ir.person_name_irs or #cite_ir.person_name_irs == 0 then
+ return cite_ir
+ end
+
+ -- for _, ir_ in ipairs(engine.disam_irs) do
+ -- util.debug(ir_.cite_item.id)
+ -- util.debug(ir_.disam_str)
+ -- end
+
+ local disam_format = DisamStringFormat:new()
+
+ local ambiguous_cite_irs = {}
+ local ambiguous_same_output_irs = {}
+
+ for ir_index, ir_ in pairs(engine.cite_irs_by_output[cite_ir.disam_str]) do
+ if ir_.cite_item.id ~= cite_ir.cite_item.id then
+ table.insert(ambiguous_cite_irs, ir_)
+ end
+ if ir_.disam_str == cite_ir.disam_str then
+ table.insert(ambiguous_same_output_irs, ir_)
+ end
+ end
+
+ for i, person_name_ir in ipairs(cite_ir.person_name_irs) do
+ -- util.debug(person_name_ir.name_output)
+ -- util.debug(person_name_ir.disam_variants)
+ if #ambiguous_cite_irs == 0 then
+ cite_ir.is_ambiguous = false
+ break
+ end
+
+ -- util.debug(person_name_ir.disam_variants)
+ while person_name_ir.disam_variants_index < #person_name_ir.disam_variants do
+ -- util.debug(person_name_ir.name_output)
+
+ local is_different_name = false
+ for _, ir_ in ipairs(ambiguous_cite_irs) do
+ if ir_.person_name_irs[i] then
+ if ir_.person_name_irs[i].full_name ~= person_name_ir.full_name then
+ -- util.debug(ir_.cite_item.id)
+ is_different_name = true
+ break
+ end
+ end
+ end
+ -- util.debug(is_different_name)
+ if not is_different_name then
+ break
+ end
+
+ for _, ir_ in ipairs(ambiguous_same_output_irs) do
+ -- util.debug(ir_.cite_item.id)
+ local person_name_ir_ = ir_.person_name_irs[i]
+ if person_name_ir_ then
+ if person_name_ir_.disam_variants_index < #person_name_ir_.disam_variants then
+ person_name_ir_.disam_variants_index = person_name_ir_.disam_variants_index + 1
+ local disam_variant = person_name_ir_.disam_variants[person_name_ir_.disam_variants_index]
+ person_name_ir_.name_output = disam_variant
+ -- util.debug(disam_variant)
+ person_name_ir_.inlines = person_name_ir_.disam_inlines[disam_variant]
+ -- Update cite ir output
+ local inlines = ir_:flatten(disam_format)
+ local disam_str = disam_format:output(inlines, nil)
+ ir_.disam_str = disam_str
+ if not engine.cite_irs_by_output[disam_str] then
+ engine.cite_irs_by_output[disam_str] = {}
+ end
+ engine.cite_irs_by_output[disam_str][ir_.ir_index] = ir_
+ end
+ end
+ end
+
+ -- update ambiguous_cite_irs and ambiguous_same_output_irs
+ ambiguous_cite_irs = {}
+ ambiguous_same_output_irs = {}
+ for ir_index, ir_ in pairs(engine.cite_irs_by_output[cite_ir.disam_str]) do
+ -- util.debug(ir_.cite_item.id)
+ if ir_.cite_item.id ~= cite_ir.cite_item.id then
+ table.insert(ambiguous_cite_irs, ir_)
+ end
+ if ir_.disam_str == cite_ir.disam_str then
+ table.insert(ambiguous_same_output_irs, ir_)
+ end
+ end
+
+ -- util.debug(#ambiguous_cite_irs)
+
+ if #ambiguous_cite_irs == 0 then
+ cite_ir.is_ambiguous = false
+ return cite_ir
+ end
+
+ end
+ end
+
+ return cite_ir
+end
+
+local function find_first_name_ir(ir)
+ if ir._type == "NameIr" then
+ return ir
+ end
+ if ir.children then
+ for _, child in ipairs(ir.children) do
+ local name_ir = find_first_name_ir(child)
+ if name_ir then
+ return name_ir
+ end
+ end
+ end
+ return nil
+end
+
+function Citation:apply_disambiguate_add_names(cite_ir, engine)
+ if not self.disambiguate_add_names then
+ return cite_ir
+ end
+
+ if not cite_ir.name_ir then
+ cite_ir.name_ir = find_first_name_ir(cite_ir)
+ end
+ local name_ir = cite_ir.name_ir
+
+ if not cite_ir.is_ambiguous then
+ return cite_ir
+ end
+
+ if not name_ir or not name_ir.et_al_abbreviation then
+ return cite_ir
+ end
+
+ -- util.debug("disambiguate_add_names: " .. cite_ir.cite_item.id)
+
+ if name_ir then
+ -- util.debug(cite_ir.disam_str)
+ -- util.debug(cite_ir.name_ir.full_name_str)
+ -- util.debug(cite_ir.is_ambiguous)
+ end
+
+ local disam_format = DisamStringFormat:new()
+
+ while cite_ir.is_ambiguous do
+ if #cite_ir.name_ir.hidden_name_irs == 0 then
+ break
+ end
+
+ local ambiguous_cite_irs = {}
+ local ambiguous_same_output_irs = {}
+ for _, ir_ in pairs(engine.cite_irs_by_output[cite_ir.disam_str]) do
+ if ir_.cite_item.id ~= cite_ir.cite_item.id then
+ table.insert(ambiguous_cite_irs, ir_)
+ end
+ if ir_.disam_str == cite_ir.disam_str then
+ table.insert(ambiguous_same_output_irs, ir_)
+ end
+ end
+
+ -- util.debug(#ambiguous_same_output_irs)
+ if #ambiguous_cite_irs == 0 then
+ cite_ir.is_ambiguous = false
+ break
+ end
+
+ -- check if the cite can be (fully) disambiguated by adding names
+ local can_be_disambuguated = false
+ for _, ir_ in ipairs(ambiguous_cite_irs) do
+ if ir_.name_ir.full_name_str ~= cite_ir.name_ir.full_name_str then
+ can_be_disambuguated = true
+ break
+ end
+ end
+ -- util.debug(can_be_disambuguated)
+ if not can_be_disambuguated then
+ break
+ end
+
+ for _, ir_ in ipairs(ambiguous_same_output_irs) do
+ local added_person_name_ir = ir_.name_ir.name_inheritance:expand_one_name(ir_.name_ir)
+ if added_person_name_ir then
+ -- util.debug("Updated: " .. ir_.cite_item.id)
+ table.insert(ir_.person_name_irs, added_person_name_ir)
+
+ if not added_person_name_ir.person_name_index then
+ added_person_name_ir.person_name_index = #engine.person_names + 1
+ table.insert(engine.person_names, added_person_name_ir)
+ end
+ local name_output = added_person_name_ir.name_output
+ if not engine.person_names_by_output[name_output] then
+ engine.person_names_by_output[name_output] = {}
+ end
+ engine.person_names_by_output[name_output][added_person_name_ir.person_name_index] = added_person_name_ir
+
+ -- Update ir output
+ local inlines = ir_:flatten(disam_format)
+ local disam_str = disam_format:output(inlines, nil)
+ -- util.debug(disam_str)
+ ir_.disam_str = disam_str
+ if not engine.cite_irs_by_output[disam_str] then
+ engine.cite_irs_by_output[disam_str] = {}
+ end
+ engine.cite_irs_by_output[disam_str][ir_.ir_index] = ir_
+ end
+ end
+
+ -- util.debug("disambiguate_add_givenname")
+
+ if self.disambiguate_add_givenname then
+ local gn_disam_rule = self.givenname_disambiguation_rule
+ if gn_disam_rule == "all-names" or gn_disam_rule == "all-names-with-initials" then
+ cite_ir = self:apply_disambiguate_add_givenname_all_names(cite_ir, engine)
+ elseif gn_disam_rule == "by-cite" then
+ cite_ir = self:apply_disambiguate_add_givenname_by_cite(cite_ir, engine)
+ end
+ end
+
+ cite_ir.is_ambiguous = self:check_ambiguity(cite_ir, engine)
+
+ -- for _, ir_ in ipairs(engine.disam_irs) do
+ -- util.debug(ir_.cite_item.id .. ": " .. ir_.disam_str)
+ -- end
+
+ end
+
+
+ return cite_ir
+end
+
+function Citation:collect_irs_with_disambiguate_branch(ir)
+ local irs_with_disambiguate_branch = {}
+ if ir.children then
+ for i, child_ir in ipairs(ir.children) do
+ if child_ir.disambiguate_branch_ir then
+ table.insert(irs_with_disambiguate_branch, child_ir)
+ elseif child_ir.children then
+ util.extend(irs_with_disambiguate_branch,
+ self:collect_irs_with_disambiguate_branch(child_ir))
+ end
+ end
+ end
+ return irs_with_disambiguate_branch
+end
+
+function Citation:apply_disambiguate_conditionals(cite_ir, engine)
+ -- util.debug(cite_ir)
+
+ cite_ir.irs_with_disambiguate_branch = self:collect_irs_with_disambiguate_branch(cite_ir)
+
+ local disam_format = DisamStringFormat:new()
+
+ while cite_ir.is_ambiguous do
+ if #cite_ir.irs_with_disambiguate_branch == 0 then
+ break
+ end
+
+ -- util.debug(cite_ir.cite_item.id)
+ -- util.debug(cite_ir.disam_str)
+
+ -- update ambiguous_same_output_irs
+ local ambiguous_same_output_irs = {}
+ for _, ir_ in pairs(engine.cite_irs_by_output[cite_ir.disam_str]) do
+ if ir_.disam_str == cite_ir.disam_str then
+ table.insert(ambiguous_same_output_irs, ir_)
+ end
+ end
+
+ for _, ir_ in ipairs(ambiguous_same_output_irs) do
+ if #ir_.irs_with_disambiguate_branch > 0 then
+ -- Disambiguation is incremental
+ -- disambiguate_IncrementalExtraText.txt
+ local condition_ir = ir_.irs_with_disambiguate_branch[1]
+ condition_ir.children[1] = condition_ir.disambiguate_branch_ir
+ condition_ir.group_var = condition_ir.disambiguate_branch_ir.group_var
+ table.remove(ir_.irs_with_disambiguate_branch, 1)
+ -- disambiguate_DisambiguateTrueReflectedInBibliography.txt
+ ir_.reference.disambiguate = true
+
+ -- Update ir output
+ local inlines = ir_:flatten(disam_format)
+ local disam_str = disam_format:output(inlines, nil)
+ -- util.debug("update: " .. ir_.cite_item.id .. ": " .. disam_str)
+ ir_.disam_str = disam_str
+ if not engine.cite_irs_by_output[disam_str] then
+ engine.cite_irs_by_output[disam_str] = {}
+ end
+ engine.cite_irs_by_output[disam_str][ir_.ir_index] = ir_
+ end
+ end
+
+ cite_ir.is_ambiguous = self:check_ambiguity(cite_ir, engine)
+ -- util.debug(cite_ir.is_ambiguous)
+ -- for _, ir_ in ipairs(engine.disam_irs) do
+ -- util.debug(ir_.cite_item.id .. ": " .. ir_.disam_str)
+ -- end
+
+ end
+ return cite_ir
+end
+
+function Citation:check_ambiguity(cite_ir, engine)
+ for _, ir_ in pairs(engine.cite_irs_by_output[cite_ir.disam_str]) do
+ if ir_.cite_item.id ~= cite_ir.cite_item.id then
+ return true
+ end
+ end
+ return false
+end
+
+function Citation:get_same_output_irs(cite_ir, engine)
+ local ambiguous_same_output_irs = {}
+ for _, ir_ in pairs(engine.cite_irs_by_output[cite_ir.disam_str]) do
+ if ir_.disam_str == cite_ir.disam_str then
+ table.insert(ambiguous_same_output_irs, ir_)
+ end
+ end
+ return ambiguous_same_output_irs
+end
+
+function Citation:apply_disambiguate_add_year_suffix(cite_ir, engine)
+ if not cite_ir.is_ambiguous or not self.disambiguate_add_year_suffix then
+ return cite_ir
+ end
+
+ local same_output_irs = self:get_same_output_irs(cite_ir, engine)
+
+ table.sort(same_output_irs, function (a, b)
+ -- return a.ir_index < b.ir_index
+ return a.reference["citation-number"] < b.reference["citation-number"]
+ end)
+
+ local year_suffix_number = 0
+ -- util.debug(cite_ir)
+
+ local disam_format = DisamStringFormat:new()
+
+ for _, ir_ in ipairs(same_output_irs) do
+ ir_.reference.year_suffix_number = nil
+ end
+
+ for _, ir_ in ipairs(same_output_irs) do
+ -- print(ir_.cite_item.id)
+ -- print(ir_.reference)
+ if not ir_.reference.year_suffix_number then
+ year_suffix_number = year_suffix_number + 1
+ ir_.reference.year_suffix_number = year_suffix_number
+ ir_.reference["year-suffix"] = self:render_year_suffix(year_suffix_number)
+ end
+
+ if not ir_.year_suffix_irs then
+ ir_.year_suffix_irs = ir_:collect_year_suffix_irs()
+ if #ir_.year_suffix_irs == 0 then
+ -- By default, the year-suffix is appended the first year rendered through cs:date
+ local year_ir = ir_:find_first_year_ir()
+ -- util.debug(year_ir)
+ if year_ir then
+ local year_suffix_ir = YearSuffix:new({}, self)
+ table.insert(year_ir.children, year_suffix_ir)
+ table.insert(ir_.year_suffix_irs, year_suffix_ir)
+ end
+ end
+ end
+
+ for _, year_suffix_ir in ipairs(ir_.year_suffix_irs) do
+ year_suffix_ir.inlines = {PlainText:new(ir_.reference["year-suffix"])}
+ year_suffix_ir.year_suffix_number = ir_.reference.year_suffix_number
+ year_suffix_ir.group_var = "important"
+ end
+
+ local inlines = ir_:flatten(disam_format)
+ local disam_str = disam_format:output(inlines, nil)
+ -- util.debug("update: " .. ir_.cite_item.id .. ": " .. disam_str)
+ ir_.disam_str = disam_str
+ if not engine.cite_irs_by_output[disam_str] then
+ engine.cite_irs_by_output[disam_str] = {}
+ end
+ engine.cite_irs_by_output[disam_str][ir_.ir_index] = ir_
+
+ end
+
+ cite_ir.is_ambiguous = false
+
+ return cite_ir
+end
+
+function Citation:render_year_suffix(year_suffix_number)
+ if year_suffix_number <= 0 then
+ return nil
+ end
+ local year_suffix = ""
+ while year_suffix_number > 0 do
+ local i = (year_suffix_number - 1) % 26
+ year_suffix = string.char(i + 97) .. year_suffix
+ year_suffix_number = (year_suffix_number - 1) // 26
+ end
+ -- util.debug(year_suffix)
+ return year_suffix
+end
+
+local function find_first(ir, check)
+ if check(ir) then
+ return ir
+ end
+ if ir.children then
+ for _, child in ipairs(ir.children) do
+ local target_ir = find_first(child, check)
+ if target_ir then
+ return target_ir
+ end
+ end
+ end
+ return nil
+end
+
+-- Find the first rendering element and it should be produced by and names element
+local function find_first_names_ir(ir)
+ if ir.first_names_ir then
+ return ir.first_names_ir
+ end
+
+ local first_rendering_ir = find_first(ir, function (ir_)
+ return (ir_._element == "text"
+ or ir_._element == "date"
+ or ir_._element == "number"
+ or ir_._element == "names"
+ or ir_._element == "label")
+ and ir_.group_var ~= "missing"
+ end)
+ local first_names_ir
+ if first_rendering_ir and first_rendering_ir._element == "names" then
+ first_names_ir = first_rendering_ir
+ end
+ if first_names_ir then
+ local disam_format = DisamStringFormat:new()
+ local inlines = first_names_ir:flatten(disam_format)
+ first_names_ir.disam_str = disam_format:output(inlines, nil)
+ end
+ ir.first_names_ir = first_names_ir
+ return first_names_ir
+
+end
+
+function Citation:_apply_special_citation_form(irs, properties, output_format, engine)
+ if properties.mode then
+ if properties.mode == "author-only" then
+ for _, ir in ipairs(irs) do
+ self:_apply_citation_mode_author_only(ir)
+ end
+ elseif properties.mode == "suppress-author" then
+ -- suppress-author mode does not work in note style
+ -- discretionary_FirstReferenceNumberWithIntext.txt
+ if engine.style.class ~= "note" then
+ for _, ir in ipairs(irs) do
+ self:_apply_suppress_author(ir)
+ end
+ end
+
+ elseif properties.mode == "composite" then
+ self:_apply_composite(irs[1], output_format, engine)
+
+ end
+
+ else
+ for _, ir in ipairs(irs) do
+ if ir.cite_item["author-only"] then
+ self:_apply_cite_author_only(ir)
+ elseif ir.cite_item["suppress-author"] then
+ self:_apply_suppress_author(ir)
+ end
+ end
+ end
+end
+
+function Citation:_apply_citation_mode_author_only(ir)
+ -- Used in pr
+ local author_ir = find_first_names_ir(ir)
+
+ if author_ir then
+ remove_name_formatting(author_ir)
+ ir.children = {author_ir}
+ else
+ ir.children = {Rendered:new({PlainText:new("[NO_PRINTED_FORM]")}, self)}
+ end
+ return ir
+end
+
+-- Citation flags with makeCitationCluster
+-- In contrast to Citation flags with processCitationCluster, this funciton
+-- looks for the first rendering element instead of names element.
+-- See discretionary_AuthorOnly.txt
+function Citation:_apply_cite_author_only(ir)
+ local author_ir = find_first(ir, function (ir_)
+ return (ir_._element == "text"
+ or ir_._element == "date"
+ or ir_._element == "number"
+ or ir_._element == "names"
+ or ir_._element == "label")
+ and ir_.group_var ~= "missing"
+ end)
+
+ if author_ir then
+ remove_name_formatting(author_ir)
+ ir.children = {author_ir}
+ else
+ ir.children = {Rendered:new({PlainText:new("[NO_PRINTED_FORM]")}, self)}
+ end
+ return ir
+end
+
+function Citation:_apply_suppress_author(ir)
+ local author_ir = find_first_names_ir(ir)
+ if author_ir then
+ -- util.debug(author_ir)
+ author_ir.collapse_suppressed = true
+ end
+ return ir
+end
+
+function Citation:_apply_composite(ir, output_format, engine)
+ -- local first_names_ir = find_first_names_ir(ir)
+
+ local first_names_ir = find_first_names_ir(ir)
+ if first_names_ir then
+ -- util.debug(first_names_ir)
+ first_names_ir.collapse_suppressed = true
+ end
+
+ local author_ir
+ if engine.style.intext then
+ local properties = {mode = "author-only"}
+ author_ir = engine.style.intext:build_fully_disambiguated_ir(ir.cite_item, output_format, engine, properties)
+ elseif first_names_ir then
+ author_ir = first_names_ir
+ end
+
+ if author_ir then
+ remove_name_formatting(author_ir)
+ ir.author_ir = author_ir
+ else
+ ir.author_ir = Rendered:new({PlainText:new("[NO_PRINTED_FORM]")}, self)
+ end
+
+ return ir
+end
+
+function Citation:group_cites(irs)
+ local disam_format = DisamStringFormat:new()
+ for _, ir in ipairs(irs) do
+ local first_names_ir = ir.first_names_ir
+ if not first_names_ir then
+ first_names_ir = find_first(ir, function (ir_)
+ return ir_._element == "names" and ir_.group_var ~= "missing"
+ end)
+ if first_names_ir then
+ local inlines = first_names_ir:flatten(disam_format)
+ first_names_ir.disam_str = disam_format:output(inlines, nil)
+ end
+ ir.first_names_ir = first_names_ir
+ end
+ end
+
+ local irs_by_name = {}
+ local name_list = {}
+
+ for _, ir in ipairs(irs) do
+ local name_str = ""
+ if ir.first_names_ir then
+ name_str = ir.first_names_ir.disam_str
+ end
+ if not irs_by_name[name_str] then
+ irs_by_name[name_str] = {}
+ table.insert(name_list, name_str)
+ end
+ table.insert(irs_by_name[name_str], ir)
+ end
+
+ local grouped = {}
+ for _, name_str in ipairs(name_list) do
+ local irs_with_same_name = irs_by_name[name_str]
+ for i, ir in ipairs(irs_with_same_name) do
+ if i < #irs_with_same_name then
+ ir.own_delimiter = self.cite_group_delimiter
+ end
+ table.insert(grouped, ir)
+ end
+ end
+ return grouped
+end
+
+function Citation:collapse_cites(irs)
+ if self.collapse == "citation-number" then
+ self:collapse_cites_by_citation_number(irs)
+ elseif self.collapse == "year" then
+ self:collapse_cites_by_year(irs)
+ elseif self.collapse == "year-suffix" then
+ self:collapse_cites_by_year_suffix(irs)
+ elseif self.collapse == "year-suffix-ranged" then
+ self:collapse_cites_by_year_suffix_ranged(irs)
+ end
+end
+
+function Citation:collapse_cites_by_citation_number(irs)
+ local cite_groups = {}
+ local current_group = {}
+ local previous_citation_number
+ for i, ir in ipairs(irs) do
+ local citation_number
+ local only_citation_number_ir = self:get_only_citation_number(ir)
+ if only_citation_number_ir then
+ -- Other irs like locators are not rendered.
+ -- collapse_CitationNumberRangesWithAffixesGrouped.txt
+ citation_number = only_citation_number_ir.citation_number
+ end
+ if i == 1 then
+ table.insert(current_group, ir)
+ elseif citation_number and previous_citation_number and
+ previous_citation_number + 1 == citation_number then
+ table.insert(current_group, ir)
+ else
+ table.insert(cite_groups, current_group)
+ current_group = {ir}
+ end
+ previous_citation_number = citation_number
+ end
+ table.insert(cite_groups, current_group)
+
+ for _, cite_group in ipairs(cite_groups) do
+ if #cite_group >= 3 then
+ cite_group[1].own_delimiter = util.unicode["en dash"]
+ for i = 2, #cite_group - 1 do
+ cite_group[i].collapse_suppressed = true
+ end
+ cite_group[#cite_group].own_delimiter = self.after_collapse_delimiter
+ end
+ end
+end
+
+function Citation:get_only_citation_number(ir)
+ if ir.citation_number then
+ return ir
+ end
+ if not ir.children then
+ return nil
+ end
+ local only_citation_number_ir
+ for _, child in ipairs(ir.children) do
+ if child.group_var ~= "missing" then
+ local citation_number_ir = self:get_only_citation_number(child)
+ if citation_number_ir then
+ if only_citation_number_ir then
+ return nil
+ else
+ only_citation_number_ir = citation_number_ir
+ end
+ else
+ return false
+ end
+ end
+ end
+ return only_citation_number_ir
+end
+
+function Citation:collapse_cites_by_year(irs)
+ local cite_groups = {{}}
+ local previous_name_str
+ for i, ir in ipairs(irs) do
+ local name_str
+ if ir.first_names_ir then
+ name_str = ir.first_names_ir.disam_str
+ end
+ if i == 1 then
+ table.insert(cite_groups[#cite_groups], ir)
+ elseif name_str and name_str == previous_name_str then
+ -- ir.first_names_ir was set in the cite grouping stage
+ -- TODO: and not previous cite suffix
+ table.insert(cite_groups[#cite_groups], ir)
+ else
+ table.insert(cite_groups, {ir})
+ end
+ previous_name_str = name_str
+ end
+
+ for _, cite_group in ipairs(cite_groups) do
+ if #cite_group > 1 then
+ for i, cite_ir in ipairs(cite_group) do
+ if i > 1 and cite_ir.first_names_ir then
+ cite_ir.first_names_ir.collapse_suppressed = true
+ end
+ if i == #cite_group then
+ cite_ir.own_delimiter = self.after_collapse_delimiter
+ elseif i < #cite_group then
+ -- The delimiter depends on the citation > sort.
+ -- https://github.com/citation-style-language/test-suite/issues/39#issuecomment-687901688
+ if cite_ir.cite_item.locator then
+ -- Special hack for
+ cite_ir.own_delimiter = self.after_collapse_delimiter
+ elseif self.cite_grouping then
+ if self.sort then
+ cite_ir.own_delimiter = self.cite_group_delimiter
+ else
+ cite_ir.own_delimiter = self.layout.delimiter
+ end
+ else
+ if self.sort then
+ cite_ir.own_delimiter = self.cite_group_delimiter
+ else
+ -- disambiguate_YearCollapseWithInstitution.txt
+ -- disambiguate_InitializeWithButNoDisambiguation.txt ?
+ cite_ir.own_delimiter = self.layout.delimiter
+ end
+ end
+ end
+ end
+ end
+ end
+end
+
+local function find_rendered_year_suffix(ir)
+ if ir._type == "YearSuffix" then
+ return ir
+ end
+ if ir.children then
+ for _, child in ipairs(ir.children) do
+ if child.group_var ~= "missing" then
+ local year_suffix = find_rendered_year_suffix(child)
+ if year_suffix then
+ return year_suffix
+ end
+ end
+ end
+ end
+ return nil
+end
+
+function Citation:collapse_cites_by_year_suffix(irs)
+ self:collapse_cites_by_year(irs)
+ -- Group by disam_str
+ -- The year-suffix is ommitted in DisamStringFormat
+ local cite_groups = {{}}
+ local previous_ir
+ local previous_year_suffix
+ for i, ir in ipairs(irs) do
+ local year_suffix = find_rendered_year_suffix(ir)
+ ir.rendered_year_suffix_ir = year_suffix
+ if i == 1 then
+ table.insert(cite_groups[#cite_groups], ir)
+ elseif year_suffix and previous_ir.disam_str == ir.disam_str and previous_year_suffix then
+ -- TODO: and not previous cite suffix
+ table.insert(cite_groups[#cite_groups], ir)
+ else
+ table.insert(cite_groups, {ir})
+ end
+ previous_ir = ir
+ previous_year_suffix = year_suffix
+ end
+
+ for _, cite_group in ipairs(cite_groups) do
+ if #cite_group > 1 then
+ for i, cite_ir in ipairs(cite_group) do
+ if i > 1 then
+ -- cite_ir.children = {cite_ir.rendered_year_suffix_ir}
+ -- Set the collapse_suppressed flag rather than removing the child irs.
+ -- This leaves the disamb ir structure unchanged.
+ self:suppress_ir_except_child(cite_ir, cite_ir.rendered_year_suffix_ir)
+ end
+ if i < #cite_group then
+ if self.cite_grouping then
+ -- In the current citeproc-js impplementation, explicitly set
+ -- cite-group-delimiter takes precedence over year-suffix-delimiter.
+ -- May be changed in the future.
+ -- https://github.com/citation-style-language/test-suite/issues/50
+ cite_ir.own_delimiter = self.cite_group_delimiter
+ else
+ cite_ir.own_delimiter = self.year_suffix_delimiter
+ end
+ elseif i == #cite_group then
+ cite_ir.own_delimiter = self.after_collapse_delimiter
+ end
+ end
+ end
+ end
+end
+
+function Citation:suppress_ir_except_child(ir, target)
+ if ir == target then
+ ir.collapse_suppressed = false
+ return false
+ end
+ ir.collapse_suppressed = true
+ if ir.children then
+ for _, child in ipairs(ir.children) do
+ if child.group_var ~= "missing" and not child.collapse_suppressed then
+ if not self:suppress_ir_except_child(child, target) then
+ ir.collapse_suppressed = false
+ end
+ end
+ end
+ end
+ return ir.collapse_suppressed
+end
+
+function Citation:collapse_cites_by_year_suffix_ranged(irs)
+ self:collapse_cites_by_year_suffix(irs)
+ -- Group by disam_str
+ local cite_groups = {{}}
+ local previous_ir
+ local previous_year_suffix
+ for i, ir in ipairs(irs) do
+ local year_suffix_ir = find_rendered_year_suffix(ir)
+ ir.rendered_year_suffix_ir = year_suffix_ir
+ if i == 1 then
+ table.insert(cite_groups[#cite_groups], ir)
+ elseif year_suffix_ir and previous_ir.disam_str == ir.disam_str and previous_year_suffix and
+ year_suffix_ir.year_suffix_number == previous_year_suffix.year_suffix_number + 1 then
+ -- TODO: and not previous cite suffix
+ table.insert(cite_groups[#cite_groups], ir)
+ else
+ table.insert(cite_groups, {ir})
+ end
+ previous_ir = ir
+ previous_year_suffix = year_suffix_ir
+ end
+
+ for _, cite_group in ipairs(cite_groups) do
+ if #cite_group > 2 then
+ for i, cite_ir in ipairs(cite_group) do
+ if i == 1 then
+ cite_ir.own_delimiter = util.unicode["en dash"]
+ elseif i < #cite_group then
+ cite_ir.collapse_suppressed = true
+ end
+ end
+ end
+ end
+end
+
+
+local InText = Citation:derive("intext", {
+ givenname_disambiguation_rule = "by-cite",
+ cite_group_delimiter = ", ",
+ near_note_distance = 5,
+})
+
+
+citation_module.Citation = Citation
+citation_module.InText = InText
+
+
+return citation_module