From 1772be123f1cfa713b25548f6fec135e7ab339a3 Mon Sep 17 00:00:00 2001 From: Norbert Preining Date: Fri, 19 Aug 2022 03:00:56 +0000 Subject: CTAN sync 202208190300 --- .../citation-style-language/citeproc-node-date.lua | 671 ++++++++++++--------- 1 file changed, 388 insertions(+), 283 deletions(-) (limited to 'biblio/citation-style-language/citeproc-node-date.lua') diff --git a/biblio/citation-style-language/citeproc-node-date.lua b/biblio/citation-style-language/citeproc-node-date.lua index e10c1a9ac2..7ec1f439b8 100644 --- a/biblio/citation-style-language/citeproc-node-date.lua +++ b/biblio/citation-style-language/citeproc-node-date.lua @@ -6,376 +6,481 @@ local date_module = {} -local element = require("citeproc-element") +local Element = require("citeproc-element").Element +local IrNode = require("citeproc-ir-node").IrNode +local Rendered = require("citeproc-ir-node").Rendered +local SeqIr = require("citeproc-ir-node").SeqIr +local PlainText = require("citeproc-output").PlainText local util = require("citeproc-util") -local Date = element.Element:new() +-- [Date](https://docs.citationstyles.org/en/stable/specification.html#date) +local Date = Element:derive("date") -function Date:render (item, context) - self:debug_info(context) - context = self:process_context(context) +function Date:from_node(node) + local o = Date:new() - if context.sorting then - return self:render_sort_key(item, context) + o:set_attribute(node, "variable") + o:set_attribute(node, "form") + o:set_attribute(node, "date-parts") + + if o.form and not o.date_parts then + o.date_parts = "year-month-day" end - local variable_name = context.options["variable"] + o:get_delimiter_attribute(node) + o:set_formatting_attributes(node) + o:set_affixes_attributes(node) + o:set_display_attribute(node) + o:set_text_case_attribute(node) - local is_locale_date - if variable_name then - context.variable = variable_name - is_locale_date = false - else - variable_name = context.variable - is_locale_date = true + o.children = {} + o:process_children_nodes(node) + + for _, date_part in ipairs(o.children) do + o[date_part.name] = date_part end - local date = self:get_variable(item, variable_name, context) - if not date then - return nil + return o +end + +function Date:build_ir(engine, state, context) + local variable + if not state.suppressed[self.variable] then + variable = context:get_variable(self.variable) + end + + if not variable then + local ir = Rendered:new({}, self) + ir.group_var = "missing" + return ir end - local res = nil - local form = context.options["form"] - if form and not is_locale_date then - for _, date_part in ipairs(self:query_selector("date-part")) do - local name = date_part:get_attribute("name") - if not context.date_part_attributes then - context.date_part_attributes = {} + local ir + + if variable["date-parts"] and #variable["date-parts"] > 0 then + + -- TODO: move input normlization in one place + for i = 1, 2 do + if variable["date-parts"][i] then + for j = 1, 3 do + local variabel_part = variable["date-parts"][i][j] + if variabel_part == 0 or variabel_part == "" then + variable["date-parts"][i][j] = nil + else + variable["date-parts"][i][j] = tonumber(variabel_part) + end + end end - if not context.date_part_attributes[name] then - context.date_part_attributes[name] = {} + end + if variable["season"] and not variable["date-parts"][1][2] then + variable["date-parts"][1][2] = 20 + tonumber(variable["season"]) + end + + variable = variable["date-parts"] + if self.form then + ir = self:build_localized_date_ir(variable, engine, state, context) + else + ir = self:build_independent_date_ir(variable, engine, state, context) + end + ir.affixes = self.affixes + + elseif variable["literal"] then + local inlines = self:render_text_inlines(variable["literal"], context) + ir = Rendered:new(inlines, self) + ir.group_var = "important" + + elseif variable["raw"] then + local inlines = self:render_text_inlines(variable["raw"], context) + ir = Rendered:new(inlines, self) + ir.group_var = "important" + + end + + if not ir then + -- date_LiteralFailGracefullyIfNoValue.txt + ir = Rendered:new({}, self) + if context.sort_key then + ir.sort_key = false + end + ir.group_var = "missing" + return ir + end + + if ir.group_var == "important" then + -- Suppress substituted name variable + if state.name_override and not context.sort_key then + state.suppressed[self.variable] = true + end + end + + if context.sort_key then + ir.sort_key = self:render_sort_key(engine, state, context) + end + + return ir +end + +function Date:build_independent_date_ir(variable, engine, state, context) + -- else + -- local literal = variable["literal"] + -- if literal then + -- res = literal + -- else + -- local raw = variable["raw"] + -- if raw then + -- res = raw + -- end + -- end + + return self:build_date_parts(self.children, variable, self.delimiter, engine, state, context) +end + +function Date:build_localized_date_ir(variable, engine, state, context) + local date_part_mask = {} + for _, part in ipairs(util.split(self.date_parts or "year-month-day", "%-")) do + date_part_mask[part] = true + end + -- local date_parts = {} + -- for _, date_part in ipairs(self.children) do + -- date_parts[date_part.name] = date_part + -- end + local localized_date = context:get_localized_date(self.form) + local date_parts = {} + for _, date_part in ipairs(localized_date.children) do + if date_part_mask[date_part.name] then + date_part = date_part:copy() + local local_date_part = self[date_part.name] + if local_date_part then + local_date_part:override(date_part) end + table.insert(date_parts, date_part) + end + end + return self:build_date_parts(date_parts, variable, localized_date.delimiter, engine, state, context) +end - for attr, value in pairs(date_part._attr) do - if attr ~= name then - context.date_part_attributes[name][attr] = value - end +function Date:build_date_parts(date_parts, variable, delimiter, engine, state, context) + if #variable >= 2 then + return self:build_date_range(date_parts, variable, delimiter, engine, state, context) + elseif #variable == 1 then + return self:build_single_date(date_parts, variable[1], delimiter, engine, state, context) + end +end + +function Date:build_single_date(date_parts, single_date, delimiter, engine, state, context) + local irs = {} + for _, date_part in ipairs(date_parts) do + local part_ir = date_part:build_ir(single_date, engine, state, context) + table.insert(irs, part_ir) + end + + local ir = SeqIr:new(irs, self) + ir.delimiter = self.delimiter + + -- return Rendered:new(inlines, self) + return ir +end + +local date_part_index = { + year = 1, + month = 2, + day = 3, +} + +function Date:build_date_range(date_parts, variable, delimiter, engine, state, context) + local first, second = variable[1], variable[2] + local diff_level = 4 + for _, date_part in ipairs(date_parts) do + local part_index = date_part_index[date_part.name] + if first[part_index] and first[part_index] ~= second[part_index] then + if part_index < diff_level then + diff_level = part_index end end - res = self:get_locale_date(context, form):render(item, context) - else - if not date["date-parts"] or #date["date-parts"] == 0 then - local literal = date["literal"] - if literal then - res = literal + end + + local irs = {} + + local range_part_queue = {} + local range_delimiter + for i, date_part in ipairs(date_parts) do + local part_index = date_part_index[date_part.name] + if part_index == diff_level then + range_delimiter = date_part.range_delimiter or util.unicode["en dash"] + end + if first[part_index] then + if part_index >= diff_level then + table.insert(range_part_queue, date_part) else - local raw = date["raw"] - if raw then - res = raw + if #range_part_queue > 0 then + table.insert(irs, self:build_date_range_parts(range_part_queue, variable, + delimiter, engine, state, context, range_delimiter)) + range_part_queue = {} end - end - - else - if #date["date-parts"] == 1 then - res = self:_render_single_date(date, context) - elseif #date["date-parts"] == 2 then - res = self:_render_date_range(date, context) + table.insert(irs, date_part:build_ir(first, engine, state, context)) end end end + if #range_part_queue > 0 then + table.insert(irs, self:build_date_range_parts(range_part_queue, variable, + delimiter, engine, state, context, range_delimiter)) + end - table.insert(context.variable_attempt, res ~= nil) + local ir = SeqIr:new(irs, self) + ir.delimiter = delimiter - res = self:format(res, context) - res = self:wrap(res, context) - return res + return ir end -function Date:get_locale_date(context, form) - local date = nil - local style = context.style - local query = string.format("date[form=\"%s\"]", form) - for _, locale in ipairs(style:get_locales()) do - date = locale:query_selector(query)[1] - if date then - break +function Date:build_date_range_parts(range_part_queue, variable, delimiter, engine, state, context, range_delimiter) + local irs = {} + local first, second = variable[1], variable[2] + + local date_part_irs = {} + for i, diff_part in ipairs(range_part_queue) do + -- if delimiter and i > 1 then + -- table.insert(date_part_irs, PlainText:new(delimiter)) + -- end + if i == #range_part_queue then + table.insert(date_part_irs, diff_part:build_ir(first, engine, state, context, "suffix")) + else + table.insert(date_part_irs, diff_part:build_ir(first, engine, state, context)) end end - if not date then - error(string.format("Failed to find '%s'", query)) + local range_part_ir = SeqIr:new(date_part_irs, self) + range_part_ir.delimiter = delimiter + table.insert(irs, range_part_ir) + + table.insert(irs, Rendered:new({PlainText:new(range_delimiter)}, self)) + + date_part_irs = {} + for i, diff_part in ipairs(range_part_queue) do + if i == 1 then + table.insert(date_part_irs, diff_part:build_ir(second, engine, state, context, "prefix")) + else + table.insert(date_part_irs, diff_part:build_ir(second, engine, state, context)) + end end - return date + range_part_ir = SeqIr:new(date_part_irs, self) + range_part_ir.delimiter = delimiter + table.insert(irs, range_part_ir) + + local ir = SeqIr:new(irs, self) + + return ir end -function Date:render_sort_key (item, context) - local variable_name = context.options["variable"] - local date = self:get_variable(item, variable_name, context) - if not date or not date["date-parts"] then - return nil +function Date:render_sort_key(engine, state, context) + local date = context:get_variable(self.variable) + if not date then + return false end + if not date["date-parts"] then + if self.literal then + return "1" .. self.literal + else + return false + end + end + local show_parts = { year = false, month = false, day = false, } - if self:get_attribute("form") then - local date_parts = self:get_attribute("date-parts") or "year-month-day" - for _, dp_name in ipairs(util.split(date_parts, "%-")) do + if self.form then + for _, dp_name in ipairs(util.split(self.date_parts, "%-")) do show_parts[dp_name] = true end else - for _, child in ipairs(self:query_selector("date-part")) do - show_parts[child:get_attribute("name")] = true + for _, date_part in ipairs(self.children) do + show_parts[date_part.name] = true end end local res = "" - for _, date_parts in ipairs(date["date-parts"]) do + for _, range_part in ipairs(date["date-parts"]) do + if res ~= "" then + res = res .. "/" + end for i, dp_name in ipairs({"year", "month", "day"}) do - local value = date_parts[i] - if not value or not show_parts[dp_name] then - value = 0 + local value = 0 + if show_parts[dp_name] and range_part[i] then + value = range_part[i] end if i == 1 then res = res .. string.format("%05d", value + 10000) else - res = res .. string.format("%02d", value) + res = res .. "-" .. string.format("%02d", value) end end end return res end -function Date:_render_single_date (date, context) - local show_parts = self:_get_show_parts(context) - local output = {} - for _, child in ipairs(self:query_selector("date-part")) do - if show_parts[child:get_attribute("name")] then - table.insert(output, child:render(date, context)) - end +-- [Date-part](https://docs.citationstyles.org/en/stable/specification.html#date-part) +local DatePart = Element:derive("date-part") + +function DatePart:from_node(node) + local o = DatePart:new() + o:set_attribute(node, "name") + o:set_attribute(node, "form") + if o.name == "month" then + o:set_strip_periods_attribute(node) end - return self:concat(output, context) + o:set_formatting_attributes(node) + o:set_text_case_attribute(node) + o:set_attribute(node, "range-delimiter") + o:set_affixes_attributes(node) + return o end -function Date:_render_date_range (date, context) - local show_parts = self:_get_show_parts(context) - local part_index = {} - - local largest_diff_part = nil - for i, name in ipairs({"year", "month", "day"}) do - part_index[name] = i - local part_value1 = date["date-parts"][1][i] - if show_parts[name] and part_value1 then - if not largest_diff_part then - largest_diff_part = name - end - end +function DatePart:build_ir(single_date, engine, state, context, suppressed_affix) + local text + if self.name == "year" then + text = self:render_year(single_date[1], engine, state, context) + elseif self.name == "month" then + text = self:render_month(single_date[2], engine, state, context) + elseif self.name == "day" then + text = self:render_day(single_date[3], single_date[2], engine, state, context) end - local date_parts = {} - for _, date_part in ipairs(self:query_selector("date-part")) do - if show_parts[date_part:get_attribute("name")] then - table.insert(date_parts, date_part) - end + if not text then + local ir = Rendered:new({}, self) + ir.group_var = "missing" + return ir end - local diff_begin = 0 - local diff_end = #date_parts - local range_delimiter = nil - - for i, date_part in ipairs(date_parts) do - local name = date_part:get_attribute("name") - if name == largest_diff_part then - range_delimiter = date_part:get_attribute("range-delimiter") - if not range_delimiter then - range_delimiter = util.unicode["en dash"] - end - end - - local index = part_index[name] - local part_value1 = date["date-parts"][1][index] - local part_value2 = date["date-parts"][2][index] - if part_value1 and part_value1 ~= part_value2 then - if diff_begin == 0 then - diff_begin = i - end - diff_end = i - end + local inlines = {PlainText:new(text)} + local output_format = context.format + if not context.is_english then + print(debug.traceback()) end + local is_english = context:is_english() + output_format:apply_text_case(inlines, self.text_case, is_english) + inlines = output_format:with_format(inlines, self.formatting) - local same_prefix = {} - local range_begin = {} - local range_end = {} - local same_suffix = {} + local ir = Rendered:new(inlines, self) + ir.group_var = "important" - local no_suffix_context = self:process_context(context) - no_suffix_context.options["suffix"] = nil - - for i, date_part in ipairs(date_parts) do - local res = nil - if i == diff_end then - res = date_part:render(date, no_suffix_context, true) - else - res = date_part:render(date, context) - end - if i < diff_begin then - table.insert(same_prefix, res) - elseif i <= diff_end then - table.insert(range_begin, res) - table.insert(range_end, date_part:render(date, context, false, true)) - else - table.insert(same_suffix, res) - end + if self.name == "year" then + ir = SeqIr:new({ir}, self) + ir.is_year = true end - local prefix_output = self:concat(same_prefix, context) or "" - local range_begin_output = self:concat(range_begin, context) or "" - local range_end_output = self:concat(range_end, context) or "" - local suffix_output = self:concat(same_suffix, context) - local range_output = range_begin_output .. range_delimiter .. range_end_output - - local res = self:concat({prefix_output, range_output, suffix_output}, context) + ir.affixes = util.clone(self.affixes) + if ir.affixes and suppressed_affix then + ir.affixes[suppressed_affix] = nil + end - return res + return ir end -function Date:_get_show_parts (context) - local show_parts = {} - local date_parts = context.options["date-parts"] or "year-month-day" - for _, date_part in ipairs(util.split(date_parts, "%-")) do - show_parts[date_part] = true +function DatePart:render_day(day, month, engine, state, context) + if not day or day == "" then + return nil + end + day = tonumber(day) + if day < 1 or day > 31 then + return nil + end + local form = self.form or "numeric" + if form == "ordinal" then + local limit_day_1 = context.locale.style_options.limit_day_ordinals_to_day_1 + if limit_day_1 and day > 1 then + form = "numeric" + end + end + if form == "numeric-leading-zeros" then + return string.format("%02d", day) + elseif form == "ordinal" then + -- When the “day” date-part is rendered in the “ordinal” form, the ordinal + -- gender is matched against that of the month term. + local gender = context.locale:get_number_gender(string.format("month-%02d", month)) + local suffix = context.locale:get_ordinal_term(day, gender) + return tostring(day) .. suffix + else -- numeric + return tostring(day) end - return show_parts end - -local DatePart = element.Element:new() - -DatePart.render = function (self, date, context, last_range_begin, range_end) - self:debug_info(context) - context = self:process_context(context) - local name = context.options["name"] - local range_delimiter = context.options["range-delimiter"] or false - - -- The attributes set on cs:date-part elements of a cs:date with form - -- attribute override those specified for the localized date formats - if context.date_part_attributes then - local context_attributes = context.date_part_attributes[name] - if context_attributes then - for attr, value in pairs(context_attributes) do - context.options[attr] = value +function DatePart:render_month(month, engine, state, context) + if not month or month == "" then + return nil + end + month = tonumber(month) + if not month or month < 1 or month > 24 then + return nil + end + local form = self.form or "long" + local res + if form == "long" or form == "short" then + if month >= 1 and month <= 12 then + res = context:get_simple_term(string.format("month-%02d", month), form) + else + local season = month % 4 + if season == 0 then + season = 4 end + res = context:get_simple_term(string.format("season-%02d", season)) end + elseif form == "numeric-leading-zeros" then + res = string.format("%02d", month) + else + res = tostring(month) end + return self:apply_strip_periods(res) +end - if last_range_begin then - context.options["suffix"] = "" +function DatePart:render_year(year, engine, state, context) + if not year or year == "" then + return nil end - - local date_parts_index = 1 - if range_end then - date_parts_index = 2 + year = tonumber(year) + if year == 0 then + return nil end - - local res = nil - if name == "day" then - local day = date["date-parts"][date_parts_index][3] - if not day then - return nil - end - day = tonumber(day) - -- range open - if day == 0 then - return nil - end - local form = context.options["form"] or "numeric" - - if form == "ordinal" then - local option = self:get_locale_option("limit-day-ordinals-to-day-1") - if option and option ~= "false" and day > 1 then - form = "numeric" - end - end - if form == "numeric" then - res = tostring(day) - elseif form == "numeric-leading-zeros" then - -- TODO: day == nil? - if not day then - return nil - end - res = string.format("%02d", day) - elseif form == "ordinal" then - res = util.to_ordinal(day) + local form = self.form or "long" + if form == "long" then + if year < 0 then + return tostring(-year) .. context:get_simple_term("bc") + elseif year < 1000 then + return tostring(year) .. context:get_simple_term("ad") + else + return tostring(year) end + elseif form == "short" then + return string.sub(tostring(year), -2) + end +end - elseif name == "month" then - local form = context.options["form"] or "long" - - local month = date["date-parts"][date_parts_index][2] - if month then - month = tonumber(month) - -- range open - if month == 0 then - return nil +function DatePart:copy() + local o = {} + for key, value in pairs(self) do + if type(value) == "table" then + o[key] = {} + for k, v in pairs(value) do + o[key][k] = v end + else + o[key] = value end + end + setmetatable(o, DatePart) + return o +end - if form == "long" or form == "short" then - local term_name = nil - if month then - if month >= 1 and month <= 12 then - term_name = string.format("month-%02d", month) - elseif month >= 13 and month <= 24 then - local season = month % 4 - if season == 0 then - season = 4 - end - term_name = string.format("season-%02d", season) - else - util.warning("Invalid month value") - return nil - end - else - local season = date["season"] - if season then - season = tonumber(season) - term_name = string.format("season-%02d", season) - else - return nil - end - end - res = self:get_term(term_name, form):render(context) - elseif form == "numeric" then - res = tostring(month) - elseif form == "numeric-leading-zeros" then - -- TODO: month == nil? - if not month then - return nil - end - res = string.format("%02d", month) - end - res = self:strip_periods(res, context) - - elseif name == "year" then - local year = date["date-parts"][date_parts_index][1] - if year then - year = tonumber(year) - -- range open - if year == 0 then - return nil - end - local form = context.options["form"] or "long" - if form == "long" then - year = tonumber(year) - if year < 0 then - res = tostring(-year) .. self:get_term("bc"):render(context) - elseif year < 1000 then - res = tostring(year) .. self:get_term("ad"):render(context) - else - res = tostring(year) - end - elseif form == "short" then - res = string.sub(tostring(year), -2) +function DatePart:override(localized_date_part) + for key, value in pairs(self) do + if type(value) == "table" and localized_date_part[key] then + for k, v in pairs(value) do + localized_date_part[key][k] = v end + else + localized_date_part[key] = value end end - res = self:case(res, context) - res = self:format(res, context) - res = self:wrap(res, context) - res = self:display(res, context) - return res end -- cgit v1.2.3