summaryrefslogtreecommitdiff
path: root/biblio/citation-style-language/citeproc-node-date.lua
diff options
context:
space:
mode:
Diffstat (limited to 'biblio/citation-style-language/citeproc-node-date.lua')
-rw-r--r--biblio/citation-style-language/citeproc-node-date.lua671
1 files changed, 388 insertions, 283 deletions
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