Module:Category handler

Revision as of 23:19, 5 December 2025 by ArrowHead294 (talk | contribs) (Wikitext debugger option)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Module documentation[view] [edit] [history] [purge]
This module may be invoked by templates using its corresponding template Template:Category handler, or used directly from other modules.
Module:Category handler is ready for use. This message indicates that a module is ready for use, or has recently been repaired. This message may be removed once this module has been used on several pages or once it is verified to work as intended.

Details: Functionally complete. Default lists may be adjusted still.

Introspection summary for Module:Category handler 
Functions provided (2)
Line Function Params
136 _category_handler (main) (cats, is_debug, ns_override, suffixes)
165 category_handler (invokable) (frame)
Lua modules required (2)
Variable Module Functions used
getArgs Module:Arguments getArgs
yesno Module:Yesno yesno

No function descriptions were provided. The Lua code may have further information.


-- This module follows [[User:Ganaram inukshuk/Provisional style guide for Lua]]
local getArgs = require("Module:Arguments").getArgs
local yesno = require("Module:Yesno")

local p = {}

-- Basic category handler, based on Wikipedia's category handler. It categorizes
-- pages, given a table of categories as input, and suppresses categorization if
-- the page is in the table of excluded namespaces, or table of excluded 
-- suffixes (subpages).
-- The most common categorizing templates that have/require complex rules are:
-- - Infoboxes; these usually shouldn't categorize if they're outside the main
--   namespace.
-- - Certain mboxes; some mboxes categorize pages, but if that mbox categorizes
--   templates/modules and are placed on that page's /doc subpage, it should
--   categorize the page on which the documentation is transcluded, not the /doc
--   page itself.
-- - Categorizing templates used on their own /doc pages as examples; a special
--   case of the previous case; passing in debug=1 disables all categorization.

-- Default list of namespaces in which to suppress categorization. Most
-- categorizing templates are expected to be placed in main, template, or
-- module; those that don't are likely special-use templates (like idiosyncratic
-- and editable user page) which likely won't need this module, or templates
-- with very basic categorization rules for which this is overkill.
-- Adjust as needed!
local DEFAULT_SUPPRESSED_NAMESPACES = {
	["main"] = false,
    ["talk"] = true,
    ["user"] = true,
    ["user talk"] = true,
    ["file talk"] = true,
    ["mediawiki talk"] = true,
    ["template talk"] = true,
    ["help"] = true,
    ["help talk"] = true,
    ["category talk"] = true,
    ["module talk"] = true,
    ["xenharmonic wiki"] = true,
    ["xenharmonic wiki talk"] = true,
    ["media"] = true,
}
-- Inversely, the following namespaces are unsuppressed: main, file, mediawiki,
-- template, category, module

-- Default list of page suffixes in which to suppress categorization.
-- Adjust as needed!
local DEFAULT_SUPPRESSED_SUFFIXES = {
	"doc",
	"sandbox",
}

-- List of namespace aliases
-- For "main", this is so editors can type in "main" since the main namespace's
-- actual name is "". For "xw"-related namespaces, these are shorthands.
local NAMESPACE_ALIASES = {
	[""] = "main",							-- "" is treated as "main"
	["xw"] = "xenharmonic wiki",			-- Shorthand
	["xw talk"] = "xenharmonic wiki talk"	-- Shorthand
}

-- Helper function
-- Converts namespace aliases to their actual names
-- Must be placed before is_suppressed_namespace()
local function normalize_ns(ns)
	ns = mw.ustring.lower(mw.text.trim(ns or ""))	-- Convert to lowercase and and trim extra spaces
	if ns == "" then ns = "main" end				-- Empty-string is assumed to be "main"
	if NAMESPACE_ALIASES[ns] then
		return NAMESPACE_ALIASES[ns]
	end
	return ns
end

-- Helper function: check if current namespace is excluded
-- Accepts an optional table, containing or overriding other namespaces' rules
local function is_suppressed_namespace(ns_override)
	-- Get current namespace, or aliased namespace, as lowercase
	local curr_ns = normalize_ns(mw.title.getCurrentTitle().nsText)
	
	-- Build table of suppressed namespaces; start with default list
	local namespaces = {}
	for k, v in pairs(DEFAULT_SUPPRESSED_NAMESPACES) do
		namespaces[normalize_ns(k)] = v
	end

	-- Then extend/override list using ns_override, if available
	if type(ns_override) == "table" then
		for k, v in pairs(ns_override) do
			namespaces[normalize_ns(k)] = v
		end
	end

	-- Return; if no namespace found, default to false
	return namespaces[curr_ns] or false
end

-- Helper function
-- Checks whether the page title ends in a suppressed suffix
-- Accepts an opiontal table, containing additional suffixes to suppress
-- Suffix and custom suffixes are lowercased to guarantee matching
local function has_suppressed_suffix(suffixes_override)
	local title = mw.title.getCurrentTitle()
	local pagename = mw.ustring.lower(title.text)

	-- Build table of suppressed suffixes, start with default suffixes
	local suffixes = {}
	for _, suffix in ipairs(DEFAULT_SUPPRESSED_SUFFIXES) do
		table.insert(suffixes, suffix)
	end

	-- Then append additional suffixes, if available (append, not replace)
	if type(suffixes_override) == "table" then
		for _, suffix in ipairs(suffixes_override) do
			table.insert(suffixes, suffix)
		end
	end

	-- Find and match suffix
	for _, suffix in ipairs(suffixes) do
		suffix = mw.ustring.lower(mw.text.trim(suffix or ''))	-- Also normalize
		if suffix ~= '' then
			local pattern = '/' .. mw.ustring.gsub(suffix, '([%^%$%(%)%%%.%[%]%*%+%-%?])', '%%%1') .. '$'
			if mw.ustring.match(pagename, pattern) then
				return true
			end
		end
	end

	return false
end

-- "Main" function; can be called by other modules
-- Categorizes a page, given a table of categories
-- Disallows categories if it's in a suppressed namespace or the page has a
-- suppressed suffix (subpage)
function p._category_handler(cats, is_debug, ns_override, suffixes)
	local cats = cats or {}
	local is_debug = yesno(is_debug, false)
	
	-- Test values; should be commented out for normal use
	--local cats = cats or {"Abstract MOS patterns", "7-tone scales"}
	--local ns_override = ns_override or { ["module"] = true }
	
	-- If in a suppressed namespace/prefix, or in debug mode (suppresses ALL
	-- categorization) don't bother
	if is_suppressed_namespace(ns_override) 
		or has_suppressed_suffix(suffixes)
		or is_debug then
		return ''
	end

	-- Otherwise, categorize
	local result = ''
	for _, cat in ipairs(cats) do
		cat = mw.text.trim(cat or '')
		if cat ~= '' then
			result = result .. string.format('[[Category:%s]]', cat)
		end
	end

	return result
end

-- Wrapper for templates calling via #invoke
function p.category_handler(frame)
	local args = getArgs(frame)
	local excluded_ns_unparsed = args["excluded_ns"] or ""		-- Additional namespaces to exclude (EG, a template that should not be used within main namespace, which is allowed by default)
	local allowed_ns_unparsed  = args["allowed_ns" ] or ""		-- Namespaces to allow; overrides default list (EG, a template that should be used within user namespace, which is disallowed by default)
	local suffixes_unparsed    = args["suffixes"   ] or ""		-- Additional page suffixes in which to disallow categories
	local is_debug = yesno(args["debug"], false)				-- Parse debug mode; setting this to TRUE disables all categories
	local wtext = yesno(args["wtext"])							-- Used to show the underlying Wikitext

	-- Parse categories
	-- Categories are entered using unnamed params
	local cats = {}
	for _, cat in ipairs(args) do
		cat = mw.text.trim(cat)
		if cat ~= "" then
			table.insert(cats, cat)
		end
	end

	-- Parse excluded namespaces
	-- These are added in addition to the default list
	-- This currently can't force-allow suppressed namespaces as template input,
	-- only disallow additional namespaces
	local ns_override = {}
	for ns in mw.text.gsplit(excluded_ns_unparsed, "[;\n]") do
		ns = mw.text.trim(ns)
		if ns ~= "" then
			ns_override[ns] = true
		end
	end
	
	-- Parse allowed namespaces
	-- This gets added to the ns_override array, with values set to FALSE
	-- EG, Template:TODO is used in several namespaces that are normally 
	-- suppressed: user, talk, help, and maybe xw talk
	for ns in mw.text.gsplit(allowed_ns_unparsed, "[;\n]") do
		ns = mw.text.trim(ns)
		if ns ~= "" then
			ns_override[ns] = false
		end
	end

	-- Parse excluded suffixes
	local suffixes = {}
	for suffix in mw.text.gsplit(suffixes_unparsed, "[;\n]") do
		suffix = mw.text.trim(suffix)
		if suffix ~= "" then
			table.insert(suffixes, suffix)
		end
	end

	local result = p._category_handler(cats, is_debug, ns_override, suffixes)
	
	if wtext then
		result = "<syntaxhighlight lang=\"wikitext\">" .. result .. "</syntaxhighlight>"
	end
	
	return frame:preprocess(result)
end

return p