Jump to content

Module:Votings-global

Permanently protected module
From Meta, a Wikimedia project coordination wiki
Module documentation

Usage

See Template:Votings-global.

-- For attribution: [[:vi:Module:CurrentCandidateList]]
require('strict')

local p = {}

local srgp_page_name = 'Steward requests/Global permissions'
local srgp_page_content = mw.title.new(srgp_page_name):getContent() --[[@as string]]

---@class PermissionInfo
---@field acronym string
---@field section_name string

---@type PermissionInfo[]
local permissions = {
	{
		acronym = 'GRN',
		section_name = 'Requests for global rename permissions'
	},
	{
		acronym = 'GR',
		section_name = 'Requests for global rollback permissions'
	},
	{
		acronym = 'GS',
		section_name = 'Requests for global sysop permissions'
	},
	{
		acronym = 'GP',
		section_name = 'Requests for other global permissions'
	}
}

---@type string[]
local not_active_statuses = {
	'done', '+',
	'cannot', 'notdone', '-',
	'alreadydone', 'withdrawn', 'redundant'
}

---@class Section
---@field name string
---@field lines string[]

---Whether `table` contains `value`.
---
---@param table any[]
---@param value any
---@return boolean
local function _table_includes(table, value)
	for _, element in ipairs(table) do
		if element == value then
			return true
		end
	end

	return false
end

---Extract the `status` argument from the given line.
---
---@param line string
---@return string?
function p._parse_status(line)
	local without_comments = mw.ustring.gsub(line, '<!--.--->', '')
	local argument = mw.ustring.match(without_comments, '%s*%|%s*status%s*=%s*(.*)')

	if not argument then
		return nil
	end

	local without_whitespace = mw.ustring.gsub(argument, '%s*', '')

	return mw.ustring.lower(without_whitespace)
end

---Whether the given section's status is not inactive.
---
---@param section Section
---@return boolean
function p._section_is_active(section)
	for _, line in ipairs(section.lines) do
		local status = p._parse_status(line)

		if status ~= nil then
			return not _table_includes(not_active_statuses, status)
		end
	end

	return true
end

---Given a line, parse it as a heading and return the level and the name.
---
---@param line string
---@return integer?, string?
local function _parse_heading(line)
	local equals, name = mw.ustring.match(line, '(==+)%s*(..-)%s*(==+)')

	if not equals then
		return nil, nil
	end

	return #equals, name
end

---Given a heading, return the corresponding permission acronym.
---
---@param heading string
---@return string?
local function _get_permission_acronym(heading)
	for _, permission in ipairs(permissions) do
		if permission.section_name == heading then
			return permission.acronym
		end
	end

	return nil
end

---Parse the page and return a map of acronyms and corresponding sections.
---
---@return table<string, Section[]>
function p._parse_sections()
	---@type table<string, Section[]>
	local acronym_to_child_sections = {}

	for _, permission in pairs(permissions) do
		acronym_to_child_sections[permission.acronym] = {}
	end

	---@type Section[]?
	local current_parent_section = nil
	---@type Section?
	local current_child_section = nil

	for line in mw.text.gsplit(srgp_page_content, '\n') do
		local heading_level, heading_name = _parse_heading(line)

		if (
			current_parent_section and current_child_section ~= nil and
			(not heading_level or heading_level > 3)
		) then
			table.insert(current_child_section.lines, line)

		elseif heading_level == 2 and heading_name then
			local acronym = _get_permission_acronym(heading_name)

			if acronym then
				current_parent_section = acronym_to_child_sections[acronym]
			else
				current_parent_section = nil
			end

			current_child_section = nil

		elseif current_parent_section and heading_level == 3 and heading_name then
			---@type Section
			local new_section = { name = heading_name, lines = {} }

			table.insert(current_parent_section, new_section)
			current_child_section = new_section
		end
	end

	return acronym_to_child_sections
end

---Put request headings into corresponding subtables of a table.
---
---@return table<string, string[]>
function p._collect_requests()
	---@type table<string, string[]>
	local acronym_to_headings = {}
	local sections = p._parse_sections()

	for acronym, subsections in pairs(sections) do
		local headings = {}

		for _, subsection in ipairs(subsections) do
			if p._section_is_active(subsection) then
				table.insert(headings, subsection.name)
			end
		end

		acronym_to_headings[acronym] = headings
	end

	return acronym_to_headings
end

---Convert the table returned by `p._count_requests`
---to a human-readable horizontal list.
---
---@param counts table<string, string[]>
---@return string
function p._join_to_human_readable_list(counts)
	local links = {}

	for _, permission in ipairs(permissions) do
		local acronym = permission.acronym
		local section_name = permission.section_name
		local count = #counts[acronym]

		if count > 0 then
			local link_to_section = srgp_page_name .. '#' .. section_name
			local link_text = count .. '&nbsp;Rf' .. acronym
			local link = '[[' .. link_to_section .. '|' .. link_text .. ']]'

			table.insert(links, '&nbsp;&bull; <b>' .. link .. '</b>')
		end
	end

	return table.concat(links, '')
end

function p.main()
	local request_counts = p._collect_requests()

	return p._join_to_human_readable_list(request_counts)
end

return p