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