Module:Message

From Remilia Wiki
Jump to navigation Jump to search

Implementation for article and inline notices.


--------------------------------------------------------------------------------
-- Module:Message
-- Unified message box system for notices, stubs, banners, and inline tags.
--
-- INSTALLATION:
-- 1. Create page: Module:Message
-- 2. Paste this entire file
-- 3. Create TemplateStyles page: Module:Message/styles.css
--
-- USAGE:
-- {{#invoke:Message|stub}}
-- {{#invoke:Message|quality|unverified}}
-- {{#invoke:Message|ambox|type=notice|text=This is a notice}}
-- {{#invoke:Message|tag|cn|date=December 2025}}
--
-- @author Remilia Wiki
-- @license MIT
--------------------------------------------------------------------------------

local p = {}

--------------------------------------------------------------------------------
-- CONFIGURATION
--------------------------------------------------------------------------------

-- Message box types with their styling
local AMBOX_TYPES = {
    notice = {
        class = 'mbox-notice',
        icon = 'Information icon',
        defaultImage = 'Information icon4.svg',
    },
    warning = {
        class = 'mbox-warning',
        icon = 'Warning icon',
        defaultImage = 'Ambox warning orange.svg',
    },
    serious = {
        class = 'mbox-serious',
        icon = 'Serious warning',
        defaultImage = 'Ambox warning red.svg',
    },
    content = {
        class = 'mbox-content',
        icon = 'Content issue',
        defaultImage = 'Edit-clear.svg',
    },
    style = {
        class = 'mbox-style',
        icon = 'Style issue',
        defaultImage = 'Edit-find-replace.svg',
    },
    delete = {
        class = 'mbox-delete',
        icon = 'Deletion',
        defaultImage = 'Ambox deletion.svg',
    },
    protection = {
        class = 'mbox-protection',
        icon = 'Protection',
        defaultImage = 'Padlock.svg',
    },
}

-- Article quality review states
-- Note: 'verified' is invisible to readers but detectable as a review marker
local QUALITY_LEVELS = {
    unverified = {
        class = 'quality-unverified',
        icon = 'Ambox warning orange.svg',
        title = 'Unverified',
        text = 'This article has not yet been reviewed for factual accuracy by editors. Check details against cited sources.',
        visible = true,
    },
    wip = {
        class = 'quality-wip',
        icon = 'Information icon4.svg',
        title = 'Work in progress',
        text = 'This article is undergoing revision or fact-checking. Details may change as editors review sources.',
        visible = true,
    },
    verified = {
        class = 'quality-verified',
        icon = 'Symbol confirmed.svg',
        title = 'Verified',
        text = 'This article has been reviewed and accepted by editors.',
        visible = false, -- Invisible to readers, but LLMs can detect the marker
    },
}

-- Inline tags
local TAGS = {
    cn = {
        text = 'citation needed',
        link = 'Wikipedia:Citation needed',
        category = 'Articles with unsourced statements',
    },
    ['citation needed'] = {
        text = 'citation needed',
        link = 'Wikipedia:Citation needed',
        category = 'Articles with unsourced statements',
    },
    clarify = {
        text = 'clarification needed',
        link = 'Wikipedia:Please clarify',
        category = 'Wikipedia articles needing clarification',
    },
    when = {
        text = 'when?',
        link = 'Wikipedia:Manual of Style/Dates and numbers',
        category = 'Articles needing additional references',
    },
    who = {
        text = 'who?',
        link = 'Wikipedia:Manual of Style/Words to watch',
        category = 'Articles with unsourced statements',
    },
    where = {
        text = 'where?',
        link = 'Wikipedia:Citing sources',
        category = 'Articles with unsourced statements',
    },
    update = {
        text = 'needs update',
        link = 'Wikipedia:Updating information',
        category = 'Articles containing potentially dated statements',
    },
}

--------------------------------------------------------------------------------
-- UTILITY FUNCTIONS
--------------------------------------------------------------------------------

local function trim(s)
    if not s then return nil end
    return tostring(s):match("^%s*(.-)%s*$")
end

local function isEmpty(s)
    if s == nil then return true end
    if type(s) == 'string' then
        return trim(s) == ''
    end
    return false
end

local function hasValue(s)
    return not isEmpty(s)
end

local function getArg(args, name, default)
    local val = args[name]
    if isEmpty(val) then return default end
    return trim(val)
end

local function getFrameArgs(frame)
    return require('Module:Arguments').getArgs(frame, {
        trim = false,
        removeBlanks = false,
    })
end

local yesno = require('Module:Yesno')

--------------------------------------------------------------------------------
-- MESSAGE BOX (AMBOX)
--------------------------------------------------------------------------------

function p.ambox(frame)
    local args = getFrameArgs(frame)

    local boxType = getArg(args, 'type', 'notice')
    local typeConfig = AMBOX_TYPES[boxType] or AMBOX_TYPES.notice

    local text = getArg(args, 'text') or getArg(args, 1)
    if isEmpty(text) then
        return '<span class="error">Error: Message text required</span>'
    end

    local image = getArg(args, 'image')
    local imageContent = getArg(args, 'image_content')
    local imageSize = getArg(args, 'imagesize', '40px')
    local small = yesno(getArg(args, 'small'), false)
    local plainlinks = yesno(getArg(args, 'plainlinks'), true)

    -- Build the message box
    local box = mw.html.create('div')
        :addClass('mbox')
        :addClass(typeConfig.class)

    if small then
        box:addClass('mbox-small')
    end
    if plainlinks then
        box:addClass('plainlinks')
    end

    -- Image cell
    if hasValue(imageContent) then
        box:tag('div')
            :addClass('mbox-image')
            :wikitext(imageContent)
    elseif image ~= 'none' then
        local imageFile = image or typeConfig.defaultImage
        if hasValue(imageFile) then
            box:tag('div')
                :addClass('mbox-image')
                :wikitext('[[File:' .. imageFile .. '|' .. imageSize .. '|link=|alt=]]')
        end
    end

    -- Text cell
    box:tag('div')
        :addClass('mbox-text')
        :wikitext(text)

    return tostring(box)
end

--------------------------------------------------------------------------------
-- ARTICLE QUALITY BANNER
--------------------------------------------------------------------------------

function p.quality(frame)
    local args = getFrameArgs(frame)

    local level = getArg(args, 1) or getArg(args, 'level') or 'unverified'
    local config = QUALITY_LEVELS[level]

    if not config then
        return '<span class="error">Error: Unknown quality level "' .. level .. '". Use: unverified, wip, or verified.</span>'
    end

    -- For invisible quality markers (verified), output a hidden span
    -- LLMs can detect this in the page source, but readers don't see it
    if not config.visible then
        local marker = mw.html.create('span')
            :addClass('quality-marker')
            :addClass(config.class)
            :attr('data-quality', level)
            :css('display', 'none')
            :wikitext('Article quality: ' .. level)
        return tostring(marker)
    end

    -- For visible banners (unverified, wip), show the full message
    local box = mw.html.create('div')
        :addClass('quality-banner')
        :addClass(config.class)

    -- Icon
    box:tag('div')
        :addClass('quality-icon')
        :wikitext('[[File:' .. config.icon .. '|40px|link=|alt=]]')

    -- Text
    local textDiv = box:tag('div')
        :addClass('quality-text')

    textDiv:tag('strong'):wikitext(config.title .. ':')
    textDiv:wikitext(' ' .. config.text)

    return tostring(box)
end

--------------------------------------------------------------------------------
-- STUB NOTICE
--------------------------------------------------------------------------------

function p._stub(args)
    local stubType = getArg(args, 1) or getArg(args, 'type')
    local icon = getArg(args, 'icon', 'Ambox stub.svg')

    local box = mw.html.create('div')
        :addClass('stub-box')
        :attr('role', 'note')

    -- Icon
    if args.image_content ~= false then
        box:tag('div')
            :addClass('stub-icon')
            :wikitext(args.image_content or ('[[File:' .. icon .. '|40px|link=|alt=]]'))
    end

    -- Text
    local textDiv = box:tag('div')
        :addClass('stub-text')

    textDiv:tag('strong'):wikitext(args.text or 'This article is a stub.')
    textDiv:wikitext(' You can help Remilia Wiki by ')
    textDiv:wikitext('[' .. tostring(mw.title.getCurrentTitle():fullUrl('action=edit')) .. ' expanding it].')
    if hasValue(args.note) then
        textDiv:tag('div'):addClass('stub-note'):wikitext(args.note)
    end

    -- Category
    local category = 'Stubs'
    if hasValue(stubType) then
        category = stubType .. ' stubs'
    end

    local categories = ''
    if mw.title.getCurrentTitle().namespace == 0 and not require('Module:Yesno')(args.nocat, false) then
        categories = args.categories or ('[[Category:' .. category .. ']]')
    end
    return tostring(box) .. categories
end

function p.stub(frame)
    return p._stub(getFrameArgs(frame))
end

--------------------------------------------------------------------------------
-- INLINE TAGS (Citation needed, etc.)
--------------------------------------------------------------------------------

local function renderTag(args, tagName)
    local config = TAGS[tagName:lower()]

    if not config then
        return '<span class="error">[unknown tag: ' .. tagName .. ']</span>'
    end

    local date = getArg(args, 'date')
    local reason = getArg(args, 'reason')

    -- Build tooltip title
    local title = 'This claim needs references to reliable sources.'
    if hasValue(date) then
        title = title .. ' (' .. date .. ')'
    end
    if hasValue(reason) then
        title = title .. ' Reason: ' .. reason
    end

    local tag = mw.html.create('sup')
        :addClass('noprint')
        :addClass('inline-tag')

    tag:wikitext('&#91;')
    tag:tag('i')
        :wikitext('[[' .. config.link .. '|')
        :tag('span')
            :attr('title', title)
            :wikitext(config.text)
        :done()
        :wikitext(']]')
    tag:wikitext('&#93;')

    -- Category
    local category = config.category
    if hasValue(date) then
        category = category .. ' from ' .. date
    end

    return tostring(tag) .. '[[Category:' .. category .. ']]'
end

function p.tag(frame)
    local args = getFrameArgs(frame)
    return renderTag(args, getArg(args, 1) or 'cn')
end

-- Convenience aliases
function p.cn(frame)
    return renderTag(getFrameArgs(frame), 'cn')
end

function p.citationNeeded(frame)
    return p.cn(frame)
end

function p.clarify(frame)
    return renderTag(getFrameArgs(frame), 'clarify')
end

function p.when(frame)
    return renderTag(getFrameArgs(frame), 'when')
end

function p.update(frame)
    return renderTag(getFrameArgs(frame), 'update')
end

--------------------------------------------------------------------------------
-- AS OF
--------------------------------------------------------------------------------

function p.asof(frame)
    local args = getFrameArgs(frame)

    local date = getArg(args, 1) or getArg(args, 'date')
    local since = yesno(getArg(args, 'since'), false)
    local lc = yesno(getArg(args, 'lc'), false)

    if isEmpty(date) then
        return '<span class="error">Error: Date required</span>'
    end

    local prefix = lc and 'as of ' or 'As of '
    if since then
        prefix = lc and 'since ' or 'Since '
    end

    local span = mw.html.create('span')
        :addClass('as-of')
        :wikitext(prefix .. date)

    return tostring(span) .. '[[Category:Articles containing potentially dated statements]]'
end

return p