Module:Infobox

From Viki
Jump to navigation Jump to search

Module:Infobox is a module that implements the {{Infobox}} template. Please see the template page for usage instructions.

Tracking categories


  1 local p = {}
  2 local args = {}
  3 local origArgs = {}
  4 local root
  5 local empty_row_categories = {}
  6 local category_in_empty_row_pattern = '%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:[^]]*]]'
  7 local has_rows = false
  8 
  9 local function fixChildBoxes(sval, tt)
 10 	local function notempty( s ) return s and s:match( '%S' ) end
 11 	
 12 	if notempty(sval) then
 13 		local marker = '<span class=special_infobox_marker>'
 14 		local s = sval
 15 		-- start moving templatestyles and categories inside of table rows
 16 		local slast = ''
 17 		while slast ~= s do
 18 			slast = s
 19 			s = mw.ustring.gsub(s, '(</[Tt][Rr]%s*>%s*)(%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:[^]]*%]%])', '%2%1')
 20 			s = mw.ustring.gsub(s, '(</[Tt][Rr]%s*>%s*)(\127[^\127]*UNIQ%-%-templatestyles%-%x+%-QINU[^\127]*\127)', '%2%1')
 21 		end
 22 		-- end moving templatestyles and categories inside of table rows
 23 		s = mw.ustring.gsub(s, '(<%s*[Tt][Rr])', marker .. '%1')
 24 		s = mw.ustring.gsub(s, '(</[Tt][Rr]%s*>)', '%1' .. marker)
 25 		if s:match(marker) then
 26 			s = mw.ustring.gsub(s, marker .. '%s*' .. marker, '')
 27 			s = mw.ustring.gsub(s, '([\r\n]|-[^\r\n]*[\r\n])%s*' .. marker, '%1')
 28 			s = mw.ustring.gsub(s, marker .. '%s*([\r\n]|-)', '%1')
 29 			s = mw.ustring.gsub(s, '(</[Cc][Aa][Pp][Tt][Ii][Oo][Nn]%s*>%s*)' .. marker, '%1')
 30 			s = mw.ustring.gsub(s, '(<%s*[Tt][Aa][Bb][Ll][Ee][^<>]*>%s*)' .. marker, '%1')
 31 			s = mw.ustring.gsub(s, '^(%{|[^\r\n]*[\r\n]%s*)' .. marker, '%1')
 32 			s = mw.ustring.gsub(s, '([\r\n]%{|[^\r\n]*[\r\n]%s*)' .. marker, '%1')
 33 			s = mw.ustring.gsub(s, marker .. '(%s*</[Tt][Aa][Bb][Ll][Ee]%s*>)', '%1')
 34 			s = mw.ustring.gsub(s, marker .. '(%s*\n|%})', '%1')
 35 		end
 36 		if s:match(marker) then
 37 			local subcells = mw.text.split(s, marker)
 38 			s = ''
 39 			for k = 1, #subcells do
 40 				if k == 1 then
 41 					s = s .. subcells[k] .. '</' .. tt .. '></tr>'
 42 				elseif k == #subcells then
 43 					local rowstyle = ' style="display:none"'
 44 					if notempty(subcells[k]) then rowstyle = ''	end
 45 					s = s .. '<tr' .. rowstyle ..'><' .. tt .. ' colspan=2>\n' ..
 46 						subcells[k]
 47 				elseif notempty(subcells[k]) then
 48 					if (k % 2) == 0 then
 49 						s = s .. subcells[k]
 50 					else
 51 						s = s .. '<tr><' .. tt .. ' colspan=2>\n' ..
 52 							subcells[k] .. '</' .. tt .. '></tr>'
 53 					end
 54 				end
 55 			end
 56 		end
 57 		-- the next two lines add a newline at the end of lists for the PHP parser
 58 		-- [[Special:Diff/849054481]]
 59 		-- remove when [[:phab:T191516]] is fixed or OBE
 60 		s = mw.ustring.gsub(s, '([\r\n][%*#;:][^\r\n]*)$', '%1\n')
 61 		s = mw.ustring.gsub(s, '^([%*#;:][^\r\n]*)$', '%1\n')
 62 		s = mw.ustring.gsub(s, '^([%*#;:])', '\n%1')
 63 		s = mw.ustring.gsub(s, '^(%{%|)', '\n%1')
 64 		return s
 65 	else
 66 		return sval
 67 	end
 68 end
 69 
 70 -- Cleans empty tables
 71 local function cleanInfobox()
 72 	root = tostring(root)
 73 	if has_rows == false then
 74 		root = mw.ustring.gsub(root, '<table[^<>]*>%s*</table>', '')
 75 	end
 76 end
 77 
 78 -- Returns the union of the values of two tables, as a sequence.
 79 local function union(t1, t2)
 80 
 81 	local vals = {}
 82 	for k, v in pairs(t1) do
 83 		vals[v] = true
 84 	end
 85 	for k, v in pairs(t2) do
 86 		vals[v] = true
 87 	end
 88 	local ret = {}
 89 	for k, v in pairs(vals) do
 90 		table.insert(ret, k)
 91 	end
 92 	return ret
 93 end
 94 
 95 -- Returns a table containing the numbers of the arguments that exist
 96 -- for the specified prefix. For example, if the prefix was 'data', and
 97 -- 'data1', 'data2', and 'data5' exist, it would return {1, 2, 5}.
 98 local function getArgNums(prefix)
 99 	local nums = {}
100 	for k, v in pairs(args) do
101 		local num = tostring(k):match('^' .. prefix .. '([1-9]%d*)$')
102 		if num then table.insert(nums, tonumber(num)) end
103 	end
104 	table.sort(nums)
105 	return nums
106 end
107 
108 -- Adds a row to the infobox, with either a header cell
109 -- or a label/data cell combination.
110 local function addRow(rowArgs)
111 	
112 	if rowArgs.header and rowArgs.header ~= '_BLANK_' then
113 		has_rows = true
114 		root
115 			:tag('tr')
116 				:addClass(rowArgs.rowclass)
117 				:cssText(rowArgs.rowstyle)
118 				:tag('th')
119 					:attr('colspan', '2')
120 					:addClass('infobox-header')
121 					:addClass(rowArgs.class)
122 					:addClass(args.headerclass)
123 					-- @deprecated next; target .infobox-<name> .infobox-header
124 					:cssText(args.headerstyle)
125 					:cssText(rowArgs.rowcellstyle)
126 					:wikitext(fixChildBoxes(rowArgs.header, 'th'))
127 		if rowArgs.data then
128 			root:wikitext(
129 				'[[Category:Pages using infobox templates with ignored data cells]]'
130 			)
131 		end
132 	elseif rowArgs.data and rowArgs.data:gsub(
133 			category_in_empty_row_pattern, ''
134 		):match('^%S') then
135 		has_rows = true
136 		local row = root:tag('tr')
137 		row:addClass(rowArgs.rowclass)
138 		row:cssText(rowArgs.rowstyle)
139 		if rowArgs.label then
140 			row
141 				:tag('th')
142 					:attr('scope', 'row')
143 					:addClass('infobox-label')
144 					-- @deprecated next; target .infobox-<name> .infobox-label
145 					:cssText(args.labelstyle)
146 					:cssText(rowArgs.rowcellstyle)
147 					:wikitext(rowArgs.label)
148 					:done()
149 		end
150 
151 		local dataCell = row:tag('td')
152 		dataCell
153 			:attr('colspan', not rowArgs.label and '2' or nil)
154 			:addClass(not rowArgs.label and 'infobox-full-data' or 'infobox-data')
155 			:addClass(rowArgs.class)
156 			-- @deprecated next; target .infobox-<name> .infobox(-full)-data
157 			:cssText(rowArgs.datastyle)
158 			:cssText(rowArgs.rowcellstyle)
159 			:wikitext(fixChildBoxes(rowArgs.data, 'td'))
160 	else
161 		table.insert(empty_row_categories, rowArgs.data or '')
162 	end
163 end
164 
165 local function renderTitle()
166 	if not args.title then return end
167 
168 	has_rows = true
169 	root
170 		:tag('caption')
171 			:addClass('infobox-title')
172 			:addClass(args.titleclass)
173 			-- @deprecated next; target .infobox-<name> .infobox-title
174 			:cssText(args.titlestyle)
175 			:wikitext(args.title)
176 end
177 
178 local function renderAboveRow()
179 	if not args.above then return end
180 
181 	has_rows = true
182 	root
183 		:tag('tr')
184 			:tag('th')
185 				:attr('colspan', '2')
186 				:addClass('infobox-above')
187 				:addClass(args.aboveclass)
188 				-- @deprecated next; target .infobox-<name> .infobox-above
189 				:cssText(args.abovestyle)
190 				:wikitext(fixChildBoxes(args.above,'th'))
191 end
192 
193 local function renderBelowRow()
194 	if not args.below then return end
195 
196 	has_rows = true
197 	root
198 		:tag('tr')
199 			:tag('td')
200 				:attr('colspan', '2')
201 				:addClass('infobox-below')
202 				:addClass(args.belowclass)
203 				-- @deprecated next; target .infobox-<name> .infobox-below
204 				:cssText(args.belowstyle)
205 				:wikitext(fixChildBoxes(args.below,'td'))
206 end
207 
208 local function addSubheaderRow(subheaderArgs)
209 	if subheaderArgs.data and
210 		subheaderArgs.data:gsub(category_in_empty_row_pattern, ''):match('^%S') then
211 		has_rows = true
212 		local row = root:tag('tr')
213 		row:addClass(subheaderArgs.rowclass)
214 
215 		local dataCell = row:tag('td')
216 		dataCell
217 			:attr('colspan', '2')
218 			:addClass('infobox-subheader')
219 			:addClass(subheaderArgs.class)
220 			:cssText(subheaderArgs.datastyle)
221 			:cssText(subheaderArgs.rowcellstyle)
222 			:wikitext(fixChildBoxes(subheaderArgs.data, 'td'))
223 	else
224 		table.insert(empty_row_categories, subheaderArgs.data or '')
225 	end
226 end
227 
228 local function renderSubheaders()
229 	if args.subheader then
230 		args.subheader1 = args.subheader
231 	end
232 	if args.subheaderrowclass then
233 		args.subheaderrowclass1 = args.subheaderrowclass
234 	end
235 	local subheadernums = getArgNums('subheader')
236 	for k, num in ipairs(subheadernums) do
237 		addSubheaderRow({
238 			data = args['subheader' .. tostring(num)],
239 			-- @deprecated next; target .infobox-<name> .infobox-subheader
240 			datastyle = args.subheaderstyle,
241 			rowcellstyle = args['subheaderstyle' .. tostring(num)],
242 			class = args.subheaderclass,
243 			rowclass = args['subheaderrowclass' .. tostring(num)]
244 		})
245 	end
246 end
247 
248 local function addImageRow(imageArgs)
249 
250 	if imageArgs.data and
251 		imageArgs.data:gsub(category_in_empty_row_pattern, ''):match('^%S') then
252 
253 		has_rows = true
254 		local row = root:tag('tr')
255 		row:addClass(imageArgs.rowclass)
256 
257 		local dataCell = row:tag('td')
258 		dataCell
259 			:attr('colspan', '2')
260 			:addClass('infobox-image')
261 			:addClass(imageArgs.class)
262 			:cssText(imageArgs.datastyle)
263 			:wikitext(fixChildBoxes(imageArgs.data, 'td'))
264 	else
265 		table.insert(empty_row_categories, imageArgs.data or '')
266 	end
267 end
268 
269 local function renderImages()
270 	if args.image then
271 		args.image1 = args.image
272 	end
273 	if args.caption then
274 		args.caption1 = args.caption
275 	end
276 	local imagenums = getArgNums('image')
277 	for k, num in ipairs(imagenums) do
278 		local caption = args['caption' .. tostring(num)]
279 		local data = mw.html.create():wikitext(args['image' .. tostring(num)])
280 		if caption then
281 			data
282 				:tag('div')
283 					:addClass('infobox-caption')
284 					-- @deprecated next; target .infobox-<name> .infobox-caption
285 					:cssText(args.captionstyle)
286 					:wikitext(caption)
287 		end
288 		addImageRow({
289 			data = tostring(data),
290 			-- @deprecated next; target .infobox-<name> .infobox-image
291 			datastyle = args.imagestyle,
292 			class = args.imageclass,
293 			rowclass = args['imagerowclass' .. tostring(num)]
294 		})
295 	end
296 end
297 
298 -- When autoheaders are turned on, preprocesses the rows
299 local function preprocessRows()
300 	if not args.autoheaders then return end
301 	
302 	local rownums = union(getArgNums('header'), getArgNums('data'))
303 	table.sort(rownums)
304 	local lastheader
305 	for k, num in ipairs(rownums) do
306 		if args['header' .. tostring(num)] then
307 			if lastheader then
308 				args['header' .. tostring(lastheader)] = nil
309 			end
310 			lastheader = num
311 		elseif args['data' .. tostring(num)] and
312 			args['data' .. tostring(num)]:gsub(
313 				category_in_empty_row_pattern, ''
314 			):match('^%S') then
315 			local data = args['data' .. tostring(num)]
316 			if data:gsub(category_in_empty_row_pattern, ''):match('%S') then
317 				lastheader = nil
318 			end
319 		end
320 	end
321 	if lastheader then
322 		args['header' .. tostring(lastheader)] = nil
323 	end
324 end
325 
326 -- Gets the union of the header and data argument numbers,
327 -- and renders them all in order
328 local function renderRows()
329 
330 	local rownums = union(getArgNums('header'), getArgNums('data'))
331 	table.sort(rownums)
332 	for k, num in ipairs(rownums) do
333 		addRow({
334 			header = args['header' .. tostring(num)],
335 			label = args['label' .. tostring(num)],
336 			data = args['data' .. tostring(num)],
337 			datastyle = args.datastyle,
338 			class = args['class' .. tostring(num)],
339 			rowclass = args['rowclass' .. tostring(num)],
340 			-- @deprecated next; target .infobox-<name> rowclass
341 			rowstyle = args['rowstyle' .. tostring(num)],
342 			rowcellstyle = args['rowcellstyle' .. tostring(num)]
343 		})
344 	end
345 end
346 
347 local function renderNavBar()
348 	if not args.name then return end
349 
350 	has_rows = true
351 	root
352 		:tag('tr')
353 			:tag('td')
354 				:attr('colspan', '2')
355 				:addClass('infobox-navbar')
356 				:wikitext(require('Module:Navbar')._navbar{
357 					args.name,
358 					mini = 1,
359 				})
360 end
361 
362 local function renderItalicTitle()
363 	local italicTitle = args['italic title'] and mw.ustring.lower(args['italic title'])
364 	if italicTitle == '' or italicTitle == 'force' or italicTitle == 'yes' then
365 		root:wikitext(mw.getCurrentFrame():expandTemplate({title = 'italic title'}))
366 	end
367 end
368 
369 -- Categories in otherwise empty rows are collected in empty_row_categories.
370 -- This function adds them to the module output. It is not affected by
371 -- args.decat because this module should not prevent module-external categories
372 -- from rendering.
373 local function renderEmptyRowCategories()
374 	for _, s in ipairs(empty_row_categories) do
375 		root:wikitext(s)
376 	end
377 end
378 
379 -- Render tracking categories. args.decat == turns off tracking categories.
380 local function renderTrackingCategories()
381 	if args.decat == 'yes' then return end
382 	if args.child == 'yes' then
383 		if args.title then
384 			root:wikitext(
385 				'[[Category:Pages using embedded infobox templates with the title parameter]]'
386 			)
387 		end
388 	elseif #(getArgNums('data')) == 0 and mw.title.getCurrentTitle().namespace == 0 then
389 		root:wikitext('[[Category:Articles using infobox templates with no data rows]]')
390 	end
391 end
392 
393 --[=[
394 Loads the templatestyles for the infobox.
395 
396 TODO: FINISH loading base templatestyles here rather than in
397 MediaWiki:Common.css. There are 4-5000 pages with 'raw' infobox tables.
398 See [[Mediawiki_talk:Common.css/to_do#Infobox]] and/or come help :).
399 When we do this we should clean up the inline CSS below too.
400 Will have to do some bizarre conversion category like with sidebar.
401 
402 ]=]
403 local function loadTemplateStyles()
404 	local frame = mw.getCurrentFrame()
405 	
406 	-- See function description
407 	local base_templatestyles = frame:extensionTag{
408 		name = 'templatestyles', args = { src = 'Module:Infobox/styles.css' }
409 	}
410 
411 	local templatestyles = ''
412 	if args['templatestyles'] then templatestyles = frame:extensionTag{
413 			name = 'templatestyles', args = { src = args['templatestyles'] }
414 		}
415 	end
416 	
417 	local child_templatestyles = ''
418 	if args['child templatestyles'] then child_templatestyles = frame:extensionTag{
419 			name = 'templatestyles', args = { src = args['child templatestyles'] }
420 		}
421 	end
422 	
423 	local grandchild_templatestyles = ''
424 	if args['grandchild templatestyles'] then grandchild_templatestyles = frame:extensionTag{
425 			name = 'templatestyles', args = { src = args['grandchild templatestyles'] }
426 		}
427 	end
428 
429 	return table.concat({
430 		base_templatestyles, -- see function description
431 		templatestyles,
432 		child_templatestyles,
433 		grandchild_templatestyles
434 	})
435 end
436 
437 -- common functions between the child and non child cases
438 local function structure_infobox_common()
439 	renderSubheaders()
440 	renderImages()
441 	preprocessRows()
442 	renderRows()
443 	renderBelowRow()
444 	renderNavBar()
445 	renderItalicTitle()
446 	renderEmptyRowCategories()
447 	renderTrackingCategories()
448 	cleanInfobox()
449 end
450 
451 -- Specify the overall layout of the infobox, with special settings if the
452 -- infobox is used as a 'child' inside another infobox.
453 local function _infobox()
454 	if args.child ~= 'yes' then
455 		root = mw.html.create('table')
456 
457 		root
458 			:addClass(args.subbox == 'yes' and 'infobox-subbox' or 'infobox')
459 			:addClass(args.bodyclass)
460 			-- @deprecated next; target .infobox-<name>
461 			:cssText(args.bodystyle)
462 
463 		renderTitle()
464 		renderAboveRow()
465 	else
466 		root = mw.html.create()
467 
468 		root
469 			:wikitext(args.title)
470 	end
471 	structure_infobox_common()
472 	
473 	return loadTemplateStyles() .. root
474 end
475 
476 -- If the argument exists and isn't blank, add it to the argument table.
477 -- Blank arguments are treated as nil to match the behaviour of ParserFunctions.
478 local function preprocessSingleArg(argName)
479 	if origArgs[argName] and origArgs[argName] ~= '' then
480 		args[argName] = origArgs[argName]
481 	end
482 end
483 
484 -- Assign the parameters with the given prefixes to the args table, in order, in
485 -- batches of the step size specified. This is to prevent references etc. from
486 -- appearing in the wrong order. The prefixTable should be an array containing
487 -- tables, each of which has two possible fields, a "prefix" string and a
488 -- "depend" table. The function always parses parameters containing the "prefix"
489 -- string, but only parses parameters in the "depend" table if the prefix
490 -- parameter is present and non-blank.
491 local function preprocessArgs(prefixTable, step)
492 	if type(prefixTable) ~= 'table' then
493 		error("Non-table value detected for the prefix table", 2)
494 	end
495 	if type(step) ~= 'number' then
496 		error("Invalid step value detected", 2)
497 	end
498 
499 	-- Get arguments without a number suffix, and check for bad input.
500 	for i,v in ipairs(prefixTable) do
501 		if type(v) ~= 'table' or type(v.prefix) ~= "string" or
502 			(v.depend and type(v.depend) ~= 'table') then
503 			error('Invalid input detected to preprocessArgs prefix table', 2)
504 		end
505 		preprocessSingleArg(v.prefix)
506 		-- Only parse the depend parameter if the prefix parameter is present
507 		-- and not blank.
508 		if args[v.prefix] and v.depend then
509 			for j, dependValue in ipairs(v.depend) do
510 				if type(dependValue) ~= 'string' then
511 					error('Invalid "depend" parameter value detected in preprocessArgs')
512 				end
513 				preprocessSingleArg(dependValue)
514 			end
515 		end
516 	end
517 
518 	-- Get arguments with number suffixes.
519 	local a = 1 -- Counter variable.
520 	local moreArgumentsExist = true
521 	while moreArgumentsExist == true do
522 		moreArgumentsExist = false
523 		for i = a, a + step - 1 do
524 			for j,v in ipairs(prefixTable) do
525 				local prefixArgName = v.prefix .. tostring(i)
526 				if origArgs[prefixArgName] then
527 					-- Do another loop if any arguments are found, even blank ones.
528 					moreArgumentsExist = true
529 					preprocessSingleArg(prefixArgName)
530 				end
531 				-- Process the depend table if the prefix argument is present
532 				-- and not blank, or we are processing "prefix1" and "prefix" is
533 				-- present and not blank, and if the depend table is present.
534 				if v.depend and (args[prefixArgName] or (i == 1 and args[v.prefix])) then
535 					for j,dependValue in ipairs(v.depend) do
536 						local dependArgName = dependValue .. tostring(i)
537 						preprocessSingleArg(dependArgName)
538 					end
539 				end
540 			end
541 		end
542 		a = a + step
543 	end
544 end
545 
546 -- Parse the data parameters in the same order that the old {{infobox}} did, so
547 -- that references etc. will display in the expected places. Parameters that
548 -- depend on another parameter are only processed if that parameter is present,
549 -- to avoid phantom references appearing in article reference lists.
550 local function parseDataParameters()
551 
552 	preprocessSingleArg('autoheaders')
553 	preprocessSingleArg('child')
554 	preprocessSingleArg('bodyclass')
555 	preprocessSingleArg('subbox')
556 	preprocessSingleArg('bodystyle')
557 	preprocessSingleArg('title')
558 	preprocessSingleArg('titleclass')
559 	preprocessSingleArg('titlestyle')
560 	preprocessSingleArg('above')
561 	preprocessSingleArg('aboveclass')
562 	preprocessSingleArg('abovestyle')
563 	preprocessArgs({
564 		{prefix = 'subheader', depend = {'subheaderstyle', 'subheaderrowclass'}}
565 	}, 10)
566 	preprocessSingleArg('subheaderstyle')
567 	preprocessSingleArg('subheaderclass')
568 	preprocessArgs({
569 		{prefix = 'image', depend = {'caption', 'imagerowclass'}}
570 	}, 10)
571 	preprocessSingleArg('captionstyle')
572 	preprocessSingleArg('imagestyle')
573 	preprocessSingleArg('imageclass')
574 	preprocessArgs({
575 		{prefix = 'header'},
576 		{prefix = 'data', depend = {'label'}},
577 		{prefix = 'rowclass'},
578 		{prefix = 'rowstyle'},
579 		{prefix = 'rowcellstyle'},
580 		{prefix = 'class'}
581 	}, 50)
582 	preprocessSingleArg('headerclass')
583 	preprocessSingleArg('headerstyle')
584 	preprocessSingleArg('labelstyle')
585 	preprocessSingleArg('datastyle')
586 	preprocessSingleArg('below')
587 	preprocessSingleArg('belowclass')
588 	preprocessSingleArg('belowstyle')
589 	preprocessSingleArg('name')
590 	-- different behaviour for italics if blank or absent
591 	args['italic title'] = origArgs['italic title']
592 	preprocessSingleArg('decat')
593 	preprocessSingleArg('templatestyles')
594 	preprocessSingleArg('child templatestyles')
595 	preprocessSingleArg('grandchild templatestyles')
596 end
597 
598 -- If called via #invoke, use the args passed into the invoking template.
599 -- Otherwise, for testing purposes, assume args are being passed directly in.
600 function p.infobox(frame)
601 	if frame == mw.getCurrentFrame() then
602 		origArgs = frame:getParent().args
603 	else
604 		origArgs = frame
605 	end
606 	
607 	parseDataParameters()
608 	
609 	return _infobox()
610 end
611 
612 -- For calling via #invoke within a template
613 function p.infoboxTemplate(frame)
614 	origArgs = {}
615 	for k,v in pairs(frame.args) do origArgs[k] = mw.text.trim(v) end
616 	
617 	parseDataParameters()
618 	
619 	return _infobox()
620 end
621 return p