-- -- Copyright (c) 2021-2022 Zeping Lee -- Released under the MIT license. -- Repository: https://github.com/zepinglee/citeproc-lua -- local names_module = {} local unicode = require("unicode") local richtext = require("citeproc-richtext") local element = require("citeproc-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 else return #names end end local output = nil local res = nil local inverted = false 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) end break 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 .. " " else output = richtext.concat(output, delimiter) 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) end if output then output = richtext.concat(output, res) else output = res end end end end local ret = self:format(output, context) ret = self:wrap(ret, context) return ret 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 end end return false 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" end local sort_separator = context.options["sort-separator"] local demote_non_dropping_particle = context.options["demote-non-dropping-particle"] -- TODO: make it a module local function _strip_quotes(str) if str then str = string.gsub(str, '"', "") str = string.gsub(str, "'", util.unicode["apostrophe"]) end return str 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 "" if family == "" then family = literal if family == "" then family = given given = "" end if family ~= "" then return family else error("Name not avaliable") end end if initialize_with then given = self:initialize(given, initialize_with, 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 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 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) end if family_name_part then family = family_name_part:format_name_part(family, context) ndp = family_name_part:format_name_part(ndp, context) 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 if demote_ndp then given = richtext.concat_list({given, dp, ndp}, " ") else family = richtext.concat_list({ndp, family}, " ") given = richtext.concat_list({given, dp}, " ") 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 order = {family, given, suffix} inverted = true else if family_name_part then family = family_name_part:format_name_part(family, context) ndp = family_name_part:format_name_part(ndp, context) 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 family = richtext.concat_list({dp, ndp, family}, " ") if name["comma-suffix"] then suffix_separator = ", " else suffix_separator = " " end family = richtext.concat_list({family, suffix}, suffix_separator) 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 order = {given, family} sort_separator = " " end res = richtext.concat_list(order, sort_separator) 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) end family = util.concat({ndp, family}, " ") if family_name_part then family = family_name_part:wrap_name_part(family, context) end res = family else error(string.format('Invalid attribute form="%s" of "name".', form)) end return res, inverted end function Name:initialize (given, terminator, context) if not given or given == "" then return "" end local initialize = context.options["initialize"] if context.options["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 util.is_lower(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 .. terminator else if initialize then if util.is_upper(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.utf8.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 .. terminator 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 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 Substitute:render (item, context) self:debug_info(context) if context.suppressed_variables then -- true in layout, not in sort context.suppress_subsequent_variables = true 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 end end end return nil end local Names = element.Element:new() function Names:render (item, context) self:debug_info(context) context = self:process_context(context) local names_delimiter = context.options["names-delimiter"] if names_delimiter then context.options["delimiter"] = names_delimiter 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 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 end if label_position then context.label_position = label_position else label_position = context.label_position or "after" end -- local name = self:get_child("name") if not name then name = context.name_element end if not name then name = self:create_element("name", {}, self) Name:set_base_class(name) end context.name_element = name -- 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 -- 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 end 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) end if ret and sub_str then ret = self:substitute_single_field(ret, context) end return ret end 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 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 names_module.Names = Names names_module.Name = Name names_module.NamePart = NamePart names_module.EtAl = EtAl names_module.Substitute = Substitute return names_module