summaryrefslogtreecommitdiff
path: root/biblio/citation-style-language/citeproc-node-names.lua
diff options
context:
space:
mode:
Diffstat (limited to 'biblio/citation-style-language/citeproc-node-names.lua')
-rw-r--r--biblio/citation-style-language/citeproc-node-names.lua1374
1 files changed, 891 insertions, 483 deletions
diff --git a/biblio/citation-style-language/citeproc-node-names.lua b/biblio/citation-style-language/citeproc-node-names.lua
index 72096a58f9..2f02367eea 100644
--- a/biblio/citation-style-language/citeproc-node-names.lua
+++ b/biblio/citation-style-language/citeproc-node-names.lua
@@ -8,320 +8,839 @@ local names_module = {}
local unicode = require("unicode")
-local richtext = require("citeproc-richtext")
-local element = require("citeproc-element")
+local IrNode = require("citeproc-ir-node").IrNode
+local NameIr = require("citeproc-ir-node").NameIr
+local PersonNameIr = require("citeproc-ir-node").PersonNameIr
+local SeqIr = require("citeproc-ir-node").SeqIr
+local Rendered = require("citeproc-ir-node").Rendered
+local InlineElement = require("citeproc-output").InlineElement
+local PlainText = require("citeproc-output").PlainText
+local SortStringFormat = require("citeproc-output").SortStringFormat
+
+local Element = require("citeproc-element").Element
local util = require("citeproc-util")
-local Name = element.Element:new()
-
-Name.default_options = {
- ["delimiter"] = ", ",
- ["delimiter-precedes-et-al"] = "contextual",
- ["delimiter-precedes-last"] = "contextual",
- ["et-al-min"] = nil,
- ["et-al-use-first"] = nil,
- ["et-al-subsequent-min"] = nil,
- ["et-al-subsequent-use-first "] = nil,
- ["et-al-use-last"] = false,
- ["form"] = "long",
- ["initialize"] = true,
- ["initialize-with"] = false,
- ["name-as-sort-order"] = false,
- ["sort-separator"] = ", ",
- ["prefix"] = "",
- ["suffix"] = "",
-}
-
-function Name:render (names, context)
- self:debug_info(context)
- context = self:process_context(context)
-
- local and_ = context.options["and"]
- local delimiter = context.options["delimiter"]
- local delimiter_precedes_et_al = context.options["delimiter-precedes-et-al"]
- local delimiter_precedes_last = context.options["delimiter-precedes-last"]
- local et_al_min = context.options["et-al-min"]
- local et_al_use_first = context.options["et-al-use-first"]
- local et_al_subsequent_min = context.options["et-al-subsequent-min"]
- local et_al_subsequent_use_first = context.options["et-al-subsequent-use-first "]
- local et_al_use_last = context.options["et-al-use-last"]
-
- -- sorting
- if context.options["names-min"] then
- et_al_min = context.options["names-min"]
- end
- if context.options["names-use-first"] then
- et_al_use_first = context.options["names-use-first"]
- end
- if context.options["names-use-last"] ~= nil then
- et_al_use_last = context.options["names-use-last"]
- end
-
- local form = context.options["form"]
-
- local et_al_truncate = et_al_min and et_al_use_first and #names >= et_al_min
- local et_al_last = et_al_use_last and et_al_use_first <= et_al_min - 2
-
- if form == "count" then
- if et_al_truncate then
- return et_al_use_first
+local Names = Element:derive("names")
+local Name = Element:derive("name", {
+ delimiter = ", ",
+ delimiter_precedes_et_al = "contextual",
+ delimiter_precedes_last = "contextual",
+ et_al_use_last = false,
+ form = "long",
+ initialize = true,
+ sort_separator = ", ",
+})
+local NamePart = Element:derive("name-part")
+local EtAl = Element:derive("et-al")
+local Substitute = Element:derive("substitute")
+
+
+-- [Names](https://docs.citationstyles.org/en/stable/specification.html#names)
+function Names:new()
+ local o = Element.new(self)
+ o.name = nil
+ o.et_al = nil
+ o.substitute = nil
+ o.label = nil
+ return o
+end
+
+function Names:from_node(node)
+ local o = Names:new()
+ o:set_attribute(node, "variable")
+ o.name = nil
+ o.et_al = nil
+ o.substitute = nil
+ o.label = nil
+ o.children = {}
+ o:process_children_nodes(node)
+ for _, child in ipairs(o.children) do
+ local element_name = child.element_name
+ if element_name == "name" then
+ o.name = child
+ elseif element_name == "et-al" then
+ o.et_al = child
+ elseif element_name == "substitute" then
+ o.substitute = child
+ elseif element_name == "label" then
+ o.label = child
+ if o.name then
+ child.after_name = true
+ end
else
- return #names
+ util.warning(string.format('Unkown element "{}".', element_name))
end
end
+ o:get_delimiter_attribute(node)
+ o:set_affixes_attributes(node)
+ o:set_display_attribute(node)
+ o:set_formatting_attributes(node)
+ o:set_text_case_attribute(node)
+ return o
+end
- local output = nil
+function Names:build_ir(engine, state, context)
+ -- names_inheritance: names and name attributes inherited from cs:style
+ -- and cs:citaiton or cs:bibliography
+ -- name_override: names, name, et-al, label elements inherited in substitute element
+ local names_inheritance = Names:new()
+ names_inheritance.delimiter = context.name_inheritance.names_delimiter
- local res = nil
- local inverted = false
+ names_inheritance.variable = self.variable
- for i, name in ipairs(names) do
- if et_al_truncate and i > et_al_use_first then
- if et_al_last then
- if i == #names then
- output = richtext.concat(output, delimiter)
- output = output .. util.unicode["horizontal ellipsis"]
- output = output .. " "
- res = self:render_single_name(name, i, context)
- output = output .. res
- end
- else
- if not self:_check_delimiter(delimiter_precedes_et_al, i, inverted) then
- delimiter = " "
- end
- if output then
- output = richtext.concat_list({output, context.et_al:render(context)}, delimiter)
+ for _, attr in ipairs({"delimiter", "affixes", "formatting", "display"}) do
+ if self[attr] then
+ names_inheritance[attr] = util.clone(self[attr])
+ elseif state.name_override and state.name_override[attr] then
+ names_inheritance[attr] = util.clone(state.name_override[attr])
+ end
+ end
+
+ if self.name then
+ names_inheritance.name = util.clone(context.name_inheritance)
+ for key, value in pairs(self.name) do
+ names_inheritance.name[key] = util.clone(value)
+ end
+ else
+ if state.name_override then
+ names_inheritance.name = util.clone(state.name_override.name)
+ else
+ names_inheritance.name = util.clone(context.name_inheritance)
+ end
+ end
+
+ if self.et_al then
+ names_inheritance.et_al = util.clone(self.et_al)
+ elseif state.name_override then
+ names_inheritance.et_al = util.clone(state.name_override.et_al)
+ else
+ names_inheritance.et_al = EtAl:new()
+ end
+
+ if self.label then
+ names_inheritance.label = util.clone(self.label)
+ elseif state.name_override then
+ names_inheritance.label = util.clone(state.name_override.label)
+ end
+
+ if context.cite then
+ local position_level = context.cite.position or context.cite.position_level
+ if position_level and position_level >= util.position_map["subsequent"] then
+ if names_inheritance.name.et_al_subsequent_min then
+ names_inheritance.name.et_al_min = names_inheritance.name.et_al_subsequent_min
+ end
+ if names_inheritance.name.et_al_subsequent_use_first then
+ names_inheritance.name.et_al_use_first = names_inheritance.name.et_al_subsequent_use_first
+ end
+ end
+ end
+
+ local irs = {}
+ local num_names = 0
+ -- util.debug(self.name)
+
+ -- The names element may not hold a variable attribute.
+ -- substitute_SubstituteOnlyOnceString.txt
+ if names_inheritance.variable then
+ for _, variable in ipairs(util.split(names_inheritance.variable)) do
+ local name_ir = names_inheritance.name:build_ir(variable, names_inheritance.et_al, names_inheritance.label, engine, state, context)
+ if name_ir and names_inheritance.name.form == "count" then
+ num_names = num_names + name_ir.name_count
+ end
+ if name_ir and name_ir.group_var ~= "missing" then
+ table.insert(irs, name_ir)
+ end
+ end
+ end
+
+ if names_inheritance.name.form == "count" then
+ if num_names > 0 then
+ local ir = Rendered:new({PlainText:new(tostring(num_names))}, self)
+ ir.name_count = num_names
+ ir.group_var = "important"
+ ir = NameIr:new({ir}, self)
+ ir.name_count = num_names
+ ir.group_var = "important"
+ -- util.debug(ir)
+ return ir
+ end
+ else
+ if #irs > 0 then
+ local ir = SeqIr:new(irs, self)
+ ir.group_var = "important"
+ ir.delimiter = names_inheritance.delimiter
+ ir.formatting = util.clone(names_inheritance.formatting)
+ ir.affixes = util.clone(names_inheritance.affixes)
+ ir.display = names_inheritance.display
+ return ir
+ end
+ end
+
+ if self.substitute then
+ local new_state = util.clone(state)
+ new_state.name_override = names_inheritance
+ for _, substitution in ipairs(self.substitute.children) do
+ local ir = substitution:build_ir(engine, new_state, context)
+ if ir and ir.group_var ~= "missing" then
+ if not ir.person_name_irs or #ir.person_name_irs == 0 then
+ -- In case of a <text variable="title"/> in <substitute>
+ local name_count = ir.name_count
+ ir = NameIr:new({ir}, self)
+ ir.name_count = name_count -- sort_AguStyle.txt
+ ir.group_var = "important"
end
- break
+ return ir
end
- else
- if i > 1 then
- if i == #names and context.options["and"] then
- if self:_check_delimiter(delimiter_precedes_last, i, inverted) then
- output = richtext.concat(output, delimiter)
- else
- output = output .. " "
- end
- local and_term = ""
- if context.options["and"] == "text" then
- and_term = self:get_term("and"):render(context)
- elseif context.options["and"] == "symbol" then
- and_term = self:escape("&")
- end
- output = output .. and_term .. " "
+ end
+ end
+
+ local ir = Rendered:new({}, self)
+ ir.group_var = "missing"
+ return ir
+
+end
+
+
+function Names:substitute_single_field(result, context)
+ if not result then
+ return nil
+ end
+ if context.build.first_rendered_names and #context.build.first_rendered_names == 0 then
+ context.build.first_rendered_names[1] = result
+ end
+ result = self:substitute_names(result, context)
+ return result
+end
+
+function Names:substitute_names(result, context)
+ if not context.build.first_rendered_names then
+ return result
+ end
+ local name_strings = {}
+ local match_all
+
+ if #context.build.first_rendered_names > 0 then
+ match_all = true
+ else
+ match_all = false
+ end
+ for i, text in ipairs(context.build.first_rendered_names) do
+ local str = text:render(context.engine.formatter, context)
+ name_strings[i] = str
+ if context.build.preceding_first_rendered_names and str ~= context.build.preceding_first_rendered_names[i] then
+ match_all = false
+ end
+ end
+
+ if context.build.preceding_first_rendered_names then
+ local sub_str = context.options["subsequent-author-substitute"]
+ local sub_rule = context.options["subsequent-author-substitute-rule"]
+
+ if sub_rule == "complete-all" then
+ if match_all then
+ if sub_str == "" then
+ result = nil
else
- output = richtext.concat(output, delimiter)
+ result.contents = {sub_str}
end
end
- res, inverted = self:render_single_name(name, i, context)
- if res and res ~= "" then
- res = richtext.new(res)
- if context.build.first_rendered_names then
- table.insert(context.build.first_rendered_names, res)
+ elseif sub_rule == "complete-each" then
+ -- In-place substitution
+ if match_all then
+ for _, text in ipairs(context.build.first_rendered_names) do
+ text.contents = {sub_str}
end
+ result = self:concat(context.build.first_rendered_names, context)
+ end
- if output then
- output = richtext.concat(output, res)
+ elseif sub_rule == "partial-each" then
+ for i, text in ipairs(context.build.first_rendered_names) do
+ if name_strings[i] == context.build.preceding_first_rendered_names[i] then
+ text.contents = {sub_str}
else
- output = res
+ break
end
end
+ result = self:concat(context.build.first_rendered_names, context)
+
+ elseif sub_rule == "partial-first" then
+ if name_strings[1] == context.build.preceding_first_rendered_names[1] then
+ context.build.first_rendered_names[1].contents = {sub_str}
+ end
+ result = self:concat(context.build.first_rendered_names, context)
end
end
- local ret = self:format(output, context)
- ret = self:wrap(ret, context)
- return ret
+ if #context.build.first_rendered_names > 0 then
+ context.build.first_rendered_names = nil
+ end
+ context.build.preceding_first_rendered_names = name_strings
+ return result
end
-function Name:_check_delimiter (delimiter_attribute, index, inverted)
- -- `delimiter-precedes-et-al` and `delimiter-precedes-last`
- if delimiter_attribute == "always" then
- return true
- elseif delimiter_attribute == "never" then
- return false
- elseif delimiter_attribute == "contextual" then
- if index > 2 then
- return true
- else
- return false
- end
- elseif delimiter_attribute == "after-inverted-name" then
- if inverted then
- return true
- else
- return false
+-- [Name](https://docs.citationstyles.org/en/stable/specification.html#name)
+function Name:new()
+ local o = Element.new(self, "name")
+
+ o.family = NamePart:new("family")
+ o.given = NamePart:new("given")
+ return o
+end
+
+function Name:from_node(node)
+ local o = Name:new()
+ o:set_attribute(node, "and")
+ o:get_delimiter_attribute(node)
+ o:set_attribute(node, "delimiter-precedes-et-al")
+ o:set_attribute(node, "delimiter-precedes-last")
+ o:set_number_attribute(node, "et-al-min")
+ o:set_number_attribute(node, "et-al-use-first")
+ o:set_number_attribute(node, "et-al-subsequent-min")
+ o:set_number_attribute(node, "et-al-subsequent-use-first")
+ o:set_bool_attribute(node, "et-al-use-last")
+ o:set_attribute(node, "form")
+ o:set_bool_attribute(node, "initialize")
+ o:set_attribute(node, "initialize-with")
+ o:set_attribute(node, "name-as-sort-order")
+ o:set_attribute(node, "sort-separator")
+ o:set_affixes_attributes(node)
+ o:set_formatting_attributes(node)
+ o:process_children_nodes(node)
+ for _, child in ipairs(o.children) do
+ if child.name == "family" then
+ o.family = child
+ elseif child.name == "given" then
+ o.given = child
end
end
- return false
+ if not o.family then
+ o.family = NamePart:new()
+ o.family.name = "family"
+ end
+ if not o.given then
+ o.given = NamePart:new()
+ o.family.name = "given"
+ end
+ return o
end
-function Name:render_single_name (name, index, context)
- local form = context.options["form"]
- local initialize = context.options["initialize"]
- local initialize_with = context.options["initialize-with"]
- local name_as_sort_order = context.options["name-as-sort-order"]
- if context.sorting then
- name_as_sort_order = "all"
+
+function Name:build_ir(variable, et_al, label, engine, state, context)
+ -- Returns NameIR
+ local names
+ if not state.suppressed[variable] then
+ names = context:get_variable(variable)
+ end
+ if not names then
+ return nil
end
- local sort_separator = context.options["sort-separator"]
- local demote_non_dropping_particle = context.options["demote-non-dropping-particle"]
+ if context.sort_key then
+ self.delimiter = " "
+ self.name_as_sort_order = "all"
+ if context.sort_key.names_min then
+ self.et_al_min = context.sort_key.names_min
+ end
+ if context.sort_key.names_use_first then
+ self.et_al_use_first = context.sort_key.names_use_first
+ end
+ if context.sort_key.names_use_last then
+ self.et_al_use_last = context.sort_key.names_use_last
+ end
+ et_al = nil
+ label = nil
+ end
- -- TODO: make it a module
- local function _strip_quotes(str)
- if str then
- str = string.gsub(str, '"', "")
- str = string.gsub(str, "'", util.unicode["apostrophe"])
+ local et_al_abbreviation = self.et_al_min and self.et_al_use_first and #names >= self.et_al_min
+ local use_last = et_al_abbreviation and self.et_al_use_last and self.et_al_use_first <= self.et_al_min - 2
+
+ if self.form == "count" then
+ local count
+ if et_al_abbreviation then
+ count = self.et_al_use_first
+ else
+ count = #names
end
- return str
+ local ir = Rendered:new({PlainText:new(tostring(count))}, {})
+ ir.name_count = count
+ ir.group_var = "important"
+ return ir
end
- local family = _strip_quotes(name["family"]) or ""
- local given = _strip_quotes(name["given"]) or ""
- local dp = _strip_quotes(name["dropping-particle"]) or ""
- local ndp = _strip_quotes(name["non-dropping-particle"]) or ""
- local suffix = _strip_quotes(name["suffix"]) or ""
- local literal = _strip_quotes(name["literal"]) or ""
+ -- TODO: only build names as needed
+ local full_name_irs = {}
+ local full_name_str = ""
+ for i, name_var in ipairs(names) do
+ local person_name_ir = self:build_person_name_ir(name_var, i == 1, context)
+ table.insert(full_name_irs, person_name_ir)
- if family == "" then
- family = literal
- if family == "" then
- family = given
- given = ""
+ local name_variants = person_name_ir.disam_variants
+ if full_name_str ~= "" then
+ full_name_str = full_name_str .. " "
end
- if family ~= "" then
- return family
- else
- error("Name not avaliable")
+ full_name_str = full_name_str .. name_variants[#name_variants]
+ end
+
+ local person_name_irs -- TODO: rename to rendered_name_irs
+ local hidden_name_irs
+ if et_al_abbreviation then
+ person_name_irs = util.slice(full_name_irs, 1, self.et_al_use_first)
+ hidden_name_irs = util.slice(full_name_irs, self.et_al_use_first + 1, #full_name_irs)
+ if use_last then
+ table.insert(person_name_irs, full_name_irs[#full_name_irs])
+ table.remove(hidden_name_irs, #hidden_name_irs)
+ end
+ else
+ person_name_irs = util.slice(full_name_irs, 1, #full_name_irs)
+ hidden_name_irs = {}
+ end
+
+
+ local and_term_ir
+ if not context.sort_key then
+ -- sort_WithAndInOneEntry.txt
+ local and_term
+ if self["and"] == "text" then
+ and_term = context.locale:get_simple_term("and")
+ elseif self["and"] == "symbol" then
+ and_term = "&"
+ end
+ if and_term then
+ and_term_ir = Rendered:new({PlainText:new(and_term .. " ")}, {})
end
end
- if initialize_with then
- given = self:initialize(given, initialize_with, context)
+ local et_al_ir
+ if et_al and et_al_abbreviation and not use_last then
+ et_al_ir = et_al:build_ir(engine, state, context)
end
- local demote_ndp = false -- only active when form == "long"
- if demote_non_dropping_particle == "display-and-sort" or
- demote_non_dropping_particle == "sort-only" and context.sorting then
- demote_ndp = true
- else -- demote_non_dropping_particle == "never"
- demote_ndp = false
+ local irs = self:join_person_name_irs(person_name_irs, and_term_ir, et_al_ir, use_last)
+
+ local ir = NameIr:new(irs, self)
+
+ ir.name_inheritance = self
+ ir.name_variable = names
+ ir.and_term_ir = and_term_ir
+ ir.et_al_ir = et_al_ir
+ ir.et_al_abbreviation = et_al_abbreviation
+ ir.use_last = use_last
+
+ ir.full_name_irs = full_name_irs
+ ir.full_name_str = full_name_str
+ ir.person_name_irs = person_name_irs
+ ir.hidden_name_irs = hidden_name_irs
+
+ -- etal_UseZeroFirst.txt: et-al-use-first="0"
+ if #irs == 0 then
+ ir.group_var = "missing"
+ return ir
+ else
+ ir.group_var = "important"
end
- local family_name_part = nil
- local given_name_part = nil
- for _, child in ipairs(self:get_children()) do
- if child:is_element() and child:get_element_name() == "name-part" then
- local name_part = child:get_attribute("name")
- if name_part == "family" then
- family_name_part = child
- elseif name_part == "given" then
- given_name_part = child
+ ir.formatting = self.formatting
+ ir.affixes = self.affixes
+
+ irs = {ir}
+
+ if label then
+ local is_plural = (label.plural == "always" or (label.plural == "contextual" and #names > 1))
+ local label_term = context.locale:get_simple_term(variable, label.form, is_plural)
+ if label_term and label_term ~= "" then
+ local inlines = label:render_text_inlines(label_term, context)
+ local label_ir = Rendered:new(inlines, label)
+ if label.after_name then
+ table.insert(irs, label_ir)
+ else
+ table.insert(irs, 1, label_ir)
end
end
end
- local res = nil
- local inverted = false
- if form == "long" then
- local order
- local suffix_separator = sort_separator
- if not util.has_romanesque_char(name["family"]) then
- order = {family, given}
- inverted = true
- sort_separator = ""
- elseif name_as_sort_order == "all" or (name_as_sort_order == "first" and index == 1) then
-
- -- "Alan al-One"
- local hyphen_parts = util.split(family, "%-", 1)
- if #hyphen_parts > 1 then
- local particle
- particle, family = table.unpack(hyphen_parts)
- particle = particle .. "-"
- ndp = richtext.concat(ndp, particle)
+ ir = SeqIr:new(irs, self)
+
+ -- Suppress substituted name variable
+ if state.name_override and not context.sort_key then
+ state.suppressed[variable] = true
+ end
+
+ return ir
+end
+
+function Name:build_person_name_ir(name, is_first, context)
+ local is_latin = util.has_romanesque_char(name.family)
+ local is_inverted = (name.family and name.family ~= "" and is_latin and
+ (self.name_as_sort_order == "all" or (self.name_as_sort_order == "first" and is_first)))
+
+ local inlines = self:render_person_name(name, is_first, is_latin, is_inverted, context)
+ local person_name_ir = PersonNameIr:new(inlines, self)
+
+ person_name_ir.is_inverted = is_inverted
+
+ local output_format = SortStringFormat:new()
+ person_name_ir.name_output = output_format:output(inlines)
+ person_name_ir.disam_variants_index = 1
+
+ person_name_ir.disam_variants = {person_name_ir.name_output}
+ person_name_ir.disam_inlines = {inlines}
+
+ if context.area.disambiguate_add_givenname and not context.sort_key then
+ local disam_name = util.clone(self)
+ if disam_name.form == "short" then
+ disam_name.form = "long"
+ if disam_name.initialize and disam_name.initialize_with then
+ local name_inlines = disam_name:render_person_name(name, is_first, is_latin, is_inverted, context)
+ local disam_variant = output_format:output(name_inlines)
+ local last_variant = person_name_ir.disam_variants[#person_name_ir.disam_variants]
+ if disam_variant ~= last_variant then
+ table.insert(person_name_ir.disam_variants, disam_variant)
+ person_name_ir.disam_inlines[disam_variant] = name_inlines
+ end
end
+ end
- if family_name_part then
- family = family_name_part:format_name_part(family, context)
- ndp = family_name_part:format_name_part(ndp, context)
+ local givenname_disambiguation_rule = context.area.givenname_disambiguation_rule
+ local only_initials = (givenname_disambiguation_rule == "all-names-with-initials" or
+ givenname_disambiguation_rule == "primary-name-with-initials")
+ if disam_name.initialize and not only_initials then
+ disam_name.initialize = false
+ local name_inlines = disam_name:render_person_name(name, is_first, is_latin, is_inverted, context)
+ local disam_variant = output_format:output(name_inlines)
+ local last_variant = person_name_ir.disam_variants[#person_name_ir.disam_variants]
+ if disam_variant ~= last_variant then
+ table.insert(person_name_ir.disam_variants, disam_variant)
+ person_name_ir.disam_inlines[disam_variant] = name_inlines
end
- if given_name_part then
- given = given_name_part:format_name_part(given, context)
- dp = family_name_part:format_name_part(dp, context)
+ end
+
+ context.sort_key = true
+ local full_name_inlines = disam_name:render_person_name(name, is_first, is_latin, is_inverted, context)
+ -- full_name is used for comparison in disambiguation
+ person_name_ir.full_name = output_format:output(full_name_inlines)
+ context.sort_key = false
+ end
+
+ return person_name_ir
+end
+
+function Name:render_person_name(name, is_first, is_latin, is_inverted, context)
+ -- Return: inlines
+ -- TODO
+ local is_sort = context.sort_key
+ local demote_ndp = (context.style.demote_non_dropping_particle == "display-and-sort" or
+ (is_sort and context.style.demote_non_dropping_particle == "sort-only"))
+
+ local name_part_tokens = self:get_display_order(name, self.form, is_latin, is_sort, is_inverted, demote_ndp)
+ -- util.debug(name)
+ -- util.debug(name_part_tokens)
+
+ local inlines = {}
+ for i, token in ipairs(name_part_tokens) do
+ if token == "family" or token == "ndp-family" or token == "dp-ndp-family-suffix" then
+ local family_inlines = self:render_family(name, token, context)
+ util.extend(inlines, family_inlines)
+
+ elseif token == "given" or token == "given-dp" or token == "given-dp-ndp" then
+ local given_inlines = self:render_given(name, token, context)
+ util.extend(inlines, given_inlines)
+
+ elseif token == "dp" or token == "dp-ndp" then
+ local particle_inlines = self:render_particle(name, token, context)
+ util.extend(inlines, particle_inlines)
+
+ elseif token == "suffix" then
+ local text = name.suffix or ""
+ util.extend(inlines, InlineElement:parse(text, context))
+
+ elseif token == "literal" then
+ local literal_inlines = self.family:format_text_case(name.literal, context)
+ util.extend(inlines, literal_inlines)
+
+ elseif token == "space" then
+ table.insert(inlines, PlainText:new(" "))
+
+ elseif token == "wide-space" then
+ table.insert(inlines, PlainText:new(" "))
+
+ elseif token == "sort-separator" then
+ table.insert(inlines, PlainText:new(self.sort_separator))
+ end
+ end
+ -- util.debug(inlines)
+ return inlines
+end
+
+-- Name-part Order
+-- https://docs.citationstyles.org/en/stable/specification.html#name-part-order
+function Name:get_display_order(name, form, is_latin, is_sort, is_inverted, demote_ndp)
+ if is_sort then
+ if not name.family then
+ -- The literal is compared with the literal
+ if self.form == "long" then
+ return {"literal", "wide-space", "wide-space", "wide-space"}
+ else
+ return {"literal", "wide-space"}
end
+ end
+ if not is_latin then
+ if form == "long" and name.given then
+ return {"family", "given"}
+ else
+ return {"family"}
+ end
+ end
+
+ if self.form == "long" then
if demote_ndp then
- given = richtext.concat_list({given, dp, ndp}, " ")
+ return {"family", "wide-space", "dp-ndp", "wide-space", "given", "wide-space", "suffix"}
else
- family = richtext.concat_list({ndp, family}, " ")
- given = richtext.concat_list({given, dp}, " ")
+ return {"ndp-family", "wide-space", "dp", "wide-space", "given", "wide-space", "suffix"}
end
+ else
+ if demote_ndp then
+ return {"family", "wide-space", "dp-ndp"}
+ else
+ return {"ndp-family", "wide-space", "dp"}
+ end
+ end
+ end
+
+ if not name.family then
+ if name.literal then
+ return {"literal"}
+ else
+ util.error("Invalid name")
+ end
+ end
+
+ if not is_latin then
+ if form == "long" and name.given then
+ return {"family", "given"}
+ else
+ return {"family"}
+ end
+ end
- if family_name_part then
- family = family_name_part:wrap_name_part(family, context)
+ if form == "short" then
+ return {"ndp-family"}
+ end
+
+ local ndp = name["non-dropping-particle"]
+ local dp = name["dropping-particle"]
+
+ local name_part_tokens = {"family"}
+ if name.given then
+ if is_inverted then
+ if demote_ndp then
+ name_part_tokens = {"family", "sort-separator", "given-dp-ndp"}
+ else
+ name_part_tokens = {"ndp-family", "sort-separator", "given-dp"}
end
- if given_name_part then
- given = given_name_part:wrap_name_part(given, context)
+ else
+ name_part_tokens = {"given", "space", "dp-ndp-family-suffix"}
+ end
+ else
+ if is_inverted then
+ if demote_ndp then
+ if ndp or dp then
+ name_part_tokens = {"family", "sort-separator", "dp-ndp"}
+ else
+ name_part_tokens = {"family"}
+ end
+ else
+ name_part_tokens = {"ndp-family"}
end
+ else
+ name_part_tokens = {"dp-ndp-family-suffix"}
+ end
+ end
- order = {family, given, suffix}
- inverted = true
+ if name.suffix and is_inverted then
+ if is_inverted or name["comma-suffix"] then
+ table.insert(name_part_tokens, "sort-separator")
+ table.insert(name_part_tokens, "suffix")
+ elseif string.match(name.suffix, "^%p") then
+ table.insert(name_part_tokens, "sort-separator")
+ table.insert(name_part_tokens, "suffix")
else
- if family_name_part then
- family = family_name_part:format_name_part(family, context)
- ndp = family_name_part:format_name_part(ndp, context)
+ table.insert(name_part_tokens, "space")
+ table.insert(name_part_tokens, "suffix")
+ end
+ end
+
+ return name_part_tokens
+end
+
+function Name:render_family(name, token, context)
+ local inlines = {}
+ local name_part
+
+ if token == "dp-ndp-family-suffix" then
+ local dp_part = name["dropping-particle"]
+ if dp_part then
+ name_part = dp_part
+ local dp_inlines = self.given:format_text_case(dp_part, context)
+ util.extend(inlines, dp_inlines)
+ end
+ end
+
+ if token == "dp-ndp-family-suffix" or token == "ndp-family" then
+ local ndp_part = name["non-dropping-particle"]
+ if ndp_part then
+ if context.sort_key then
+ ndp_part = self:format_sort_particle(ndp_part)
end
- if given_name_part then
- given = given_name_part:format_name_part(given, context)
- dp = family_name_part:format_name_part(dp, context)
+ if #inlines > 0 then
+ table.insert(inlines, PlainText:new(" "))
end
+ name_part = ndp_part
+ local ndp_inlines = self.family:format_text_case(ndp_part, context)
+ util.extend(inlines, ndp_inlines)
+ end
+ end
- family = richtext.concat_list({dp, ndp, family}, " ")
- if name["comma-suffix"] then
- suffix_separator = ", "
+ local family = name.family
+ if context.sort_key then
+ -- Remove brackets for sorting: sort_NameVariable.txt
+ family = string.gsub(family, "[%[%]]", "")
+ end
+
+ local family_inlines = self.family:format_text_case(family, context)
+ if #inlines > 0 then
+ if not string.match(name_part, "^%l'$") and
+ not string.match(name_part, "^%lā€™$") and
+ not util.endswith(name_part, "-") then
+ table.insert(inlines, PlainText:new(" "))
+ end
+ end
+ util.extend(inlines, family_inlines)
+
+ if token == "dp-ndp-family-suffix" then
+ local suffix_part = name.suffix
+ if suffix_part then
+ if name["comma-suffix"] or util.startswith(suffix_part, "!") then
+ -- force use sort-separator exclamation prefix: magic_NameSuffixWithComma.txt
+ -- "! Jr." => "Jr."
+ table.insert(inlines, PlainText:new(self.sort_separator))
+ suffix_part = string.gsub(suffix_part, "^%p%s*", "")
else
- suffix_separator = " "
+ table.insert(inlines, PlainText:new(" "))
end
- family = richtext.concat_list({family, suffix}, suffix_separator)
+ table.insert(inlines, PlainText:new(suffix_part))
+ end
+ end
- if family_name_part then
- family = family_name_part:wrap_name_part(family, context)
- end
- if given_name_part then
- given = given_name_part:wrap_name_part(given, context)
- end
+ inlines = self.family:affixed(inlines)
+ return inlines
+end
+
+function Name:render_given(name, token, context)
+ local given = name.given
- order = {given, family}
- sort_separator = " "
+ if context.sort_key then
+ -- The empty given name is needed for evaluate the sort key.
+ if not given then
+ return {PlainText:new("")}
end
- res = richtext.concat_list(order, sort_separator)
+ -- Remove brackets for sorting: sort_NameVariable.txt
+ given = string.gsub(given, "[%[%]]", "")
+ end
- elseif form == "short" then
- if family_name_part then
- family = family_name_part:format_name_part(family, context)
- ndp = family_name_part:format_name_part(ndp, context)
+ if self.initialize_with then
+ given = self:initialize_name(given, self.initialize_with, context.style.initialize_with_hyphen)
+ end
+ local inlines = self.given:format_text_case(given, context)
+
+ if token == "given-dp" or token == "given-dp-ndp" then
+ local name_part = name["dropping-particle"]
+ if name_part then
+ table.insert(inlines, PlainText:new(" "))
+ local dp_inlines = self.given:format_text_case(name_part, context)
+ util.extend(inlines, dp_inlines)
end
- family = util.concat({ndp, family}, " ")
- if family_name_part then
- family = family_name_part:wrap_name_part(family, context)
+ end
+
+ if token == "given-dp-ndp" then
+ local name_part = name["non-dropping-particle"]
+ if name_part then
+ table.insert(inlines, PlainText:new(" "))
+ local ndp_inlines = self.family:format_text_case(name_part, context)
+ util.extend(inlines, ndp_inlines)
+ end
+ end
+
+ inlines = self.given:affixed(inlines)
+ return inlines
+end
+
+-- sort_LeadingApostropheOnNameParticle.txt
+-- "ā€™t " => "t"
+function Name:format_sort_particle(particle)
+ particle = string.gsub(particle, "^'", "")
+ particle = string.gsub(particle, "^ā€™", "")
+ return particle
+end
+
+function Name:render_particle(name, token, context)
+ local inlines = {}
+
+ local dp_part = name["dropping-particle"]
+ if dp_part then
+ dp_part = self:format_sort_particle(dp_part)
+ local dp_inlines = self.given:format_text_case(dp_part, context)
+ util.extend(inlines, dp_inlines)
+ end
+
+ if token == "dp-ndp" then
+ local ndp_part = name["non-dropping-particle"]
+ if ndp_part then
+ if #inlines > 0 then
+ table.insert(inlines, PlainText:new(" "))
end
- res = family
- else
- error(string.format('Invalid attribute form="%s" of "name".', form))
+ ndp_part = self:format_sort_particle(ndp_part)
+ local ndp_inlines = self.family:format_text_case(ndp_part, context)
+ util.extend(inlines, ndp_inlines)
+ end
+ end
+
+ return inlines
+end
+
+function Name:_check_delimiter(delimiter_attribute, num_first_names, inverted)
+ -- `delimiter-precedes-et-al` and `delimiter-precedes-last`
+ if delimiter_attribute == "always" then
+ return true
+ elseif delimiter_attribute == "never" then
+ return false
+ elseif delimiter_attribute == "contextual" then
+ if num_first_names > 1 then
+ return true
+ else
+ return false
+ end
+ elseif delimiter_attribute == "after-inverted-name" then
+ if inverted then
+ return true
+ else
+ return false
+ end
end
- return res, inverted
+ return false
end
-function Name:initialize (given, terminator, context)
+-- TODO: initialize name with markups
+-- name_InTextMarkupInitialize.txt
+-- name_InTextMarkupNormalizeInitials.txt
+function Name:initialize_name(given, with, initialize_with_hyphen)
if not given or given == "" then
return ""
end
- local initialize = context.options["initialize"]
- if context.options["initialize-with-hyphen"] == false then
+ if initialize_with_hyphen == false then
given = string.gsub(given, "-", " ")
end
@@ -367,9 +886,9 @@ function Name:initialize (given, terminator, context)
name_list[i-1] = name_list[i-1] .. " "
end
elseif is_abbreviation then
- name_list[i] = name .. terminator
+ name_list[i] = name .. with
else
- if initialize then
+ if self.initialize then
if util.is_upper(name) then
name = first_letter
else
@@ -389,7 +908,7 @@ function Name:initialize (given, terminator, context)
end
name = abbreviation
end
- name_list[i] = name .. terminator
+ name_list[i] = name .. with
else
name_list[i] = name .. " "
end
@@ -412,287 +931,176 @@ function Name:initialize (given, terminator, context)
end
-local NamePart = element.Element:new()
-
-function NamePart:format_name_part(name_part, context)
- context = self:process_context(context)
- local res = self:case(name_part, context)
- res = self:format(res, context)
- return res
-end
-
-function NamePart:wrap_name_part(name_part, context)
- context = self:process_context(context)
- local res = self:wrap(name_part, context)
- return res
-end
-
-
-local EtAl = element.Element:new()
-
-EtAl.default_options = {
- term = "et-al",
-}
-
-EtAl.render = function (self, context)
- self:debug_info(context)
- context = self:process_context(context)
- local res = self:get_term(context.options["term"]):render(context)
- res = self:format(res, context)
- return res
-end
-
-
-local Substitute = element.Element:new()
+function Name:join_person_name_irs(rendered_name_irs, and_term_ir, et_al_ir, use_last)
+ local first_items = rendered_name_irs
+ local last_item
+ if et_al_ir then
+ first_items = rendered_name_irs
+ last_item = et_al_ir
+ elseif #rendered_name_irs > 1 then
+ first_items = util.slice(rendered_name_irs, 1, #rendered_name_irs - 1)
+ last_item = rendered_name_irs[#rendered_name_irs]
+ end
-function Substitute:render (item, context)
- self:debug_info(context)
+ local irs = {}
- if context.suppressed_variables then
- -- true in layout, not in sort
- context.suppress_subsequent_variables = true
+ for i, person_name_ir in ipairs(first_items) do
+ if i > 1 then
+ table.insert(irs, Rendered:new({PlainText:new(self.delimiter)}, self))
+ end
+ table.insert(irs, person_name_ir)
end
- for i, child in ipairs(self:get_children()) do
- if child:is_element() then
- local result = child:render(item, context)
- if result and result ~= "" then
- return result
+ if last_item then
+ if use_last then
+ local delimiter = self.delimiter .. util.unicode["horizontal ellipsis"] .. " "
+ table.insert(irs, Rendered:new({PlainText:new(delimiter)}, self))
+ table.insert(irs, last_item)
+ elseif et_al_ir then
+ if #first_items > 0 then
+ local inverted = first_items[#first_items].is_inverted
+ local use_delimiter = self:_check_delimiter(self.delimiter_precedes_et_al, #first_items, inverted)
+ if use_delimiter then
+ table.insert(irs, Rendered:new({PlainText:new(self.delimiter)}, self))
+ elseif not et_al_ir.starts_with_cjk then
+ -- name_EtAlWithCombined.txt
+ table.insert(irs, Rendered:new({PlainText:new(" ")}, self))
+ end
+ table.insert(irs, last_item)
end
+ else
+ local inverted = first_items[#first_items].is_inverted
+ local use_delimiter = self:_check_delimiter(self.delimiter_precedes_last, #first_items, inverted)
+ if use_delimiter or not and_term_ir then
+ table.insert(irs, Rendered:new({PlainText:new(self.delimiter)}, self))
+ else
+ table.insert(irs, Rendered:new({PlainText:new(" ")}, self))
+ end
+ if and_term_ir and not et_al_ir then
+ table.insert(irs, and_term_ir)
+ end
+ table.insert(irs, last_item)
end
end
- return nil
-end
-
-local Names = element.Element:new()
-
-function Names:render (item, context)
- self:debug_info(context)
- context = self:process_context(context)
+ return irs
+end
- local names_delimiter = context.options["names-delimiter"]
- if names_delimiter then
- context.options["delimiter"] = names_delimiter
+-- For use in disambiguate-add-names
+function Name:expand_one_name(name_ir)
+ local rendered_name_irs = name_ir.person_name_irs
+ local hidden_name_irs = name_ir.hidden_name_irs
+ if #hidden_name_irs == 0 then
+ return nil
end
-
- -- Inherit attributes of parent `names` element
- local names_element = context.names_element
- if names_element then
- for key, value in pairs(names_element._attr) do
- context.options[key] = value
- end
- for key, value in pairs(self._attr) do
- context.options[key] = value
- end
+ local person_name_ir_to_add = hidden_name_irs[1]
+ if name_ir.use_last then
+ table.insert(rendered_name_irs, #rendered_name_irs, person_name_ir_to_add)
else
- context.names_element = self
- context.variable = context.options["variable"]
- end
-
- local name, et_al, label
- -- The position of cs:label relative to cs:name determines the order of
- -- the name and label in the rendered text.
- local label_position = nil
- for _, child in ipairs(self:get_children()) do
- if child:is_element() then
- local element_name = child:get_element_name()
- if element_name == "name" then
- name = child
- if label then
- label_position = "before"
- end
- elseif element_name == "et-al" then
- et_al = child
- elseif element_name == "label" then
- label = child
- if name then
- label_position = "after"
- end
- end
- end
+ table.insert(rendered_name_irs, person_name_ir_to_add)
end
- if label_position then
- context.label_position = label_position
- else
- label_position = context.label_position or "after"
+ table.remove(hidden_name_irs, 1)
+ if #hidden_name_irs == 0 then
+ if name_ir.et_al_abbreviation then
+ name_ir.et_al_abbreviation = false
+ end
+ if name_ir.use_last then
+ name_ir.use_last = false
+ end
end
- -- local name = self:get_child("name")
- if not name then
- name = context.name_element
+ local and_term_ir = name_ir.and_term_ir
+ local et_al_ir
+ if name_ir.et_al_abbreviation then
+ et_al_ir = name_ir.et_al_ir
end
- if not name then
- name = self:create_element("name", {}, self)
- Name:set_base_class(name)
- end
- context.name_element = name
+ local use_last = name_ir.use_last
- -- local et_al = self:get_child("et-al")
- if not et_al then
- et_al = context.et_al
- end
- if not et_al then
- et_al = self:create_element("et-al", {}, self)
- EtAl:set_base_class(et_al)
- end
- context.et_al = et_al
+ name_ir.children = self:join_person_name_irs(rendered_name_irs, and_term_ir, et_al_ir, use_last)
+ return person_name_ir_to_add
+end
- -- local label = self:get_child("label")
- if label then
- context.label = label
- else
- label = context.label
- end
-
- local sub_str = nil
- if context.mode == "bibliography" and not context.sorting then
- sub_str = context.options["subsequent-author-substitute"]
- -- if sub_str and #context.build.preceding_first_rendered_names == 0 then
- -- context.rendered_names = {}
- -- else
- -- sub_str = nil
- -- context.rendered_names = nil
- -- end
- end
-
- local variable_names = context.options["variable"] or context.variable
- local ret = nil
-
- if variable_names then
- local output = {}
- local num_names = 0
- for _, role in ipairs(util.split(variable_names)) do
- local names = self:get_variable(item, role, context)
-
- table.insert(context.variable_attempt, names ~= nil)
-
- if names then
- local res = name:render(names, context)
- if res then
- if type(res) == "number" then -- name[form="count"]
- num_names = num_names + res
- elseif label and not context.sorting then
- -- drop name label in sorting
- local label_result = label:render(role, context)
- if label_result then
- if label_position == "before" then
- res = richtext.concat(label_result, res)
- else
- res = richtext.concat(res, label_result)
- end
- end
- end
- end
- table.insert(output, res)
- end
- end
- if num_names > 0 then
- ret = tostring(num_names)
- else
- ret = self:concat(output, context)
- if ret and sub_str and context.build.first_rendered_names then
- ret = self:substitute_names(ret, context)
- end
- end
+-- [Name-part](https://docs.citationstyles.org/en/stable/specification.html#name-part-formatting)
+function NamePart:new(name)
+ local o = Element.new(self)
+ o.name = name
+ return o
+end
+
+function NamePart:from_node(node)
+ local o = NamePart:new()
+ o:set_attribute(node, "name")
+ o:set_formatting_attributes(node)
+ o:set_text_case_attribute(node)
+ o:set_affixes_attributes(node)
+ return o
+end
+
+function NamePart:format_text_case(text, context)
+ local output_format = context.format
+ local inlines = InlineElement:parse(text, context)
+ local is_english = context:is_english()
+ if not output_format then
+ print(debug.traceback())
end
+ output_format:apply_text_case(inlines, self.text_case, is_english)
- if ret then
- ret = self:format(ret, context)
- ret = self:wrap(ret, context)
- ret = self:display(ret, context)
- return ret
- else
- local substitute = self:get_child("substitute")
- if substitute then
- ret = substitute:render(item, context)
+ inlines = output_format:with_format(inlines, self.formatting)
+ return inlines
+end
+
+function NamePart:affixed(inlines)
+ if self.affixes then
+ if self.affixes.prefix then
+ table.insert(inlines, 1, PlainText:new(self.affixes.prefix))
end
- if ret and sub_str then
- ret = self:substitute_single_field(ret, context)
+ if self.affixes.suffix then
+ table.insert(inlines, PlainText:new(self.affixes.suffix))
end
- return ret
end
+ return inlines
end
-function Names:substitute_single_field(result, context)
- if not result then
- return nil
- end
- if context.build.first_rendered_names and #context.build.first_rendered_names == 0 then
- context.build.first_rendered_names[1] = result
- end
- result = self:substitute_names(result, context)
- return result
-end
-function Names:substitute_names(result, context)
- if not context.build.first_rendered_names then
- return result
- end
- local name_strings = {}
- local match_all
+-- [Et-al](https://docs.citationstyles.org/en/stable/specification.html#et-al)
+EtAl.term = "et-al"
- if #context.build.first_rendered_names > 0 then
- match_all = true
- else
- match_all = false
+function EtAl:from_node(node)
+ local o = EtAl:new()
+ o:set_attribute(node, "term")
+ o:set_formatting_attributes(node)
+ return o
+end
+
+function EtAl:build_ir(engine, state, context)
+ local term = context.locale:get_simple_term(self.term)
+ if not term then
+ return term
end
- for i, text in ipairs(context.build.first_rendered_names) do
- local str = text:render(context.engine.formatter, context)
- name_strings[i] = str
- if context.build.preceding_first_rendered_names and str ~= context.build.preceding_first_rendered_names[i] then
- match_all = false
- end
+ local inlines = InlineElement:parse(term, context)
+ if #inlines == 0 then
+ return nil
end
- if context.build.preceding_first_rendered_names then
- local sub_str = context.options["subsequent-author-substitute"]
- local sub_rule = context.options["subsequent-author-substitute-rule"]
+ inlines = context.format:with_format(inlines, self.formatting)
- if sub_rule == "complete-all" then
- if match_all then
- if sub_str == "" then
- result = nil
- else
- result.contents = {sub_str}
- end
- end
+ local ir = Rendered:new(inlines, self)
- elseif sub_rule == "complete-each" then
- -- In-place substitution
- if match_all then
- for _, text in ipairs(context.build.first_rendered_names) do
- text.contents = {sub_str}
- end
- result = self:concat(context.build.first_rendered_names, context)
- end
-
- elseif sub_rule == "partial-each" then
- for i, text in ipairs(context.build.first_rendered_names) do
- if name_strings[i] == context.build.preceding_first_rendered_names[i] then
- text.contents = {sub_str}
- else
- break
- end
- end
- result = self:concat(context.build.first_rendered_names, context)
-
- elseif sub_rule == "partial-first" then
- if name_strings[1] == context.build.preceding_first_rendered_names[1] then
- context.build.first_rendered_names[1].contents = {sub_str}
- end
- result = self:concat(context.build.first_rendered_names, context)
- end
+ if util.is_cjk_char(utf8.codepoint(term, 1)) then
+ ir.starts_with_cjk = true
end
- if #context.build.first_rendered_names > 0 then
- context.build.first_rendered_names = nil
- end
- context.build.preceding_first_rendered_names = name_strings
- return result
+ return ir
end
+function Substitute:from_node(node)
+ local o = Substitute:new()
+ o:process_children_nodes(node)
+ return o
+end
+
+
names_module.Names = Names
names_module.Name = Name
names_module.NamePart = NamePart