Module:Message box
Revision as of 11:48, 19 August 2020 by classes>Louis Alarcon (1 revision imported)
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 = 'Relevant discussion may be found on'
271 if talkArgIsTalkPage then
272 talkText = string.format(
273 '%s [[%s|%s]].',
274 talkText,
275 talk,
276 talkTitle.prefixedText
277 )
278 else
279 talkText = string.format(
280 '%s the [[%s#%s|talk page]].',
281 talkText,
282 talkTitle.prefixedText,
283 talk
284 )
285 end
286 self.talk = talkText
287 end
288 end
289
290 -- Get other values.
291 self.fix = args.fix ~= '' and args.fix or nil
292 local date
293 if args.date and args.date ~= '' then
294 date = args.date
295 elseif args.date == '' and self.isTemplatePage then
296 date = lang:formatDate('F Y')
297 end
298 if date then
299 self.date = string.format(" <small class='date-container'>''(<span class='date'>%s</span>)''</small>", date)
300 end
301 self.info = args.info
302 if yesno(args.removalnotice) then
303 self.removalNotice = cfg.removalNotice
304 end
305 end
306
307 -- Set the non-collapsible text field. At the moment this is used by all box
308 -- types other than ambox, and also by ambox when small=yes.
309 if self.isSmall then
310 self.text = args.smalltext or args.text
311 else
312 self.text = args.text
313 end
314
315 -- Set the below row.
316 self.below = cfg.below and args.below
317
318 -- General image settings.
319 self.imageCellDiv = not self.isSmall and cfg.imageCellDiv
320 self.imageEmptyCell = cfg.imageEmptyCell
321 if cfg.imageEmptyCellStyle then
322 self.imageEmptyCellStyle = 'border:none;padding:0px;width:1px'
323 end
324
325 -- Left image settings.
326 local imageLeft = self.isSmall and args.smallimage or args.image
327 if cfg.imageCheckBlank and imageLeft ~= 'blank' and imageLeft ~= 'none'
328 or not cfg.imageCheckBlank and imageLeft ~= 'none'
329 then
330 self.imageLeft = imageLeft
331 if not imageLeft then
332 local imageSize = self.isSmall
333 and (cfg.imageSmallSize or '30x30px')
334 or '40x40px'
335 self.imageLeft = string.format('[[File:%s|%s|link=|alt=]]', self.typeImage
336 or 'Imbox notice.png', imageSize)
337 end
338 end
339
340 -- Right image settings.
341 local imageRight = self.isSmall and args.smallimageright or args.imageright
342 if not (cfg.imageRightNone and imageRight == 'none') then
343 self.imageRight = imageRight
344 end
345 end
346
347 function MessageBox:setMainspaceCategories()
348 local args = self.args
349 local cfg = self.cfg
350
351 if not cfg.allowMainspaceCategories then
352 return nil
353 end
354
355 local nums = {}
356 for _, prefix in ipairs{'cat', 'category', 'all'} do
357 args[prefix .. '1'] = args[prefix]
358 nums = union(nums, getArgNums(args, prefix))
359 end
360
361 -- The following is roughly equivalent to the old {{Ambox/category}}.
362 local date = args.date
363 date = type(date) == 'string' and date
364 local preposition = 'from'
365 for _, num in ipairs(nums) do
366 local mainCat = args['cat' .. tostring(num)]
367 or args['category' .. tostring(num)]
368 local allCat = args['all' .. tostring(num)]
369 mainCat = type(mainCat) == 'string' and mainCat
370 allCat = type(allCat) == 'string' and allCat
371 if mainCat and date and date ~= '' then
372 local catTitle = string.format('%s %s %s', mainCat, preposition, date)
373 self:addCat(0, catTitle)
374 catTitle = getTitleObject('Category:' .. catTitle)
375 if not catTitle or not catTitle.exists then
376 self:addCat(0, 'Articles with invalid date parameter in template')
377 end
378 elseif mainCat and (not date or date == '') then
379 self:addCat(0, mainCat)
380 end
381 if allCat then
382 self:addCat(0, allCat)
383 end
384 end
385 end
386
387 function MessageBox:setTemplateCategories()
388 local args = self.args
389 local cfg = self.cfg
390
391 -- Add template categories.
392 if cfg.templateCategory then
393 if cfg.templateCategoryRequireName then
394 if self.isTemplatePage then
395 self:addCat(10, cfg.templateCategory)
396 end
397 elseif not self.title.isSubpage then
398 self:addCat(10, cfg.templateCategory)
399 end
400 end
401
402 -- Add template error categories.
403 if cfg.templateErrorCategory then
404 local templateErrorCategory = cfg.templateErrorCategory
405 local templateCat, templateSort
406 if not self.name and not self.title.isSubpage then
407 templateCat = templateErrorCategory
408 elseif self.isTemplatePage then
409 local paramsToCheck = cfg.templateErrorParamsToCheck or {}
410 local count = 0
411 for i, param in ipairs(paramsToCheck) do
412 if not args[param] then
413 count = count + 1
414 end
415 end
416 if count > 0 then
417 templateCat = templateErrorCategory
418 templateSort = tostring(count)
419 end
420 if self.categoryNums and #self.categoryNums > 0 then
421 templateCat = templateErrorCategory
422 templateSort = 'C'
423 end
424 end
425 self:addCat(10, templateCat, templateSort)
426 end
427 end
428
429 function MessageBox:setAllNamespaceCategories()
430 -- Set categories for all namespaces.
431 if self.invalidTypeError then
432 local allSort = (self.title.namespace == 0 and 'Main:' or '') .. self.title.prefixedText
433 self:addCat('all', 'Wikipedia message box parameter needs fixing', allSort)
434 end
435 if self.isSubstituted then
436 self:addCat('all', 'Pages with incorrectly substituted templates')
437 end
438 end
439
440 function MessageBox:setCategories()
441 if self.title.namespace == 0 then
442 self:setMainspaceCategories()
443 elseif self.title.namespace == 10 then
444 self:setTemplateCategories()
445 end
446 self:setAllNamespaceCategories()
447 end
448
449 function MessageBox:renderCategories()
450 if not self.hasCategories then
451 -- No categories added, no need to pass them to Category handler so,
452 -- if it was invoked, it would return the empty string.
453 -- So we shortcut and return the empty string.
454 return ""
455 end
456 -- Convert category tables to strings and pass them through
457 -- [[Module:Category handler]].
458 return require('Module:Category handler')._main{
459 main = table.concat(self.categories[0] or {}),
460 template = table.concat(self.categories[10] or {}),
461 all = table.concat(self.categories.all or {}),
462 nocat = self.args.nocat,
463 page = self.args.page
464 }
465 end
466
467 function MessageBox:export()
468 local root = mw.html.create()
469
470 -- Add the subst check error.
471 if self.isSubstituted and self.name then
472 root:tag('b')
473 :addClass('error')
474 :wikitext(string.format(
475 'Template <code>%s[[Template:%s|%s]]%s</code> has been incorrectly substituted.',
476 mw.text.nowiki('{{'), self.name, self.name, mw.text.nowiki('}}')
477 ))
478 end
479
480 -- Create the box table.
481 local boxTable = root:tag('table')
482 boxTable:attr('id', self.id or nil)
483 for i, class in ipairs(self.classes or {}) do
484 boxTable:addClass(class or nil)
485 end
486 boxTable
487 :cssText(self.style or nil)
488 :attr('role', 'presentation')
489
490 if self.attrs then
491 boxTable:attr(self.attrs)
492 end
493
494 -- Add the left-hand image.
495 local row = boxTable:tag('tr')
496 if self.imageLeft then
497 local imageLeftCell = row:tag('td'):addClass('mbox-image')
498 if self.imageCellDiv then
499 -- If we are using a div, redefine imageLeftCell so that the image
500 -- is inside it. Divs use style="width: 52px;", which limits the
501 -- image width to 52px. If any images in a div are wider than that,
502 -- they may overlap with the text or cause other display problems.
503 imageLeftCell = imageLeftCell:tag('div'):css('width', '52px')
504 end
505 imageLeftCell:wikitext(self.imageLeft or nil)
506 elseif self.imageEmptyCell then
507 -- Some message boxes define an empty cell if no image is specified, and
508 -- some don't. The old template code in templates where empty cells are
509 -- specified gives the following hint: "No image. Cell with some width
510 -- or padding necessary for text cell to have 100% width."
511 row:tag('td')
512 :addClass('mbox-empty-cell')
513 :cssText(self.imageEmptyCellStyle or nil)
514 end
515
516 -- Add the text.
517 local textCell = row:tag('td'):addClass('mbox-text')
518 if self.useCollapsibleTextFields then
519 -- The message box uses advanced text parameters that allow things to be
520 -- collapsible. At the moment, only ambox uses this.
521 textCell:cssText(self.textstyle or nil)
522 local textCellDiv = textCell:tag('div')
523 textCellDiv
524 :addClass('mbox-text-span')
525 :wikitext(self.issue or nil)
526 if (self.talk or self.fix) and not self.isSmall then
527 textCellDiv:tag('span')
528 :addClass('hide-when-compact')
529 :wikitext(self.talk and (' ' .. self.talk) or nil)
530 :wikitext(self.fix and (' ' .. self.fix) or nil)
531 end
532 textCellDiv:wikitext(self.date and (' ' .. self.date) or nil)
533 if self.info and not self.isSmall then
534 textCellDiv
535 :tag('span')
536 :addClass('hide-when-compact')
537 :wikitext(self.info and (' ' .. self.info) or nil)
538 end
539 if self.removalNotice then
540 textCellDiv:tag('small')
541 :addClass('hide-when-compact')
542 :tag('i')
543 :wikitext(string.format(" (%s)", self.removalNotice))
544 end
545 else
546 -- Default text formatting - anything goes.
547 textCell
548 :cssText(self.textstyle or nil)
549 :wikitext(self.text or nil)
550 end
551
552 -- Add the right-hand image.
553 if self.imageRight then
554 local imageRightCell = row:tag('td'):addClass('mbox-imageright')
555 if self.imageCellDiv then
556 -- If we are using a div, redefine imageRightCell so that the image
557 -- is inside it.
558 imageRightCell = imageRightCell:tag('div'):css('width', '52px')
559 end
560 imageRightCell
561 :wikitext(self.imageRight or nil)
562 end
563
564 -- Add the below row.
565 if self.below then
566 boxTable:tag('tr')
567 :tag('td')
568 :attr('colspan', self.imageRight and '3' or '2')
569 :addClass('mbox-text')
570 :cssText(self.textstyle or nil)
571 :wikitext(self.below or nil)
572 end
573
574 -- Add error message for invalid type parameters.
575 if self.invalidTypeError then
576 root:tag('div')
577 :css('text-align', 'center')
578 :wikitext(string.format(
579 'This message box is using an invalid "type=%s" parameter and needs fixing.',
580 self.type or ''
581 ))
582 end
583
584 -- Add categories.
585 root:wikitext(self:renderCategories() or nil)
586
587 return tostring(root)
588 end
589
590 --------------------------------------------------------------------------------
591 -- Exports
592 --------------------------------------------------------------------------------
593
594 local p, mt = {}, {}
595
596 function p._exportClasses()
597 -- For testing.
598 return {
599 MessageBox = MessageBox
600 }
601 end
602
603 function p.main(boxType, args, cfgTables)
604 local box = MessageBox.new(boxType, args, cfgTables or mw.loadData(CONFIG_MODULE))
605 box:setParameters()
606 box:setCategories()
607 return box:export()
608 end
609
610 function mt.__index(t, k)
611 return function (frame)
612 if not getArgs then
613 getArgs = require('Module:Arguments').getArgs
614 end
615 return t.main(k, getArgs(frame, {trim = false, removeBlanks = false}))
616 end
617 end
618
619 return setmetatable(p, mt)