This module is used by Template:Q.
The intent of this module was to make a quotations library which can be built and used asynchronously. An editor can reference a work or author which is not yet coded, and the display will still look fine, and if the information is later coded, the template will make use of it at that time.
Each language needs its own data module, such as that found at Module:Quotations/grc/data, which is processed by the methods in the main module. Additional methods may be added for processing the data at, for example, Module:Quotations/grc, which is used as a starting point for building the language module. If this does not exist, the methods in the main module will still be available.
The data module should be a table, with entries for each author. Each author table should include information about the author, such as years active and the title of the Wikipedia article for the author, as well as tables for each of its works, with information such as the year the work was written, the title of its Wikipedia article, and the title of its Wiksource article. Alias tables can be used for convergence, such as when a work might have two common titles, as well as abbreviations.
Note that the module is designed to work with whatever data is available, and it should not be considered necessary to add all possible data; some is better than none.
Note: In order for a language module to be recognized, the corresponding language code must be added to the hasData
table in this module.
One of the more complex aspects of coding the data set lies in the reference link, which is meant to be a fairly dynamic link which formats itself to account for information given. It is formatted in the author's data table as a table of strings and tables. Strings not prefixed by a period are inserted as is. Strings prefixed with a period indicate variable references. Tables which begin with a function run that function with the table's following elements as parameters. Tables not beginning with a function are nested variable addresses. Take the reference link format table data.Plato.rlFormat2
, which is used solely by Plato's Republic:
{'s:el:', '.rlTitle', '/', {'.chapterSelect', {'authorData', 'republicChapters'}, '.ref1'}, '#p', '.ref1', {'.lower', '.ref2'} }
The first element, 's:el'
, is a simple string, and will be inserted as-is into the link target; it is the prefix for the Greek Wikisource, where a native language version of The Republic is found.
The second element, '.rlTitle'
, begins with a period, indicating that it should be replaced by the variable 'rlTitle'
, which is given in The Republic’s data table as 'Πολιτεία'
.
The third element, '/'
, is a standard string, and will be inserted into the link as-is.
The fourth element is a function call, using the function chapterSelect
, which is called with two parameters. The second parameter is the variable ref1
, which is the chapter given by the user. The first parameter is formatted as a nested reference, it will be the variable republicChapters
, which is found within data.Plato
.
There is no getting around the fact that it is not the easiest format in the world, but it does make for a powerful and flexible engine to interpret data and create the proper link.
If the source is not wikilinkable, then you'll need an external reference link instead of a reference link. You can use the same method as reference links, except use '.xrlFormat'
instead of '.rlFormat'
, and the URL to link to is the '.xurl'
parameter for the work. For example:
data.xrlFormat1 = {'.xurl'}
data.works = {
= { = 'c. 334–337', = 'https://archive.org/details/matheseoslibrivi02firmuoft', = 1}
}
-- Prevent substitution.
if mw.isSubsting() then
return require("Module:unsubst")
end
local export = {}
local debug_track_module = "Module:debug/track"
local foreign_numerals_module = "Module:foreign numerals"
local functions_module = "Module:fun"
local languages_module = "Module:languages"
local links_module = "Module:links"
local load_module = "Module:load"
local quotations_date_validation_module = "Module:Quotations/date validation"
local script_utilities_module = "Module:script utilities"
local string_nowiki_module = "Module:string/nowiki"
local string_utilities_module = "Module:string utilities"
local table_module = "Module:table"
local utilities_module = "Module:utilities"
local add_warning = mw.addWarning
local byte = string.byte
local concat = table.concat
local find = string.find
local floor = math.floor
local format = string.format
local insert = table.insert
local match = string.match
local rep = string.rep
local require = require
local sub = string.sub
local tonumber = tonumber
local tostring = tostring
local umatch = mw.ustring.match
local unpack = unpack or table.unpack -- Lua 5.2 compatibility
--[==[
Loaders for functions in other modules, which overwrite themselves with the target function when called. This ensures modules are only loaded when needed, retains the speed/convenience of locally-declared pre-loaded functions, and has no overhead after the first call, since the target functions are called directly in any subsequent calls.]==]
local function date_validation(...)
date_validation = require(quotations_date_validation_module).main
return date_validation(...)
end
local function format_categories(...)
format_categories = require(utilities_module).format_categories
return format_categories(...)
end
local function get_lang(...)
get_lang = require(languages_module).getByCode
return get_lang(...)
end
local function is_callable(...)
is_callable = require(functions_module).is_callable
return is_callable(...)
end
local function is_Latin_script(...)
is_Latin_script = require(script_utilities_module).is_Latin_script
return is_Latin_script(...)
end
local function keys_to_list(...)
keys_to_list = require(table_module).keysToList
return keys_to_list(...)
end
local function lang_err(...)
lang_err = require(languages_module).err
return lang_err(...)
end
local function language_link(...)
language_link = require(links_module).language_link
return language_link(...)
end
local function load_data(...)
load_data = require(load_module).load_data
return load_data(...)
end
local function nowiki(...)
nowiki = require(string_nowiki_module)
return nowiki(...)
end
local function remove_links(...)
remove_links = require(links_module).remove_links
return remove_links(...)
end
local function safe_require(...)
safe_require = require(load_module).safe_require
return safe_require(...)
end
local function split(...)
split = require(string_utilities_module).split
return split(...)
end
local function tag_text(...)
tag_text = require(script_utilities_module).tag_text
return tag_text(...)
end
local function tag_transcription(...)
tag_transcription = require(script_utilities_module).tag_transcription
return tag_transcription(...)
end
local function tag_translit(...)
tag_translit = require(script_utilities_module).tag_translit
return tag_translit(...)
end
local function to_Devanagari(...)
to_Devanagari = require(foreign_numerals_module).to_Devanagari
return to_Devanagari(...)
end
local function track(...)
track = require(debug_track_module)
return track(...)
end
local function ugsub(...)
ugsub = require(string_utilities_module).gsub
return ugsub(...)
end
local function ulower(...)
ulower = require(string_utilities_module).lower
return ulower(...)
end
local function uupper(...)
uupper = require(string_utilities_module).upper
return uupper(...)
end
local LanguageModule = {}
LanguageModule.__index = LanguageModule
LanguageModule.period = "."
local hasData = {
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
}
export.hasData = hasData
function export.create(frame)
return export.Create(frame:getParent().args, frame)
end
local function warn_about_unrecognized_args(unrecognized_args)
track("Quotations/param error")
add_warning('Unrecognized parameters in ' .. nowiki('{{Q}}: ' .. concat(keys_to_list(unrecognized_args), ', ')))
end
function export.Create(args, frame)
-- Set up our initial variables; set empty parameters to false
local processed_args = {}
local unrecognized_args = {}
local params = {
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true,
= true, -- This is simply ignored if quote is in Latin script.
= true, -- This is simply ignored if quote is in Latin script.
= true, -- This is simply ignored if quote is in Latin script.
= true,
}
local max_numbered_param = 4
for k, v in pairs(args) do
if type(k) == 'number' then
if k > max_numbered_param then
max_numbered_param = k
end
elseif not params then
unrecognized_args = v
end
if v == '' then
if k == "lang" then
processed_args = nil
else
processed_args = false
end
else
processed_args = v
end
end
if next(unrecognized_args) then
warn_about_unrecognized_args(unrecognized_args)
end
-- Ensure that all numbered parameters up to the greatest numbered parameter
-- are not nil.
for i = 1, max_numbered_param do
processed_args = processed_args or false
end
-- Apply aliases.
-- This should be cleaned up when more robust parameter parsing is implemented.
processed_args = processed_args or processed_args or processed_args
args = processed_args -- Overwrite original args.
local lang = args
lang = get_lang(lang) or lang_err(lang, 1)
local ante = {}
if hasData then
local m_langModule = LanguageModule.new(lang)
ante = m_langModule:expand(args)
else
track("Quotations/no data")
track("Quotations/no data/" .. lang:getCode())
end
if ante.author == nil then
ante.author = args
end
if ante.work == nil then
ante.work = args
end
if ante.ref == nil then
local ref = {}
for i = 4, 10 do
if args then
insert(ref, args)
else
break
end
end
ante.ref = concat(ref, '.')
end
for k in pairs(args) do
if type(k) ~= "number" then
ante = args
end
end
local penult = { = '', = '', = '', = '', = '', = '',
= '', = '', = {}, = '', = '',
= '', = '', = ''}
local comma = false
--Language specific modules are responsible for first line parameters.
--Base formatting module will poll for other parameters,
--pulling them only if the language module hasn't returned them.
local otherOtherLineStuff = {'quote', 'transyear', 'transauthor', 'trans', 'termlang'}
for _, item in ipairs(otherOtherLineStuff) do
ante = ante or args
end
if not ante.code then
penult.elAttr = ' class="wiktQuote" data-validation="white">'
else
penult.elAttr = ' class="wiktQuote" data-validation="' .. ante.code .. '">'
end
if ante.year then
penult.year = "'''" .. date_validation(ante.year) .. "'''"
comma = true
end
if ante.author then
penult.s1 = (comma and ', ' or '')
penult.author = ante.author
comma = true
end
if ante.work then
penult.s2 = (comma and ', ' or '')
penult.work = ante.work
comma = true
end
if ante.object then
penult.s5 = (comma and ' ' or '')
penult.object = '(' .. ante.object .. ')'
comma = true
end
if ante.ref and ante.ref ~= '' then
penult.s3 = (comma and ' ' or '')
penult.ref = ante.ref
end
if ante.style == 'no' or penult.work == '' then
-- Leave as-is
elseif ante.style == 'q' then
penult.work = '“' .. penult.work .. '”'
else
penult.work = "''" .. penult.work .. "''"
end
if ante.termlang then
ante.termlang = get_lang(ante.termlang) or lang_err(ante.termlang, 1)
penult.termlang = ' (in ' .. lang:getCanonicalName() .. ')'
end
local form = args or 'full'
local ultimate
if form == 'full' then
local categories = {}
if ante.notes then
penult.s4 = (comma and ', ' or '')
penult.notes = '(' .. ante.notes .. ')'
end
if ante.refn and frame then
penult.refn = frame:preprocess(ante.refn)
end
if ante.t then
ante.trans = ante.t
end
if ante.quote or (ante.trans and ante.trans ~= "-") then
penult.refn = ":" .. penult.refn
local translitwithtrans = false
insert(penult.otherLines, "<dl><dd>")
if ante.quote then
local sc = lang:findBestScript(ante.quote)
local quote = ante.quote
-- fix up links with accents/macrons/etc.
if find(quote, "[[", 1, true) then
quote = language_link{term = quote, lang = lang}
end
insert(penult.otherLines, tag_text(quote, lang, sc, nil, "e-quotation"))
if args.nocat then
track("Quotations/nocat")
elseif ante.termlang then
insert(categories, ante.termlang:getCanonicalName() .. " terms with quotations")
else
insert(categories, lang:getCanonicalName() .. " terms with quotations")
end
if not is_Latin_script(sc) or lang:getCode() == "egy" then
-- Handle subst=
local subbed_quote = remove_links(quote)
if args.subst then
local substs = split(args.subst, ",")
for _, subpair in ipairs(substs) do
local subsplit = split(subpair, find(subpair, "//", 1, true) and "//" or "/")
subbed_quote = ugsub(subbed_quote, subsplit, subsplit)
end
end
local transliteration = args.tr or (lang:transliterate(subbed_quote, sc))
if transliteration then
transliteration = "<dd>" .. tag_translit(transliteration, lang, "usex") .. "</dd>"
end
local transcription = args.ts and "<dd>/" .. tag_transcription(args.ts, lang, "usex") .. "/</dd>"
if transliteration or transcription then
local translitend = "</dl>"
if ante.trans and ante.trans ~= "-" and not ante.transyear and not ante.transauthor then
translitwithtrans = true
translitend = ""
end
insert(penult.otherLines, "<dl>" .. (transliteration or "") .. (transcription or "") .. translitend)
end
end
end
if ante.trans == "-" then
ante.trans = nil
insert(categories, "Omitted translation in the main namespace")
elseif ante.trans then
local litline = ""
if ante.lit then
litline = "<dd>(literally, “" .. ante.lit .. "”)</dd>"
end
if ante.transyear or ante.transauthor then
insert(penult.otherLines, "<ul><li>")
if ante.transyear then
insert(penult.otherLines, "'''" .. ante.transyear .. "''' translation")
else
insert(penult.otherLines, "Translation")
end
if ante.transauthor then
insert(penult.otherLines, " by " .. ante.transauthor)
end
insert(penult.otherLines, "<dl><dd>" .. ante.trans .. "</dd>" .. litline .. "</dl></li></ul>")
else
if not ante.quote then
insert(penult.otherLines, ante.trans)
else
local transstart = "<dl><dd>"
if translitwithtrans then
transstart = "<dd>"
end
insert(penult.otherLines, transstart .. ante.trans .. "</dd>" .. litline .. "</dl>")
end
end
elseif lang:getCode() ~= "en" and lang:getCode() ~= "und" then
insert(penult.otherLines, "<dl><dd><small>(please ] of this quotation)</small>")
-- add trreq category if translation is unspecified and language is not English or undetermined
insert(categories, "Requests for translations of " .. lang:getCanonicalName() .. " quotations")
end
insert(penult.otherLines, "</dd></dl>")
end
penult.otherLines = concat(penult.otherLines)
ultimate = '<div' .. penult.elAttr .. penult.year .. penult.s1 .. penult.author .. penult.s2 .. penult.work .. penult.termlang .. penult.s5 .. penult.object .. penult.s3 .. penult.ref .. penult.s4 .. penult.notes .. penult.refn .. penult.otherLines .. '</div>' .. format_categories(categories, lang)
elseif form == 'inline' then
ultimate = '<span' .. penult.elAttr .. penult.author .. penult.s2 .. penult.work .. penult.termlang .. penult.s5 .. penult.object .. penult.s3 .. penult.ref .. '</span>'
elseif form == 'work' then
ultimate = '<span' .. penult.elAttr .. penult.work .. penult.termlang .. penult.s5 .. penult.object .. penult.s3 .. penult.ref .. '</span>'
elseif form == 'ref' then
ultimate = '<span' .. penult.elAttr .. penult.ref .. '</span>'
end
return ultimate
end
function LanguageModule.new(lang)
local sema = safe_require("Module:Quotations/" .. lang:getCode()) or {}
sema.library = load_data("Module:Quotations/" .. lang:getCode() .. "/data")
return setmetatable(sema, LanguageModule)
end
function LanguageModule:changeCode(color)
if color == 'orange' then
self.code = 'orange'
end
if (color == 'yellow') and (self.code == 'green') then
self.code = 'yellow'
end
end
function LanguageModule:reroute(route)
local temp = {}
local data = self.library.data
for k, v in pairs(route) do
temp = self:interpret(v)
end
for k, v in pairs(temp) do
self = v
end
if self.author ~= nil and data then
self.aData = data
if self.work ~= nil and self.aData.works then
self.wData = self.aData.works
end
end
end
function LanguageModule:lower(input)
return input ~= nil and ulower(input) or nil
end
function LanguageModule:upper(input)
return input ~= nil and uupper(input) or nil
end
function LanguageModule:isLetter(input)
return not tonumber(input)
end
function LanguageModule:digits(width, num)
return format('%' .. width .. 'd', num)
end
function LanguageModule:numToDeva(input)
return input ~= nil and to_Devanagari(input) or nil
end
function LanguageModule:separ(values, separator)
return concat(values, separator)
end
function LanguageModule:roundDown(period, verse)
if tonumber(verse) then
return floor(verse / period) * period
end
self:changeCode('orange')
end
function LanguageModule:chapterSelect(rubric, verse)
verse = tonumber(match(verse, "^%d+"))
for k, v in pairs(rubric) do
if v <= verse and verse <= v then
return k
end
end
self:changeCode('orange')
end
function LanguageModule:interpret(item)
local output
if type(item) == 'string' then
if #item > 1 and byte(item) == 0x2E then
local address = sub(item, 2)
local returnable = self or self.library.data.Sundry and self.library.data.Sundry
output = returnable
else
output = item
end
elseif type(item) == 'table' then
--If it's a table, it's either a function call or a nested address.
local presumedFunction = self:interpret(item)
if is_callable(presumedFunction) then
local parameters = {}
for i = 2, 30 do
if item ~= nil then
insert(parameters, self:interpret(item))
else
break
end
end
output = presumedFunction(self, unpack(parameters))
else
local nested = self
for i = 1, 30 do
local address = item
if address and nested then
nested = nested
else
break
end
end
output = nested
end
else
output = item
end
return output
end
function LanguageModule:convert(scheme, initiate)
if is_callable(scheme) then
local initiate = tonumber(initiate) or initiate
local converted = scheme(self, initiate)
if converted == nil then
self:changeCode('orange')
end
return converted
elseif type(scheme) == "table" then
local initiate = tonumber(initiate) or initiate
local converted = scheme
if converted == nil then
self:changeCode('orange')
end
return converted
end
self:changeCode('orange')
end
function LanguageModule:numToRoman(item)
local j = tonumber(item)
if (j == nil) then
return item
end
if (j <= 0) then
return item
end
local ints = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}
local nums = {'M', 'CM', 'D', 'CD','C', 'XC','L','XL','X','IX','V','IV','I'}
local result = ""
for k = 1, #ints do
local count = floor(j / ints)
result = result .. rep(nums, count)
j = j - ints * count
end
return result
end
-- Iterate through "array" and its sublevels. Find indices in "array" that
-- contain a string matching "valToFind". Return last index where that string
-- (minus its first letter) is the key for a field in "self", as well as last
-- index where that string was found.
-- Used to locate the place where the "rlformat" should be skipped out of,
-- because there's no ".ref" value supplied for what comes next.
-- For instance, if book but not line number has been supplied.
local function findLastValidRefIndex(self, array, valToFind)
local lastValidIndex, lastIndex
for i, val in ipairs(array) do
if type(val) == 'table' then
local res1, res2 = findLastValidRefIndex(self, val, valToFind)
if res1 then
lastValidIndex = i
end
if res2 then
lastIndex = i
end
elseif type(val) == "string" and match(val, valToFind) then
lastIndex = i
if self then
lastValidIndex = i
end
end
end
return lastValidIndex, lastIndex
end
function LanguageModule:expand(args)
--Instantiate our variables.
local results = {}
self.code = 'green'
local data = self.library.data
self.author = args or args
self.work = args or args
for i = 1, 5 do
local refName = 'ref' .. i
local paramNumber = i + 3
self = args or args
end
--Check if we've been given an author alias.
if data.authorAliases then
self.author = data.authorAliases
end
if not data then
self:changeCode('yellow')
else
self.aData = data
if self.aData.reroute then
self:reroute(self.aData.reroute)
else
if self.aData.aliases and self.aData.aliases then
self.work = self.aData.aliases
end
if not (self.aData.works and self.aData.works) then
self:changeCode('yellow')
else
self.wData = self.aData.works
if self.wData.reroute then
self:reroute(self.wData.reroute)
end
end
end
end
--Load all author-level data.
if self.aData and self.aData.aLink then
results.author = ']'
else
results.author = self.author
end
if self.aData and self.aData.year then
results.year = self.aData.year
end
--If the database has a link for the work, incorporate it.
if not self.wData or not self.wData then
results.work = self.work
else
results.work = ' .. '|' .. self.work .. ']]'
end
--Some works have info which overrides the author-level info or fills other parameters.
if self.wData then
if self.wData then
results.year = self.wData.year
end
if self.wData ~= nil then
results.author = self.wData.author
end
if self.wData then
results.object = self.wData.object
end
if self.wData then
results.style = self.wData.style
end
if self.wData then
results.refn = self.wData.refn
end
end
self.thru = args or args
--Custom formatter for displayed reference
if self.aData and self.aData.rdFormat and self.aData.rdFormat.custom then
local formatted = {}
for _, current in ipairs(self.aData.rdFormat.custom) do
insert(formatted, self:interpret(current))
end
self.refDisplay = concat(formatted)
else
--The displayed reference usually consists of all the ref argument(s) joined with a period.
self.refDisplay = self.ref1 and '' or (self.wData and self.wData or false)
local separator_num = 1
for i = 1, 5 do
local whichRef = 'ref' .. tostring(i)
if self then
local ref = self
local separator
-- no separator before a letter
if umatch(ref, "^%a$") then
separator = ""
-- to allow colon between biblical chapter and verse
elseif self.aData and self.aData.rdFormat and self.aData.rdFormat.separator then
separator = self.aData.rdFormat.separator
elseif self.aData and self.aData.rdFormat and self.aData.rdFormat.separators then
separator = self.aData.rdFormat.separators
else
separator = "."
end
if i > 1 then
self.refDisplay = self.refDisplay .. separator
separator_num = separator_num + 1
end
self.refDisplay = self.refDisplay .. self
else
break
end
end
if self.thru then
self.refDisplay = self.refDisplay .. '–' .. self.thru
end
end
-- Apply custom work formatting, if specified
if self.wData and self.wData then
self.workFormat = self.aData
elseif self.aData and self.aData then
self.workFormat = self.aData
end
if self.workFormat then
results.style = 'no' -- Ignore/override style parameter
local formatted = {}
for _, current in ipairs(self.workFormat) do
insert(formatted, self:interpret(current))
end
results.work = concat(formatted)
end
--[[ If the work is not in the database,
or we don't have a source text link,
the ref is simply the display.
Otherwise, we have to create a reference link,
easily the most challenging function of this script. ]]
if self.wData and self.wData then
self.rlFormat = self.aData
elseif self.aData and self.aData then
self.rlFormat = self.aData
end
if self.wData and self.rlFormat then
self.rlTitle = self.wData
-- Go through indices in "self.rlFormat" that contain a string
-- beginning in ".ref" (either in the first level of "self.rlFormat"
-- or a sublevel). Return the index of the string that has a
-- corresponding field in "self", as well as the index of the last
-- such string.
local lastValidIndex, lastIndex = findLastValidRefIndex(self, self.rlFormat, '^%.ref(%d+)$')
-- If there isn't another ".ref" string after the last valid index,
-- then there is no need to cut short the rlFormat.
self.refLink = {}
if lastIndex and lastValidIndex and lastIndex > lastValidIndex then
for i, current in ipairs(self.rlFormat) do
if i > lastValidIndex then
break
end
insert(self.refLink, self:interpret(current))
end
else
for _, current in ipairs(self.rlFormat) do
insert(self.refLink, self:interpret(current))
end
end
self.refLink = concat(self.refLink)
end
if self.wData and self.wData then
self.xrlFormat = self.aData
elseif self.aData and self.aData then
self.xrlFormat = self.aData
end
if self.xrlFormat then
self.xurl = self.wData
-- Go through indices in "self.xrlFormat" that contain a string
-- beginning in ".ref" (either in the first level of "self.xrlFormat"
-- or a sublevel). Return the index of the string that has a
-- corresponding field in "self", as well as the index of the last
-- such string.
local lastValidIndex, lastIndex = findLastValidRefIndex(self, self.xrlFormat, '^%.ref(%d+)$')
-- If there isn't another ".ref" string after the last valid index,
-- then there is no need to cut short the rlFormat.
self.xrefLink = {}
if lastIndex and lastValidIndex and lastIndex > lastValidIndex then
for i, current in ipairs(self.xrlFormat) do
if i > lastValidIndex then
break
end
insert(self.xrefLink, self:interpret(current))
end
else
for _, current in ipairs(self.xrlFormat) do
insert(self.xrefLink, self:interpret(current))
end
end
self.xrefLink = concat(self.xrefLink)
end
if self.refLink and self.refDisplay then
results.ref = ']'
elseif self.xrefLink and self.refDisplay then
results.ref = ''
else
results.ref = self.refDisplay or ''
end
if args then
results.notes = args.notes
end
results.code = self.code
return results
end
return export