diff options
Diffstat (limited to 'Master/texmf-dist/scripts/citation-style-language/citeproc-node-names.lua')
-rw-r--r-- | Master/texmf-dist/scripts/citation-style-language/citeproc-node-names.lua | 696 |
1 files changed, 696 insertions, 0 deletions
diff --git a/Master/texmf-dist/scripts/citation-style-language/citeproc-node-names.lua b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-names.lua new file mode 100644 index 00000000000..9958e28999e --- /dev/null +++ b/Master/texmf-dist/scripts/citation-style-language/citeproc-node-names.lua @@ -0,0 +1,696 @@ +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 |