-- centuries.lua -- A Pandoc Lua filter that automatically formats references to centuries. -- Copyright 2022 Bastien Dumont (bastien.dumont [at] posteo.net) -- This file is under the MIT License: see LICENSE for more details local ERROR_CONTENT_SPAN = 'The content of a span of class ".cty" must be a positive or negative ' .. 'number other than 0 possibly preceded by "+" or "-".' local eras = {} function eras:new(name, before, after, unspecified) self[name] = { before = before, after = after, unspecified = unspecified } end eras:new('christian', ' BC', ' AD', '') local default_config = { -- default_unit_form is one of 'sg', 'pl' or 'none' default_unit_form = 'sg', -- default_era, unit_sg and unit_pl are plain strings default_era = 'christian', unit_sg = ' century', unit_pl = ' centuries', -- numeral_type is one of 'roman', 'arabic', 'spelled_out' numeral_type = 'arabic', -- numeral_style is one of 'lowercase', 'uppercase', 'smallcaps' numeral_style = 'lowercase', -- ordinal_suffix_pos is one of 'normal', 'exponent' ordinal_suffix_pos = 'normal', --[[ ordinal_suffixes is a table of ordinal suffixes corresponding to their index number. If the requested number is superior to the length of the table or 0, the last entry will be used. --]] ordinal_suffixes = { 'rst', 'nd', 'rd', 'th' }, -- spelled_out_ordinals is an array of strings spelled_out_ordinals = { 'first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seventh', 'eighth', 'ninth', 'tenth', 'eleventh', 'twelfth', 'thirteenth', 'fourteenth', 'fifteenth', 'sixteenth', 'seventeenth', 'eighteenth', 'nineteenth', 'twentieth', 'twenty-first' }, -- number_part_for_ordinal_suffix is one of 'whole' or 'last_digit' number_part_for_ordinal_suffix = 'last_digit', -- parts_order is an array containing -- "number", "suffix", "unit" and "era" in any order parts_order = { "number", "suffix", "unit", "era" } } local config = {} local function restore_default_config(to_restore) if to_restore == nil then for key, value in pairs(default_config) do config[key] = value end else config[to_restore] = default_config[to_restore] end end local function metadata_to_config(key) return string.gsub(string.gsub(key, '^cty%-', ''), '%-', '_') end local function inlines_to_string(inlines) result = '' for i = 1, #inlines do if inlines[i].t == 'Str' then result = result .. inlines[i].text elseif inlines[i].t == 'Space' then result = result .. ' ' end end return result end local function metalist_to_configlist(metalist) -- metalist is a MetaList of MetaInlines containing Str and Space -- objects. -- Returns an array containing the values of the corresponding -- strings. configlist = {} for i = 1, #metalist do configlist[i] = inlines_to_string(metalist[i]) end return configlist end local function get_metamap_entry_value(entry) if entry[1] then return entry[1].text else return '' end end local function process_metadata_to_config(key, value) local config_key = metadata_to_config(key) if key == 'cty-ordinal-suffixes' then config.ordinal_suffixes = {} for i = 1, #value do table.insert(config.ordinal_suffixes, value[i][1].text) end elseif key == 'cty-other-eras' then for i = 1, #value do local this_era = value[i] eras:new( get_metamap_entry_value(this_era['name']), get_metamap_entry_value(this_era['before-zero']), get_metamap_entry_value(this_era['after-zero']), get_metamap_entry_value(this_era['unspecified']) ) end elseif key == 'cty-number-part-for-ordinal-suffix' or key == 'cty-numeral-type' then config[config_key] = metadata_to_config(value[1].text) elseif key == 'cty-parts-order' then config[config_key] = metalist_to_configlist(value) else config[config_key] = value[1].text end end local function set_config(meta) restore_default_config() for key, value in pairs(meta) do if string.match(key, '^cty%-') then process_metadata_to_config(key, value) end end end local function get_abs_number(number) return math.ceil(math.abs(tonumber(number))) end local function number_to_numeral(unformatted_century) if not tonumber(unformatted_century) or unformatted_century == '0' then error(ERROR_CONTENT_SPAN, 2) end local abs_century_number = get_abs_number(unformatted_century) local century_numeral if config.numeral_type == 'roman' then century_numeral = pandoc.utils.to_roman_numeral(abs_century_number) elseif config.numeral_type == 'arabic' then century_numeral = abs_century_number elseif config.numeral_type == 'spelled_out' then century_numeral = config.spelled_out_ordinals[abs_century_number] else error('"numeral_type" must be set either to "arabic", "roman" ' .. 'or "spelled_out". Here set to "' .. config.numeral_type .. '".') end return century_numeral end local function format_numeral(numeral) local formatted_numeral if config.numeral_style == 'lowercase' then formatted_numeral = pandoc.Str(string.lower(numeral)) elseif config.numeral_style == 'uppercase' then formatted_numeral = pandoc.Str(string.upper(numeral)) elseif config.numeral_style == 'smallcaps' then formatted_numeral = pandoc.SmallCaps(pandoc.Str(string.lower(numeral))) else error('"numeral_style" must be set either to "lowercase", "uppercase" ' .. 'or "smallcaps". Here set to "' .. config.numeral_style .. '".') end return formatted_numeral end local function format_century_number(unformatted_century) century_numeral = number_to_numeral(unformatted_century) formatted_century_number = format_numeral(century_numeral) return formatted_century_number end local function get_last_digit(number) return tonumber(string.sub(tostring(number), -1)) end local function get_ordinal_suffix_for_number(raw_number) if config.numeral_type == 'spelled_out' then return '' else local abs_number = get_abs_number(raw_number) return config.ordinal_suffixes[abs_number] or config.ordinal_suffixes[#config.ordinal_suffixes] end end local function get_raw_ordinal_suffix(unformatted_century) -- unformatted_century is an integer local raw_ordinal_suffix if config.number_part_for_ordinal_suffix == 'whole' then raw_ordinal_suffix = get_ordinal_suffix_for_number(unformatted_century) elseif config.number_part_for_ordinal_suffix == 'last_digit' then local the_last_digit = get_last_digit(unformatted_century) raw_ordinal_suffix = get_ordinal_suffix_for_number(the_last_digit) else error('"number_part_for_ordinal_suffix" must be set either to "whole" ' .. 'or to "last_digit". Here set to "' .. config.number_part_for_ordinal_suffix .. '".') end return raw_ordinal_suffix end local function format_ordinal_suffix(raw_ordinal_suffix) local formatted_ordinal_suffix if config.ordinal_suffix_pos == 'normal' then formatted_ordinal_suffix = pandoc.Str(raw_ordinal_suffix) elseif config.ordinal_suffix_pos == 'exponent' then formatted_ordinal_suffix = pandoc.Superscript(pandoc.Str(raw_ordinal_suffix)) else error('"ordinal_suffix_pos" must be set either to "normal" ' .. 'or to "exponent". Here set to "' .. config.ordinal_suffix_pos .. '".') end return formatted_ordinal_suffix end local function create_ordinal_suffix(unformatted_century) -- unformatted_century is an integer local raw_ordinal_suffix = get_raw_ordinal_suffix(unformatted_century) local formatted_ordinal_suffix = format_ordinal_suffix(raw_ordinal_suffix) return formatted_ordinal_suffix end local function format_unit(unit_form) local unit if unit_form == 'sg' then unit = config.unit_sg elseif unit_form == 'pl' then unit = config.unit_pl elseif unit_form == 'none' then unit = '' else error('Attribute "ctyunit" and "config.default_unit_form" must be set ' .. 'to either "sg", "pl" or "none". Here set to "' .. unit_form .. '".') end return pandoc.Str(unit) end local function get_era_indication(unformatted_century, era) local first_char = string.sub(unformatted_century, 1, 1) local era_indication if eras[era] == nil then error('Use of undefined era "' .. era .. '".') end if first_char == '+' then era_indication = eras[era].after elseif first_char == '-' then era_indication = eras[era].before else era_indication = eras[era].unspecified end return pandoc.Str(era_indication) end local function reorder_parts(parts_array) local part_name_to_value = { number = parts_array[1], suffix = parts_array[2], unit = parts_array[3], era = parts_array[4] } local reordered_array = {} for i = 1, #config.parts_order do reordered_array[i] = part_name_to_value[config.parts_order[i]] end return reordered_array end local function format_century(unformatted_century, unit_form, era) -- unformatted_century is an integer possibly preceded by '+' or '-' local century_number = format_century_number(unformatted_century) local ordinal_suffix = create_ordinal_suffix(unformatted_century) local unit = format_unit(unit_form) local era_indication = get_era_indication(unformatted_century, era) local formatted_century = reorder_parts( { century_number, ordinal_suffix, unit, era_indication }) return formatted_century end function Span(span) if span.classes:includes('cty', 1) then if #span.content == 1 and span.content[1].t == 'Str' then local unformatted_century = span.content[1].text local unit_form = span.attributes.ctyunit or config.default_unit_form local era = span.attributes.ctyera or config.default_era span.content = format_century(unformatted_century, unit_form, era) return span else error(ERROR_CONTENT_SPAN) end end end return { { Meta = set_config }, { Span = Span } }