Module:WikiProjectBanner/K

MyWikiBiz, Author Your Legacy — Saturday December 28, 2024
Jump to navigationJump to search

Documentation for this module may be created at Module:WikiProjectBanner/K/doc

local export = {}

-- TODO: a submodule for categorisation may be needed
-- these also need to be checked for existence by the documentation page

local m_common_data = require('Module:WikiProjectBanner/common data')

local
	importance_grades,
	quality_grades,
	importance_scales,
	quality_scales,
	stock_notices
	=
	m_common_data.importance_grades,
	m_common_data.quality_grades,
	m_common_data.importance_scales,
	m_common_data.quality_scales,
	m_common_data.stock_notices

-- creates a wrapper object which tracks unused template arguments
local function track_usage(args)
	local tracker = {}
	
	for key in pairs(args) do
		tracker[key] = true
	end

	return setmetatable({}, {
		__index = function (self, key)
			local value = args[key]
			tracker[key] = nil
			self[key] = value
			return value
		end
	}), tracker
end

-- resolves a quality or importance assessment grade; returns a grade data table and status
local function resolve_grade(scale_config, scales, grades, args, grade_param, title)
	if args == true then
		return grades.na, 'demo'
	end

	if type(scale_config) == "string" then
		scale_config = scales[scale_config]
	elseif not scale_config then
		scale_config = scales.standard
	end

	local ns
	ns = mw.site.namespaces[title.namespace].subject
	if ns.id == 0 then
		ns = "_MAIN"
	else
		ns = ns.canonicalName
	end

	if title.isRedirect and scale_config._REDIRECT then
		local redir_grade = scale_config._REDIRECT[ns] or scale_config._REDIRECT._OTHER
		if redir_grade then
			return grades[redir_grade], 'redirect'
		end
	end

	scale_config = scale_config[ns] or scale_config._OTHER
	if not scale_config then
		return nil	
	end

	if type(scale_config) == "string" then
		return grades[scale_config]	
	end

	local resolver = {}
	for _, item in ipairs(scale_config) do
		local grade = grades[item]
		resolver[item] = grade
		if grade.aliases then
			for _, item in ipairs(grade.aliases) do
				resolver[item] = grade	
			end
		end
	end

	local grade = args[grade_param]
	if grade then
		grade = tostring(grade):lower()
		if resolver[grade] then
			return resolver[grade], 'valid'
		else
			return resolver[scale_config[1]], 'invalid'
		end
	else
		return resolver[scale_config[1]], 'default'
	end
end

-- constructs banner markup and the category list. for internal use only (which includes unit tests).
function export.build_banner(banner_config, banner_hooks, title, banner_args, out, categories)
	local yesno = require('Module:yesno')

	out.root = mw.html.create('')
	local state = {} -- for use by hooks only
	
	local function call_hook(hookfunc, ...)
		if not hookfunc then
			return true
		end
		return hookfunc(--[[ not yet determined ]])
	end

	-- basic skeleton
	out.wrapper =
		out.root:tag('table')
			:addClass('tmbox tmbox-notice collapsible innercollapse wpb')
	
	out.header_row =
		out.wrapper:tag('tr')
			:addClass('wpb-header')
	out.header_name =
		out.header_row:tag('td')
			:css('text-align', 'right')
			:css('padding', '0.3em 1em 0.3em 0.3em')
			:css('width', '50%')
			:css('font-weight', 'bold')
	out.header_rating =
		out.header_row:tag('th')
			:css('text-align', 'left')
			:css('width', '50%')
			:css('padding', '0.3em 0.3em 0.3em 0')

	out.content =
		out.wrapper:tag('tr')
			:tag('td')
				:addClass('mbox-text')
				:css('padding', '3px 0 3px 5px')
				:attr('colspan', '2')
				:tag('table')
					:css('background', 'transparent')
					:css('border', 'none')
					:css('padding', '0')
					:css('width', '100%')
					:attr('cellspacing', '0')
	
	out.has_more = false
	out.content_more =
		mw.html.create('table')
			:addClass('collapsible collapsed')
			:css('width', '100%')
			:css('background', 'transparent')
			:tag('tr')
				:tag('th')
					:attr('colspan', '3')
					:css('text-align', 'left')
					:css('padding', '0.2em 2px 0.2em 0')
					:wikitext(banner_config.more_header or "More information")
				:done()
			:done()

	-- does anyone still use this?
	local is_small = (banner_args ~= true) and yesno(banner_args.small)
	if is_small then
		out.wrapper:addClass("mbox-small")
	end

	-- the blurb
	local page_type = require('Module:pagetype')._main { page = title.fullText }
	
	local blurb_row = out.content:tag('tr')
	if banner_config.image_left then
		out.blurb_image_left = blurb_row:tag('td'):addClass('mbox-image')
	end
	out.blurb_text = blurb_row:tag('td'):addClass('mbox-text')
	if banner_config.image_right then
		out.blurb_image_right = blurb_row:tag('td'):addClass('mbox-imageright')
	end
		
	if banner_config.image_left then
		out.blurb_image_left:wikitext(('[[File:%s|%s]]'):format(
			banner_config.image_left,
			is_small
			and (banner_config.image_left_size_small or "40px")
			or (banner_config.image_left_size_big or "80px")
		))
	end

	if banner_config.image_right then
		out.blurb_image_right:wikitext(('[[File:%s|%s]]'):format(
			banner_config.image_right,
			is_small
			and (banner_config.image_right_size_small or "40px")
			or (banner_config.image_right_size_big or "80px")
		))
	end

	out.blurb_text:attr('colspan',
		(banner_config.image_left and 1 or 0) +
		(banner_config.image_right and 1 or 0) + 
		1
	)

	if banner_config.portal then
		local m_portal = require("Module:Portal")
		out.blurb_text:wikitext(m_portal._portal({ banner_config.portal }, {}))
	end
	
	local project_link = banner_config.project_link or ("Wikipedia:WikiProject " .. banner_config.project)
	local project_name = banner_config.project_name or ("WikiProject " .. banner_config.project)
	if banner_config.blurb then
		out.blurb_text:wikitext(banner_config.blurb)
	else
		local project_scope = banner_config.project_scope or ("[[" .. banner_config.project .. "]]")
		local project_link_talk = project_link:gsub("^Wikipedia:", "Wikipedia talk:") -- XXX: avoiding title objects because they are "expensive" to create
		
		out.blurb_text:wikitext((
			"This %s is within the scope of '''[[%s|%s]]''', a collaborative effort " ..
			"to improve the coverage of %s on Wikipedia. If you would like to participate, " ..
			"please visit the project page, where you can join the [[%s|discussion]] and " ..
			"see a list of open tasks."
		):format(
			page_type, project_link, project_name,
			project_scope, project_link_talk
		))
	end

	out.header_name:wikitext(("[[%s|%s]]"):format(project_link, project_name))

	function out.row_pair(in_more)
		local parent = out.content
		if in_more then
			out.has_more = true
			parent = out.content_more
		end
		local row = parent:tag('tr')
		local cell_img, cell_text
		
		cell_img = row:tag('td')
		cell_text = row:tag('td')
			:attr('colspan', '2')
			:addClass('mbox-text')
		
		return cell_img, cell_text
	end

	-- normalise parameters
	local quality_grade, quality_grade_status = resolve_grade(
		banner_config.quality_scale,
		quality_scales,
		quality_grades,
		banner_args, 'class',
		title
	)
	
	if quality_grade_status == 'invalid' then
		-- TODO: add a category
	end

	local imp_grade, imp_grade_status
	if quality_grade and quality_grade.force_imp then
		imp_grade, imp_grade_status = quality_grade.force_imp, 'forced'
	else
		imp_grade, imp_grade_status = resolve_grade(
			banner_config.importance_scale,
			importance_scales,
			importance_grades,
			banner_args, banner_config.importance_param or 'importance',
			title
		)
	end
	
	if imp_grade_status == 'invalid' then
		-- TODO: add a category
	end

	if quality_grade then
		out.qual_label, out.qual_text = out.row_pair()
		out.qual_label
			:addClass('assess')
			:css('text-align', 'center')
			:css('white-space', 'nowrap')
			:css('font-weight', 'bold')
			:css('background', quality_grade.color)

		if quality_grade.icon then
			out.qual_label
				:wikitext('[[File:' .. quality_grade.icon .. '|16px]] ')
		end
		out.qual_label:wikitext(quality_grade.short)
		out.qual_text:wikitext(("This %s %s on the project's [[%s|quality scale]]."):format(
			page_type, quality_grade.rated_text or
			"has been rated as '''" .. quality_grade.full .. "'''",
			banner_config.quality_scale_link or
			(project_link .. "/Assessment#Quality scale")
		))
	
		-- TODO: add a category
		-- TODO: assessment checklists
	end

	if imp_grade then
		out.imp_label, out.imp_text = out.row_pair()
		out.imp_label
			:addClass('import')	
			:css('text-align', 'center')
			:css('white-space', 'nowrap')
			:css('font-weight', 'bold')
			:css('background', imp_grade.color)
		out.imp_label:wikitext(imp_grade.name)
		out.imp_text:wikitext(("This %s %s on the project's [[%s|importance scale]]."):format(
			page_type, quality_grade.rated_text or
			"has been rated as '''" .. imp_grade.name .. "-importance'''",
			banner_config.importance_scale_link or
			(project_link .. "/Assessment#Importance scale")
		))

		-- TODO: add a category
	end
	
	-- rating text for banner headers inside {{WikiProjectBannerShell}}
	if quality_grade or imp_grade then
		out.header_rating:wikitext("(Rated ")
		if quality_grade then
			out.header_rating:wikitext(quality_grade.short)
			if imp_grade then
				out.header_rating:wikitext(", ")
			end
		end
		if imp_grade then
			out.header_rating:wikitext(imp_grade.name .. "-importance")
		end
		out.header_rating:wikitext(")")
	end

	-- field, like in {{WikiProject Systems}} or {{Maths rating}}
	if banner_config.field then
		local field_config = banner_config.field
		local field

		if banner_args ~= true then
			field = false
			local field_id = banner_args[field_config.arg_name or 'field']
			
			if field_id then
				field = field_config.fields[field_id]
				if type(field) == 'string' then
					field = field_config.fields[field]
				end
			end
		end
		
		out.field_icon, out.field_text = out.row_pair()
		if field then
			out.field_icon:wikitext(("[[File:%s|link=%s]]"):format(
				field.icon, field.link
			))
			out.field_text:wikitext(("This %s is within the field of [[%s|%s]]."):format(
				page_type, field.link, field.name
			))
		else
			out.field_icon:wikitext(("[[File:Purple question mark.svg|link=%s]]"):format(
				field_config.unassessed_link
			))
			out.field_text:wikitext(("This %s is [[%s|not associated with a particular field]]."):format(
				page_type, field_config.unassessed_link
			))

			if field == false then
				-- TODO: add a category because an invalid field has been specified
			end
		end
	end

	-- task forces
	for _, tf_info in ipairs(banner_config.task_forces or {}) do
		-- TODO: is this row needed?
		local needed = (banner_args == true) or banner_args[tf_info.param]
		if tf_info.force then -- {{WikiProject Software}} forces WikiProject Computing's banner for example
			needed = true
		end

		if needed then
			local short_name = tf_info.short_name or tf_info.name
			if tf_info.link then
				out.header_name:wikitext((" / [[%s|%s]]"):format(tf_info.link, short_name))
			else
				out.header_name:wikitext((" / %s"):format(short_name))
			end

			local node_icon, node_text = out.row_pair()
			
			if tf_info.icon then
				node_icon:wikitext(('[[File:%s|x25px|link=%s]]'):format(tf_info.icon, tf_info.link or ''))
			end

			-- TODO: node_text:wikitext("This %s is supported by %s.")
			-- TODO: add categories
		end
	end

	-- requests and other notices (photograph, maps, attention, etc.)
	for _, nt_info in ipairs(banner_config.notices or { stock_notices.auto, stock_notices.attention }) do
		if type(nt_info) == 'string' then
			nt_info = stock_notices[nt_info] or
				error("Invalid stock notice '" .. nt_info .. "' specified in banner configuration")
		end

		local needed = (banner_args == true) or banner_args[nt_info.param]

		if needed then
			local node_icon, node_text = out.row_pair()

			if nt_info.icon then
				node_icon:wikitext(('[[File:%s|x25px|link=%s]]'):format(nt_info.icon, nt_info.link or ''))
			end

			-- TODO: fill node_text
			-- TODO: add categories
		end
	end

	if out.has_more then
		out.content:wikitext(tostring(out.content_more))
	end
end

function export.render_banner(frame)
	local banner_name, is_templ = frame:getParent():getTitle():gsub("^Template:", "")
	banner_name = banner_name:match("^(.*)/sandbox$") or banner_name
	if is_templ == 0 then
		error("This module must be invoked from within a template")
	end

	local demo = false
	if mw.isSubsting() then
		local result = { }
		for key, value in pairs(frame:getParent().args) do
			table.insert(result, "|" .. key .. "=" .. value)
		end

		return "{{" .. banner_name .. table.concat(result) .. "}}"
	elseif mw.title.getCurrentTitle().fullText == frame:getParent():getTitle() then
		demo = true
	elseif mw.title.getCurrentTitle().namespace == mw.site.namespaces.Module.id then
		-- we are viewing it on the banner config page (?)
		demo = true	
	end

	local banner_args, unused_args
	if not demo then
		banner_args, unused_args = track_usage(frame:getParent().args)
	else
		banner_args, unused_args = frame:getParent().args, {}
	end

	local success, banner_config, banner_data

	success, banner_config = pcall(mw.loadData, "Module:WikiProjectBanner/config/" .. banner_name)
	if not success then
		error("Banner data page [[Module:WikiProjectBanner/config/" .. banner_name .. "]] does not exist")
	end

	success, banner_hooks = pcall(require, "Module:WikiProjectBanner/config/" .. banner_name .. "/hooks") or {}
	if not success then
		banner_hooks = {}
	end

	local out, categories = {}, {}

	export.build_banner(
		banner_config, banner_hooks,                      -- banner config
		mw.title.getCurrentTitle(), demo or banner_args,  -- current environment
		out, categories                                   -- output
	)

	if next(unused_args) then
		-- TODO: output a category for unused arguments
	end

	-- categories
	local sort_key = banner_args.listas or mw.title.getCurrentTitle().text
	if (#categories > 0) and yesno(banner_args.category, true) then
		categories = "[[Category:" .. table.concat(categories, "|" .. sort_key .. "]][[Category:") .. "|" .. sort_key .. "]]"
	else
		categories = ""
	end

	return tostring(out.root) .. categories
end

return export