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)