Module:Message box: Difference between revisions

From Vigyanwiki
Template>Mr. Stradivarius
(better error message)
Template>Mr. Stradivarius
(add mbox, various other fixes)
Line 1: Line 1:
-- This is a meta-module for producing message box templates, including {{mbox}}, {{ambox}}, {{imbox}}, {{tmbox}}, {{ombox}}, {{cmbox}} and {{fmbox}}.
-- This is a meta-module for producing message box templates, including {{mbox}}, {{ambox}}, {{imbox}}, {{tmbox}}, {{ombox}}, {{cmbox}} and {{fmbox}}.


-- Require necessary modules.
local htmlBuilder = require('Module:HtmlBuilder')
local htmlBuilder = require('Module:HtmlBuilder')
local nsDetect = require('Module:Namespace detect')
local categoryHandler = require('Module:Category handler').main
local categoryHandler = require('Module:Category handler').main
local yesno = require('Module:Yesno')
local yesno = require('Module:Yesno')
-- Get a language object for formatDate and ucfirst.
local lang = mw.language.getContentLanguage()
-- Set aliases for often-used functions to reduce table lookups.
local format = mw.ustring.format
local tinsert = table.insert
local tconcat = table.concat


local p = {}
local p = {}
Line 12: Line 20:
         -- Get the title object, passing the function through pcall  
         -- Get the title object, passing the function through pcall  
         -- in case we are over the expensive function count limit.
         -- in case we are over the expensive function count limit.
         local success
         local success, title = pcall(mw.title.new, page)
        success, page = pcall(mw.title.new, page)
         if success then
         if not success then
             return title
             page = nil
         end
         end
     end
     end
    return page
end
end


Line 37: Line 43:
     local preposition = 'from'
     local preposition = 'from'
     if cat and date then
     if cat and date then
         local catTitle = mw.ustring.format('Category:%s %s %s', cat, preposition, date)
         local catTitle = format('Category:%s %s %s', cat, preposition, date)
         table.insert(ret, mw.ustring.format('[[%s]]', catTitle))
         tinsert(ret, format('[[%s]]', catTitle))
         catTitle = getTitleObject(catTitle)
         catTitle = getTitleObject(catTitle)
         if not catTitle or not catTitle.exists then
         if not catTitle or not catTitle.exists then
             table.insert(ret, '[[Category:Articles with invalid date parameter in template]]')
             tinsert(ret, '[[Category:Articles with invalid date parameter in template]]')
         end
         end
     elseif cat and not date then
     elseif cat and not date then
         table.insert(ret, mw.ustring.format('[[Category:%s]]', cat))
         tinsert(ret, format('[[Category:%s]]', cat))
     end
     end
     if all then
     if all then
         table.insert(ret, mw.ustring.format('[[Category:%s]]', all))
         tinsert(ret, format('[[Category:%s]]', all))
     end
     end
     return table.concat(ret)
     return tconcat(ret)
end
end


Line 62: Line 68:
     end
     end
     local ret = {}
     local ret = {}
     for k, v in pairs(vals) do
     for k in pairs(vals) do
         table.insert(ret, k)
         tinsert(ret, k)
     end
     end
    table.sort(ret)
     return ret
     return ret
end
end
Line 73: Line 80:
         local num = mw.ustring.match(tostring(k), '^' .. prefix .. '([1-9]%d*)$')
         local num = mw.ustring.match(tostring(k), '^' .. prefix .. '([1-9]%d*)$')
         if num then
         if num then
             table.insert(nums, tonumber(num))
             tinsert(nums, tonumber(num))
         end
         end
     end
     end
     table.sort(nums)
     table.sort(nums)
     return nums
     return nums
end
local function getNamespaceId(ns)
    if type(ns) == 'string' then
        ns = lang:ucfirst(mw.ustring.lower(ns))
        if ns == 'Main' then
            ns = 0
        end
    end
    local nsTable = mw.site.namespaces[ns]
    if nsTable then
        return nsTable.id
    end
end
local function getMboxType(nsid)
    -- Gets the mbox type from a namespace number.
    if nsid == 0 then
        return 'ambox' -- main namespace
    elseif nsid == 6 then
        return 'imbox' -- file namespace
    elseif nsid == 14 then
        return 'cmbox' -- category namespace
    else
        local nsTable = mw.site.namespaces[nsid]
        if nsTable and nsTable.isTalk then
            return 'tmbox' -- any talk namespace
        else
            return 'ombox' -- other namespaces or invalid input
        end
    end
end
end


function p.build(boxType, args)
function p.build(boxType, args)
    if type(args) ~= 'table' then
        error(format('invalid "args" parameter type; expected type "table", got type "%s"', type(args)), 2)
    end
    -- Get the title object and the namespace.
    local title = getTitleObject(args.page) or mw.title.getCurrentTitle()
    local nsid = getNamespaceId(args.demospace) or title.namespace
     -- Get the box config data from the data page.
     -- Get the box config data from the data page.
    if boxType == 'mbox' then
        boxType = getMboxType(nsid)
    end
     local dataTables = mw.loadData('Module:Message box/data')
     local dataTables = mw.loadData('Module:Message box/data')
     local data = dataTables[boxType]
     local data = dataTables[boxType]
Line 87: Line 136:
         local boxTypes = {}
         local boxTypes = {}
         for k, v in pairs(dataTables) do
         for k, v in pairs(dataTables) do
             table.insert(boxTypes, mw.ustring.format('"%s"', k))
             tinsert(boxTypes, format('"%s"', k))
         end
         end
         error(mw.ustring.format('invalid message box type "%s"; valid types are %s', tostring(boxType), mw.text.listToText(boxTypes)), 2)
        tinsert(boxTypes, '"mbox"')
         error(format('invalid message box type "%s"; valid types are %s', tostring(boxType), mw.text.listToText(boxTypes)), 2)
     end
     end
    -- Get the title object and the namespace.
    local title = mw.title.getCurrentTitle()
    local nsid = title.namespace
    -- Get a language object for formatDate.
    local lang = mw.language.getContentLanguage()
   
-- Commenting this out for now - this will require tinkering with Namespace detect to differentiate between
-- invalid titles and pages where the expensive parser function count has been exceeded.
--[[
    local title = nsDetect.getPageObject(args.page)
    local namespace = nsDetect.main{
        page = args.page,
        demospace = args.demospace,
        main = 'main',
        talk = 'talk',
        file = 'file',
        category = 'category',
        other = 'other'
    }
]]


     ------------------------ Process config data ----------------------------
     ------------------------ Process config data ----------------------------
Line 129: Line 157:
         local sect = args.sect
         local sect = args.sect
         if presentButBlank(sect) then
         if presentButBlank(sect) then
             sect = mw.ustring.format('This %s ', data.sectionDefault or 'page')
             sect = format('This %s ', data.sectionDefault or 'page')
         elseif type(sect) == 'string' then
         elseif type(sect) == 'string' then
             sect = 'This ' .. sect .. ' '
             sect = 'This ' .. sect .. ' '
Line 161: Line 189:
             local talkText = ' Relevant discussion may be found on'
             local talkText = ' Relevant discussion may be found on'
             if talkTitle.isTalkPage then
             if talkTitle.isTalkPage then
                 talkText = mw.ustring.format('%s [[%s|%s]].', talkText, talk, talkTitle.prefixedText)
                 talkText = format('%s [[%s|%s]].', talkText, talk, talkTitle.prefixedText)
             else
             else
                 talkText = mw.ustring.format('%s the [[%s#%s|talk page]].', talkText, talkTitle.prefixedText, talk)
                 talkText = format('%s the [[%s#%s|talk page]].', talkText, talkTitle.prefixedText, talk)
             end
             end
             talk = talkText
             talk = talkText
Line 200: Line 228:
             local cat = args['cat' .. tostring(num)] or args['category' .. tostring(num)]
             local cat = args['cat' .. tostring(num)] or args['category' .. tostring(num)]
             local all = args['all' .. tostring(num)]
             local all = args['all' .. tostring(num)]
             table.insert(mainCats, formatCategory(cat, args.date, all))
             tinsert(mainCats, formatCategory(cat, args.date, all))
         end
         end
     end
     end
Line 207: Line 235:
     local templateCats = {}
     local templateCats = {}
     if data.templateCategory and not title.isSubpage and not yesno(args.nocat) then
     if data.templateCategory and not title.isSubpage and not yesno(args.nocat) then
         table.insert(templateCats, mw.ustring.format('[[Category:%s]]', data.templateCategory))
         tinsert(templateCats, format('[[Category:%s]]', data.templateCategory))
     end
     end


Line 215: Line 243:
         local templateCat
         local templateCat
         if not name and not title.isSubpage then
         if not name and not title.isSubpage then
             templateCat = mw.ustring.format('[[Category:%s]]', catName)
             templateCat = format('[[Category:%s]]', catName)
         elseif type(name) == 'string' and title.prefixedText == ('Template:' .. name) then
         elseif type(name) == 'string' and title.prefixedText == ('Template:' .. name) then
             local paramsToCheck = data.templateErrorParamsToCheck or {}
             local paramsToCheck = data.templateErrorParamsToCheck or {}
Line 225: Line 253:
             end
             end
             if count > 0 then
             if count > 0 then
                 templateCat = mw.ustring.format('[[Category:%s|%d]]', catName, count)
                 templateCat = format('[[Category:%s|%d]]', catName, count)
             end
             end
             if origCategoryNums and #origCategoryNums > 0 then
             if origCategoryNums and #origCategoryNums > 0 then
                 templateCat = mw.ustring.format('[[Category:%s|C]]', catName)
                 templateCat = format('[[Category:%s|C]]', catName)
             end
             end
         end
         end
         table.insert(templateCats, templatecat)
         tinsert(templateCats, templatecat)
     end
     end


Line 238: Line 266:
     if invalidType then
     if invalidType then
         local catsort = (nsid == 0 and 'Main:' or '') .. title.prefixedText
         local catsort = (nsid == 0 and 'Main:' or '') .. title.prefixedText
         table.insert(allCats, mw.ustring.format('[[Category:Wikipedia message box parameter needs fixing|%s]]', catsort))
         tinsert(allCats, format('[[Category:Wikipedia message box parameter needs fixing|%s]]', catsort))
     end
     end


Line 251: Line 279:
                 .tag('b')
                 .tag('b')
                     .addClass('error')
                     .addClass('error')
                     .wikitext(mw.ustring.format(
                     .wikitext(format(
                         'Template <code>%s%s%s</code> has been incorrectly substituted.',
                         'Template <code>%s%s%s</code> has been incorrectly substituted.',
                         mw.text.nowiki('{{'), name, mw.text.nowiki('}}')
                         mw.text.nowiki('{{'), name, mw.text.nowiki('}}')
                     ))
                     ))
         end
         end
         table.insert(allCats, '[[Category:Pages with incorrectly substituted templates]]')
         tinsert(allCats, '[[Category:Pages with incorrectly substituted templates]]')
     end
     end


Line 284: Line 312:
         end
         end
         imageLeftCell
         imageLeftCell
             .wikitext(image or mw.ustring.format('[[File:%s|%s|link=|alt=]]', typeData.image, imageSize))
             .wikitext(image or format('[[File:%s|%s|link=|alt=]]', typeData.image, imageSize))
     elseif data.imageEmptyCell then
     elseif data.imageEmptyCell then
         row.tag('td')
         row.tag('td')
Line 310: Line 338:
         end
         end
         textCellSpan
         textCellSpan
             .wikitext(date and mw.ustring.format(" <small>''(%s)''</small>", date))
             .wikitext(date and format(" <small>''(%s)''</small>", date))
         if not isSmall then
         if not isSmall then
             textCellSpan
             textCellSpan
Line 351: Line 379:
                 .addClass('error')
                 .addClass('error')
                 .css('text-align', 'center')
                 .css('text-align', 'center')
                 .wikitext(mw.ustring.format('This message box is using an invalid type parameter (<code>type=%s</code>) and needs fixing.', args.type or ''))
                 .wikitext(format('This message box is using an invalid type parameter (<code>type=%s</code>) and needs fixing.', args.type or ''))
     end
     end


Line 357: Line 385:
     root
     root
         .wikitext(categoryHandler{
         .wikitext(categoryHandler{
             main = table.concat(mainCats),
             main = tconcat(mainCats),
             template = table.concat(templateCats),
             template = tconcat(templateCats),
             all = table.concat(allCats)
             all = tconcat(allCats)
         })
         })
      
      
Line 395: Line 423:
end
end


p.mbox = makeWrapper('mbox')
p.ambox = makeWrapper('ambox')
p.ambox = makeWrapper('ambox')
p.cmbox = makeWrapper('cmbox')
p.fmbox = makeWrapper('fmbox')
p.fmbox = makeWrapper('fmbox')
p.imbox = makeWrapper('imbox')
p.imbox = makeWrapper('imbox')
p.ombox = makeWrapper('ombox')
p.ombox = makeWrapper('ombox')
p.cmbox = makeWrapper('cmbox')
p.tmbox = makeWrapper('tmbox')
p.tmbox = makeWrapper('tmbox')


return p
return p

Revision as of 18:16, 26 September 2013

Lua error in package.lua at line 80: module 'Module:HtmlBuilder' not found. Lua error in package.lua at line 80: module 'Module:HtmlBuilder' not found. Lua error in package.lua at line 80: module 'Module:HtmlBuilder' not found. Lua error in package.lua at line 80: module 'Module:HtmlBuilder' not found.

This is a meta-module that implements the message box templates {{mbox}}, {{ambox}}, {{cmbox}}, {{fmbox}}, {{imbox}}, {{ombox}}, and {{tmbox}}. It is intended to be used from Lua modules, and should not be used directly from wiki pages. If you want to use this module's functionality from a wiki page, please use the individual message box templates instead.

Usage

To use this module from another Lua module, first you need to load it.

local messageBox = require('Module:Message box')

To create a message box, use the main function. It takes two parameters: the first is the box type (as a string), and the second is a table containing the message box parameters.

local box = messageBox.main( boxType, {
    param1 = param1,
    param2 = param2,
    -- More parameters...
})

There are seven available box types:

Box type Template Purpose
mbox {{mbox}} For message boxes to be used in multiple namespaces
ambox {{ambox}} For article message boxes
cmbox {{cmbox}} For category message boxes
fmbox {{fmbox}} For interface message boxes
imbox {{imbox}} For file namespace message boxes
tmbox {{tmbox}} For talk page message boxes
ombox {{ombox}} For message boxes in other namespaces

See the template page of each box type for the available parameters.

Usage from #invoke

As well as the main function, this module has separate functions for each box type. They are accessed using the code {{#invoke:Message box|mbox|...}}, {{#invoke:Message box|ambox|...}}, etc. These will work when called from other modules, but they access code used to process arguments passed from #invoke, and so calling them will be less efficient than calling main.

Technical details

The module uses the same basic code for each of the templates listed above; the differences between each of them are configured using the data at Module:Message box/configuration. Here are the various configuration options and what they mean:

  • types – a table containing data used by the type parameter of the message box. The table keys are the values that can be passed to the type parameter, and the table values are tables containing the class and the image used by that type.
  • default – the type to use if no value was passed to the type parameter, or if an invalid value was specified.
  • showInvalidTypeError – whether to show an error if the value passed to the type parameter was invalid.
  • allowBlankParams – usually blank values are stripped from parameters passed to the module. However, whitespace is preserved for the parameters included in the allowBlankParams table.
  • allowSmall – whether a small version of the message box can be produced with "small=yes".
  • smallParam – a custom name for the small parameter. For example, if set to "left" you can produce a small message box using "small=left".
  • smallClass – the class to use for small message boxes.
  • substCheck – whether to perform a subst check or not.
  • classes – an array of classes to use with the message box.
  • imageEmptyCell – whether to use an empty ‎<td>...‎</td> cell if there is no image set. This is used to preserve spacing for message boxes with a width of less than 100% of the screen.
  • imageEmptyCellStyle – whether empty image cells should be styled.
  • imageCheckBlank – whether "image=blank" results in no image being displayed.
  • imageSmallSize – usually, images used in small message boxes are set to 30x30px. This sets a custom size.
  • imageCellDiv – whether to enclose the image in a div enforcing a maximum image size.
  • useCollapsibleTextFields – whether to use text fields that can be collapsed, i.e. "issue", "fix", "talk", etc. Currently only used in ambox.
  • imageRightNone – whether imageright=none results in no image being displayed on the right-hand side of the message box.
  • sectionDefault – the default name for the "section" parameter. Depends on useCollapsibleTextFields.
  • allowMainspaceCategories – allow categorisation in the main namespace.
  • templateCategory – the name of a category to be placed on the template page.
  • templateCategoryRequireName – whether the name parameter is required to display the template category.
  • templateErrorCategory – the name of the error category to be used on the template page.
  • templateErrorParamsToCheck – an array of parameter names to check. If any are absent, the templateErrorCategory is applied to the template page.

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

-- Require necessary modules.
local htmlBuilder = require('Module:HtmlBuilder')
local categoryHandler = require('Module:Category handler').main
local yesno = require('Module:Yesno')

-- Get a language object for formatDate and ucfirst.
local lang = mw.language.getContentLanguage()

-- Set aliases for often-used functions to reduce table lookups.
local format = mw.ustring.format
local tinsert = table.insert
local tconcat = table.concat

local p = {}

local function getTitleObject(page)
    if type(page) == 'string' then
        -- Get the title object, passing the function through pcall 
        -- in case we are over the expensive function count limit.
        local success, title = pcall(mw.title.new, page)
        if success then
            return title
        end
    end
end

local function presentButBlank(s)
    if type(s) ~= 'string' then return end
    if s and not mw.ustring.find(s, '%S') then
        return true
    else
        return false
    end
end

local function formatCategory(cat, date, all)
    local ret = {}
    cat = type(cat) == 'string' and cat
    date = type(date) == 'string' and date
    all = type(all) == 'string' and all
    local preposition = 'from'
    if cat and date then
        local catTitle = format('Category:%s %s %s', cat, preposition, date)
        tinsert(ret, format('[[%s]]', catTitle))
        catTitle = getTitleObject(catTitle)
        if not catTitle or not catTitle.exists then
            tinsert(ret, '[[Category:Articles with invalid date parameter in template]]')
        end
    elseif cat and not date then
        tinsert(ret, format('[[Category:%s]]', cat))
    end
    if all then
        tinsert(ret, format('[[Category:%s]]', all))
    end
    return tconcat(ret)
end

local function union(t1, t2)
    -- Returns the union of two arrays.
    local vals = {}
    for i, v in ipairs(t1) do
        vals[v] = true
    end
    for i, v in ipairs(t2) do
        vals[v] = true
    end
    local ret = {}
    for k in pairs(vals) do
        tinsert(ret, k)
    end
    table.sort(ret)
    return ret
end

local function getArgNums(args, prefix)
    local nums = {}
    for k, v in pairs(args) do
        local num = mw.ustring.match(tostring(k), '^' .. prefix .. '([1-9]%d*)$')
        if num then
            tinsert(nums, tonumber(num))
        end
    end
    table.sort(nums)
    return nums
end

local function getNamespaceId(ns)
    if type(ns) == 'string' then
        ns = lang:ucfirst(mw.ustring.lower(ns))
        if ns == 'Main' then
            ns = 0
        end
    end
    local nsTable = mw.site.namespaces[ns]
    if nsTable then
        return nsTable.id
    end
end

local function getMboxType(nsid)
    -- Gets the mbox type from a namespace number.
    if nsid == 0 then
        return 'ambox' -- main namespace
    elseif nsid == 6 then
        return 'imbox' -- file namespace
    elseif nsid == 14 then
        return 'cmbox' -- category namespace
    else
        local nsTable = mw.site.namespaces[nsid]
        if nsTable and nsTable.isTalk then
            return 'tmbox' -- any talk namespace
        else
            return 'ombox' -- other namespaces or invalid input
        end
    end
end

function p.build(boxType, args)
    if type(args) ~= 'table' then
        error(format('invalid "args" parameter type; expected type "table", got type "%s"', type(args)), 2)
    end

    -- Get the title object and the namespace.
    local title = getTitleObject(args.page) or mw.title.getCurrentTitle()
    local nsid = getNamespaceId(args.demospace) or title.namespace

    -- Get the box config data from the data page.
    if boxType == 'mbox' then
        boxType = getMboxType(nsid)
    end
    local dataTables = mw.loadData('Module:Message box/data')
    local data = dataTables[boxType]
    if not data then
        local boxTypes = {}
        for k, v in pairs(dataTables) do
            tinsert(boxTypes, format('"%s"', k))
        end
        tinsert(boxTypes, '"mbox"')
        error(format('invalid message box type "%s"; valid types are %s', tostring(boxType), mw.text.listToText(boxTypes)), 2)
    end

    ------------------------ Process config data ----------------------------

    -- Type data.
    local typeData = data.types[args.type]
    local invalidType = args.type and not typeData and true or false
    typeData = typeData or data.types[data.default]

    -- Process data for collapsible text fields
    local name, issue, talk, fix, date, info
    if data.useCollapsibleTextFields then
        name = args.name
        local nameTitle = getTitleObject(name)
        local isTemplatePage = nameTitle and title.prefixedText == ('Template:' .. nameTitle.text) and true or false
        local sect = args.sect
        if presentButBlank(sect) then
            sect = format('This %s ', data.sectionDefault or 'page')
        elseif type(sect) == 'string' then
            sect = 'This ' .. sect .. ' '
        end
        issue = (sect or '') .. (args.issue or '') .. ' ' .. (args.text or '')
        talk = args.talk
        if presentButBlank(talk) and isTemplatePage then
            talk = '#'
        end
        fix = args.fix
        date = args.date
        if presentButBlank(date) and isTemplatePage then
            date = lang:formatDate('F Y')
        end
        info = args.info
    end

    -- Process the talk link, if present.
    if talk then
        -- See if the talk link exists and is for a talk or a content namespace.
        local talkTitle = type(talk) == 'string' and getTitleObject(talk)
        if not talkTitle or not talkTitle.isTalkPage then
            -- If we couldn't process the talk page link, get the talk page of the current page.
            local success
            success, talkTitle = pcall(title.talkPageTitle, title)
            if not success then
                talkTitle = nil
            end
        end
        if talkTitle and talkTitle.exists then
            local talkText = ' Relevant discussion may be found on'
            if talkTitle.isTalkPage then
                talkText = format('%s [[%s|%s]].', talkText, talk, talkTitle.prefixedText)
            else
                talkText = format('%s the [[%s#%s|talk page]].', talkText, talkTitle.prefixedText, talk)
            end
            talk = talkText
        end
    end

    -- Find whether we are using a small message box and process our data accordingly.
    local isSmall = data.allowSmall and (args.small == 'yes' or args.small == true) and true or false
    local smallClass, image, imageRight, text, imageSize
    if isSmall then
        smallClass = data.smallClass or 'mbox-small'
        image = args.smallimage or args.image
        imageRight = args.smallimageright or args.imageright
        if data.useCollapsibleTextFields then
            text = args.smalltext or issue
        else
            text = args.smalltext or args.text
        end
        imageSize = data.imageSmallSize or '30x30px'
    else
        image = args.image
        imageRight = args.imageright
        imageSize = '40x40px'
        text = args.text
    end

    -- Process mainspace categories.
    local mainCats = {}
    local origCategoryNums -- origCategoryNums might be used in computing the template error category.
    if data.allowMainspaceCategories then
        -- Categories for the main namespace.
        local origCatNums = getArgNums(args, 'cat')
        local origCategoryNums = getArgNums(args, 'category')
        local catNums = union(origCatNums, origCategoryNums)
        for _, num in ipairs(catNums) do
            local cat = args['cat' .. tostring(num)] or args['category' .. tostring(num)]
            local all = args['all' .. tostring(num)]
            tinsert(mainCats, formatCategory(cat, args.date, all))
        end
    end

    -- Process template namespace categories
    local templateCats = {}
    if data.templateCategory and not title.isSubpage and not yesno(args.nocat) then
        tinsert(templateCats, format('[[Category:%s]]', data.templateCategory))
    end

    -- Add an error category for the template namespace if appropriate.
    if data.templateErrorCategory then
        local catName = data.templateErrorCategory
        local templateCat
        if not name and not title.isSubpage then
            templateCat = format('[[Category:%s]]', catName)
        elseif type(name) == 'string' and title.prefixedText == ('Template:' .. name) then
            local paramsToCheck = data.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 = format('[[Category:%s|%d]]', catName, count)
            end
            if origCategoryNums and #origCategoryNums > 0 then
                templateCat = format('[[Category:%s|C]]', catName)
            end
        end
        tinsert(templateCats, templatecat)
    end

    -- Categories for all namespaces.
    local allCats = {}
    if invalidType then
        local catsort = (nsid == 0 and 'Main:' or '') .. title.prefixedText
        tinsert(allCats, format('[[Category:Wikipedia message box parameter needs fixing|%s]]', catsort))
    end

    ------------------------ Build the box ----------------------------
    
    local root = htmlBuilder.create()

    -- Do the subst check.
    if data.substCheck and args.subst == 'SUBST' then
        if type(name) == 'string' then
            root
                .tag('b')
                    .addClass('error')
                    .wikitext(format(
                        'Template <code>%s%s%s</code> has been incorrectly substituted.',
                        mw.text.nowiki('{{'), name, mw.text.nowiki('}}')
                    ))
        end
        tinsert(allCats, '[[Category:Pages with incorrectly substituted templates]]')
    end

    -- Create the box table.
    local box = root.tag('table')
    box
        .attr('id', args.id)
    for i, class in ipairs(data.classes) do
        box
            .addClass(class)
    end
    box
        .addClass(isSmall and smallClass)
        .addClass(data.classPlainlinksYesno and yesno(args.plainlinks or true) and 'plainlinks')
        .addClass(typeData.class)
        .addClass(args.class)
        .cssText(args.style)
        .attr('role', 'presentation')

    -- Add the left-hand image.
    local row = box.tag('tr')
    local imageCheckBlank = data.imageCheckBlank
    if image ~= 'none' and not imageCheckBlank or image ~= 'none' and imageCheckBlank and image ~= 'blank' then
        local imageLeftCell = row.tag('td').addClass('mbox-image')
        if not isSmall and data.imageCellDiv then
            imageLeftCell = imageLeftCell.tag('div').css('width', '52px') -- If we are using a div, redefine imageLeftCell so that the image is inside it.
        end
        imageLeftCell
            .wikitext(image or format('[[File:%s|%s|link=|alt=]]', typeData.image, imageSize))
    elseif data.imageEmptyCell then
        row.tag('td')
            .addClass('mbox-empty-cell') -- No image. Cell with some width or padding necessary for text cell to have 100% width.
            .cssText(data.imageEmptyCellStyle and 'border:none;padding:0px;width:1px')
    end

    -- Add the text.
    local textCell = row.tag('td').addClass('mbox-text')
    if data.useCollapsibleTextFields then
        textCell
            .cssText(args.textstyle)
        local textCellSpan = textCell.tag('span')
        textCellSpan
            .addClass('mbox-text-span')
            .wikitext(issue)
        if not isSmall then
            textCellSpan
                .tag('span')
                    .addClass('hide-when-compact')
                    .wikitext(talk)
                    .wikitext(' ')
                    .wikitext(fix)
                    .done()
        end
        textCellSpan
            .wikitext(date and format(" <small>''(%s)''</small>", date))
        if not isSmall then
            textCellSpan
                .tag('span')
                    .addClass('hide-when-compact')
                    .wikitext(info and ' ' .. info)
        end
    else
        textCell
            .cssText(args.textstyle)
            .wikitext(text)
    end

    -- Add the right-hand image.
    if imageRight and not (data.imageRightNone and imageRight == 'none') then
        local imageRightCell = row.tag('td').addClass('mbox-imageright')
        if not isSmall and data.imageCellDiv then
            imageRightCell = imageRightCell.tag('div').css('width', '52px') -- If we are using a div, redefine imageRightCell so that the image is inside it.
        end
        imageRightCell
            .wikitext(imageRight)
    end

    -- Add the below row.
    if data.below and args.below then
        box.tag('tr')
            .tag('td')
                .attr('colspan', args.imageright and '3' or '2')
                .addClass('mbox-text')
                .cssText(args.textstyle)
                .wikitext(args.below)
    end

    ------------------------ Error messages and categories ----------------------------

    -- Add error message for invalid type parameters.
    if invalidType then
        root
            .tag('div')
                .addClass('error')
                .css('text-align', 'center')
                .wikitext(format('This message box is using an invalid type parameter (<code>type=%s</code>) and needs fixing.', args.type or ''))
    end

    -- Add categories using categoryHandler.
    root
        .wikitext(categoryHandler{
            main = tconcat(mainCats),
            template = tconcat(templateCats),
            all = tconcat(allCats)
        })
    
    return tostring(root)
end

local function makeWrapper(boxType)
    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 ~= '' or k == 'talk' or k == 'sect' or k == 'date' then
                args[k] = v
            end
        end
        return p.build(boxType, args)
    end
end

p.mbox = makeWrapper('mbox')
p.ambox = makeWrapper('ambox')
p.cmbox = makeWrapper('cmbox')
p.fmbox = makeWrapper('fmbox')
p.imbox = makeWrapper('imbox')
p.ombox = makeWrapper('ombox')
p.tmbox = makeWrapper('tmbox')

return p