Difference between revisions of "Module:Message box"

From BESA® Wiki
Jump to: navigation, search
m (1 revision)
(start work on a module for displaying Template:Mbox-family message boxes)
(55 intermediate revisions by 5 users not shown)
Line 1: Line 1:
-- This is a meta-module for producing message box templates, including
+
-- This is a meta-module for producing message box templates, including {{mbox}}, {{ambox}}, {{imbox}}, {{tmbox}}, {{ombox}}, {{cmbox}} and {{fmbox}}.
-- {{mbox}}, {{ambox}}, {{imbox}}, {{tmbox}}, {{ombox}}, {{cmbox}} and {{fmbox}}.
+
  
-- Load necessary modules.
+
local htmlBuilder = require('Module:HtmlBuilder')
require('Module:No globals')
+
local getArgs
+
local categoryHandler = require('Module:Category handler')._main
+
local yesno = require('Module:Yesno')
+
  
-- Get a language object for formatDate and ucfirst.
+
local p = {}
local lang = mw.language.getContentLanguage()
+
  
--------------------------------------------------------------------------------
+
function p.build(data, args)
-- Helper functions
+
    -- Process config data using the args passed to the template.
--------------------------------------------------------------------------------
+
    local isSmall = args.small == 'yes' or args.small == true
 +
    local typeData = data.types[args.type]
 +
    local invalidType = args.type and not typeData and true or false
 +
    typeData = typeData or data.types[data.default]
  
local function getTitleObject(...)
+
    -- Build the box.
-- Get the title object, passing the function through pcall
+
    local root = htmlBuilder.create() -- The template root. Includes error messages and categories added after the box.
-- in case we are over the expensive function count limit.
+
    local box = root.tag('table') -- The box.
local success, title = pcall(mw.title.new, ...)
+
    box
if success then
+
        .attr('id', args.id)
return title
+
    for i, class in ipairs(data.classes) do
end
+
        box
end
+
            .addClass(class)
 +
    end
 +
    box
 +
        .addClass(typeData.class)
 +
        .addClass(args.class)
 +
        .cssText(args.style)
 +
        .attr('role', 'presentation')
  
local function union(t1, t2)
+
    -- Add the left-hand image.
-- Returns the union of two arrays.
+
    local row = box.tag('tr')
local vals = {}
+
    if args.image ~= 'none' then
for i, v in ipairs(t1) do
+
        row.tag('td')
vals[v] = true
+
            .addClass('mbox-image')
end
+
            .wikitext(args.image or mw.ustring.format(
for i, v in ipairs(t2) do
+
                '[[File:%s|%s|link=|alt=]]',
vals[v] = true
+
                typeData.image,
end
+
                (args.small == 'yes' or args.small == true) and data.imageSizeSmall or data.imageSizeLarge or data.imageSize
local ret = {}
+
            ))
for k in pairs(vals) do
+
    elseif data.imageEmptyCell then
table.insert(ret, k)
+
        row.tag('td')
end
+
            .addClass('mbox-empty-cell')
table.sort(ret)
+
    end
return ret
+
end
+
  
local function getArgNums(args, prefix)
+
    -- Add the text.
local nums = {}
+
    row.tag('td')
for k, v in pairs(args) do
+
        .addClass('mbox-text')
local num = mw.ustring.match(tostring(k), '^' .. prefix .. '([1-9]%d*)$')
+
        .cssText(args.textstyle)
if num then
+
        .wikitext(args.text)
table.insert(nums, tonumber(num))
+
end
+
end
+
table.sort(nums)
+
return nums
+
end
+
  
--------------------------------------------------------------------------------
+
    -- Add the right-hand image.
-- Box class definition
+
    if args.imageright then
--------------------------------------------------------------------------------
+
        row.tag('td')
 +
            .addClass('mbox-imageright')
 +
            .wikitext(args.imageright)
 +
    end
  
local MessageBox = {}
+
    -- Add error messages and categories.
MessageBox.__index = MessageBox
+
    if invalidType then
 
+
        local title = mw.title.getCurrentTitle()
function MessageBox.new(boxType, args, cfg)
+
        local catsort = (title.namespace == 0 and 'Main:' or '') .. title.prefixedText
args = args or {}
+
        root.tag('div')
local obj = {}
+
            .css('text-align', 'center')
 
+
            .wikitext(mw.ustring.format('This message box is using an invalid "type=%s" parameter and needs fixing.', args.type or ''))
-- Set the title object and the namespace.
+
            .done()
obj.title = getTitleObject(args.page) or mw.title.getCurrentTitle()
+
        .wikitext(mw.ustring.format('[[Category:Wikipedia message box parameter needs fixing|%s]]', catsort))
 
+
    end
-- Set the config for our box type.
+
    return tostring(root)
obj.cfg = cfg[boxType]
+
if not obj.cfg then
+
local ns = obj.title.namespace
+
-- boxType is "mbox" or invalid input
+
if ns == 0 then
+
obj.cfg = cfg.ambox -- main namespace
+
elseif ns == 6 then
+
obj.cfg = cfg.imbox -- file namespace
+
elseif ns == 14 then
+
obj.cfg = cfg.cmbox -- category namespace
+
else
+
local nsTable = mw.site.namespaces[ns]
+
if nsTable and nsTable.isTalk then
+
obj.cfg = cfg.tmbox -- any talk namespace
+
else
+
obj.cfg = cfg.ombox -- other namespaces or invalid input
+
end
+
end
+
end
+
 
+
-- Set the arguments, and remove all blank arguments except for the ones
+
-- listed in cfg.allowBlankParams.
+
do
+
local newArgs = {}
+
for k, v in pairs(args) do
+
if v ~= '' then
+
newArgs[k] = v
+
end
+
end
+
for i, param in ipairs(obj.cfg.allowBlankParams or {}) do
+
newArgs[param] = args[param]
+
end
+
obj.args = newArgs
+
end
+
 
+
-- Define internal data structure.
+
obj.categories = {}
+
obj.classes = {}
+
 
+
return setmetatable(obj, MessageBox)
+
 
end
 
end
  
function MessageBox:addCat(ns, cat, sort)
+
function p._fmbox(args)
if not cat then
+
    local data = {}
return nil
+
    data.types = {
end
+
        warning = {
if sort then
+
            class = 'fmbox-warning',
cat = string.format('[[Category:%s|%s]]', cat, sort)
+
            image = 'Cmbox deletion.png'
else
+
        },
cat = string.format('[[Category:%s]]', cat)
+
        editnotice = {
end
+
            class = 'fmbox-editnotice',
self.categories[ns] = self.categories[ns] or {}
+
            image = 'Imbox notice.png'
table.insert(self.categories[ns], cat)
+
        },
 +
        system = {
 +
            class = 'fmbox-system',
 +
            image = 'Imbox notice.png'
 +
        }
 +
    }
 +
    data.default = 'system'
 +
    data.classes = { 'plainlinks', 'fmbox' }
 +
    data.imageSize = '40x40px'
 +
    data.imageEmptyCell = false
 +
    return p.build(data, args)
 
end
 
end
  
function MessageBox:addClass(class)
+
function p._ombox(args)
if not class then
+
    local data = {}
return nil
+
    data.types = {
end
+
        speedy = {
table.insert(self.classes, class)
+
            class = 'ombox-speedy',
 +
            image = 'Imbox speedy deletion.png'
 +
        },
 +
        delete = {
 +
            class = 'ombox-delete',
 +
            image = 'Imbox deletion.png'
 +
        },
 +
        content = {
 +
            class = 'ombox-content',
 +
            image = 'Imbox content.png'
 +
        },
 +
        style = {
 +
            class = 'ombox-style',
 +
            image = 'Edit-clear.svg'
 +
        },
 +
        move = {
 +
            class = 'ombox-move',
 +
            image = 'Imbox move.png'
 +
        },
 +
        protection = {
 +
            class = 'ombox-protection',
 +
            image = 'Imbox protection.png'
 +
        },
 +
        notice = {
 +
            class = 'ombox-notice',
 +
            image = 'Imbox notice.png'
 +
        }
 +
    }
 +
    data.default = 'notice'
 +
    data.classes = {'plainlinks', 'ombox'}
 +
    data.imageSizeLarge = '40x40px'
 +
    data.imageSizeSmall = '30x30px'
 +
    data.imageEmptyCell = true
 +
    return p.build(data, args)
 
end
 
end
  
function MessageBox:setParameters()
+
local function makeWrapper(func)
local args = self.args
+
    return function (frame)
local cfg = self.cfg
+
        -- If called via #invoke, use the args passed into the invoking
 
+
        -- template, or the args passed to #invoke if any exist. Otherwise
-- Get type data.
+
        -- assume args are being passed directly in from the debug console
self.type = args.type
+
        -- or from another Lua module.
local typeData = cfg.types[self.type]
+
        local origArgs
self.invalidTypeError = cfg.showInvalidTypeError
+
        if frame == mw.getCurrentFrame() then
and self.type
+
            origArgs = frame:getParent().args
and not typeData
+
            for k, v in pairs(frame.args) do
typeData = typeData or cfg.types[cfg.default]
+
                origArgs = frame.args
self.typeClass = typeData.class
+
                break
self.typeImage = typeData.image
+
            end
 
+
        else
-- Find if the box has been wrongly substituted.
+
            origArgs = frame
self.isSubstituted = cfg.substCheck and args.subst == 'SUBST'
+
        end
 
+
        -- Trim whitespace and remove blank arguments.
-- Find whether we are using a small message box.
+
        local args = {}
self.isSmall = cfg.allowSmall and (
+
        for k, v in pairs(origArgs) do
cfg.smallParam and args.small == cfg.smallParam
+
            if type(v) == 'string' then
or not cfg.smallParam and yesno(args.small)
+
                v = mw.text.trim(v)
)
+
            end
 
+
            if v ~= '' then
-- Add attributes, classes and styles.
+
                args[k] = v
self.id = args.id
+
            end
self:addClass(
+
        end
cfg.usePlainlinksParam and yesno(args.plainlinks or true) and 'plainlinks'
+
        return func(args)
)
+
    end
for _, class in ipairs(cfg.classes or {}) do
+
self:addClass(class)
+
end
+
if self.isSmall then
+
self:addClass(cfg.smallClass or 'mbox-small')
+
end
+
self:addClass(self.typeClass)
+
self:addClass(args.class)
+
self.style = args.style
+
self.attrs = args.attrs
+
 
+
-- Set text style.
+
self.textstyle = args.textstyle
+
 
+
-- Find if we are on the template page or not. This functionality is only
+
-- used if useCollapsibleTextFields is set, or if both cfg.templateCategory
+
-- and cfg.templateCategoryRequireName are set.
+
self.useCollapsibleTextFields = cfg.useCollapsibleTextFields
+
if self.useCollapsibleTextFields
+
or cfg.templateCategory
+
and cfg.templateCategoryRequireName
+
then
+
self.name = args.name
+
if self.name then
+
local templateName = mw.ustring.match(
+
self.name,
+
'^[tT][eE][mM][pP][lL][aA][tT][eE][%s_]*:[%s_]*(.*)$'
+
) or self.name
+
templateName = 'Template:' .. templateName
+
self.templateTitle = getTitleObject(templateName)
+
end
+
self.isTemplatePage = self.templateTitle
+
and mw.title.equals(self.title, self.templateTitle)
+
end
+
 
+
-- Process data for collapsible text fields. At the moment these are only
+
-- used in {{ambox}}.
+
if self.useCollapsibleTextFields then
+
-- Get the self.issue value.
+
if self.isSmall and args.smalltext then
+
self.issue = args.smalltext
+
else
+
local sect
+
if args.sect == '' then
+
sect = 'This ' .. (cfg.sectionDefault or 'page')
+
elseif type(args.sect) == 'string' then
+
sect = 'This ' .. args.sect
+
end
+
local issue = args.issue
+
issue = type(issue) == 'string' and issue ~= '' and issue or nil
+
local text = args.text
+
text = type(text) == 'string' and text or nil
+
local issues = {}
+
table.insert(issues, sect)
+
table.insert(issues, issue)
+
table.insert(issues, text)
+
self.issue = table.concat(issues, ' ')
+
end
+
 
+
-- Get the self.talk value.
+
local talk = args.talk
+
-- Show talk links on the template page or template subpages if the talk
+
-- parameter is blank.
+
if talk == ''
+
and self.templateTitle
+
and (
+
mw.title.equals(self.templateTitle, self.title)
+
or self.title:isSubpageOf(self.templateTitle)
+
)
+
then
+
talk = '#'
+
elseif talk == '' then
+
talk = nil
+
end
+
if talk then
+
-- If the talk value is a talk page, make a link to that page. Else
+
-- assume that it's a section heading, and make a link to the talk
+
-- page of the current page with that section heading.
+
local talkTitle = getTitleObject(talk)
+
local talkArgIsTalkPage = true
+
if not talkTitle or not talkTitle.isTalkPage then
+
talkArgIsTalkPage = false
+
talkTitle = getTitleObject(
+
self.title.text,
+
mw.site.namespaces[self.title.namespace].talk.id
+
)
+
end
+
if talkTitle and talkTitle.exists then
+
local talkText = 'Relevant discussion may be found on'
+
if talkArgIsTalkPage then
+
talkText = string.format(
+
'%s [[%s|%s]].',
+
talkText,
+
talk,
+
talkTitle.prefixedText
+
)
+
else
+
talkText = string.format(
+
'%s the [[%s#%s|talk page]].',
+
talkText,
+
talkTitle.prefixedText,
+
talk
+
)
+
end
+
self.talk = talkText
+
end
+
end
+
 
+
-- Get other values.
+
self.fix = args.fix ~= '' and args.fix or nil
+
local date
+
if args.date and args.date ~= '' then
+
date = args.date
+
elseif args.date == '' and self.isTemplatePage then
+
date = lang:formatDate('F Y')
+
end
+
if date then
+
self.date = string.format(" <small>''(%s)''</small>", date)
+
end
+
self.info = args.info
+
end
+
 
+
-- Set the non-collapsible text field. At the moment this is used by all box
+
-- types other than ambox, and also by ambox when small=yes.
+
if self.isSmall then
+
self.text = args.smalltext or args.text
+
else
+
self.text = args.text
+
end
+
 
+
-- Set the below row.
+
self.below = cfg.below and args.below
+
 
+
-- General image settings.
+
self.imageCellDiv = not self.isSmall and cfg.imageCellDiv
+
self.imageEmptyCell = cfg.imageEmptyCell
+
if cfg.imageEmptyCellStyle then
+
self.imageEmptyCellStyle = 'border:none;padding:0px;width:1px'
+
end
+
 
+
-- Left image settings.
+
local imageLeft = self.isSmall and args.smallimage or args.image
+
if cfg.imageCheckBlank and imageLeft ~= 'blank' and imageLeft ~= 'none'
+
or not cfg.imageCheckBlank and imageLeft ~= 'none'
+
then
+
self.imageLeft = imageLeft
+
if not imageLeft then
+
local imageSize = self.isSmall
+
and (cfg.imageSmallSize or '30x30px')
+
or '40x40px'
+
self.imageLeft = string.format('[[File:%s|%s|link=|alt=]]', self.typeImage
+
or 'Imbox notice.png', imageSize)
+
end
+
end
+
 
+
-- Right image settings.
+
local imageRight = self.isSmall and args.smallimageright or args.imageright
+
if not (cfg.imageRightNone and imageRight == 'none') then
+
self.imageRight = imageRight
+
end
+
 
end
 
end
  
function MessageBox:setMainspaceCategories()
+
p.fmbox = makeWrapper(p._fmbox)
local args = self.args
+
p.ombox = makeWrapper(p._ombox)
local cfg = self.cfg
+
 
+
if not cfg.allowMainspaceCategories then
+
return nil
+
end
+
 
+
local nums = {}
+
for _, prefix in ipairs{'cat', 'category', 'all'} do
+
args[prefix .. '1'] = args[prefix]
+
nums = union(nums, getArgNums(args, prefix))
+
end
+
 
+
-- The following is roughly equivalent to the old {{Ambox/category}}.
+
local date = args.date
+
date = type(date) == 'string' and date
+
local preposition = 'from'
+
for _, num in ipairs(nums) do
+
local mainCat = args['cat' .. tostring(num)]
+
or args['category' .. tostring(num)]
+
local allCat = args['all' .. tostring(num)]
+
mainCat = type(mainCat) == 'string' and mainCat
+
allCat = type(allCat) == 'string' and allCat
+
if mainCat and date and date ~= '' then
+
local catTitle = string.format('%s %s %s', mainCat, preposition, date)
+
self:addCat(0, catTitle)
+
catTitle = getTitleObject('Category:' .. catTitle)
+
if not catTitle or not catTitle.exists then
+
self:addCat(0, 'Articles with invalid date parameter in template')
+
end
+
elseif mainCat and (not date or date == '') then
+
self:addCat(0, mainCat)
+
end
+
if allCat then
+
self:addCat(0, allCat)
+
end
+
end
+
end
+
 
+
function MessageBox:setTemplateCategories()
+
local args = self.args
+
local cfg = self.cfg
+
 
+
-- Add template categories.
+
if cfg.templateCategory then
+
if cfg.templateCategoryRequireName then
+
if self.isTemplatePage then
+
self:addCat(10, cfg.templateCategory)
+
end
+
elseif not self.title.isSubpage then
+
self:addCat(10, cfg.templateCategory)
+
end
+
end
+
 
+
-- Add template error categories.
+
if cfg.templateErrorCategory then
+
local templateErrorCategory = cfg.templateErrorCategory
+
local templateCat, templateSort
+
if not self.name and not self.title.isSubpage then
+
templateCat = templateErrorCategory
+
elseif self.isTemplatePage then
+
local paramsToCheck = cfg.templateErrorParamsToCheck or {}
+
local count = 0
+
for i, param in ipairs(paramsToCheck) do
+
if not args[param] then
+
count = count + 1
+
end
+
end
+
if count > 0 then
+
templateCat = templateErrorCategory
+
templateSort = tostring(count)
+
end
+
if self.categoryNums and #self.categoryNums > 0 then
+
templateCat = templateErrorCategory
+
templateSort = 'C'
+
end
+
end
+
self:addCat(10, templateCat, templateSort)
+
end
+
end
+
 
+
function MessageBox:setAllNamespaceCategories()
+
-- Set categories for all namespaces.
+
if self.invalidTypeError then
+
local allSort = (self.title.namespace == 0 and 'Main:' or '') .. self.title.prefixedText
+
self:addCat('all', 'Wikipedia message box parameter needs fixing', allSort)
+
end
+
if self.isSubstituted then
+
self:addCat('all', 'Pages with incorrectly substituted templates')
+
end
+
end
+
 
+
function MessageBox:setCategories()
+
if self.title.namespace == 0 then
+
self:setMainspaceCategories()
+
elseif self.title.namespace == 10 then
+
self:setTemplateCategories()
+
end
+
self:setAllNamespaceCategories()
+
end
+
 
+
function MessageBox:renderCategories()
+
-- Convert category tables to strings and pass them through
+
-- [[Module:Category handler]].
+
return categoryHandler{
+
main = table.concat(self.categories[0] or {}),
+
template = table.concat(self.categories[10] or {}),
+
all = table.concat(self.categories.all or {}),
+
nocat = self.args.nocat,
+
page = self.args.page
+
}
+
end
+
 
+
function MessageBox:export()
+
local root = mw.html.create()
+
 
+
-- Add the subst check error.
+
if self.isSubstituted and self.name then
+
root:tag('b')
+
:addClass('error')
+
:wikitext(string.format(
+
'Template <code>%s[[Template:%s|%s]]%s</code> has been incorrectly substituted.',
+
mw.text.nowiki('{{'), self.name, self.name, mw.text.nowiki('}}')
+
))
+
end
+
 
+
-- Create the box table.
+
local boxTable = root:tag('table')
+
boxTable:attr('id', self.id or nil)
+
for i, class in ipairs(self.classes or {}) do
+
boxTable:addClass(class or nil)
+
end
+
boxTable
+
:cssText(self.style or nil)
+
:attr('role', 'presentation')
+
 
+
if self.attrs then
+
boxTable:attr(self.attrs)
+
end
+
 
+
-- Add the left-hand image.
+
local row = boxTable:tag('tr')
+
if self.imageLeft then
+
local imageLeftCell = row:tag('td'):addClass('mbox-image')
+
if self.imageCellDiv then
+
-- If we are using a div, redefine imageLeftCell so that the image
+
-- is inside it. Divs use style="width: 52px;", which limits the
+
-- image width to 52px. If any images in a div are wider than that,
+
-- they may overlap with the text or cause other display problems.
+
imageLeftCell = imageLeftCell:tag('div'):css('width', '52px')
+
end
+
imageLeftCell:wikitext(self.imageLeft or nil)
+
elseif self.imageEmptyCell then
+
-- Some message boxes define an empty cell if no image is specified, and
+
-- some don't. The old template code in templates where empty cells are
+
-- specified gives the following hint: "No image. Cell with some width
+
-- or padding necessary for text cell to have 100% width."
+
row:tag('td')
+
:addClass('mbox-empty-cell')
+
:cssText(self.imageEmptyCellStyle or nil)
+
end
+
 
+
-- Add the text.
+
local textCell = row:tag('td'):addClass('mbox-text')
+
if self.useCollapsibleTextFields then
+
-- The message box uses advanced text parameters that allow things to be
+
-- collapsible. At the moment, only ambox uses this.
+
textCell:cssText(self.textstyle or nil)
+
local textCellSpan = textCell:tag('span')
+
textCellSpan
+
:addClass('mbox-text-span')
+
:wikitext(self.issue or nil)
+
if not self.isSmall then
+
textCellSpan:tag('span')
+
:addClass('hide-when-compact')
+
:wikitext(self.talk and (' ' .. self.talk) or nil)
+
:wikitext(self.fix and (' ' .. self.fix) or nil)
+
end
+
textCellSpan:wikitext(self.date and (' ' .. self.date) or nil)
+
if not self.isSmall then
+
textCellSpan
+
:tag('span')
+
:addClass('hide-when-compact')
+
:wikitext(self.info and (' ' .. self.info) or nil)
+
end
+
else
+
-- Default text formatting - anything goes.
+
textCell
+
:cssText(self.textstyle or nil)
+
:wikitext(self.text or nil)
+
end
+
 
+
-- Add the right-hand image.
+
if self.imageRight then
+
local imageRightCell = row:tag('td'):addClass('mbox-imageright')
+
if self.imageCellDiv then
+
-- If we are using a div, redefine imageRightCell so that the image
+
-- is inside it.
+
imageRightCell = imageRightCell:tag('div'):css('width', '52px')
+
end
+
imageRightCell
+
:wikitext(self.imageRight or nil)
+
end
+
 
+
-- Add the below row.
+
if self.below then
+
boxTable:tag('tr')
+
:tag('td')
+
:attr('colspan', self.imageRight and '3' or '2')
+
:addClass('mbox-text')
+
:cssText(self.textstyle or nil)
+
:wikitext(self.below or nil)
+
end
+
 
+
-- Add error message for invalid type parameters.
+
if self.invalidTypeError then
+
root:tag('div')
+
:css('text-align', 'center')
+
:wikitext(string.format(
+
'This message box is using an invalid "type=%s" parameter and needs fixing.',
+
self.type or ''
+
))
+
end
+
 
+
-- Add categories.
+
root:wikitext(self:renderCategories() or nil)
+
 
+
return tostring(root)
+
end
+
 
+
--------------------------------------------------------------------------------
+
-- Exports
+
--------------------------------------------------------------------------------
+
 
+
local p, mt = {}, {}
+
 
+
function p._exportClasses()
+
-- For testing.
+
return {
+
MessageBox = MessageBox
+
}
+
end
+
 
+
function p.main(boxType, args, cfgTables)
+
local box = MessageBox.new(boxType, args, cfgTables or mw.loadData('Module:Message box/configuration'))
+
box:setParameters()
+
box:setCategories()
+
return box:export()
+
end
+
 
+
function mt.__index(t, k)
+
return function (frame)
+
if not getArgs then
+
getArgs = require('Module:Arguments').getArgs
+
end
+
return t.main(k, getArgs(frame, {trim = false, removeBlanks = false}))
+
end
+
end
+
  
return setmetatable(p, mt)
+
return p

Revision as of 14:37, 19 September 2013

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

Script error: Lua error: Internal error: Error reading from pipe.

-- This is a meta-module for producing message box templates, including {{mbox}}, {{ambox}}, {{imbox}}, {{tmbox}}, {{ombox}}, {{cmbox}} and {{fmbox}}.

local htmlBuilder = require('Module:HtmlBuilder')

local p = {}

function p.build(data, args)
    -- Process config data using the args passed to the template.
    local isSmall = args.small == 'yes' or args.small == true
    local typeData = data.types[args.type]
    local invalidType = args.type and not typeData and true or false
    typeData = typeData or data.types[data.default]

    -- Build the box.
    local root = htmlBuilder.create() -- The template root. Includes error messages and categories added after the box.
    local box = root.tag('table') -- The box.
    box
        .attr('id', args.id)
    for i, class in ipairs(data.classes) do
        box
            .addClass(class)
    end
    box
        .addClass(typeData.class)
        .addClass(args.class)
        .cssText(args.style)
        .attr('role', 'presentation')

    -- Add the left-hand image.
    local row = box.tag('tr')
    if args.image ~= 'none' then
        row.tag('td')
            .addClass('mbox-image')
            .wikitext(args.image or mw.ustring.format(
                '[[File:%s|%s|link=|alt=]]',
                typeData.image,
                (args.small == 'yes' or args.small == true) and data.imageSizeSmall or data.imageSizeLarge or data.imageSize
            ))
    elseif data.imageEmptyCell then
        row.tag('td')
            .addClass('mbox-empty-cell')
    end

    -- Add the text.
    row.tag('td')
        .addClass('mbox-text')
        .cssText(args.textstyle)
        .wikitext(args.text)

    -- Add the right-hand image.
    if args.imageright then
        row.tag('td')
            .addClass('mbox-imageright')
            .wikitext(args.imageright)
    end

    -- Add error messages and categories.
    if invalidType then
        local title = mw.title.getCurrentTitle()
        local catsort = (title.namespace == 0 and 'Main:' or '') .. title.prefixedText
        root.tag('div')
            .css('text-align', 'center')
            .wikitext(mw.ustring.format('This message box is using an invalid "type=%s" parameter and needs fixing.', args.type or ''))
            .done()
        .wikitext(mw.ustring.format('[[Category:Wikipedia message box parameter needs fixing|%s]]', catsort))
    end
    return tostring(root)
end

function p._fmbox(args)
    local data = {}
    data.types = {
        warning = {
            class = 'fmbox-warning',
            image = 'Cmbox deletion.png'
        },
        editnotice = {
            class = 'fmbox-editnotice',
            image = 'Imbox notice.png'
        },
        system = {
            class = 'fmbox-system',
            image = 'Imbox notice.png'
        }
    }
    data.default = 'system'
    data.classes = { 'plainlinks', 'fmbox' }
    data.imageSize = '40x40px'
    data.imageEmptyCell = false
    return p.build(data, args)
end

function p._ombox(args)
    local data = {}
    data.types = {
        speedy = {
            class = 'ombox-speedy',
            image = 'Imbox speedy deletion.png'
        },
        delete = {
            class = 'ombox-delete',
            image = 'Imbox deletion.png'
        },
        content = {
            class = 'ombox-content',
            image = 'Imbox content.png'
        },
        style = {
            class = 'ombox-style',
            image = 'Edit-clear.svg'
        },
        move = {
            class = 'ombox-move',
            image = 'Imbox move.png'
        },
        protection = {
            class = 'ombox-protection',
            image = 'Imbox protection.png'
        },
        notice = {
            class = 'ombox-notice',
            image = 'Imbox notice.png'
        }
    }
    data.default = 'notice'
    data.classes = {'plainlinks', 'ombox'}
    data.imageSizeLarge = '40x40px'
    data.imageSizeSmall = '30x30px'
    data.imageEmptyCell = true
    return p.build(data, args)
end

local function makeWrapper(func)
    return function (frame)
        -- If called via #invoke, use the args passed into the invoking
        -- template, or the args passed to #invoke if any exist. Otherwise
        -- assume args are being passed directly in from the debug console
        -- or from another Lua module.
        local origArgs
        if frame == mw.getCurrentFrame() then
            origArgs = frame:getParent().args
            for k, v in pairs(frame.args) do
                origArgs = frame.args
                break
            end
        else
            origArgs = frame
        end
        -- Trim whitespace and remove blank arguments.
        local args = {}
        for k, v in pairs(origArgs) do
            if type(v) == 'string' then
                v = mw.text.trim(v)
            end
            if v ~= '' then
                args[k] = v
            end
        end
        return func(args)
    end
end

p.fmbox = makeWrapper(p._fmbox)
p.ombox = makeWrapper(p._ombox)

return p