Module:Citation

From Remilia Wiki
Jump to navigation Jump to search

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