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