Module:Cite
From the Super Mario Wiki, the Mario encyclopedia
Jump to navigationJump to search
This module formats citations for the {{cite}} template.
-- Module:Cite
-- Renders citations
-- Lua replacement for Template:Cite
local p = {}
---------------------------------------------------------------------------
-- Configuration
---------------------------------------------------------------------------
-- Quote marks by lowercased language code: { opening, closing }.
-- Closing marks carry the space that precedes the dash, except CJK ones.
local QUOTES = {}
local function setQuotes(codes, open, close)
for _, code in ipairs(codes) do
QUOTES[code] = { open, close }
end
end
setQuotes({ 'de' }, '„', '“ ')
setQuotes({ 'de-ch', 'el', 'es-es', 'fr-ch', 'it', 'pt-pt', 'ru' }, '«', '» ')
setQuotes({ 'fr', 'fr-ca' }, '« ', ' » ')
setQuotes({ 'ja', 'yue', 'zh', 'zh-hant', 'zh-hk', 'zh-tw' }, '「', '」')
local DEFAULT_QUOTES = { '"', '" ' }
-- Every parameter the template accepts; anything else is tracked.
local KNOWN_PARAMS = {}
for _, name in ipairs{
'quote', 'language', 'translation', 'author', 'date', 'title', 'url', 'login',
'format', 'timestamp', 'translator', 'publisher', 'location', 'isbn', 'page',
'tab', 'accessdate', 'website', 'ipaddress', 'deadlink', 'archive',
'archivedate', 'archiver',
} do
KNOWN_PARAMS[name] = true
end
local ARCHIVE_TODAY_TLDS = { 'today', 'is', 'ph', 'li', 'vn', 'md', 'fo' }
---------------------------------------------------------------------------
-- Text helpers
---------------------------------------------------------------------------
local function nonempty(s)
return s ~= nil and mw.text.trim(s) ~= ''
end
-- Joins the non-empty values with sep
local function joinNonEmpty(values, sep)
local out = {}
for _, v in ipairs(values) do
if nonempty(v) then
out[#out + 1] = v
end
end
return table.concat(out, sep)
end
-- Whether text already ends with . ? ! or … (looking past closing
-- italics/bold, HTML tags and <nowiki> content)
local function endsTerminal(s)
local check, prev = mw.text.unstripNoWiki(s), nil
while check ~= prev do
prev = check
check = check:gsub("'+$", ''):gsub('%s+$', ''):gsub('<[^<>]*>$', '')
end
return check:find('[.?!]$') ~= nil or check:find('…$') ~= nil
end
-- Adds a period unless the text already ends with terminal punctuation
local function terminate(s)
return endsTerminal(s) and s or (s .. '.')
end
-- Escapes single [ and ] so they can't end an external link's text early.
-- [[wikilinks]] are left alone: they are swapped out for the control
-- characters \1 and \2 (which never occur in wikitext) and back again.
local function escapeBrackets(s)
return (s:gsub('%[%[', '\1'):gsub('%]%]', '\2')
:gsub('%[', '['):gsub('%]', ']')
:gsub('\1', '[['):gsub('\2', ']]'))
end
---------------------------------------------------------------------------
-- URL helpers
---------------------------------------------------------------------------
-- Adds https:// to scheme-less URLs. Leading slashes are dropped first, so
-- protocol-relative "//host/…" and mistyped "/host/…" both work, and
-- "http:/" or "https:/" with a missing slash is repaired.
local function withScheme(url)
if url:find('^%a[%w+.-]*://') then
return url
end
if url:find('^[Hh][Tt][Tt][Pp][Ss]?:/') then
return (url:gsub('^(%a+):/+', '%1://'))
end
return 'https://' .. url:gsub('^/+', '')
end
-- URL without its scheme or any leading slashes
local function stripScheme(url)
return (url:gsub('^%a[%w+.-]*:/+', ''):gsub('^/+', ''))
end
-- Lowercased host name of a URL, without "www."
local function hostOf(url)
local host = stripScheme(url):match('^[^/?#:]*') or ''
return (host:lower():gsub('^www%.', ''))
end
---------------------------------------------------------------------------
-- Archives
---------------------------------------------------------------------------
-- Which archive service a URL belongs to: 'wayback', 'internet-archive',
-- 'megalodon', 'archive.today', or nil for any other site
local function archiveService(url)
local host = hostOf(url)
if host == 'archive.org' or host:find('%.archive%.org$') then
-- Snapshots live under /web/; anything else is an Internet Archive item
local snapshot = host == 'web.archive.org' or host == 'wayback.archive.org'
or url:find('archive%.org/web/') ~= nil
return snapshot and 'wayback' or 'internet-archive'
end
if host == 'megalodon.jp' then
return 'megalodon'
end
for _, tld in ipairs(ARCHIVE_TODAY_TLDS) do
if host == 'archive.' .. tld then
return 'archive.today'
end
end
return nil
end
-- Archive date read from the snapshot timestamp at the start of the URL's
-- path (after /web/ for the Wayback Machine), as in:
-- web.archive.org/web/20150304050607/… (also with id_, if_ etc.)
-- megalodon.jp/2019-0815-1020-30/… (Japan time)
-- archive.ph/2020.01.01-000000/…
-- A full timestamp is shown as "March 4, 2015, 05:06:07 UTC"; a partial one
-- as "March 4, 2015", "March 2015" or "2015" (only for known services, since
-- other sites often have years in their paths). Returns '' if there is none.
local function archiveDate(url, service)
local path = stripScheme(url):match('^[^/]*(/.*)$')
if not path then
return ''
end
local segment = path:match('^/web/([^/]*)') or path:match('^/([^/]*)')
if not segment or segment:find('*', 1, true) then
return '' -- a wildcard is a capture list, not a snapshot
end
local digits = segment:gsub('[.-]', ''):match('^%d+')
if not digits or #digits < 4 then
return ''
end
local lang = mw.getContentLanguage()
local function format(pattern, timestamp)
local ok, out = pcall(lang.formatDate, lang, pattern, timestamp)
return ok and out or ''
end
local y, m, d = digits:sub(1, 4), digits:sub(5, 6), digits:sub(7, 8)
if #digits >= 14 then
local stamp = digits:sub(1, 14)
if service == 'megalodon' then
stamp = stamp .. ' JST'
end
return format('F j, Y, H:i:s T', stamp)
elseif not service then
return ''
elseif #digits >= 8 then
return format('F j, Y', y .. '-' .. m .. '-' .. d)
elseif #digits >= 6 then
return format('F Y', y .. '-' .. m .. '-01')
end
return y
end
-- Name shown after "via" when no archiver is given
local function archiverName(frame, url, service)
if service == 'wayback' then
return 'Wayback Machine'
elseif service == 'internet-archive' then
return 'Internet Archive'
elseif service == 'megalodon' then
-- Megalodon also stores copies of archive.today snapshots
for _, tld in ipairs(ARCHIVE_TODAY_TLDS) do
if url:find('archive.' .. tld, 1, true) then
return 'Megalodon, from an archive.today snapshot'
end
end
return 'Megalodon'
elseif service == 'archive.today' then
return 'archive.today' .. frame:expandTemplate{ title = 'deprecated link' }
end
local host = hostOf(url)
return host ~= '' and host or url
end
---------------------------------------------------------------------------
-- Languages
---------------------------------------------------------------------------
-- English name for a known language code, or nil for an unknown one
local function languageName(code)
if not nonempty(code) then
return nil
end
local ok, name = pcall(mw.language.fetchLanguageName, code, 'en')
if ok and name and name:match('^[A-Z]') then
return name
end
return nil
end
---------------------------------------------------------------------------
-- Citation pieces
--
-- Each takes the citation context (see p._main) and returns its piece of
-- text, or nil when it has nothing to show.
---------------------------------------------------------------------------
-- "Quote" (translation) –
local function quotePart(c)
if not c.hasArg('quote') then
return nil
end
local quote = c.args.quote
local language = c.args.language
local lcLanguage = language and language:lower() or ''
-- Quotes that already open with a quote mark aren't wrapped again.
-- 《 counts as one except in Chinese, where it marks a title.
local base = lcLanguage:match('^[^-]*')
local openers = '"“”„「«»' .. ((base == 'yue' or base == 'zh') and '' or '《')
local quoted = mw.ustring.find(quote, '^[' .. openers .. ']') ~= nil
-- lang attribute: the given code if it's known (or blank), else English
local code = (language == '' or languageName(language)) and language or 'en'
local marks = QUOTES[lcLanguage] or DEFAULT_QUOTES
local s = (quoted and '' or marks[1])
.. '<span lang="' .. code .. '">' .. quote .. '</span>'
.. (quoted and ' ' or marks[2])
if c.hasArg('translation') then
s = s .. '("<span style="font-style:italic">' .. c.args.translation .. '</span>") '
end
return s .. '–'
end
-- Author (date).
local function authorPart(c)
if c.hasArg('author') and c.hasArg('date') then
return c.args.author .. ' (' .. c.args.date .. ').'
elseif c.hasArg('author') or c.hasArg('date') then
return terminate(c.arg('author') .. c.arg('date'))
end
return nil
end
-- [url Title] (requires log in) (format, timestamp).
-- For dead links the title links to the archive copy instead.
local function titlePart(c)
if not nonempty(c.arg('title') .. c.arg('url')) then
return nil
end
local target = c.deadlink and c.argOr('archive', c.arg('url')) or c.arg('url')
-- The period is skipped only if the piece ends with the title itself
local endsWithTitle = c.hasArg('title')
local s
if nonempty(target) then
local link = withScheme(target)
if c.hasArg('title') then
-- Link text can't span lines, so line breaks become spaces
local text = escapeBrackets(c.args.title):gsub('%s*\n%s*', ' ')
s = '[' .. link .. ' ' .. text .. ']'
else
s = link
end
if c.hasArg('login') then
s = s .. ' (requires log in)'
endsWithTitle = false
end
else
s = c.arg('title')
end
local details = joinNonEmpty({ c.arg('format'), c.arg('timestamp') }, ', ')
if details ~= '' then
s = s .. ' (' .. details .. ')'
endsWithTitle = false
end
if endsWithTitle and endsTerminal(c.args.title) then
return s
end
return s .. '.'
end
-- Translated by Translator.
local function translatorPart(c)
if c.hasArg('translator') then
return terminate('Translated by ' .. c.args.translator)
end
return nil
end
-- Location: Publisher (Language).
local function publisherPart(c)
local language = c.args.language
local languageText = c.hasArg('language') and (languageName(language) or language)
if c.hasArg('publisher') then
local s = '<span style="font-style:italic">' .. c.args.publisher .. '</span>'
if c.hasArg('location') then
s = c.args.location .. ': ' .. s
end
if languageText then
s = s .. ' (' .. languageText .. ')'
end
return terminate(s)
elseif languageText then
return terminate(languageText)
end
return nil
end
-- ISBN …. Page …. Tab ….
local function bookParts(c)
local parts = {}
if c.hasArg('isbn') then
local isbn = c.args.isbn
parts[#parts + 1] = 'ISBN [[Special:BookSources/' .. isbn .. '|' .. isbn .. ']].'
end
if c.hasArg('page') then
parts[#parts + 1] = terminate('Page ' .. c.args.page)
end
if c.hasArg('tab') then
parts[#parts + 1] = terminate('Tab ' .. c.args.tab)
end
return table.concat(parts, ' ')
end
-- Retrieved date from website. (live links only)
local function retrievalPart(c)
if c.deadlink or not (c.hasArg('accessdate') or nonempty