pandoc-lua-filters/centuries/centuries.lua

305 lines
9.8 KiB
Lua
Raw Permalink Normal View History

2022-02-08 21:27:34 +00:00
-- 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 }
}