Module:Quotations
- The following documentation is located at Module:Quotations/documentation. [edit]
- Useful links: subpage list • links • transclusions • testcases • sandbox
This module is used by Template:Q.
Coding
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.
Language modules
Each language needs its own module, such as that found at Module:Quotations/grc, which can be used as a starting point in creating a language module which does not yet exist. This module takes the information and does the lookups in its data module (in this case Module:Quotations/grc/data), and returns the results to be formatted by the main module.
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.
Reference link
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.
External Reference 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['Julius Firmicus Maternus'].xrlFormat1 = {'.xurl'}
data['Julius Firmicus Maternus'].works = {
['Matheseos Libri VIII'] = {['year'] = 'c. 334–337', ['xurl'] = 'https://archive.org/details/matheseoslibrivi02firmuoft', ['xrlFormat'] = 1}
}
Language-specific submodules
local m_scriptutils = require("Module:script utilities")
local date_validation = require("Module:Quotations/date validation")
local loadModule -- a forward declaration
local export = {}
local LanguageModule = {}
LanguageModule.__index = LanguageModule
local hasData = {
['ae'] = true,
['ar'] = true,
['axm'] = true,
['az'] = true,
['bra'] = true,
['chg'] = true,
['cy'] = true,
['egy'] = true,
['en'] = true,
['fa'] = true,
['fro'] = true,
['gmq-ogt'] = true,
['gmq-pro'] = true,
['grc'] = true,
['he'] = true,
['hi'] = true,
['hy'] = true,
['inc-apa'] = true,
['inc-mgu'] = true,
['inc-oaw'] = true,
['inc-ogu'] = true,
['inc-ohi'] = true,
['inc-opa'] = true,
['inc-pra'] = true,
['la'] = true,
['lzz'] = true,
['mt'] = true,
['mxi'] = true,
['oge'] = true,
['omr'] = true,
['ota'] = true,
['peo'] = true,
['pmh'] = true,
['sa'] = true,
['scn'] = true,
['sv'] = true,
['vah'] = true,
['xcl'] = true,
}
export.hasData = hasData
function export.create(frame)
local passed, results = pcall(function () return export.Create(frame:getParent().args) end)
if passed then
return results
else
--[[Special:WhatLinksHere/Wiktionary:Tracking/Quotations/error]]
require('Module:debug').track('Quotations/error')
return '<span class="wiktQuote previewonly error" data-validation="red">'..results..'</span>'
end
end
local function warn_about_unrecognized_args(unrecognized_args)
require('Module:debug').track('Quotations/param error')
mw.addWarning('Unrecognized parameters in '
.. mw.text.nowiki('{{Q}}: '
.. table.concat(require('Module:table').keysToList(unrecognized_args), ', ')))
end
function export.Create(args)
-- Set up our initial variables; set empty parameters to false
local processed_args = {}
local unrecognized_args = {}
local params = {
['thru'] = true,
['quote'] = true,
['trans'] = true,
['transauthor'] = true,
['transyear'] = true,
['t'] = true,
['lit'] = true,
['style'] = true,
['object'] = true,
['notes'] = true,
['refn'] = true,
['form'] = true,
['year'] = true,
['termlang'] = true,
['tr'] = true, -- This is simply ignored if quote is in Latin script.
['ts'] = true, -- This is simply ignored if quote is in Latin script.
['subst'] = true, -- This is simply ignored if quote is in Latin script.
['nocat'] = 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[k] then
unrecognized_args[k] = v
end
if v == '' then
if k == "lang" then
processed_args[k] = nil
else
processed_args[k] = false
end
else
processed_args[k] = 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[i] = processed_args[i] or false
end
args = processed_args -- Overwrite original args.
local lang = args[1]
lang = require("Module:languages").getByCode(lang) or require("Module:languages").err(lang, 1)
local ante = {}
if hasData[lang:getCode()] then
local m_langModule = LanguageModule.new(lang)
ante = m_langModule:expand(args)
else
require("Module:debug").track {
'Quotations/no data',
'Quotations/no data/' .. lang:getCode(),
}
end
if ante.author == nil then
ante.author = args[2]
end
if ante.work == nil then
ante.work = args[3]
end
if ante.ref == nil then
local ref = {}
for i = 4, 10 do
if args[i] then
table.insert(ref, args[i])
else
break
end
end
ante.ref = table.concat(ref, '.')
end
for k,v in pairs(args) do
if type(k) ~= 'number' then
ante[k] = args[k]
end
end
local penult = {['year'] = '', ['author'] = '', ['work'] = '', ['object'] = '', ['ref'] = '', ['termlang'] = '',
['notes'] = '', ['refn'] = '', ['otherLines'] = {}, ['s1'] = '', ['s2'] = '',
['s3'] = '', ['s4'] = '', ['s5'] = '', ['style1'] = '', ['style2'] = ''}
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[item] = ante[item] or args[item]
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.main(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 then
penult.s3 = (comma and ' ' or '')
penult.ref = ante.ref
end
if ante.style == 'no' or penult.work == '' then
penult.style1 = ''
penult.style2 = ''
elseif ante.style == 'q' then
penult.style1 = '“'
penult.style2 = '”'
else
penult.style1 = "''"
penult.style2 = "''"
end
if ante.termlang then
ante.termlang = require("Module:languages").getByCode(ante.termlang) or require("Module:languages").err(ante.termlang, 1)
penult.termlang = ' (in '..lang:getCanonicalName()..')'
end
local form = args['form'] or 'full'
local ultimate
if form == 'full' then
local categories = {}
local namespace = mw.title.getCurrentTitle().nsText
if ante.notes then
penult.s4 = (comma and ', ' or '')
penult.notes = '('..ante.notes..')'
end
if ante.refn then
penult.refn = 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
table.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 quote:find("[[", 1, true) then
quote = require("Module:links").language_link{term = quote, lang = lang}
end
table.insert(penult.otherLines, m_scriptutils.tag_text(quote, lang, sc, nil, "e-quotation"))
if (namespace == "" or namespace == "Reconstruction") and not args.nocat then
if ante.termlang then
table.insert(categories, ante.termlang:getCanonicalName() .. " terms with quotations")
else
table.insert(categories, lang:getCanonicalName() .. " terms with quotations")
end
end
if not m_scriptutils.is_Latin_script(sc) or lang:getCode() == "egy" then
-- Handle subst=
local subbed_quote = require("Module:links").remove_links(quote)
if args.subst then
local substs = mw.text.split(args.subst, ",")
for _, subpair in ipairs(substs) do
local subsplit = mw.text.split(subpair, mw.ustring.find(subpair, "//") and "//" or "/")
subbed_quote = mw.ustring.gsub(subbed_quote, subsplit[1], subsplit[2])
end
end
local transliteration = args.tr or (lang:transliterate(subbed_quote, sc))
if transliteration then
transliteration = "<dd>" .. m_scriptutils.tag_translit(transliteration, lang, "usex") .. "</dd>"
end
local transcription = args.ts and "<dd>/" .. m_scriptutils.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
table.insert(penult.otherLines, "<dl>" .. (transliteration or "") .. (transcription or "") .. translitend)
end
end
end
if ante.trans == "-" then
ante.trans = nil
table.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
table.insert(penult.otherLines, "<ul><li>")
if ante.transyear then
table.insert(penult.otherLines, "'''" .. ante.transyear .. "''' translation")
else
table.insert(penult.otherLines, "Translation")
end
if ante.transauthor then
table.insert(penult.otherLines, " by " .. ante.transauthor)
end
table.insert(penult.otherLines, "<dl><dd>" .. ante.trans .. "</dd>" .. litline .. "</dl></li></ul>")
else
if not ante.quote then
table.insert(penult.otherLines, ante.trans)
else
local transstart = "<dl><dd>"
if translitwithtrans then
transstart = "<dd>"
end
table.insert(penult.otherLines, transstart .. ante.trans .. "</dd>" .. litline .. "</dl>")
end
end
elseif lang:getCode() ~= "en" and lang:getCode() ~= "und" then
-- add trreq category if translation is unspecified and language is not English or undetermined
table.insert(categories, "Requests for translations of " .. lang:getCanonicalName() .. " quotations")
end
table.insert(penult.otherLines, "</dd></dl>")
end
penult.otherLines = table.concat(penult.otherLines)
ultimate = '<div'..penult.elAttr..penult.year..penult.s1..penult.author..penult.s2..penult.style1..penult.work..penult.termlang..penult.style2..penult.s5..penult.object..penult.s3..penult.ref..penult.s4..penult.notes..penult.refn..penult.otherLines..'</div>'..require("Module:utilities").format_categories(categories, lang)
elseif form == 'inline' then
ultimate = '<span'..penult.elAttr..penult.author..penult.s2..penult.style1..penult.work..penult.termlang..penult.style2..penult.s5..penult.object..penult.s3..penult.ref..'</span>'
elseif form == 'work' then
ultimate = '<span'..penult.elAttr..penult.style1..penult.work..penult.termlang..penult.style2..penult.s5..penult.object..penult.s3..penult.ref..'</span>'
elseif form == 'ref' then
ultimate = '<span'..penult.elAttr..penult.ref..'</span>'
end
return ultimate
end
local function add_self(func)
return function(self, ...)
return func(...)
end
end
function LanguageModule.new(lang)
local sema = require('Module:Quotations/' .. lang:getCode())
sema.library = mw.loadData("Module:Quotations/" .. lang:getCode() .. "/data")
setmetatable(sema, LanguageModule)
-- Have to insert unused self parameter or Lua–PHP interface complains about
-- table with boolean keys.
sema.lower = add_self(mw.ustring.lower)
sema.period = '.'
return sema
end
setmetatable(LanguageModule, { __index = function (self, key)
if key == 'numToIndian' then
-- This should only be loaded if needed so that [[Module:foreign numerals]]
-- is not transcluded on every page that [[Module:Quotations]] is.
local func = require('Module:foreign numerals').to_Indian
self.numToIndian = func
return func
end
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[k] = self:interpret(v)
end
for k, v in pairs(temp) do
self[k] = v
end
if self.author ~= nil and data[self.author] then
self.aData = data[self.author]
if self.work ~= nil and self.aData.works[self.work] then
self.wData = self.aData.works[self.work]
end
end
end
function LanguageModule:choose(choice, optionA, optionB)
optionB = optionB or ''
choice = self:interpret(choice)
local chosenPath = {}
if choice then
chosenPath = optionA
else
chosenPath = optionB
end
for j=1, 30 do
local innerCurrent = chosenPath[j]
if innerCurrent then
table.insert(self.refLink, self:interpret(current))
else
break
end
end
local ongoingDecision
decision = self:interpret(decision)
return decision
end
function LanguageModule:isLetter(input)
local isit = not tonumber(input)
return isit
end
function LanguageModule:digits(width, num)
local decimal = '%' .. width .. 'd'
return string.format(decimal, num)
end
function LanguageModule:separ(values, separator)
return table.concat(values, separator)
end
function LanguageModule:roundDown(period, verse)
if not tonumber(verse) then
self:changeCode('orange')
else
local rounded = math.floor(verse/period) * period
return rounded
end
end
function LanguageModule:chapterSelect(rubric, verse)
verse = tonumber(verse)
for k,v in pairs(rubric) do
if v[1] <= verse and verse <= v[2] then
return k
end
end
self:changeCode('orange')
end
function LanguageModule:interpret(item)
local output
if type(item) == 'string' then
if string.len(item) > 1 and string.sub(item, 1, 1) == '.' then
local address = string.sub(item, 2)
local returnable = self[address] or self.library.data.Sundry and self.library.data.Sundry[address]
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[1])
if type(presumedFunction) == 'function' then
local parameters = {}
for i = 2, 30 do
if item[i] ~= nil then
table.insert(parameters, self:interpret(item[i]))
else
break
end
end
output = presumedFunction(self, unpack(parameters))
else
local nested = self
for i = 1, 30 do
local address = item[i]
if address and nested then
nested = nested[address]
else
break
end
end
output = nested
end
else
output = item
end
return output
end
function LanguageModule:convert(scheme, initiate)
if type(scheme) == "table" then
local initiate = tonumber(initiate) or initiate
local converted = scheme[initiate]
if converted == nil then
self:changeCode('orange')
end
return converted
end
if type(scheme) == "function" then
local initiate = tonumber(initiate) or initiate
local converted = scheme(initiate)
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 = math.floor(j / ints[k])
result = result .. string.rep(nums[k], count)
j = j - ints[k]*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 val:find(valToFind) then
lastIndex = i
if self[val:sub(2)] 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
local ultimate = ''
self.author = args['author'] or args[2]
self.work = args['work'] or args[3]
for i = 1, 5 do
local refName = 'ref' .. i
local paramNumber = i + 3
self[refName] = args[refName] or args[paramNumber]
end
--Check if we've been given an author alias.
if data.authorAliases[self.author] then
self.author = data.authorAliases[self.author]
end
if not data[self.author] then
self:changeCode('yellow')
else
self.aData = data[self.author]
if self.aData.reroute then
self:reroute(self.aData.reroute)
else
if self.aData.aliases and self.aData.aliases[self.work] then
self.work = self.aData.aliases[self.work]
end
if not (self.aData.works and self.aData.works[self.work]) then
self:changeCode('yellow')
else
self.wData = self.aData.works[self.work]
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 = '[[w:'..self.aData.aLink..'|'..self.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['wLink'] then
results.work = self.work
else
results.work = '[[w:'..self.wData['wLink']..'|'..self.work..']]'
end
--Some works have info which overrides the author-level info or fills other parameters.
if self.wData then
if self.wData['year'] then
results.year = self.wData.year
end
if self.wData['author'] ~= nil then
results.author = self.wData.author
end
if self.wData['object'] then
results.object = self.wData.object
end
if self.wData['style'] then
results.style = self.wData.style
end
end
--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['refDefaultDisplay'] or false)
local separator_num = 1
for i = 1, 5 do
local whichRef = 'ref' .. tostring(i)
if self[whichRef] then
local ref = self[whichRef]
local separator
-- no separator before a letter
if mw.ustring.match(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[separator_num]
else
separator = "."
end
if i > 1 then
self.refDisplay = self.refDisplay .. separator
separator_num = separator_num + 1
end
self.refDisplay = self.refDisplay .. self[whichRef]
else
break
end
end
if args['through'] then
args['thru'] = args['through']
end
if args['thru'] then
self.refDisplay = self.refDisplay..'–'..args['thru']
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['rlFormat'] then
self.rlFormat = self.aData['rlFormat'..tostring(self.wData.rlFormat)]
if self.rlFormat then
self.rlTitle = self.wData['rlTitle']
-- 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.
local indexToStopAt
if lastIndex and lastValidIndex and lastIndex > lastValidIndex then
indexToStopAt = lastValidIndex
else
indexToStopAt = math.huge
end
self.refLink = {}
for i, current in ipairs(self.rlFormat) do
if i > indexToStopAt then
break
end
table.insert(self.refLink, self:interpret(current))
end
self.refLink = table.concat(self.refLink)
end
end
if self.wData and self.wData['xrlFormat'] then
self.xrlFormat = self.aData['xrlFormat'..tostring(self.wData.xrlFormat)]
if self.xrlFormat then
self.xurl = self.wData['xurl']
-- 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.
local indexToStopAt
if lastIndex and lastValidIndex and lastIndex > lastValidIndex then
indexToStopAt = lastValidIndex
else
indexToStopAt = math.huge
end
self.xrefLink = {}
for i, current in ipairs(self.xrlFormat) do
if i > indexToStopAt then
break
end
table.insert(self.xrefLink, self:interpret(current))
end
self.xrefLink = table.concat(self.xrefLink)
end
end
if self.refLink and self.refDisplay then
results.ref = '[['..self.refLink..'|'..self.refDisplay..']]'
elseif self.xrefLink and self.refDisplay then
results.ref = '['..self.xrefLink..' '..self.refDisplay..']'
else
results.ref = self.refDisplay or ''
end
if args['notes'] then
results.notes = args.notes
end
results.code = self.code
return results
end
return export