-- -- Copyright (c) 2021-2024 Zeping Lee -- Released under the MIT license. -- Repository: https://github.com/zepinglee/citeproc-lua -- local names_module = {} local unicode local element local ir_node local output local util if kpse then unicode = require("citeproc-unicode") element = require("citeproc-element") ir_node = require("citeproc-ir-node") output = require("citeproc-output") util = require("citeproc-util") else unicode = require("citeproc.unicode") element = require("citeproc.element") ir_node = require("citeproc.ir-node") output = require("citeproc.output") util = require("citeproc.util") end local IrNode = ir_node.IrNode local NameIr = ir_node.NameIr local PersonNameIr = ir_node.PersonNameIr local SeqIr = ir_node.SeqIr local Rendered = ir_node.Rendered local GroupVar = ir_node.GroupVar local InlineElement = output.InlineElement local PlainText = output.PlainText local SortStringFormat = output.SortStringFormat local Element = element.Element local Position = util.Position ---@class Names: Element ---@field variable string ---@field delimiter string? ---@field name Name ---@field et_al EtAl ---@field label Label ---@field substitute Substitute local Names = Element:derive("names") ---@class Name: Element ---@field and string? ---@field delimiter string? ---@field delimiter_precedes_et_al string? ---@field delimiter_precedes_last string? ---@field et_al_min integer? ---@field et_al_use_first integer? ---@field et_al_subsequent_min integer? ---@field et_al_subsequent_use_first integer? ---@field et_al_subsequent_use_last integer? ---@field form string? ---@field initialize boolean? ---@field initialize_with string? ---@field name_as_sort_order string? ---@field sort_separator string? ---@field prefix string? ---@field suffix string? ---@field font_style string? ---@field font_variant string? ---@field font_weight string? ---@field text_decoration string? ---@field vertical_align string? ---@field family NamePart ---@field given NamePart 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 = ", ", }) ---@class NamePart: Element ---@field name string ---@field text_case string? ---@field prefix string? ---@field suffix string? local NamePart = Element:derive("name-part") ---@class EtAl: Element local EtAl = Element:derive("et-al") ---@class Substitute: Element 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 util.warning(string.format('Unknown element "%s".', 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 function Names:build_ir(engine, state, context) -- names_inheritance: names and name attributes inherited from cs:style -- and cs:citation 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 names_inheritance.variable = self.variable 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 >= Position.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) local index_by_variable = {} -- The names element may have an empty 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 ~= GroupVar.Missing then table.insert(irs, name_ir) index_by_variable[variable] = #irs end end end -- editor & translator local editor_ir = irs[index_by_variable.editor] local translator_ir = irs[index_by_variable.translator] if editor_ir and translator_ir then local editor_name_ir local editor_label_ir local translator_name_ir local translator_label_ir for _, ir in ipairs(editor_ir.children) do if ir._type == "NameIr" then editor_name_ir = ir elseif ir._element_name == "label" then editor_label_ir = ir end end for _, ir in ipairs(translator_ir.children) do if ir._type == "NameIr" then translator_name_ir = ir elseif ir._element_name == "label" then translator_label_ir = ir end end if editor_name_ir.full_name_str == translator_name_ir.full_name_str then local names = context:get_variable("editor") local editor_translator_label_ir = names_inheritance.name:build_name_label(names_inheritance.label, "editortranslator", names, context) if editor_translator_label_ir then local first_index = index_by_variable.editor local second_index = index_by_variable.translator if first_index > second_index then local tmp = first_index first_index = second_index second_index = tmp end table.remove(irs, second_index) for i, ir in ipairs(irs[first_index].children) do if ir._element_name == "label" then irs[first_index].children[i] = editor_translator_label_ir break end end 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 = GroupVar.Important ir = NameIr:new({ir}, self) ir.name_count = num_names ir.group_var = GroupVar.Important -- util.debug(ir) return ir end else if #irs > 0 then local ir = SeqIr:new(irs, self) ir.group_var = GroupVar.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 == GroupVar.Important or ir.group_var == GroupVar.Plain) then if not ir.person_name_irs or #ir.person_name_irs == 0 then -- In case of a in local name_count = ir.name_count ir = NameIr:new({ir}, self) ir.name_count = name_count -- sort_AguStyle.txt ir.group_var = GroupVar.Important end return ir end end end local ir = Rendered:new({}, self) ir.group_var = GroupVar.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 result.contents = {sub_str} end end 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 -- FIXME: Resolve the undefined concat() method. 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 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 end -- [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 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:build_name_label(label, variable, names, context) 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 not label_term or label_term == "" then return nil end local inlines = label:render_text_inlines(label_term, context) local label_ir = Rendered:new(inlines, label) return label_ir end 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 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 local et_al_abbreviation = self.et_al_min and self.et_al_use_first and #names >= self.et_al_min and #names > self.et_al_use_first 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 local ir = Rendered:new({PlainText:new(tostring(count))}, {}) ir.name_count = count ir.group_var = GroupVar.Important return ir end -- 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) local name_variants = person_name_ir.disam_variants if full_name_str ~= "" then full_name_str = full_name_str .. " " end 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 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 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 = GroupVar.Missing return ir else ir.group_var = GroupVar.Important end irs = {ir} if label then local label_ir = self:build_name_label(label, variable, names, context); if label_ir then if label.after_name then table.insert(irs, label_ir) else table.insert(irs, 1, label_ir) end end end 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) -- discretionary_ExampleSeveralAuthorsWithIntext.txt person_name_ir.formatting = self.formatting person_name_ir.affixes = self.affixes 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 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 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 return {"family", "wide-space", "dp-ndp", "wide-space", "given", "wide-space", "suffix"} else 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 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 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 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 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 #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 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 table.insert(inlines, PlainText:new(" ")) end table.insert(inlines, PlainText:new(suffix_part)) end end inlines = self.family:affixed(inlines) return inlines end function Name:render_given(name, token, context) local given = name.given if context.sort_key then -- The empty given name is needed for evaluate the sort key. if not given then return {PlainText:new("")} end -- Remove brackets for sorting: sort_NameVariable.txt given = string.gsub(given, "[%[%]]", "") end 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 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 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 false end -- 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 if initialize_with_hyphen == false then given = string.gsub(given, "-", " ") end -- Split the given name to name_list (e.g., {"John", "M." "E"}) -- Compound names are splitted too but are marked in punc_list. local name_list = {} local punct_list = {} local last_position = 1 for name, pos in string.gmatch(given, "([^-.%s]+[-.%s]+)()") do table.insert(name_list, string.match(name, "^[^-%s]+")) if string.match(name, "%-") then table.insert(punct_list, "-") else table.insert(punct_list, "") end last_position = pos end if last_position <= #given then table.insert(name_list, util.strip(string.sub(given, last_position))) table.insert(punct_list, "") end for i, name in ipairs(name_list) do local is_particle = false local is_abbreviation = false local first_letter = utf8.char(utf8.codepoint(name)) if unicode.islower(first_letter) then is_particle = true elseif #name == 1 then is_abbreviation = true else local abbreviation = string.match(name, "^([^.]+)%.$") if abbreviation then is_abbreviation = true name = abbreviation end end if is_particle then name_list[i] = name .. " " if i > 1 and not string.match(name_list[i-1], "%s$") then name_list[i-1] = name_list[i-1] .. " " end elseif is_abbreviation then name_list[i] = name .. with else if self.initialize then if unicode.isupper(name) then name = first_letter else -- Long abbreviation: "TSerendorjiin" -> "Ts." local abbreviation = "" for _, c in utf8.codes(name) do local char = utf8.char(c) local lower = unicode.lower(char) if lower == char then break end if abbreviation == "" then abbreviation = char else abbreviation = abbreviation .. lower end end name = abbreviation end name_list[i] = name .. with else name_list[i] = name .. " " end end -- Handle the compound names if i > 1 and punct_list[i-1] == "-" then if is_particle then -- special case "Guo-ping" name_list[i] = "" else name_list[i-1] = util.rstrip(name_list[i-1]) name_list[i] = "-" .. name_list[i] end end end local res = util.concat(name_list, "") res = util.strip(res) return res end 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 local irs = {} 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 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 irs end -- 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 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 table.insert(rendered_name_irs, person_name_ir_to_add) end 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 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 local use_last = name_ir.use_last 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 -- [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()) -- assert(output_format) -- end output_format:apply_text_case(inlines, self.text_case, is_english) 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 self.affixes.suffix then table.insert(inlines, PlainText:new(self.affixes.suffix)) end end return inlines end -- [Et-al](https://docs.citationstyles.org/en/stable/specification.html#et-al) EtAl.term = "et-al" 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 local inlines = InlineElement:parse(term, context) if #inlines == 0 then return nil end inlines = context.format:with_format(inlines, self.formatting) local ir = Rendered:new(inlines, self) if util.is_cjk_char(utf8.codepoint(term, 1)) then ir.starts_with_cjk = true end 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 names_module.EtAl = EtAl names_module.Substitute = Substitute return names_module