Module:Message box
Jump to navigation
Jump to search
Documentation for this module may be created at Module:Message box/doc
1 -- This is a meta-module for producing message box templates, including
2 -- {{mbox}}, {{ambox}}, {{imbox}}, {{tmbox}}, {{ombox}}, {{cmbox}} and {{fmbox}}.
3
4 -- Load necessary modules.
5 require('Module:No globals')
6 local getArgs
7 local yesno = require('Module:Yesno')
8
9 -- Get a language object for formatDate and ucfirst.
10 local lang = mw.language.getContentLanguage()
11
12 -- Define constants
13 local CONFIG_MODULE = 'Module:Message box/configuration'
14 local DEMOSPACES = {talk = 'tmbox', image = 'imbox', file = 'imbox', category = 'cmbox', article = 'ambox', main = 'ambox'}
15
16 --------------------------------------------------------------------------------
17 -- Helper functions
18 --------------------------------------------------------------------------------
19
20 local function getTitleObject(...)
21 -- Get the title object, passing the function through pcall
22 -- in case we are over the expensive function count limit.
23 local success, title = pcall(mw.title.new, ...)
24 if success then
25 return title
26 end
27 end
28
29 local function union(t1, t2)
30 -- Returns the union of two arrays.
31 local vals = {}
32 for i, v in ipairs(t1) do
33 vals[v] = true
34 end
35 for i, v in ipairs(t2) do
36 vals[v] = true
37 end
38 local ret = {}
39 for k in pairs(vals) do
40 table.insert(ret, k)
41 end
42 table.sort(ret)
43 return ret
44 end
45
46 local function getArgNums(args, prefix)
47 local nums = {}
48 for k, v in pairs(args) do
49 local num = mw.ustring.match(tostring(k), '^' .. prefix .. '([1-9]%d*)$')
50 if num then
51 table.insert(nums, tonumber(num))
52 end
53 end
54 table.sort(nums)
55 return nums
56 end
57
58 --------------------------------------------------------------------------------
59 -- Box class definition
60 --------------------------------------------------------------------------------
61
62 local MessageBox = {}
63 MessageBox.__index = MessageBox
64
65 function MessageBox.new(boxType, args, cfg)
66 args = args or {}
67 local obj = {}
68
69 -- Set the title object and the namespace.
70 obj.title = getTitleObject(args.page) or mw.title.getCurrentTitle()
71
72 -- Set the config for our box type.
73 obj.cfg = cfg[boxType]
74 if not obj.cfg then
75 local ns = obj.title.namespace
76 -- boxType is "mbox" or invalid input
77 if args.demospace and args.demospace ~= '' then
78 -- implement demospace parameter of mbox
79 local demospace = string.lower(args.demospace)
80 if DEMOSPACES[demospace] then
81 -- use template from DEMOSPACES
82 obj.cfg = cfg[DEMOSPACES[demospace]]
83 elseif string.find( demospace, 'talk' ) then
84 -- demo as a talk page
85 obj.cfg = cfg.tmbox
86 else
87 -- default to ombox
88 obj.cfg = cfg.ombox
89 end
90 elseif ns == 0 then
91 obj.cfg = cfg.ambox -- main namespace
92 elseif ns == 6 then
93 obj.cfg = cfg.imbox -- file namespace
94 elseif ns == 14 then
95 obj.cfg = cfg.cmbox -- category namespace
96 else
97 local nsTable = mw.site.namespaces[ns]
98 if nsTable and nsTable.isTalk then
99 obj.cfg = cfg.tmbox -- any talk namespace
100 else
101 obj.cfg = cfg.ombox -- other namespaces or invalid input
102 end
103 end
104 end
105
106 -- Set the arguments, and remove all blank arguments except for the ones
107 -- listed in cfg.allowBlankParams.
108 do
109 local newArgs = {}
110 for k, v in pairs(args) do
111 if v ~= '' then
112 newArgs[k] = v
113 end
114 end
115 for i, param in ipairs(obj.cfg.allowBlankParams or {}) do
116 newArgs[param] = args[param]
117 end
118 obj.args = newArgs
119 end
120
121 -- Define internal data structure.
122 obj.categories = {}
123 obj.classes = {}
124 -- For lazy loading of [[Module:Category handler]].
125 obj.hasCategories = false
126
127 return setmetatable(obj, MessageBox)
128 end
129
130 function MessageBox:addCat(ns, cat, sort)
131 if not cat then
132 return nil
133 end
134 if sort then
135 cat = string.format('[[Category:%s|%s]]', cat, sort)
136 else
137 cat = string.format('[[Category:%s]]', cat)
138 end
139 self.hasCategories = true
140 self.categories[ns] = self.categories[ns] or {}
141 table.insert(self.categories[ns], cat)
142 end
143
144 function MessageBox:addClass(class)
145 if not class then
146 return nil
147 end
148 table.insert(self.classes, class)
149 end
150
151 function MessageBox:setParameters()
152 local args = self.args
153 local cfg = self.cfg
154
155 -- Get type data.
156 self.type = args.type
157 local typeData = cfg.types[self.type]
158 self.invalidTypeError = cfg.showInvalidTypeError
159 and self.type
160 and not typeData
161 typeData = typeData or cfg.types[cfg.default]
162 self.typeClass = typeData.class
163 self.typeImage = typeData.image
164
165 -- Find if the box has been wrongly substituted.
166 self.isSubstituted = cfg.substCheck and args.subst == 'SUBST'
167
168 -- Find whether we are using a small message box.
169 self.isSmall = cfg.allowSmall and (
170 cfg.smallParam and args.small == cfg.smallParam
171 or not cfg.smallParam and yesno(args.small)
172 )
173
174 -- Add attributes, classes and styles.
175 self.id = args.id
176 self.name = args.name
177 if self.name then
178 self:addClass('box-' .. string.gsub(self.name,' ','_'))
179 end
180 if yesno(args.plainlinks) ~= false then
181 self:addClass('plainlinks')
182 end
183 for _, class in ipairs(cfg.classes or {}) do
184 self:addClass(class)
185 end
186 if self.isSmall then
187 self:addClass(cfg.smallClass or 'mbox-small')
188 end
189 self:addClass(self.typeClass)
190 self:addClass(args.class)
191 self.style = args.style
192 self.attrs = args.attrs
193
194 -- Set text style.
195 self.textstyle = args.textstyle
196
197 -- Find if we are on the template page or not. This functionality is only
198 -- used if useCollapsibleTextFields is set, or if both cfg.templateCategory
199 -- and cfg.templateCategoryRequireName are set.
200 self.useCollapsibleTextFields = cfg.useCollapsibleTextFields
201 if self.useCollapsibleTextFields
202 or cfg.templateCategory
203 and cfg.templateCategoryRequireName
204 then
205 if self.name then
206 local templateName = mw.ustring.match(
207 self.name,
208 '^[tT][eE][mM][pP][lL][aA][tT][eE][%s_]*:[%s_]*(.*)$'
209 ) or self.name
210 templateName = 'Template:' .. templateName
211 self.templateTitle = getTitleObject(templateName)
212 end
213 self.isTemplatePage = self.templateTitle
214 and mw.title.equals(self.title, self.templateTitle)
215 end
216
217 -- Process data for collapsible text fields. At the moment these are only
218 -- used in {{ambox}}.
219 if self.useCollapsibleTextFields then
220 -- Get the self.issue value.
221 if self.isSmall and args.smalltext then
222 self.issue = args.smalltext
223 else
224 local sect
225 if args.sect == '' then
226 sect = 'This ' .. (cfg.sectionDefault or 'page')
227 elseif type(args.sect) == 'string' then
228 sect = 'This ' .. args.sect
229 end
230 local issue = args.issue
231 issue = type(issue) == 'string' and issue ~= '' and issue or nil
232 local text = args.text
233 text = type(text) == 'string' and text or nil
234 local issues = {}
235 table.insert(issues, sect)
236 table.insert(issues, issue)
237 table.insert(issues, text)
238 self.issue = table.concat(issues, ' ')
239 end
240
241 -- Get the self.talk value.
242 local talk = args.talk
243 -- Show talk links on the template page or template subpages if the talk
244 -- parameter is blank.
245 if talk == ''
246 and self.templateTitle
247 and (
248 mw.title.equals(self.templateTitle, self.title)
249 or self.title:isSubpageOf(self.templateTitle)
250 )
251 then
252 talk = '#'
253 elseif talk == '' then
254 talk = nil
255 end
256 if talk then
257 -- If the talk value is a talk page, make a link to that page. Else
258 -- assume that it's a section heading, and make a link to the talk
259 -- page of the current page with that section heading.
260 local talkTitle = getTitleObject(talk)
261 local talkArgIsTalkPage = true
262 if not talkTitle or not talkTitle.isTalkPage then
263 talkArgIsTalkPage = false
264 talkTitle = getTitleObject(
265 self.title.text,
266 mw.site.namespaces[self.title.namespace].talk.id
267 )
268 end
269 if talkTitle and talkTitle.exists then
270 local talkText
271 if self.isSmall then
272 local talkLink = talkArgIsTalkPage and talk or (talkTitle.prefixedText .. '#' .. talk)
273 talkText = string.format('([[%s|talk]])', talkLink)
274 else
275 talkText = 'Relevant discussion may be found on'
276 if talkArgIsTalkPage then
277 talkText = string.format(
278 '%s [[%s|%s]].',
279 talkText,
280 talk,
281 talkTitle.prefixedText
282 )
283 else
284 talkText = string.format(
285 '%s the [[%s#%s|talk page]].',
286 talkText,
287 talkTitle.prefixedText,
288 talk
289 )
290 end
291 end
292 self.talk = talkText
293 end
294 end
295
296 -- Get other values.
297 self.fix = args.fix ~= '' and args.fix or nil
298 local date
299 if args.date and args.date ~= '' then
300 date = args.date
301 elseif args.date == '' and self.isTemplatePage then
302 date = lang:formatDate('F Y')
303 end
304 if date then
305 self.date = string.format(" <span class='date-container'>''(<span class='date'>%s</span>)''</span>", date)
306 end
307 self.info = args.info
308 if yesno(args.removalnotice) then
309 self.removalNotice = cfg.removalNotice
310 end
311 end
312
313 -- Set the non-collapsible text field. At the moment this is used by all box
314 -- types other than ambox, and also by ambox when small=yes.
315 if self.isSmall then
316 self.text = args.smalltext or args.text
317 else
318 self.text = args.text
319 end
320
321 -- Set the below row.
322 self.below = cfg.below and args.below
323
324 -- General image settings.
325 self.imageCellDiv = not self.isSmall and cfg.imageCellDiv
326 self.imageEmptyCell = cfg.imageEmptyCell
327 if cfg.imageEmptyCellStyle then
328 self.imageEmptyCellStyle = 'border:none;padding:0;width:1px'
329 end
330
331 -- Left image settings.
332 local imageLeft = self.isSmall and args.smallimage or args.image
333 if cfg.imageCheckBlank and imageLeft ~= 'blank' and imageLeft ~= 'none'
334 or not cfg.imageCheckBlank and imageLeft ~= 'none'
335 then
336 self.imageLeft = imageLeft
337 if not imageLeft then
338 local imageSize = self.isSmall
339 and (cfg.imageSmallSize or '30x30px')
340 or '40x40px'
341 self.imageLeft = string.format('[[File:%s|%s|link=|alt=]]', self.typeImage
342 or 'Imbox notice.png', imageSize)
343 end
344 end
345
346 -- Right image settings.
347 local imageRight = self.isSmall and args.smallimageright or args.imageright
348 if not (cfg.imageRightNone and imageRight == 'none') then
349 self.imageRight = imageRight
350 end
351 end
352
353 function MessageBox:setMainspaceCategories()
354 local args = self.args
355 local cfg = self.cfg
356
357 if not cfg.allowMainspaceCategories then
358 return nil
359 end
360
361 local nums = {}
362 for _, prefix in ipairs{'cat', 'category', 'all'} do
363 args[prefix .. '1'] = args[prefix]
364 nums = union(nums, getArgNums(args, prefix))
365 end
366
367 -- The following is roughly equivalent to the old {{Ambox/category}}.
368 local date = args.date
369 date = type(date) == 'string' and date
370 local preposition = 'from'
371 for _, num in ipairs(nums) do
372 local mainCat = args['cat' .. tostring(num)]
373 or args['category' .. tostring(num)]
374 local allCat = args['all' .. tostring(num)]
375 mainCat = type(mainCat) == 'string' and mainCat
376 allCat = type(allCat) == 'string' and allCat
377 if mainCat and date and date ~= '' then
378 local catTitle = string.format('%s %s %s', mainCat, preposition, date)
379 self:addCat(0, catTitle)
380 catTitle = getTitleObject('Category:' .. catTitle)
381 if not catTitle or not catTitle.exists then
382 self:addCat(0, 'Articles with invalid date parameter in template')
383 end
384 elseif mainCat and (not date or date == '') then
385 self:addCat(0, mainCat)
386 end
387 if allCat then
388 self:addCat(0, allCat)
389 end
390 end
391 end
392
393 function MessageBox:setTemplateCategories()
394 local args = self.args
395 local cfg = self.cfg
396
397 -- Add template categories.
398 if cfg.templateCategory then
399 if cfg.templateCategoryRequireName then
400 if self.isTemplatePage then
401 self:addCat(10, cfg.templateCategory)
402 end
403 elseif not self.title.isSubpage then
404 self:addCat(10, cfg.templateCategory)
405 end
406 end
407
408 -- Add template error categories.
409 if cfg.templateErrorCategory then
410 local templateErrorCategory = cfg.templateErrorCategory
411 local templateCat, templateSort
412 if not self.name and not self.title.isSubpage then
413 templateCat = templateErrorCategory
414 elseif self.isTemplatePage then
415 local paramsToCheck = cfg.templateErrorParamsToCheck or {}
416 local count = 0
417 for i, param in ipairs(paramsToCheck) do
418 if not args[param] then
419 count = count + 1
420 end
421 end
422 if count > 0 then
423 templateCat = templateErrorCategory
424 templateSort = tostring(count)
425 end
426 if self.categoryNums and #self.categoryNums > 0 then
427 templateCat = templateErrorCategory
428 templateSort = 'C'
429 end
430 end
431 self:addCat(10, templateCat, templateSort)
432 end
433 end
434
435 function MessageBox:setAllNamespaceCategories()
436 -- Set categories for all namespaces.
437 if self.invalidTypeError then
438 local allSort = (self.title.namespace == 0 and 'Main:' or '') .. self.title.prefixedText
439 self:addCat('all', 'Wikipedia message box parameter needs fixing', allSort)
440 end
441 if self.isSubstituted then
442 self:addCat('all', 'Pages with incorrectly substituted templates')
443 end
444 end
445
446 function MessageBox:setCategories()
447 if self.title.namespace == 0 then
448 self:setMainspaceCategories()
449 elseif self.title.namespace == 10 then
450 self:setTemplateCategories()
451 end
452 self:setAllNamespaceCategories()
453 end
454
455 function MessageBox:renderCategories()
456 if not self.hasCategories then
457 -- No categories added, no need to pass them to Category handler so,
458 -- if it was invoked, it would return the empty string.
459 -- So we shortcut and return the empty string.
460 return ""
461 end
462 -- Convert category tables to strings and pass them through
463 -- [[Module:Category handler]].
464 return require('Module:Category handler')._main{
465 main = table.concat(self.categories[0] or {}),
466 template = table.concat(self.categories[10] or {}),
467 all = table.concat(self.categories.all or {}),
468 nocat = self.args.nocat,
469 page = self.args.page
470 }
471 end
472
473 function MessageBox:export()
474 local root = mw.html.create()
475
476 -- Add the subst check error.
477 if self.isSubstituted and self.name then
478 root:tag('b')
479 :addClass('error')
480 :wikitext(string.format(
481 'Template <code>%s[[Template:%s|%s]]%s</code> has been incorrectly substituted.',
482 mw.text.nowiki('{{'), self.name, self.name, mw.text.nowiki('}}')
483 ))
484 end
485
486 -- Create the box table.
487 local boxTable = root:tag('table')
488 boxTable:attr('id', self.id or nil)
489 for i, class in ipairs(self.classes or {}) do
490 boxTable:addClass(class or nil)
491 end
492 boxTable
493 :cssText(self.style or nil)
494 :attr('role', 'presentation')
495
496 if self.attrs then
497 boxTable:attr(self.attrs)
498 end
499
500 -- Add the left-hand image.
501 local row = boxTable:tag('tr')
502 if self.imageLeft then
503 local imageLeftCell = row:tag('td'):addClass('mbox-image')
504 if self.imageCellDiv then
505 -- If we are using a div, redefine imageLeftCell so that the image
506 -- is inside it. Divs use style="width: 52px;", which limits the
507 -- image width to 52px. If any images in a div are wider than that,
508 -- they may overlap with the text or cause other display problems.
509 imageLeftCell = imageLeftCell:tag('div'):css('width', '52px')
510 end
511 imageLeftCell:wikitext(self.imageLeft or nil)
512 elseif self.imageEmptyCell then
513 -- Some message boxes define an empty cell if no image is specified, and
514 -- some don't. The old template code in templates where empty cells are
515 -- specified gives the following hint: "No image. Cell with some width
516 -- or padding necessary for text cell to have 100% width."
517 row:tag('td')
518 :addClass('mbox-empty-cell')
519 :cssText(self.imageEmptyCellStyle or nil)
520 end
521
522 -- Add the text.
523 local textCell = row:tag('td'):addClass('mbox-text')
524 if self.useCollapsibleTextFields then
525 -- The message box uses advanced text parameters that allow things to be
526 -- collapsible. At the moment, only ambox uses this.
527 textCell:cssText(self.textstyle or nil)
528 local textCellDiv = textCell:tag('div')
529 textCellDiv
530 :addClass('mbox-text-span')
531 :wikitext(self.issue or nil)
532 if (self.talk or self.fix) then
533 textCellDiv:tag('span')
534 :addClass('hide-when-compact')
535 :wikitext(self.talk and (' ' .. self.talk) or nil)
536 :wikitext(self.fix and (' ' .. self.fix) or nil)
537 end
538 textCellDiv:wikitext(self.date and (' ' .. self.date) or nil)
539 if self.info and not self.isSmall then
540 textCellDiv
541 :tag('span')
542 :addClass('hide-when-compact')
543 :wikitext(self.info and (' ' .. self.info) or nil)
544 end
545 if self.removalNotice then
546 textCellDiv:tag('span')
547 :addClass('hide-when-compact')
548 :tag('i')
549 :wikitext(string.format(" (%s)", self.removalNotice))
550 end
551 else
552 -- Default text formatting - anything goes.
553 textCell
554 :cssText(self.textstyle or nil)
555 :wikitext(self.text or nil)
556 end
557
558 -- Add the right-hand image.
559 if self.imageRight then
560 local imageRightCell = row:tag('td'):addClass('mbox-imageright')
561 if self.imageCellDiv then
562 -- If we are using a div, redefine imageRightCell so that the image
563 -- is inside it.
564 imageRightCell = imageRightCell:tag('div'):css('width', '52px')
565 end
566 imageRightCell
567 :wikitext(self.imageRight or nil)
568 end
569
570 -- Add the below row.
571 if self.below then
572 boxTable:tag('tr')
573 :tag('td')
574 :attr('colspan', self.imageRight and '3' or '2')
575 :addClass('mbox-text')
576 :cssText(self.textstyle or nil)
577 :wikitext(self.below or nil)
578 end
579
580 -- Add error message for invalid type parameters.
581 if self.invalidTypeError then
582 root:tag('div')
583 :css('text-align', 'center')
584 :wikitext(string.format(
585 'This message box is using an invalid "type=%s" parameter and needs fixing.',
586 self.type or ''
587 ))
588 end
589
590 -- Add categories.
591 root:wikitext(self:renderCategories() or nil)
592
593 return tostring(root)
594 end
595
596 --------------------------------------------------------------------------------
597 -- Exports
598 --------------------------------------------------------------------------------
599
600 local p, mt = {}, {}
601
602 function p._exportClasses()
603 -- For testing.
604 return {
605 MessageBox = MessageBox
606 }
607 end
608
609 function p.main(boxType, args, cfgTables)
610 local box = MessageBox.new(boxType, args, cfgTables or mw.loadData(CONFIG_MODULE))
611 box:setParameters()
612 box:setCategories()
613 return box:export()
614 end
615
616 function mt.__index(t, k)
617 return function (frame)
618 if not getArgs then
619 getArgs = require('Module:Arguments').getArgs
620 end
621 return t.main(k, getArgs(frame, {trim = false, removeBlanks = false}))
622 end
623 end
624
625 return setmetatable(p, mt)