Module:Citation
Documentation for this module may be created at Module:Citation/doc
--[[
Module:Citation
Core citation rendering module for RemiliaWiki
Architecture:
- Single render() function handles all citation types
- Citation types defined in TYPES configuration table
- Templates are thin wrappers that pass type and args
Usage from template:
{{#invoke:Citation|main|type=tweet}}
]]
local p = {}
local Archive = require('Module:Citation/Archive')
--------------------------------------------------------------------------------
-- CITATION TYPE CONFIGURATION
--------------------------------------------------------------------------------
local TYPES = {
tweet = {
-- Field mappings: internal name -> { display, parameter aliases }
authorFormat = 'handle', -- Use @handle (Author) format
handleParam = 'user',
handlePrefix = '@',
defaultWork = 'X',
contentParam = 'tweet',
screenshotDefault = 'link',
},
message = {
authorFormat = 'plain',
contentParam = 'message',
screenshotDefault = 'embed',
screenshotRequired = true,
locationFields = { 'server', 'channel', 'platform' },
},
web = {
authorFormat = 'standard',
titleParam = 'title',
screenshotDefault = 'link',
},
post = {
authorFormat = 'standard',
titleParam = 'title',
screenshotDefault = 'link',
showType = true, -- Show [Essay], [Newsletter] etc.
},
news = {
authorFormat = 'standard',
titleParam = 'title',
screenshotDefault = 'link',
},
video = {
authorFormat = 'standard',
titleParam = 'title',
titleItalic = true,
screenshotDefault = 'link',
showMedium = true,
showTime = true,
},
interview = {
authorFormat = 'interviewee',
titleParam = 'title',
screenshotDefault = 'link',
showMedium = true,
showTime = true,
},
book = {
authorFormat = 'standard',
titleParam = 'title',
titleItalic = true,
dateParam = 'year',
showEdition = true,
showPages = true,
showIsbn = true,
},
journal = {
authorFormat = 'standard',
titleParam = 'title',
dateParam = 'year',
showVolume = true,
showDoi = true,
showPmid = true,
},
}
--------------------------------------------------------------------------------
-- UTILITIES
--------------------------------------------------------------------------------
local function trim(s)
if not s then return nil end
return tostring(s):match('^%s*(.-)%s*$')
end
local function hasValue(s)
return s ~= nil and trim(s) ~= ''
end
local function getArg(args, ...)
for _, name in ipairs({...}) do
if hasValue(args[name]) then
return trim(args[name])
end
end
return nil
end
--------------------------------------------------------------------------------
-- FORMATTING HELPERS
--------------------------------------------------------------------------------
-- Format author based on type config
local function formatAuthor(args, config)
local format = config.authorFormat or 'standard'
if format == 'handle' then
-- @handle (Display Name)
local handle = getArg(args, config.handleParam or 'user')
local name = getArg(args, 'author')
local prefix = config.handlePrefix or ''
if hasValue(handle) then
local result = prefix .. handle
if hasValue(name) then
result = result .. ' (' .. name .. ')'
end
return result
elseif hasValue(name) then
return name
end
return nil
elseif format == 'interviewee' then
-- Name (interviewee)
local name = getArg(args, 'author', 'last')
if hasValue(args.last) and hasValue(args.first) then
name = args.last .. ', ' .. args.first
end
if hasValue(name) then
return name .. ' (interviewee)'
end
return nil
else
-- Standard: Last, First or Author
local author = getArg(args, 'author')
if hasValue(author) then
return author
end
local last = getArg(args, 'last')
local first = getArg(args, 'first')
if hasValue(last) then
if hasValue(first) then
return last .. ', ' .. first
end
return last
end
return nil
end
end
-- Format title with optional link and italics
local function formatTitle(title, url, italic)
if not hasValue(title) then return nil end
if italic then
if hasValue(url) then
return "[" .. url .. " ''" .. title .. "'']"
end
return "''" .. title .. "''"
else
if hasValue(url) then
return '[' .. url .. ' "' .. title .. '"]'
end
return '"' .. title .. '"'
end
end
-- Format work/publication (always italic)
local function formatWork(work)
if not hasValue(work) then return nil end
return "''" .. work .. "''"
end
-- Format screenshot based on display mode
local function formatScreenshot(filename, mode)
if not hasValue(filename) then return nil end
if mode == 'embed' then
return '[[File:' .. filename .. '|thumb|Screenshot]]'
elseif mode == 'hide' then
return nil
else -- 'link' is default
return '[[:File:' .. filename .. '|screenshot]]'
end
end
--------------------------------------------------------------------------------
-- MAIN RENDERER
--------------------------------------------------------------------------------
local function render(citationType, args)
local config = TYPES[citationType]
if not config then
return '<span class="error">Unknown citation type: ' .. tostring(citationType) .. '</span>'
end
local parts = {}
-- Author and date
local author = formatAuthor(args, config)
local date = getArg(args, config.dateParam or 'date', 'year')
if hasValue(author) then
if hasValue(date) then
table.insert(parts, author .. ' (' .. date .. ')')
else
table.insert(parts, author)
end
elseif hasValue(date) then
table.insert(parts, date)
end
-- Interviewer (for interviews)
local interviewer = getArg(args, 'interviewer')
if hasValue(interviewer) then
table.insert(parts, 'Interview with ' .. interviewer)
end
-- Title or content
local titleParam = config.titleParam or 'title'
local contentParam = config.contentParam
local url = getArg(args, 'url')
if hasValue(contentParam) and hasValue(args[contentParam]) then
-- Content-based citation (tweet, message)
local content = trim(args[contentParam])
if hasValue(url) then
table.insert(parts, '[' .. url .. ' "' .. content .. '"]')
else
table.insert(parts, '"' .. content .. '"')
end
elseif hasValue(args[titleParam]) then
-- Title-based citation
table.insert(parts, formatTitle(
trim(args[titleParam]),
url,
config.titleItalic
))
end
-- Chapter (for books)
local chapter = getArg(args, 'chapter')
if hasValue(chapter) then
table.insert(parts, '"' .. chapter .. '"')
end
-- Medium (for video/interview)
if config.showMedium then
local medium = getArg(args, 'medium')
if hasValue(medium) then
table.insert(parts, '[' .. medium .. ']')
end
end
-- Type (for posts: Essay, Newsletter, etc.)
if config.showType then
local contentType = getArg(args, 'type')
if hasValue(contentType) then
table.insert(parts, '[' .. contentType .. ']')
end
end
-- Work/publication
local work = getArg(args, 'work', 'website', 'blog', 'platform', 'newspaper', 'magazine', 'journal')
if hasValue(work) then
-- For journals, add volume/issue/pages inline
if config.showVolume then
local volume = getArg(args, 'volume')
local issue = getArg(args, 'issue')
local pages = getArg(args, 'pages')
local workPart = "''" .. work .. "''"
if hasValue(volume) then
workPart = workPart .. ' ' .. volume
end
if hasValue(issue) then
workPart = workPart .. ' (' .. issue .. ')'
end
if hasValue(pages) then
workPart = workPart .. ': ' .. pages
end
table.insert(parts, workPart)
else
table.insert(parts, formatWork(work))
end
elseif hasValue(config.defaultWork) then
table.insert(parts, formatWork(config.defaultWork))
end
-- Location fields (for messages: server, channel, platform)
if config.locationFields then
local server = getArg(args, 'server', 'group')
local channel = getArg(args, 'channel')
local platform = getArg(args, 'platform')
local loc = ''
if hasValue(server) then
loc = server
if hasValue(channel) then
loc = loc .. ', #' .. channel
end
end
if hasValue(platform) then
if loc ~= '' then
loc = loc .. ' [' .. platform .. ']'
else
loc = '[' .. platform .. ']'
end
end
if loc ~= '' then
table.insert(parts, loc)
end
end
-- Publisher
local publisher = getArg(args, 'publisher')
if hasValue(publisher) then
table.insert(parts, publisher)
end
-- Location (for books/news)
local location = getArg(args, 'location')
if hasValue(location) and not config.locationFields then
table.insert(parts, location)
end
-- Edition (for books)
if config.showEdition then
local edition = getArg(args, 'edition')
if hasValue(edition) then
table.insert(parts, '(' .. edition .. ' ed.)')
end
end
-- Pages (for books)
if config.showPages then
local pages = getArg(args, 'pages')
if hasValue(pages) then
table.insert(parts, 'pp. ' .. pages)
end
end
-- ISBN (for books)
if config.showIsbn then
local isbn = getArg(args, 'isbn')
if hasValue(isbn) then
table.insert(parts, 'ISBN ' .. isbn)
end
end
-- DOI (for journals)
if config.showDoi then
local doi = getArg(args, 'doi')
if hasValue(doi) then
table.insert(parts, 'doi:[https://doi.org/' .. doi .. ' ' .. doi .. ']')
end
end
-- PMID (for journals)
if config.showPmid then
local pmid = getArg(args, 'pmid')
if hasValue(pmid) then
table.insert(parts, 'PMID [https://pubmed.ncbi.nlm.nih.gov/' .. pmid .. '/ ' .. pmid .. ']')
end
end
-- Timestamp (for video)
if config.showTime then
local time = getArg(args, 'time')
if hasValue(time) then
table.insert(parts, 'at ' .. time)
end
end
-- Via
local via = getArg(args, 'via')
if hasValue(via) then
table.insert(parts, 'via ' .. via)
end
-- Access date
local accessDate = getArg(args, 'access-date')
if hasValue(accessDate) then
table.insert(parts, 'Retrieved ' .. accessDate)
end
-- Archives
local archives = Archive.extract(args)
local archiveText = Archive.render(archives)
if hasValue(archiveText) then
table.insert(parts, archiveText)
end
-- Screenshot
local screenshot = getArg(args, 'screenshot')
local screenshotMode = getArg(args, 'screenshot-display') or config.screenshotDefault or 'link'
if hasValue(screenshot) then
local screenshotText = formatScreenshot(screenshot, screenshotMode)
if hasValue(screenshotText) then
table.insert(parts, screenshotText)
end
elseif config.screenshotRequired then
table.insert(parts, "'''Screenshot needed'''")
end
-- Repost-at
local repostAt = getArg(args, 'repost-at')
if hasValue(repostAt) then
table.insert(parts, 'Reposted at [[' .. repostAt .. ']]')
end
-- Quote (highlighted excerpt)
local quote = getArg(args, 'quote')
if hasValue(quote) then
table.insert(parts, 'Quote: "' .. quote .. '"')
end
-- Join with periods
local result = table.concat(parts, '. ')
-- Ensure ends with period
if result ~= '' and result:sub(-1) ~= '.' then
result = result .. '.'
end
-- Clean up double periods
result = result:gsub('%.%.', '.')
return result
end
--------------------------------------------------------------------------------
-- TEMPLATE ENTRY POINTS
--------------------------------------------------------------------------------
-- Get args from frame (handles both direct and transcluded calls)
local function getArgs(frame)
local args = {}
local parent = frame:getParent()
if parent then
for k, v in pairs(parent.args) do
args[k] = v
end
end
for k, v in pairs(frame.args) do
args[k] = v
end
return args
end
-- Main entry point
-- Usage: {{#invoke:Citation|main|type=tweet}}
function p.main(frame)
local args = getArgs(frame)
local citationType = args.type or args[1] or 'web'
return render(citationType, args)
end
-- Direct entry points for each type (alternative to using type= parameter)
function p.tweet(frame) return render('tweet', getArgs(frame)) end
function p.message(frame) return render('message', getArgs(frame)) end
function p.web(frame) return render('web', getArgs(frame)) end
function p.post(frame) return render('post', getArgs(frame)) end
function p.news(frame) return render('news', getArgs(frame)) end
function p.video(frame) return render('video', getArgs(frame)) end
function p.interview(frame) return render('interview', getArgs(frame)) end
function p.book(frame) return render('book', getArgs(frame)) end
function p.journal(frame) return render('journal', getArgs(frame)) end
return p