Module:WikiProjectBanner/Banner

< Module:WikiProjectBanner
Revision as of 07:58, 16 July 2021 by Zoran (talk | contribs) (Pywikibot 6.4.0)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

This is the Banner class of Module:WikiProjectBanner. It does all of the heavy lifting; it renders the banner, calls the row objects, and calls the hooks.

Hooks

This is the normal HTML structure. Everything that is not a tag or an HTML comment is an HTML object that you can add to.

<syntaxhighlight lang="html5"> preWrapper

headerName headerRating
preBlurb postBlurb postQuality postImportance postTaskForces postRequests postNotices postContent
blurbImageLeft blurbText blurbImageRight

postWrapper </syntaxhighlight>

This is the HTML structure if the requests and notices are collapsed.

<syntaxhighlight lang="html5"> preWrapper

headerName headerRating
preBlurb postBlurb postQuality postImportance postTaskForces postContent
blurbImageLeft blurbText blurbImageRight
preCollapsedContent postRequests postNotices

postWrapper </syntaxhighlight>


-------------------------------------------------------------------------------
--                               Banner class                                --
-- This module contains the Banner class used in Module:WikiProjectBanner.   --
-- It is used to generate the banner HTML and categories.                    --
-------------------------------------------------------------------------------

-- Load required modules.
local yesno = require('Module:Yesno')
local mShared = require('Module:WikiProjectBanner/shared')

-- Lazily load modules we might not need.
local Grade

local Banner = {}
Banner.__index = Banner

Banner.rowModules = {
	quality = 'Module:WikiProjectBanner/AssessmentRow',
	importance = 'Module:WikiProjectBanner/AssessmentRow',
	taskForces = 'Module:WikiProjectBanner/TaskForce',
	requests = 'Module:WikiProjectBanner/Note',
	notices = 'Module:WikiProjectBanner/Note'
}

-- Define the classes table. This loads modules containing the row classes
-- on demand; if it is not indexed, the class is not loaded.
Banner.rowClasses = setmetatable({}, {
	__index = function (t, key)
		local module = Banner.rowModules[key]
		if module then
			module = require(module)
			Banner.rowClasses[key] = module
			return module
		end
	end
})

function Banner.new(bannerName, args, cfg, bannerCfg)
	-- Set data we were passed.
	local obj, data = {}, {}
	data.bannerName = bannerName
	obj.args = args
	obj.cfg = cfg

	-- Set title objects.
	data.currentTitle = mw.title.getCurrentTitle()
	data.subjectTitle = data.currentTitle.subjectPageTitle

	-- Set banner config.
	-- We use a metatable to make it read-only, to try and limit hook
	-- interaction.
	bannerCfg = bannerCfg or mShared.maybeRequire(
		'Module:WikiProjectBanner/banners/' .. data.bannerName
	)
	if not bannerCfg then
		error(
			'banner data page [[Module:WikiProjectBanner/banners/' ..
			bannerName ..
			']] does not exist',
			0
		)
	end
	bannerCfg.hooks = bannerCfg.hooks or {}
	obj.bannerCfg = setmetatable({}, {
		__index = bannerCfg,
		__newindex = function ()
			error('the banner config is read-only', 2)
		end,
		__pairs = function ()
			return pairs(bannerCfg)
		end,
		__ipairs = function ()
			return ipairs(bannerCfg)
		end
	})

	-- Set banner data.
	do
		-- Find if we are in demo mode.
		local currentPage = data.currentTitle.prefixedText
		local template = mw.site.namespaces[10].name .. ':' .. data.bannerName
		if currentPage == template
			or currentPage == template .. '/sandbox'
		then
			data.isDemo = true
		end
	end
	data.pageType = require('Module:Pagetype')._main{}
	data.project = bannerCfg.project or bannerName:gsub('^WikiProject ', '')
	data.projectLink = bannerCfg.projectLink
		or 'Wikipedia:WikiProject ' .. data.project
	data.projectName = bannerCfg.projectName
		or 'WikiProject ' .. data.project
	data.projectScope = bannerCfg.projectScope or '[[' .. data.project .. ']]'
	data.projectLinkTalk = mw.title.new(data.projectLink).talkPageTitle.prefixedText
	data.isSmall = yesno(args.small)

	-- Assessment link
	if bannerCfg.assessmentLink ~= nil then
		-- Custom link or false for no link
		data.assessmentLink = bannerCfg.assessmentLink
	else
		local assessmentPage = data.projectLink .. '/Assessment'
		local assessmentTitle = mw.title.new(assessmentPage)
		data.assessmentLink = assessmentTitle
			and assessmentTitle.exists
			and assessmentTitle.prefixedText
	end

	-- Create the HTML fragments.
	do
		local fragmentKeys = {
			'headerName',
			'headerRating',
			'blurbImageLeft',
			'blurbText',
			'blurbImageRight',
		}
		local html = {}
		local create = mw.html.create
		for _, key in ipairs(fragmentKeys) do
			html[key] = create()
		end
		obj.html = html
	end

	-- Define the rest of the object structure.
	data.grades = {}
	obj.data = data
	obj.categories = {}
	obj.rows = {
		quality = {},
		importance = {},
		taskForces = {},
		requests = {},
		notices = {}
	}

	return setmetatable(obj, Banner)
end

function Banner:addRow(key, isActiveByDefault, rowCfg, ...)
	-- This function adds one row object to the banner object's rows subtable.
	if not key or not Banner.rowModules[key] then
		error(string.format(
			"'%s' is not a valid key for Banner:addRow",
			key
		), 2)
	end
	local isActive = isActiveByDefault or mShared.isActiveRow(
		self.args,
		self.data,
		self.cfg,
		rowCfg
	)
	if isActive then
		-- Create the object and add it to the banner's rows table.
		local aClass = Banner.rowClasses[key]
		local obj = aClass.new(self.args, self.data, self.cfg, rowCfg, ...)
		table.insert(self.rows[key], obj)

		-- Add the categories to the banner object's categories table.
		for i, category in ipairs(obj:exportCategories()) do
			table.insert(self.categories, category)
		end
	end
end

function Banner:addRows()
	local bannerCfg = self.bannerCfg

	-- Quality
	if bannerCfg.quality ~= false then
		Grade = Grade or require('Module:WikiProjectBanner/Grade')
		local gradeCfg = {}
		local gradeObj = Grade.new(
			'quality',
			self.args,
			self.data,
			self.cfg,
			gradeCfg
		)
		for i, category in ipairs(gradeObj:exportCategories()) do
			table.insert(self.categories, category)
		end
		local rowCfg = bannerCfg.quality or {}
		self:addRow('quality', true, rowCfg, gradeObj)
		self.data.grades.quality = gradeObj
	end

	-- Importance
	if bannerCfg.importance ~= false then
		Grade = Grade or require('Module:WikiProjectBanner/Grade')
		local gradeCfg = {}
		local qualityGrade = self.data.grades.quality
		if qualityGrade then
			gradeCfg.forceGrade = qualityGrade:exportData().forceImportance
		end
		local gradeObj = Grade.new(
			'importance',
			self.args,
			self.data,
			self.cfg,
			gradeCfg
		)
		for i, category in ipairs(gradeObj:exportCategories()) do
			table.insert(self.categories, category)
		end
		local rowCfg = bannerCfg.importance or {}
		if gradeObj:exportData().short ~= 'NA' then
			self:addRow('importance', true, rowCfg, gradeObj)
		end
	end

	-- Task forces, requests, and notices
	for i, key in ipairs{'taskForces', 'requests', 'notices'} do
		for j, rowCfg in ipairs(bannerCfg[key] or {}) do
			self:addRow(key, false, rowCfg)
		end
	end

	-- Count the requests and notices.
	local nNotes = #self.rows.requests + #self.rows.notices
	if nNotes > (bannerCfg.collapsed or self.cfg.collapsed or 3) then
		self.data.isCollapsed = true
	end
end

function Banner:callHook(key)
	local hookFunc = self.bannerCfg.hooks[key]
	if type(hookFunc) == 'function' then
		return hookFunc{
			categories = self.categories,
			data = self.data,
			args = self.args,
			cfg = self.cfg,
			bannerCfg = self.bannerCfg
		}
	end
end
	
function Banner:renderBlurb()
	self:renderImage('Left')

	if self.bannerCfg.portal and not self.data.isSmall then
		local portal = mShared.makePortal(self.bannerCfg.portal)
		self.html.blurbText:wikitext(portal)
	end

	self:renderBlurbText()
	self:renderImage('Right')
end

function Banner:renderImage(position)
	-- Valid positions are "Left" and "Right". They must be capitalised.
	local filename = self.bannerCfg['image' .. position]
	if filename then
		local size = self.data.isSmall
			and (self.bannerCfg['image' .. position .. 'Small'] or "40px")
			or (self.bannerCfg['image' .. position .. 'Large'] or "80px")
		local fileLink = string.format('[[File:%s|%s]]', filename, size)
		self.html['blurbImage' .. position]:wikitext(fileLink)
	end
end

function Banner:renderBlurbText()
	local blurbText = self:callHook('blurb') or self.bannerCfg.blurb
	if blurbText then
		self.html.blurbText:wikitext(blurbText)
	else
		local data = self.data
		local msg
		if data.isSmall then
			msg = "This $5 is within the scope of '''[[$2|$1]]''', a collaborative effort " ..
				"to improve the coverage of $3 on Wikipedia."
		else
			msg = "This $5 is within the scope of '''[[$2|$1]]''', a collaborative effort " ..
				"to improve the coverage of $3 on Wikipedia. If you would like to participate, " ..
				"please visit the project page, where you can join the [[$4|discussion]] and " ..
				"see a list of open tasks."
		end
		msg = mShared.substituteParams(
			msg,
			data.projectName,
			data.projectLink,
			data.projectScope,
			data.projectLinkTalk,
			data.pageType
		)
		self.html.blurbText:wikitext(msg)
	end

end

function Banner:getRowHtml(rowType)
	-- Render the rows.
	local rowHtml = mw.html.create()
	for i, obj in ipairs(self.rows[rowType] or {}) do
		rowHtml:node(obj:exportHtml())
	end
	return rowHtml
end

function Banner:renderCategories()
	local sortKey = self.args.listas
	local ret = {}
	if yesno(self.args.category) ~= false then
		local categoryNsText = mw.site.namespaces[14].name
		for _, category in ipairs(self.categories) do
			if sortKey then
				ret[#ret + 1] = string.format(
					'[[%s:%s|%s]]',
					categoryNsText,
					category,
					sortKey
				)
			else
				ret[#ret + 1] = string.format(
					'[[%s:%s]]',
					categoryNsText,
					category
				)
			end
		end
	end
	return table.concat(ret)
end

function Banner:bindHtmlFragments()
	local html = self.html

	local root = mw.html.create()

	local wrapper = root
		:node(self:callHook('preWrapper'))
		:tag('table')
			:addClass('tmbox tmbox-notice collapsible innercollapse wpb')
			:addClass(self.data.isSmall and 'mbox-small' or nil)

	wrapper
		:tag('tr')
			:addClass('wpb-header')
			:tag('td')
				:css('text-align', 'right')
				:css('padding', '0.3em 1em 0.3em 0.3em')
				:css('width', '50%')
				:css('font-weight', 'bold')
				:node(self:callHook('headerName'))
				:done()
			:tag('th')
				:css('text-align', 'left')
				:css('width', '50%')
				:css('padding', '0.3em 0.3em 0.3em 0')
				:node(self:callHook('headerRating'))

	local content = 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')

	content
		:node(self:callHook('preBlurb'))

	local blurbRow = content:tag('tr')

	-- blurb image left
	if self.bannerCfg.imageLeft then
		blurbRow:tag('td')
			:addClass('mbox-image')
			:node(html.blurbImageLeft)
	end

	-- blurb text
	do
		local colspan = (self.bannerCfg.imageLeft and 1 or 0) +
			(self.bannerCfg.imageRight and 1 or 0) + 
			1
		colspan = colspan > 1 and colspan or nil
		blurbRow:tag('td')
			:addClass('mbox-text')
			:attr('colspan', colspan)
			:node(html.blurbText)
	end

	-- blurb image right
	if self.bannerCfg.imageRight then
		blurbRow:tag('td')
			:addClass('mbox-imageright')
			:node(html.blurbImageRight)
	end

	content
		:node(self:callHook('postBlurb'))
		:node(self:getRowHtml('quality'))
		:node(self:callHook('postQuality'))
		:node(self:getRowHtml('importance'))
		:node(self:callHook('postImportance'))
		:node(self:getRowHtml('taskForces'))
		:node(self:callHook('postTaskForces'))
	
	if self.data.isCollapsed then
		-- We are collapsing requests and notices, so define the collapsible
		-- table and add the requests and notices to it.
		content
			:tag('tr')
				:tag('td')
					:attr('colspan', '3')
					:css('padding', '0')
					:tag('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(
									self.bannerCfg.moreHeader
										or 'More information'
								)
								:done()
							:done()
						:node(self:callHook('preCollapsedContent'))
						:node(self:getRowHtml('requests'))
						:node(self:callHook('postRequests'))
						:node(self:getRowHtml('notices'))
						:node(self:callHook('postNotices'))
	else
		-- We aren't collapsing requests and notices, so add them to the normal
		-- content node.
		content
			:node(self:getRowHtml('requests'))
			:node(self:callHook('postRequests'))
			:node(self:getRowHtml('notices'))
			:node(self:callHook('postNotices'))
	end

	content
		:node(self:callHook('postContent'))

	root
		:wikitext(self:renderCategories())
		:node(self:callHook('postWrapper'))

	html.root = root
end

function Banner:__tostring()
	self:addRows()
	self:callHook()
	self:renderBlurb()
	self:bindHtmlFragments()
	local root = self.html.root or mw.html.create()
	return tostring(root)
end

return Banner