Module:Message box: Difference between revisions
meta>Mr. Stradivarius pass nocat, page, and demospace parameters to Module:Category handler, and add a choice for which arguments to allow as blank |
meta>Mr. Stradivarius rewrite with a "box" object to make the code a little less spaghetti-like |
||
Line 14: | Line 14: | ||
local tconcat = table.concat | local tconcat = table.concat | ||
local | local box = {} | ||
local function getTitleObject(page) | local function getTitleObject(page) | ||
Line 34: | Line 34: | ||
return false | return false | ||
end | end | ||
end | end | ||
Line 87: | Line 65: | ||
end | end | ||
function box.getNamespaceId(ns) | |||
if not ns then return end | if not ns then return end | ||
if type(ns) == 'string' then | if type(ns) == 'string' then | ||
Line 101: | Line 79: | ||
end | end | ||
function box.getMboxType(nsid) | |||
-- Gets the mbox type from a namespace number. | -- Gets the mbox type from a namespace number. | ||
if nsid == 0 then | if nsid == 0 then | ||
Line 119: | Line 97: | ||
end | end | ||
function | function box:addCat(ns, cat, sort) | ||
if type( | if type(cat) ~= 'string' then return end | ||
local nsVals = {'main', 'template', 'all'} | |||
local tname | |||
for i, val in ipairs(nsVals) do | |||
if ns == val then | |||
tname = ns .. 'Cats' | |||
end | |||
end | end | ||
if not tname then | |||
for i, val in ipairs(nsVals) do | |||
nsVals[i] = format('"%s"', val) | |||
end | |||
error('invalid ns parameter passed to box:addCat; valid values are ' .. mw.text.listToText(nsVals, nil, ' or ')) | |||
end | |||
self[tname] = self[tname] or {} | |||
if type(sort) == 'string' then | |||
tinsert(self[tname], format('[[Category:%s|%s]]', cat, sort)) | |||
else | |||
tinsert(self[tname], format('[[Category:%s]]', cat)) | |||
end | |||
end | |||
function box:addClass(class) | |||
if type(class) ~= 'string' then return end | |||
self.classes = self.classes or {} | |||
tinsert(self.classes, class) | |||
end | |||
function box:setTitle(args) | |||
-- Get the title object and the namespace. | -- Get the title object and the namespace. | ||
local pageTitle = getTitleObject(args.page ~= '' and args.page) | local pageTitle = getTitleObject(args.page ~= '' and args.page) | ||
self.title = pageTitle or mw.title.getCurrentTitle() | |||
local demospace = getNamespaceId(args.demospace ~= '' and args.demospace) | local demospace = box.getNamespaceId(args.demospace ~= '' and args.demospace) | ||
self.nsid = demospace or self.title.namespace | |||
end | |||
function box:getConfig(boxType) | |||
-- Get the box config data from the data page. | -- Get the box config data from the data page. | ||
if boxType == 'mbox' then | if boxType == 'mbox' then | ||
boxType = getMboxType(nsid) | boxType = box.getMboxType(self.nsid) | ||
end | end | ||
local | local cfgTables = mw.loadData('Module:Message box/configuration') | ||
local | local cfg = cfgTables[boxType] | ||
if not | if not cfg then | ||
local boxTypes = {} | local boxTypes = {} | ||
for k, v in pairs(dataTables) do | for k, v in pairs(dataTables) do | ||
Line 144: | Line 149: | ||
error(format('invalid message box type "%s"; valid types are %s', tostring(boxType), mw.text.listToText(boxTypes)), 2) | error(format('invalid message box type "%s"; valid types are %s', tostring(boxType), mw.text.listToText(boxTypes)), 2) | ||
end | end | ||
return cfg | |||
end | |||
-- Only allow blank arguments for the parameter names listed in | function box:removeBlankArgs(cfg, args) | ||
-- Only allow blank arguments for the parameter names listed in cfg.allowBlankParams. | |||
local newArgs = {} | local newArgs = {} | ||
for k, v in pairs(args) do | for k, v in pairs(args) do | ||
for i, param in ipairs( | for i, param in ipairs(cfg.allowBlankParams or {}) do | ||
if v ~= '' or k == param then | if v ~= '' or k == param then | ||
newArgs[k] = v | newArgs[k] = v | ||
Line 154: | Line 162: | ||
end | end | ||
end | end | ||
args = | return newArgs | ||
end | |||
function box:setBoxParameters(cfg, args) | |||
-- Get type data. | |||
self.type = args.type | |||
local typeData = cfg.types[self.type] | |||
self.invalidType = self.type and not typeData and true or false | |||
typeData = typeData or cfg.types[cfg.default] | |||
self.typeClass = typeData.class | |||
self.typeImage = typeData.image | |||
-- Find if the box has been wrongly substituted. | |||
if cfg.substCheck and args.subst == 'SUBST' then | |||
self.isSubstituted = true | |||
end | |||
-- Find whether we are using a small message box. | |||
self.isSmall = cfg.allowSmall and (args.small == 'yes' or args.small == true) and true or false | |||
--- | -- Add attributes, classes and styles. | ||
self.id = args.id | |||
self:addClass(cfg.usePlainlinksParam and yesno(args.plainlinks or true) and 'plainlinks') | |||
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.style = args.style | |||
-- | -- Set text style. | ||
self.textstyle = args.textstyle | |||
-- Process data for collapsible text fields | -- Process data for collapsible text fields. At the moment these are only used in {{ambox}}. | ||
self.useCollapsibleTextFields = cfg.useCollapsibleTextFields | |||
if | if self.useCollapsibleTextFields then | ||
name = args.name | self.name = args.name | ||
local nameTitle = getTitleObject(name) | local nameTitle = getTitleObject(name) | ||
self.isTemplatePage = nameTitle and title.prefixedText == ('Template:' .. nameTitle.text) and true or false | |||
-- Get the self.issue value. | |||
local sect = args.sect | local sect = args.sect | ||
if presentButBlank(sect) then | if presentButBlank(sect) then | ||
sect = | sect = 'This ' .. (cfg.sectionDefault or 'page') | ||
elseif type(sect) == 'string' then | elseif type(sect) == 'string' then | ||
sect = 'This ' .. sect | sect = 'This ' .. sect | ||
else | |||
sect = nil | |||
end | end | ||
issue = ( | local issue = args.issue | ||
talk = args.talk | issue = type(issue) == 'string' and issue or nil | ||
if presentButBlank(talk) and isTemplatePage then | local text = args.text | ||
text = type(text) == 'string' and text or nil | |||
local issues = {} | |||
tinsert(issues, sect) | |||
tinsert(issues, issue) | |||
tinsert(issues, text) | |||
self.issue = tconcat(issues, ' ') | |||
-- Get the self.talk value. | |||
local talk = args.talk | |||
if presentButBlank(talk) and self.isTemplatePage then | |||
talk = '#' | talk = '#' | ||
end | end | ||
if talk then | |||
-- See if the talk link exists and is for a talk or a content namespace. | |||
local talkTitle = 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 | |||
self.talk = talkText | |||
end | |||
end | end | ||
-- Get other values. | |||
self.fix = args.fix | |||
local date = args.date | |||
self.date = date and format(" <small>''(%s)''</small>", date) | |||
if | if presentButBlank(self.date) and self.isTemplatePage then | ||
self.date = lang:formatDate('F Y') | |||
end | end | ||
self.info = args.info | |||
end | 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 | |||
if isSmall then | if self.useCollapsibleTextFields then | ||
self.text = args.smalltext or self.issue | |||
if | |||
text = args.smalltext or issue | |||
else | else | ||
text = args.smalltext or args.text | self.text = args.smalltext or args.text | ||
end | end | ||
else | 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 and true or false | |||
self.imageEmptyCell = cfg.imageEmptyCell | |||
if cfg.imageEmptyCellStyle then | |||
self.imageEmptyCellStyle = 'border:none;padding:0px;width:1px' | |||
end | end | ||
-- | -- Left image settings. | ||
local | local imageCheckBlank = cfg.imageCheckBlank | ||
local | local imageLeft = self.isSmall and args.smallimage or args.image | ||
if | if imageLeft ~= 'none' and not imageCheckBlank or imageLeft ~= 'none' and imageCheckBlank and image ~= 'blank' then | ||
-- | self.imageLeft = imageLeft | ||
if not imageLeft then | |||
local imageSize = self.isSmall and (cfg.imageSmallSize or '30x30px') or '40x40px' | |||
self.imageLeft = 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 | |||
-- Add mainspace categories. At the moment these are only used in {{ambox}}. | |||
if cfg.allowMainspaceCategories then | |||
if args.cat then | if args.cat then | ||
args.cat1 = args.cat | args.cat1 = args.cat | ||
end | end | ||
self.catNums = getArgNums(args, 'cat') | |||
if args.category then | if args.category then | ||
args.category1 = args.category | args.category1 = args.category | ||
end | end | ||
self.categoryNums = getArgNums(args, 'category') | |||
self.categoryParamNums = union(self.catNums, self.categoryNums) | |||
for _, num in ipairs( | -- The following is roughly equivalent to the old {{Ambox/category}}. | ||
local | local date = args.date | ||
local | date = type(date) == 'string' and date | ||
local preposition = 'from' | |||
for _, num in ipairs(self.categoryParamNums) 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 then | |||
local catTitle = format('%s %s %s', mainCat, preposition, date) | |||
self:addCat('main', catTitle) | |||
catTitle = getTitleObject('Category:' .. catTitle) | |||
if not catTitle or not catTitle.exists then | |||
self:addCat('main', 'Articles with invalid date parameter in template') | |||
end | |||
elseif mainCat and not date then | |||
self:addCat('main', mainCat) | |||
end | |||
if allCat then | |||
self:addCat('main', allCat) | |||
end | |||
end | end | ||
end | end | ||
-- | -- Add template-namespace categories. | ||
self.isTemplatePage = type(self.name) == 'string' and title.prefixedText == ('Template:' .. self.name) | |||
if cfg.templateCategory then | |||
if | if self.name then | ||
if name then | if self.isTemplatePage then | ||
if isTemplatePage then | self:addCat('template', cfg.templateCategory) | ||
end | end | ||
elseif not title.isSubpage then | elseif not self.title.isSubpage then | ||
self:addCat('template', cfg.templateCategory) | |||
end | end | ||
end | end | ||
-- Add | -- Add template error category. | ||
if | if cfg.templateErrorCategory then | ||
local | local templateErrorCategory = cfg.templateErrorCategory | ||
local templateCat | local templateCat, templateSort | ||
if not name and not title.isSubpage then | if not self.name and not self.title.isSubpage then | ||
templateCat = | templateCat = templateErrorCategory | ||
elseif type(name) == 'string' and title.prefixedText == ('Template:' .. name) then | elseif type(self.name) == 'string' and title.prefixedText == ('Template:' .. name) then | ||
local paramsToCheck = | local paramsToCheck = cfg.templateErrorParamsToCheck or {} | ||
local count = 0 | local count = 0 | ||
for i, param in ipairs(paramsToCheck) do | for i, param in ipairs(paramsToCheck) do | ||
Line 281: | Line 362: | ||
end | end | ||
if count > 0 then | if count > 0 then | ||
templateCat = | templateCat = templateErrorCategory | ||
templateSort = tostring(count) | |||
end | end | ||
if | if self.categoryNums and #self.categoryNums > 0 then | ||
templateCat = | templateCat = templateErrorCategory | ||
templateSort = 'C' | |||
end | end | ||
end | end | ||
self:addCat('template', templateCat, templateSort) | |||
end | end | ||
-- Categories for all namespaces. | -- Categories for all namespaces. | ||
if self.invalidType then | |||
if invalidType then | local allSort = (nsid == 0 and 'Main:' or '') .. title.prefixedText | ||
local | 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 | ||
-- | -- Convert category tables to strings and pass them through [[Module:Category handler]]. | ||
self.categories = categoryHandler{ | |||
main = tconcat(self.mainCats or {}), | |||
template = tconcat(self.templateCats or {}), | |||
all = tconcat(self.allCats or {}), | |||
nocat = args.nocat, | |||
demospace = self.demospace and args.demospace or nil, | |||
page = self.pageTitle and pageTitle.prefixedText or nil | |||
} | |||
end | |||
function box:export() | |||
local root = htmlBuilder.create() | local root = htmlBuilder.create() | ||
-- | -- Add the subst check error. | ||
if | if self.isSubstituted and self.name then | ||
root | |||
.tag('b') | |||
.addClass('error') | |||
.wikitext(format( | |||
'Template <code>%s%s%s</code> has been incorrectly substituted.', | |||
mw.text.nowiki('{{'), self.name, mw.text.nowiki('}}') | |||
)) | |||
end | end | ||
-- Create the box table. | -- Create the box table. | ||
local | local boxTable = root.tag('table') | ||
boxTable | |||
.attr('id', | .attr('id', self.id) | ||
for i, class in ipairs( | for i, class in ipairs(self.classes or {}) do | ||
boxTable | |||
.addClass(class) | .addClass(class) | ||
end | end | ||
boxTable | |||
.cssText(self.style) | |||
.cssText( | |||
.attr('role', 'presentation') | .attr('role', 'presentation') | ||
-- Add the left-hand image. | -- Add the left-hand image. | ||
local row = | local row = boxTable.tag('tr') | ||
if self.imageLeft then | |||
local imageLeftCell = row.tag('td').addClass('mbox-image') | local imageLeftCell = row.tag('td').addClass('mbox-image') | ||
if | if self.imageCellDiv then | ||
imageLeftCell = imageLeftCell.tag('div').css('width', '52px') | -- If we are using a div, redefine imageLeftCell so that the image is inside it. | ||
-- Not sure why only some box types use divs, but it probably has something to do | |||
-- with that style="width: 52px;". @TODO: find out exactly what this does and fix this comment. | |||
imageLeftCell = imageLeftCell.tag('div').css('width', '52px') | |||
end | end | ||
imageLeftCell | imageLeftCell | ||
.wikitext( | .wikitext(self.imageLeft) | ||
elseif | 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') | row.tag('td') | ||
.addClass('mbox-empty-cell') | .addClass('mbox-empty-cell') | ||
.cssText( | .cssText(self.imageEmptyCellStyle) | ||
end | end | ||
-- Add the text. | -- Add the text. | ||
local textCell = row.tag('td').addClass('mbox-text') | local textCell = row.tag('td').addClass('mbox-text') | ||
if | 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 | textCell | ||
.cssText( | .cssText(self.textstyle) | ||
local textCellSpan = textCell.tag('span') | local textCellSpan = textCell.tag('span') | ||
textCellSpan | textCellSpan | ||
.addClass('mbox-text-span') | .addClass('mbox-text-span') | ||
.wikitext(issue) | .wikitext(self.issue) | ||
if not isSmall then | if not isSmall then | ||
textCellSpan | textCellSpan | ||
.tag('span') | .tag('span') | ||
.addClass('hide-when-compact') | .addClass('hide-when-compact') | ||
.wikitext(talk) | .wikitext(self.talk and ' ' .. self.talk) | ||
.wikitext(' ' | .wikitext(self.fix and ' ' .. self.fix) | ||
end | end | ||
textCellSpan | textCellSpan | ||
.wikitext(date and | .wikitext(self.date and ' ' .. self.date) | ||
if not isSmall then | if not isSmall then | ||
textCellSpan | textCellSpan | ||
.tag('span') | .tag('span') | ||
.addClass('hide-when-compact') | .addClass('hide-when-compact') | ||
.wikitext(info and ' ' .. info) | .wikitext(self.info and ' ' .. self.info) | ||
end | end | ||
else | else | ||
-- Default text formatting - anything goes. | |||
textCell | textCell | ||
.cssText( | .cssText(self.textstyle) | ||
.wikitext(text) | .wikitext(self.text) | ||
end | end | ||
-- Add the right-hand image. | -- Add the right-hand image. | ||
if | if self.imageRight then | ||
local imageRightCell = row.tag('td').addClass('mbox-imageright') | local imageRightCell = row.tag('td').addClass('mbox-imageright') | ||
if not | if not self.imageCellDiv then | ||
imageRightCell = imageRightCell.tag('div').css('width', '52px') -- If we are using a div, redefine imageRightCell so that the image is inside it. | imageRightCell = imageRightCell.tag('div').css('width', '52px') -- If we are using a div, redefine imageRightCell so that the image is inside it. | ||
end | end | ||
imageRightCell | imageRightCell | ||
.wikitext(imageRight) | .wikitext(self.imageRight) | ||
end | end | ||
-- Add the below row. | -- Add the below row. | ||
if | if self.below then | ||
boxTable.tag('tr') | |||
.tag('td') | .tag('td') | ||
.attr('colspan', | .attr('colspan', self.imageRight and '3' or '2') | ||
.addClass('mbox-text') | .addClass('mbox-text') | ||
.cssText( | .cssText(self.textstyle) | ||
.wikitext( | .wikitext(self.below) | ||
end | end | ||
-- Add error message for invalid type parameters. | -- Add error message for invalid type parameters. | ||
if invalidType then | if self.invalidType then | ||
root | root | ||
.tag('div') | .tag('div') | ||
.addClass('error') | .addClass('error') | ||
.css('text-align', 'center') | .css('text-align', 'center') | ||
.wikitext(format('This message box is using an invalid type parameter (<code>type=%s</code>) and needs fixing.', | .wikitext(format('This message box is using an invalid type parameter (<code>type=%s</code>) and needs fixing.', self.type or '')) | ||
end | end | ||
-- Add categories | -- Add categories. | ||
root | root | ||
.wikitext( | .wikitext(self.categories) | ||
return tostring(root) | return tostring(root) | ||
end | |||
local function makeBox(boxType, args) | |||
box:setTitle(args) | |||
local cfg = box:getConfig(boxType) | |||
args = box:removeBlankArgs(cfg, args) | |||
box:setBoxParameters(cfg, args) | |||
return box:export() | |||
end | end | ||
Line 430: | Line 523: | ||
-- assume args are being passed directly in from the debug console | -- assume args are being passed directly in from the debug console | ||
-- or from another Lua module. | -- or from another Lua module. | ||
local | local args | ||
if frame == mw.getCurrentFrame() then | if frame == mw.getCurrentFrame() then | ||
args = frame:getParent().args | |||
for k, v in pairs(frame.args) do | for k, v in pairs(frame.args) do | ||
args = frame.args | |||
break | break | ||
end | end | ||
else | else | ||
args = frame | |||
end | end | ||
return makeBox(boxType, args) | |||
return | |||
end | end | ||
end | end | ||
return { | |||
box = box, | |||
makeBox = makeBox, | |||
mbox = makeWrapper('mbox'), | |||
ambox = makeWrapper('ambox'), | |||
cmbox = makeWrapper('cmbox'), | |||
fmbox = makeWrapper('fmbox'), | |||
imbox = makeWrapper('imbox'), | |||
ombox = makeWrapper('ombox'), | |||
tmbox = makeWrapper('tmbox') | |||
} |
Revision as of 07:15, 28 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.
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.
<syntaxhighlight lang="lua"> local messageBox = require('Module:Message box') </syntaxhighlight>
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.
<syntaxhighlight lang="lua"> local box = messageBox.main( boxType, {
param1 = param1, param2 = param2, -- More parameters...
}) </syntaxhighlight>
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 onuseCollapsibleTextFields
.allowMainspaceCategories
– allow categorisation in the main namespace.templateCategory
– the name of a category to be placed on the template page.templateCategoryRequireName
– whether thename
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, thetemplateErrorCategory
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 box = {}
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 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
function box.getNamespaceId(ns)
if not ns then return end
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
function box.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 box:addCat(ns, cat, sort)
if type(cat) ~= 'string' then return end
local nsVals = {'main', 'template', 'all'}
local tname
for i, val in ipairs(nsVals) do
if ns == val then
tname = ns .. 'Cats'
end
end
if not tname then
for i, val in ipairs(nsVals) do
nsVals[i] = format('"%s"', val)
end
error('invalid ns parameter passed to box:addCat; valid values are ' .. mw.text.listToText(nsVals, nil, ' or '))
end
self[tname] = self[tname] or {}
if type(sort) == 'string' then
tinsert(self[tname], format('[[Category:%s|%s]]', cat, sort))
else
tinsert(self[tname], format('[[Category:%s]]', cat))
end
end
function box:addClass(class)
if type(class) ~= 'string' then return end
self.classes = self.classes or {}
tinsert(self.classes, class)
end
function box:setTitle(args)
-- Get the title object and the namespace.
local pageTitle = getTitleObject(args.page ~= '' and args.page)
self.title = pageTitle or mw.title.getCurrentTitle()
local demospace = box.getNamespaceId(args.demospace ~= '' and args.demospace)
self.nsid = demospace or self.title.namespace
end
function box:getConfig(boxType)
-- Get the box config data from the data page.
if boxType == 'mbox' then
boxType = box.getMboxType(self.nsid)
end
local cfgTables = mw.loadData('Module:Message box/configuration')
local cfg = cfgTables[boxType]
if not cfg 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
return cfg
end
function box:removeBlankArgs(cfg, args)
-- Only allow blank arguments for the parameter names listed in cfg.allowBlankParams.
local newArgs = {}
for k, v in pairs(args) do
for i, param in ipairs(cfg.allowBlankParams or {}) do
if v ~= '' or k == param then
newArgs[k] = v
end
end
end
return newArgs
end
function box:setBoxParameters(cfg, args)
-- Get type data.
self.type = args.type
local typeData = cfg.types[self.type]
self.invalidType = self.type and not typeData and true or false
typeData = typeData or cfg.types[cfg.default]
self.typeClass = typeData.class
self.typeImage = typeData.image
-- Find if the box has been wrongly substituted.
if cfg.substCheck and args.subst == 'SUBST' then
self.isSubstituted = true
end
-- Find whether we are using a small message box.
self.isSmall = cfg.allowSmall and (args.small == 'yes' or args.small == true) and true or false
-- Add attributes, classes and styles.
self.id = args.id
self:addClass(cfg.usePlainlinksParam and yesno(args.plainlinks or true) and 'plainlinks')
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.style = args.style
-- Set text style.
self.textstyle = args.textstyle
-- Process data for collapsible text fields. At the moment these are only used in {{ambox}}.
self.useCollapsibleTextFields = cfg.useCollapsibleTextFields
if self.useCollapsibleTextFields then
self.name = args.name
local nameTitle = getTitleObject(name)
self.isTemplatePage = nameTitle and title.prefixedText == ('Template:' .. nameTitle.text) and true or false
-- Get the self.issue value.
local sect = args.sect
if presentButBlank(sect) then
sect = 'This ' .. (cfg.sectionDefault or 'page')
elseif type(sect) == 'string' then
sect = 'This ' .. sect
else
sect = nil
end
local issue = args.issue
issue = type(issue) == 'string' and issue or nil
local text = args.text
text = type(text) == 'string' and text or nil
local issues = {}
tinsert(issues, sect)
tinsert(issues, issue)
tinsert(issues, text)
self.issue = tconcat(issues, ' ')
-- Get the self.talk value.
local talk = args.talk
if presentButBlank(talk) and self.isTemplatePage then
talk = '#'
end
if talk then
-- See if the talk link exists and is for a talk or a content namespace.
local talkTitle = 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
self.talk = talkText
end
end
-- Get other values.
self.fix = args.fix
local date = args.date
self.date = date and format(" <small>''(%s)''</small>", date)
if presentButBlank(self.date) and self.isTemplatePage then
self.date = lang:formatDate('F Y')
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
if self.useCollapsibleTextFields then
self.text = args.smalltext or self.issue
else
self.text = args.smalltext or args.text
end
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 and true or false
self.imageEmptyCell = cfg.imageEmptyCell
if cfg.imageEmptyCellStyle then
self.imageEmptyCellStyle = 'border:none;padding:0px;width:1px'
end
-- Left image settings.
local imageCheckBlank = cfg.imageCheckBlank
local imageLeft = self.isSmall and args.smallimage or args.image
if imageLeft ~= 'none' and not imageCheckBlank or imageLeft ~= 'none' and imageCheckBlank and image ~= 'blank' then
self.imageLeft = imageLeft
if not imageLeft then
local imageSize = self.isSmall and (cfg.imageSmallSize or '30x30px') or '40x40px'
self.imageLeft = 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
-- Add mainspace categories. At the moment these are only used in {{ambox}}.
if cfg.allowMainspaceCategories then
if args.cat then
args.cat1 = args.cat
end
self.catNums = getArgNums(args, 'cat')
if args.category then
args.category1 = args.category
end
self.categoryNums = getArgNums(args, 'category')
self.categoryParamNums = union(self.catNums, self.categoryNums)
-- 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(self.categoryParamNums) 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 then
local catTitle = format('%s %s %s', mainCat, preposition, date)
self:addCat('main', catTitle)
catTitle = getTitleObject('Category:' .. catTitle)
if not catTitle or not catTitle.exists then
self:addCat('main', 'Articles with invalid date parameter in template')
end
elseif mainCat and not date then
self:addCat('main', mainCat)
end
if allCat then
self:addCat('main', allCat)
end
end
end
-- Add template-namespace categories.
self.isTemplatePage = type(self.name) == 'string' and title.prefixedText == ('Template:' .. self.name)
if cfg.templateCategory then
if self.name then
if self.isTemplatePage then
self:addCat('template', cfg.templateCategory)
end
elseif not self.title.isSubpage then
self:addCat('template', cfg.templateCategory)
end
end
-- Add template error category.
if cfg.templateErrorCategory then
local templateErrorCategory = cfg.templateErrorCategory
local templateCat, templateSort
if not self.name and not self.title.isSubpage then
templateCat = templateErrorCategory
elseif type(self.name) == 'string' and title.prefixedText == ('Template:' .. name) 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('template', templateCat, templateSort)
end
-- Categories for all namespaces.
if self.invalidType then
local allSort = (nsid == 0 and 'Main:' or '') .. 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
-- Convert category tables to strings and pass them through [[Module:Category handler]].
self.categories = categoryHandler{
main = tconcat(self.mainCats or {}),
template = tconcat(self.templateCats or {}),
all = tconcat(self.allCats or {}),
nocat = args.nocat,
demospace = self.demospace and args.demospace or nil,
page = self.pageTitle and pageTitle.prefixedText or nil
}
end
function box:export()
local root = htmlBuilder.create()
-- Add the subst check error.
if self.isSubstituted and self.name then
root
.tag('b')
.addClass('error')
.wikitext(format(
'Template <code>%s%s%s</code> has been incorrectly substituted.',
mw.text.nowiki('{{'), self.name, mw.text.nowiki('}}')
))
end
-- Create the box table.
local boxTable = root.tag('table')
boxTable
.attr('id', self.id)
for i, class in ipairs(self.classes or {}) do
boxTable
.addClass(class)
end
boxTable
.cssText(self.style)
.attr('role', 'presentation')
-- 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.
-- Not sure why only some box types use divs, but it probably has something to do
-- with that style="width: 52px;". @TODO: find out exactly what this does and fix this comment.
imageLeftCell = imageLeftCell.tag('div').css('width', '52px')
end
imageLeftCell
.wikitext(self.imageLeft)
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)
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)
local textCellSpan = textCell.tag('span')
textCellSpan
.addClass('mbox-text-span')
.wikitext(self.issue)
if not isSmall then
textCellSpan
.tag('span')
.addClass('hide-when-compact')
.wikitext(self.talk and ' ' .. self.talk)
.wikitext(self.fix and ' ' .. self.fix)
end
textCellSpan
.wikitext(self.date and ' ' .. self.date)
if not isSmall then
textCellSpan
.tag('span')
.addClass('hide-when-compact')
.wikitext(self.info and ' ' .. self.info)
end
else
-- Default text formatting - anything goes.
textCell
.cssText(self.textstyle)
.wikitext(self.text)
end
-- Add the right-hand image.
if self.imageRight then
local imageRightCell = row.tag('td').addClass('mbox-imageright')
if not self.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(self.imageRight)
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)
.wikitext(self.below)
end
-- Add error message for invalid type parameters.
if self.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.', self.type or ''))
end
-- Add categories.
root
.wikitext(self.categories)
return tostring(root)
end
local function makeBox(boxType, args)
box:setTitle(args)
local cfg = box:getConfig(boxType)
args = box:removeBlankArgs(cfg, args)
box:setBoxParameters(cfg, args)
return box:export()
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 args
if frame == mw.getCurrentFrame() then
args = frame:getParent().args
for k, v in pairs(frame.args) do
args = frame.args
break
end
else
args = frame
end
return makeBox(boxType, args)
end
end
return {
box = box,
makeBox = makeBox,
mbox = makeWrapper('mbox'),
ambox = makeWrapper('ambox'),
cmbox = makeWrapper('cmbox'),
fmbox = makeWrapper('fmbox'),
imbox = makeWrapper('imbox'),
ombox = makeWrapper('ombox'),
tmbox = makeWrapper('tmbox')
}