--
-- 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