Module:Infobox

Iz Medžuviki, svobodnoj encyklopedije
Jump to navigation Jump to search

Modulj, ktory okaže Template:Kartka. Jest osnovany na w:Module:Infobox.

local p = {}
local args = {}
local origArgs = {}
local root
local templatestyles_page = 'Module:Infobox/styles.css'
local empty_row_categories = {}
local category_in_empty_row_pattern = '%[%[%s*[Cc][Aa][Tt][Ee][Gg][Oo][Rr][Yy]%s*:[^]]*]]'

local isv_converter = require('Module:Alphabet')

-- ↓ Convert to Cyrillic if needed
function isv_cyrl(value)
	return isv_converter._global_cyrl(value)
end

-- ↓ Adds wiki link around the parameter if it is not blank
local function isv_autolink(value, enabled)
	if value == '' or value == nil then
		return ''
	end
	
	if not enabled or value:match('[^\n][%[%{*:;#]') then
		return value
	end
	
	return '[[' .. value .. ']]'
end


local function fixChildBoxes(sval, tt)
	local function notempty( s ) return s and s:match( '%S' ) end
	
	if notempty(sval) then
		local marker = '<span class=special_infobox_marker>'
		local s = sval
		s = mw.ustring.gsub(s, '(<%s*[Tt][Rr])', marker .. '%1')
		s = mw.ustring.gsub(s, '(</[Tt][Rr]%s*>)', '%1' .. marker)
		if s:match(marker) then
			s = mw.ustring.gsub(s, marker .. '%s*' .. marker, '')
			s = mw.ustring.gsub(s, '([\r\n]|-[^\r\n]*[\r\n])%s*' .. marker, '%1')
			s = mw.ustring.gsub(s, marker .. '%s*([\r\n]|-)', '%1')
			s = mw.ustring.gsub(s, '(</[Cc][Aa][Pp][Tt][Ii][Oo][Nn]%s*>%s*)' .. marker, '%1')
			s = mw.ustring.gsub(s, '(<%s*[Tt][Aa][Bb][Ll][Ee][^<>]*>%s*)' .. marker, '%1')
			s = mw.ustring.gsub(s, '^(%{|[^\r\n]*[\r\n]%s*)' .. marker, '%1')
			s = mw.ustring.gsub(s, '([\r\n]%{|[^\r\n]*[\r\n]%s*)' .. marker, '%1')
			s = mw.ustring.gsub(s, marker .. '(%s*</[Tt][Aa][Bb][Ll][Ee]%s*>)', '%1')
			s = mw.ustring.gsub(s, marker .. '(%s*\n|%})', '%1')
		end
		if s:match(marker) then
			local subcells = mw.text.split(s, marker)
			s = ''
			for k = 1, #subcells do
				if k == 1 then
					s = s .. subcells[k] .. '</' .. tt .. '></tr>'
				elseif k == #subcells then
					local rowstyle = ' style="display:none"'
					if notempty(subcells[k]) then rowstyle = ''	end
					s = s .. '<tr' .. rowstyle ..'><' .. tt .. ' colspan=2>\n' .. subcells[k]
				elseif notempty(subcells[k]) then
					if (k % 2) == 0 then
						s = s .. subcells[k]
					else
						s = s .. '<tr><' .. tt .. ' colspan=2>\n' .. subcells[k] .. '</' .. tt .. '></tr>'
					end
				end
			end
		end
		-- the next two lines add a newline at the end of lists for the PHP parser
		-- [[Special:Diff/849054481]]
		-- remove when [[:phab:T191516]] is fixed or OBE
		s = mw.ustring.gsub(s, '([\r\n][%*#;:][^\r\n]*)$', '%1\n')
		s = mw.ustring.gsub(s, '^([%*#;:][^\r\n]*)$', '%1\n')
		s = mw.ustring.gsub(s, '^([%*#;:])', '\n%1')
		s = mw.ustring.gsub(s, '^(%{%|)', '\n%1')
		return s
	else
		return sval
	end
end

-- Returns the union of the values of two tables, as a sequence.
local function union(t1, t2)

	local vals = {}
	for k, v in pairs(t1) do
		vals[v] = true
	end
	for k, v in pairs(t2) do
		vals[v] = true
	end
	local ret = {}
	for k, v in pairs(vals) do
		table.insert(ret, k)
	end
	return ret
end

-- Returns a table containing the numbers of the arguments that exist
-- for the specified prefix. For example, if the prefix was 'data', and
-- 'data1', 'data2', and 'data5' exist, it would return {1, 2, 5}.
local function getArgNums(prefix)
	local nums = {}
	for k, v in pairs(args) do
		local num = tostring(k):match('^' .. prefix .. '([1-9]%d*)$')
		if num then table.insert(nums, tonumber(num)) end
	end
	table.sort(nums)
	return nums
end

-- Adds a row to the infobox, with either a header cell
-- or a label/data cell combination.
local function addRow(rowArgs)
	if rowArgs.header and rowArgs.header ~= '_BLANK_' then
		root
			:tag('tr')
				:addClass(rowArgs.rowclass)
				:cssText(rowArgs.rowstyle)
				:tag('th')
					:attr('colspan', '2')
					:addClass('infobox-header is-separated')
					:addClass(rowArgs.class)
					:addClass(args.headerclass)
					-- :cssText(args.headerstyle)
					-- :cssText(rowArgs.rowcellstyle)
					:wikitext(fixChildBoxes(isv_cyrl(rowArgs.header), 'th'))
		if rowArgs.data then
			root:wikitext(
				'[[Category:Pages which use infobox templates with ignored data cells]]'
			)
		end
	elseif rowArgs.data and rowArgs.data:gsub(
		category_in_empty_row_pattern, ''
		):match('^%S') then
		local row = root:tag('tr')
		row:addClass(rowArgs.rowclass)
		row:cssText(rowArgs.rowstyle)
		if rowArgs.label then
			row
				:tag('th')
					:attr('scope', 'row')
					:addClass('infobox-label')
					-- :cssText(args.labelstyle)
					-- :cssText(rowArgs.rowcellstyle)
					:wikitext(isv_cyrl(rowArgs.label))
					:done()
		end

		-- ↓ Add plainlist class to all data cells
		if not rowArgs.class then
			rowArgs.class = 'plainlist'
		else
			if not mw.ustring.find(rowArgs.class, 'noplainlist') then
				rowArgs.class = 'plainlist ' .. rowArgs.class
			else
				rowArgs.class = mw.ustring.gsub(rowArgs.class, 'noplainlist', '')
			end
		end
		
		local dataCell = row:tag('td')
		dataCell
			:attr('colspan', not rowArgs.label and '2' or nil)
			:addClass(not rowArgs.label and 'infobox-data is-full-width' or 'infobox-data')
			:addClass(rowArgs.class)
			-- :cssText(rowArgs.datastyle)
			-- :cssText(rowArgs.rowcellstyle)
			:wikitext(fixChildBoxes(isv_autolink(rowArgs.data, rowArgs.autolink), 'td'))
	else
		table.insert(empty_row_categories, rowArgs.data or '')
	end
end

local function renderTitle()
	if not args.title then
		if not args.above then return end
	end

	-- ↓ Use above= as a subtitle
	local data = mw.html.create()
	if args.title and args.above then
		data
			:tag('div')
				:addClass('infobox-above')
				:addClass(args.aboveclass)
				:wikitext(isv_cyrl(args.above))
	end
	if args.title then
		data:wikitext(isv_cyrl(args.title))
	else
		data:wikitext(isv_cyrl(args.above))
	end

	root
		:tag('caption')
			:addClass('infobox-title')
			:addClass(args.titleclass)
			-- :cssText(args.titlestyle)
			:wikitext(tostring(data))
end

local function renderBelowRow()
	if not args.below then return end

	root
		:tag('tr')
			:tag('td')
				:attr('colspan', '2')
				:addClass('infobox-below')
				:addClass(args.belowclass)
				-- :cssText(args.belowstyle)
				:wikitext(fixChildBoxes(args.below,'td'))
end

local function addSubheaderRow(subheaderArgs)
	if subheaderArgs.data and
		subheaderArgs.data:gsub(category_in_empty_row_pattern, ''):match('^%S') then
		local row = root:tag('tr')
		row:addClass(subheaderArgs.rowclass)
		
		-- ↓ Add plainlist class to all data cells
		if not subheaderArgs.class then
			subheaderArgs.class = 'plainlist'
		else
			if not mw.ustring.find(subheaderArgs.class, 'noplainlist') then
				subheaderArgs.class = 'plainlist ' .. subheaderArgs.class
			else
				subheaderArgs.class = mw.ustring.gsub(subheaderArgs.class, 'noplainlist', '')
			end
		end

		local dataCell = row:tag('td')
		dataCell
			:attr('colspan', '2')
			:addClass('infobox-subheader')
			:addClass(subheaderArgs.class)
			-- :cssText(subheaderArgs.datastyle)
			-- :cssText(subheaderArgs.rowcellstyle)
			:wikitext(fixChildBoxes(subheaderArgs.data, 'td'))
	else
		table.insert(empty_row_categories, subheaderArgs.data or '')
	end
end

local function renderSubheaders()
	if args.subheader then
		args.subheader1 = args.subheader
	end
	if args.subheaderrowclass then
		args.subheaderrowclass1 = args.subheaderrowclass
	end
	local subheadernums = getArgNums('subheader')
	for k, num in ipairs(subheadernums) do
		-- ↓ Add a separator to first subheader
		local subheaderclass = args.subheaderclass
		if num == 1 then
			subheaderclass = 'is-separated ' .. (subheaderclass or '')
		end
		
		addSubheaderRow({
			data = args['subheader' .. tostring(num)],
			-- datastyle = args.subheaderstyle,
			-- rowcellstyle = args['subheaderstyle' .. tostring(num)],
			class = subheaderclass,
			rowclass = args['subheaderrowclass' .. tostring(num)]
		})
	end
end

local function addImageRow(imageArgs)
	if imageArgs.data and
		imageArgs.data:gsub(category_in_empty_row_pattern, ''):match('^%S') then
		
		local row = root:tag('tr')
		row:addClass(imageArgs.rowclass)

		local dataCell = row:tag('td')
		dataCell
			:attr('colspan', '2')
			:addClass('infobox-image')
			:addClass(imageArgs.class)
			-- :cssText(imageArgs.datastyle)
			:wikitext(fixChildBoxes(imageArgs.data, 'td'))
	else
		table.insert(empty_row_categories, imageArgs.data or '')
	end
end

local function renderImages()
	if args.image then
		args.image1 = args.image
	end
	local imagenums = getArgNums('image')
	for k, num in ipairs(imagenums) do
		-- ↓ Add a separator to first image
		local imageclass = args.imageclass
		if num == 1 then
			imageclass = 'is-separated ' .. (imageclass or '')
		end
		
		local data = mw.html.create():wikitext(args['image' .. tostring(num)])
		addImageRow({
			data = tostring(data),
			-- datastyle = args.imagestyle,
			class = imageclass,
			rowclass = args['imagerowclass' .. tostring(num)]
		})
	end
end

-- When autoheaders are turned on, preprocesses the rows
local function preprocessRows()
	if not args.autoheaders then return end
	
	local rownums = union(getArgNums('header'), getArgNums('data'))
	table.sort(rownums)
	local lastheader
	for k, num in ipairs(rownums) do
		if args['header' .. tostring(num)] then
			if lastheader then
				args['header' .. tostring(lastheader)] = nil
			end
			lastheader = num
		elseif args['data' .. tostring(num)] and
			args['data' .. tostring(num)]:gsub(
				category_in_empty_row_pattern, ''
			):match('^%S') then
			local data = args['data' .. tostring(num)]
			if data:gsub(category_in_empty_row_pattern, ''):match('%S') then
				lastheader = nil
			end
		end
	end
	if lastheader then
		args['header' .. tostring(lastheader)] = nil
	end
end

-- Gets the union of the header and data argument numbers,
-- and renders them all in order
local function renderRows()
	local rownums = union(getArgNums('header'), getArgNums('data'))
	table.sort(rownums)
	for k, num in ipairs(rownums) do
		addRow({
			header = args['header' .. tostring(num)],
			label = args['label' .. tostring(num)],
			data = args['data' .. tostring(num)],
			-- datastyle = args.datastyle,
			class = args['class' .. tostring(num)],
			rowclass = args['rowclass' .. tostring(num)],
			-- ↓ Add an ability to automatically wrap output into wikilinks
			autolink = args['autolink' .. tostring(num)],
			-- rowstyle = args['rowstyle' .. tostring(num)],
			-- rowcellstyle = args['rowcellstyle' .. tostring(num)]
		})
	end
end

local function renderItalicTitle()
	local italicTitle = args['italic title'] and mw.ustring.lower(args['italic title'])
	if italicTitle == '' or italicTitle == 'force' or italicTitle == 'yes' then
		root:wikitext(mw.getCurrentFrame():expandTemplate({title = 'italic title'}))
	end
end

-- Categories in otherwise empty rows are collected in empty_row_categories.
-- This function adds them to the module output. It is not affected by
-- args.decat because this module should not prevent module-external categories
-- from rendering.
local function renderEmptyRowCategories()
	for _, s in ipairs(empty_row_categories) do
		root:wikitext(s)
	end
end

-- Render tracking categories. args.decat == turns off tracking categories.
local function renderTrackingCategories()
	if args.decat == 'yes' then return end
	if args.child == 'yes' then
		if args.title then
			root:wikitext(
				'[[Category:Pages which use embedded infobox templates with the title parameter]]'
			)
		end
	elseif #(getArgNums('data')) == 0 and mw.title.getCurrentTitle().namespace == 0 then
		root:wikitext('[[Category:Articles which use infobox templates with no data rows]]')
	end
end

-- ↓ Load TemplateStyles page for the infobox.
local function loadTemplateStyles()
	local frame = mw.getCurrentFrame()
	
	return frame:extensionTag{
		name = 'templatestyles', args = { src = templatestyles_page }
	}
end

-- Specify the overall layout of the infobox, with special settings if the
-- infobox is used as a 'child' inside another infobox.
local function _infobox()
	if args.child ~= 'yes' then
		root = mw.html.create('table')
		root:attr('data-name', args.name)

		root
			:addClass(args.subbox == 'yes' and 'infobox-subbox' or 'infobox')
			:addClass(args.bodyclass)
			-- :cssText(args.bodystyle)

		renderTitle()
		-- renderAboveRow()
	else
		root = mw.html.create()

		root
			:wikitext(args.title)
	end

	renderSubheaders()
	renderImages()
	preprocessRows()
	renderRows()
	renderBelowRow()
	renderItalicTitle()
	renderEmptyRowCategories()
	renderTrackingCategories()

	return loadTemplateStyles() .. tostring(root)
end

-- If the argument exists and isn't blank, add it to the argument table.
-- Blank arguments are treated as nil to match the behaviour of ParserFunctions.
local function preprocessSingleArg(argName)
	if origArgs[argName] and origArgs[argName] ~= '' then
		args[argName] = origArgs[argName]
	end
end

-- Assign the parameters with the given prefixes to the args table, in order, in
-- batches of the step size specified. This is to prevent references etc. from
-- appearing in the wrong order. The prefixTable should be an array containing
-- tables, each of which has two possible fields, a "prefix" string and a
-- "depend" table. The function always parses parameters containing the "prefix"
-- string, but only parses parameters in the "depend" table if the prefix
-- parameter is present and non-blank.
local function preprocessArgs(prefixTable, step)
	if type(prefixTable) ~= 'table' then
		error("Non-table value detected for the prefix table", 2)
	end
	if type(step) ~= 'number' then
		error("Invalid step value detected", 2)
	end

	-- Get arguments without a number suffix, and check for bad input.
	for i,v in ipairs(prefixTable) do
		if type(v) ~= 'table' or type(v.prefix) ~= "string" or
			(v.depend and type(v.depend) ~= 'table') then
			error('Invalid input detected to preprocessArgs prefix table', 2)
		end
		preprocessSingleArg(v.prefix)
		-- Only parse the depend parameter if the prefix parameter is present and not blank.
		if args[v.prefix] and v.depend then
			for j, dependValue in ipairs(v.depend) do
				if type(dependValue) ~= 'string' then
					error('Invalid "depend" parameter value detected in preprocessArgs')
				end
				preprocessSingleArg(dependValue)
			end
		end
	end

	-- Get arguments with number suffixes.
	local a = 1 -- Counter variable.
	local moreArgumentsExist = true
	while moreArgumentsExist == true do
		moreArgumentsExist = false
		for i = a, a + step - 1 do
			for j,v in ipairs(prefixTable) do
				local prefixArgName = v.prefix .. tostring(i)
				if origArgs[prefixArgName] then
					-- Do another loop if any arguments are found, even blank ones.
					moreArgumentsExist = true
					preprocessSingleArg(prefixArgName)
				end
				-- Process the depend table if the prefix argument is present
				-- and not blank, or we are processing "prefix1" and "prefix" is
				-- present and not blank, and if the depend table is present.
				if v.depend and (args[prefixArgName] or (i == 1 and args[v.prefix])) then
					for j,dependValue in ipairs(v.depend) do
						local dependArgName = dependValue .. tostring(i)
						preprocessSingleArg(dependArgName)
					end
				end
			end
		end
		a = a + step
	end
end

-- Parse the data parameters in the same order that the old {{infobox}} did, so
-- that references etc. will display in the expected places. Parameters that
-- depend on another parameter are only processed if that parameter is present,
-- to avoid phantom references appearing in article reference lists.
local function parseDataParameters()
	preprocessSingleArg('autoheaders')
	preprocessSingleArg('child')
	preprocessSingleArg('bodyclass')
	preprocessSingleArg('subbox')
	preprocessSingleArg('title')
	preprocessSingleArg('titleclass')
	preprocessSingleArg('subtitle')
	preprocessSingleArg('subtitleclass')
	preprocessSingleArg('above')
	preprocessSingleArg('aboveclass')
	preprocessArgs({
		{prefix = 'subheader', depend = {'subheaderrowclass'}}
	}, 10)
	preprocessSingleArg('subheaderclass')
	preprocessArgs({
		{prefix = 'image', depend = {'imagerowclass'}}
	}, 10)
	preprocessSingleArg('imageclass')
	preprocessArgs({
		{prefix = 'header'},
		{prefix = 'data', depend = {'label'}},
		{prefix = 'rowclass'},
		{prefix = 'class'},
		-- ↓ Add an ability to automatically wrap output into wikilinks
		{prefix = 'autolink'},
	}, 50)
	preprocessSingleArg('headerclass')
	preprocessSingleArg('below')
	preprocessSingleArg('belowclass')
	preprocessSingleArg('name')
	-- different behaviour for italics if blank or absent
	args['italic title'] = origArgs['italic title']
	preprocessSingleArg('decat')
end

-- If called via #invoke, use the args passed into the invoking template.
-- Otherwise, for testing purposes, assume args are being passed directly in.
function p.infobox(frame)
	if frame == mw.getCurrentFrame() then
		origArgs = frame:getParent().args
	else
		origArgs = frame
	end
	
	parseDataParameters()
	
	return _infobox()
end

-- For calling via #invoke within a template
function p.infoboxTemplate(frame)
	origArgs = {}
	for k,v in pairs(frame.args) do origArgs[k] = mw.text.trim(v) end
	
	parseDataParameters()
	
	return _infobox()
end
return p