305 lines
9.8 KiB
Lua
305 lines
9.8 KiB
Lua
|
-- 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 }
|
||
|
}
|