Editing Module:Message box
Jump to navigation
Jump to search
The edit can be undone. Please check the comparison below to verify that this is what you want to do, and then publish the changes below to finish undoing the edit.
Latest revision | Your text | ||
Line 1: | Line 1: | ||
require('Module: | -- This is a meta-module for producing message box templates, including {{mbox}}, {{ambox}}, {{imbox}}, {{tmbox}}, {{ombox}}, {{cmbox}} and {{fmbox}}. | ||
local | |||
-- Require necessary modules. | |||
local htmlBuilder = require('Module:HtmlBuilder') | |||
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() | local lang = mw.language.getContentLanguage() | ||
local | -- Set aliases for often-used functions to reduce table lookups. | ||
local | 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 | 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 | end | ||
local function union(t1, t2) | 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 | end | ||
local function getArgNums(args, prefix) | 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 | 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 | |||
local | 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 | 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. | |||
self.pageTitle = getTitleObject(args.page ~= '' and args.page) | |||
self.title = self.pageTitle or mw.title.getCurrentTitle() | |||
self.demospace = args.demospace ~= '' and args.demospace or nil | |||
self.nsid = box.getNamespaceId(self.demospace) or self.title.namespace | |||
end | end | ||
function | 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 | end | ||
function | 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 | |||
if v ~= '' then | |||
newArgs[k] = v | |||
end | |||
end | |||
for i, param in ipairs(cfg.allowBlankParams or {}) do | |||
newArgs[param] = args[param] | |||
end | |||
return newArgs | |||
end | end | ||
function | function box:setBoxParameters(cfg, args) | ||
-- Get type data. | |||
self.type = args.type | |||
local typeData = cfg.types[self.type] | |||
self.invalidTypeError = cfg.showInvalidTypeError and 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. | |||
if cfg.allowSmall and ( | |||
cfg.smallParam and args.small == cfg.smallParam | |||
or not cfg.smallParam and yesno(args.small) | |||
) | |||
then | |||
self.isSmall = true | |||
else | |||
self.isSmall = false | |||
end | |||
-- 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:addClass(args.class) | |||
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(self.name) | |||
self.isTemplatePage = nameTitle and self.title.prefixedText == ('Template:' .. nameTitle.text) and true or false | |||
-- Get the self.issue value. | |||
if self.isSmall and args.smalltext then | |||
self.issue = args.smalltext | |||
else | |||
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, ' ') | |||
end | |||
-- 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(self.title.talkPageTitle, self.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) | |||
local talkText | |||
if | |||
talkText = | |||
else | else | ||
talkText = | talkText = format('%s the [[%s#%s|talk page]].', talkText, talkTitle.prefixedText, talk) | ||
end | 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 | |||
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 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 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 = 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 | |||
-- 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 self.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 self.title.prefixedText == ('Template:' .. self.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 | |||
end | |||
-- Categories for all namespaces. | |||
if self.invalidTypeError then | |||
local allSort = (nsid == 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 | ||
-- 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, | |||
page = self.pageTitle and self.pageTitle.prefixedText or nil | |||
} | |||
end | end | ||
function | 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[[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) | |||
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. | |||
-- 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) | |||
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 self.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 self.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 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. | |||
end | 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.invalidTypeError 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 | end | ||
function | 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 | ||
function | 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 | end | ||
return | 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') | |||
} |