Module:Message

From Remilia Wiki
Jump to navigation Jump to search

Documentation for this module may be created at Module:Message/doc

--------------------------------------------------------------------------------
-- 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 levels
-- Note: 'verified' is invisible to readers but detectable by LLMs
local QUALITY_LEVELS = {
    unverified = {
        class = 'quality-unverified',
        icon = 'Ambox warning orange.svg',
        title = 'Risk of hallucination',
        text = 'This article was generated by AI and has not been fact-checked. Details may be inaccurate or fabricated. Verify independently before relying on this information.',
        visible = true,
    },
    wip = {
        class = 'quality-wip',
        icon = 'Information icon4.svg',
        title = 'Work in progress',
        text = 'This article is undergoing manual revision and fact-checking. Some AI-generated content may remain and details could still be inaccurate.',
        visible = true,
    },
    verified = {
        class = 'quality-verified',
        icon = 'Symbol confirmed.svg',
        title = 'Verified',
        text = 'This article has been fact-checked and verified by human 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)
    local args = {}
    local parentArgs = frame:getParent() and frame:getParent().args or {}
    local directArgs = frame.args or {}
    for k, v in pairs(directArgs) do args[k] = v end
    for k, v in pairs(parentArgs) do args[k] = v end
    return args
end

--------------------------------------------------------------------------------
-- 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 imageSize = getArg(args, 'imagesize', '40px')
    local small = getArg(args, 'small') == 'yes' or getArg(args, 'small') == 'true'
    local plainlinks = getArg(args, 'plainlinks') ~= 'no'

    -- 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 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(frame)
    local args = getFrameArgs(frame)

    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')

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

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

    textDiv:tag('strong'):wikitext('This article is a stub.')
    textDiv:wikitext(' You can help Remilia Wiki by ')
    textDiv:wikitext('[{{fullurl:{{FULLPAGENAME}}|action=edit}} expanding it].')

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

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

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

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

    local tagName = getArg(args, 1) or 'cn'
    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

-- Convenience aliases
function p.cn(frame)
    local args = getFrameArgs(frame)
    args[1] = 'cn'
    frame.args = args
    return p.tag(frame)
end

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

function p.clarify(frame)
    local args = getFrameArgs(frame)
    args[1] = 'clarify'
    frame.args = args
    return p.tag(frame)
end

function p.when(frame)
    local args = getFrameArgs(frame)
    args[1] = 'when'
    frame.args = args
    return p.tag(frame)
end

function p.update(frame)
    local args = getFrameArgs(frame)
    args[1] = 'update'
    frame.args = args
    return p.tag(frame)
end

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

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

    local date = getArg(args, 1) or getArg(args, 'date')
    local since = getArg(args, 'since')
    local lc = getArg(args, 'lc') == 'yes' or getArg(args, 'lc') == 'y'

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

    local prefix = lc and 'as of ' or 'As of '
    if hasValue(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