<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en">
	<id>https://wiki.2swagg.net/index.php?action=history&amp;feed=atom&amp;title=Module%3AInfobox_categorizer</id>
	<title>Module:Infobox categorizer - Revision history</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.2swagg.net/index.php?action=history&amp;feed=atom&amp;title=Module%3AInfobox_categorizer"/>
	<link rel="alternate" type="text/html" href="https://wiki.2swagg.net/index.php?title=Module:Infobox_categorizer&amp;action=history"/>
	<updated>2026-11-13T00:49:31Z</updated>
	<subtitle>Revision history for this page on the wiki</subtitle>
	<generator>MediaWiki 1.43.9</generator>
	<entry>
		<id>https://wiki.2swagg.net/index.php?title=Module:Infobox_categorizer&amp;diff=1587&amp;oldid=prev</id>
		<title>Bulgogi2: Created page with &quot;local p = {} local parser = require(&#039;Module:WikitextParser&#039;) local yesno = require(&#039;Module:Yesno&#039;) local catmark = require(&#039;Module:CatMark&#039;) -- read assistance  local deverrormsg = &#039;Categorizer config error: &#039;  local function trim(value) 	if value == nil then return &#039;&#039; end 	return mw.text.trim(tostring(value)) end  local function normalize(value, mode) 	local text = mw.ustring.lower(trim(value))  	if mode == nil or mode == &#039;compact&#039; then 		return (mw.ustring.gsub(text, &#039;...&quot;</title>
		<link rel="alternate" type="text/html" href="https://wiki.2swagg.net/index.php?title=Module:Infobox_categorizer&amp;diff=1587&amp;oldid=prev"/>
		<updated>2026-10-11T01:08:41Z</updated>

		<summary type="html">&lt;p&gt;Created page with &amp;quot;local p = {} local parser = require(&amp;#039;Module:WikitextParser&amp;#039;) local yesno = require(&amp;#039;Module:Yesno&amp;#039;) local catmark = require(&amp;#039;Module:CatMark&amp;#039;) -- read assistance  local deverrormsg = &amp;#039;Categorizer config error: &amp;#039;  local function trim(value) 	if value == nil then return &amp;#039;&amp;#039; end 	return mw.text.trim(tostring(value)) end  local function normalize(value, mode) 	local text = mw.ustring.lower(trim(value))  	if mode == nil or mode == &amp;#039;compact&amp;#039; then 		return (mw.ustring.gsub(text, &amp;#039;...&amp;quot;&lt;/p&gt;
&lt;p&gt;&lt;b&gt;New page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;local p = {}&lt;br /&gt;
local parser = require(&amp;#039;Module:WikitextParser&amp;#039;)&lt;br /&gt;
local yesno = require(&amp;#039;Module:Yesno&amp;#039;)&lt;br /&gt;
local catmark = require(&amp;#039;Module:CatMark&amp;#039;) -- read assistance&lt;br /&gt;
&lt;br /&gt;
local deverrormsg = &amp;#039;Categorizer config error: &amp;#039;&lt;br /&gt;
&lt;br /&gt;
local function trim(value)&lt;br /&gt;
	if value == nil then return &amp;#039;&amp;#039; end&lt;br /&gt;
	return mw.text.trim(tostring(value))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function normalize(value, mode)&lt;br /&gt;
	local text = mw.ustring.lower(trim(value))&lt;br /&gt;
&lt;br /&gt;
	if mode == nil or mode == &amp;#039;compact&amp;#039; then&lt;br /&gt;
		return (mw.ustring.gsub(text, &amp;#039;[^%w]&amp;#039;, &amp;#039;&amp;#039;))&lt;br /&gt;
	elseif mode == &amp;#039;spaced&amp;#039; then&lt;br /&gt;
		text = mw.ustring.gsub(text, &amp;#039;[^%w]+&amp;#039;, &amp;#039; &amp;#039;)&lt;br /&gt;
		return trim(text)&lt;br /&gt;
	end&lt;br /&gt;
	error(deverrormsg .. &amp;#039;Unknown normalization &amp;quot;&amp;#039; .. tostring(mode) .. &amp;#039;&amp;quot;&amp;#039;, 0)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function devError(message)&lt;br /&gt;
	error(deverrormsg .. message, 0)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function isDevError(err)&lt;br /&gt;
	return type(err) == &amp;#039;string&amp;#039; and string.sub(err, 1, #deverrormsg) == deverrormsg&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function need(tbl, key, where)&lt;br /&gt;
	if tbl == nil then devError(where .. &amp;#039; is missing&amp;#039;) end&lt;br /&gt;
	local value = tbl[key]&lt;br /&gt;
	if value == nil then devError(where .. &amp;#039;.&amp;#039; .. key .. &amp;#039; is missing&amp;#039;) end&lt;br /&gt;
	return value&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local List = {}&lt;br /&gt;
List.__index = List&lt;br /&gt;
&lt;br /&gt;
local function newList()&lt;br /&gt;
	return setmetatable({ items = {}, seen = {} }, List)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function List:add(name)&lt;br /&gt;
	name = trim(name)&lt;br /&gt;
&lt;br /&gt;
	if name ~= &amp;#039;&amp;#039; and not self.seen[name] then&lt;br /&gt;
		self.seen[name] = true&lt;br /&gt;
		self.items[#self.items + 1] = name&lt;br /&gt;
	end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
function List:list()&lt;br /&gt;
	local copy = {}&lt;br /&gt;
	for index, name in ipairs(self.items) do&lt;br /&gt;
		copy[index] = name&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	return copy&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function fill(pattern, value)&lt;br /&gt;
	if pattern == nil then return nil end&lt;br /&gt;
	local replacement = trim(value)&lt;br /&gt;
	return (mw.ustring.gsub(pattern, &amp;#039;%$1&amp;#039;, function()&lt;br /&gt;
		return replacement&lt;br /&gt;
	end))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local fileNamespace = 6&lt;br /&gt;
local categoryNamespace = 14&lt;br /&gt;
&lt;br /&gt;
local function replacePlain(text, needle, replacement)&lt;br /&gt;
	if needle == &amp;#039;&amp;#039; then return text end&lt;br /&gt;
	local parts = {}&lt;br /&gt;
	local position = 1&lt;br /&gt;
&lt;br /&gt;
	while true do&lt;br /&gt;
		local start, finish = string.find(text, needle, position, true)&lt;br /&gt;
		if not start then break end&lt;br /&gt;
		parts[#parts + 1] = string.sub(text, position, start - 1)&lt;br /&gt;
		parts[#parts + 1] = replacement&lt;br /&gt;
		position = finish + 1&lt;br /&gt;
	end&lt;br /&gt;
	parts[#parts + 1] = string.sub(text, position)&lt;br /&gt;
	return table.concat(parts)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function stripHidden(value)&lt;br /&gt;
	local text = tostring(value or &amp;#039;&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
	for _, reference in ipairs(parser.getReferences(text)) do&lt;br /&gt;
		text = replacePlain(text, reference, &amp;#039; &amp;#039;)&lt;br /&gt;
	end&lt;br /&gt;
	return mw.text.killMarkers(text)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function linkDisplayText(link)&lt;br /&gt;
	local inner = string.match(link, &amp;#039;^%[%[(.*)%]%]$&amp;#039;)&lt;br /&gt;
	if not inner then return link end&lt;br /&gt;
	local target, label = string.match(inner, &amp;#039;^([^|]*)|(.*)$&amp;#039;)&lt;br /&gt;
	if not target then target = inner end&lt;br /&gt;
	local visibleLink = string.match(target, &amp;#039;^%s*:&amp;#039;) ~= nil&lt;br /&gt;
	local titleText = mw.ustring.gsub(target, &amp;#039;^%s*:%s*&amp;#039;, &amp;#039;&amp;#039;)&lt;br /&gt;
	titleText = mw.text.split(titleText, &amp;#039;#&amp;#039;, true)[1]&lt;br /&gt;
	local title = mw.title.new(trim(titleText))&lt;br /&gt;
	if title and not visibleLink and (title.namespace == fileNamespace or title.namespace == categoryNamespace) then&lt;br /&gt;
		return &amp;#039;&amp;#039;&lt;br /&gt;
	end&lt;br /&gt;
	if label and trim(label) ~= &amp;#039;&amp;#039; then return label end&lt;br /&gt;
&lt;br /&gt;
	return (mw.ustring.gsub(target, &amp;#039;^%s*:%s*&amp;#039;, &amp;#039;&amp;#039;))&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function clean(value)&lt;br /&gt;
	local text = stripHidden(value)&lt;br /&gt;
&lt;br /&gt;
	for _, link in ipairs(parser.getLinks(text)) do&lt;br /&gt;
		text = replacePlain(text, link, linkDisplayText(link))&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	text = mw.ustring.gsub(text, &amp;#039;%[%s*(%a*:?//[^%s%]]*)([^%]]*)%]&amp;#039;, function(url, label)&lt;br /&gt;
		local scheme = string.lower(string.match(url, &amp;#039;^(%a*:?)//&amp;#039;) or &amp;#039;&amp;#039;)&lt;br /&gt;
		if scheme == &amp;#039;http:&amp;#039; or scheme == &amp;#039;https:&amp;#039; or scheme == &amp;#039;&amp;#039; then return &amp;#039; &amp;#039; .. label .. &amp;#039; &amp;#039; end&lt;br /&gt;
	end)&lt;br /&gt;
&lt;br /&gt;
	text = mw.ustring.gsub(text, &amp;#039;(%a+)://[^%s&amp;lt;&amp;gt;%[%]&amp;quot;]+&amp;#039;, function(scheme)&lt;br /&gt;
		scheme = string.lower(scheme)&lt;br /&gt;
		if scheme == &amp;#039;http&amp;#039; or scheme == &amp;#039;https&amp;#039; then return &amp;#039; &amp;#039; end&lt;br /&gt;
	end)&lt;br /&gt;
&lt;br /&gt;
	text = mw.ustring.gsub(text, &amp;#039;&amp;lt;!%-%-.-%-%-&amp;gt;&amp;#039;, &amp;#039; &amp;#039;)&lt;br /&gt;
	text = mw.ustring.gsub(text, &amp;#039;&amp;lt;/?%a[^&amp;lt;&amp;gt;]*&amp;gt;&amp;#039;, &amp;#039; &amp;#039;)&lt;br /&gt;
	text = mw.text.decode(text, true)&lt;br /&gt;
&lt;br /&gt;
	return trim(text)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function findYears(value)&lt;br /&gt;
	local text = clean(value)&lt;br /&gt;
	local years = {}&lt;br /&gt;
	local seen = {}&lt;br /&gt;
&lt;br /&gt;
	for year in mw.ustring.gmatch(text, &amp;#039;%f[%d](%d%d%d%d)%f[%D]&amp;#039;) do&lt;br /&gt;
		if not seen[year] then&lt;br /&gt;
			seen[year] = true&lt;br /&gt;
			years[#years + 1] = year&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return years&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function findYearRanges(value) -- to handle statements like 2022 - 2026, another function is needed, with interpretation left to the submodules&lt;br /&gt;
	local text = clean(value)&lt;br /&gt;
	local ranges = {}&lt;br /&gt;
	local seen = {}&lt;br /&gt;
&lt;br /&gt;
	for first, second in mw.ustring.gmatch(text, &amp;#039;%f[%d](%d%d%d%d)%s*[%-–—][^%-–—]-%f[%d](%d%d%d%d)%f[%D]&amp;#039;) do&lt;br /&gt;
		local key = first .. &amp;#039;-&amp;#039; .. second&lt;br /&gt;
&lt;br /&gt;
		if not seen[key] then&lt;br /&gt;
			seen[key] = true&lt;br /&gt;
			ranges[#ranges + 1] = { first, second }&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return ranges -- return all ranges, regardless of how many. active in 2021 - 2022, 2025 - 2026, return all, interpret at submodule level&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function findLinkTargets(value, opts)&lt;br /&gt;
	local namespaces = {}&lt;br /&gt;
&lt;br /&gt;
	for _, namespace in ipairs((opts and opts.namespaces) or { 0 }) do&lt;br /&gt;
		namespaces[namespace] = true&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local text = stripHidden(value)&lt;br /&gt;
	local targets = {}&lt;br /&gt;
	local seen = {}&lt;br /&gt;
&lt;br /&gt;
	for _, link in ipairs(parser.getLinks(text)) do&lt;br /&gt;
		local inner = string.match(link, &amp;#039;^%[%[(.*)%]%]$&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
		if inner then&lt;br /&gt;
			local target = mw.text.split(inner, &amp;#039;|&amp;#039;, true)[1]&lt;br /&gt;
			target = mw.text.split(target, &amp;#039;#&amp;#039;, true)[1]&lt;br /&gt;
			target = trim((mw.ustring.gsub(target, &amp;#039;^%s*:%s*&amp;#039;, &amp;#039;&amp;#039;)))&lt;br /&gt;
&lt;br /&gt;
			if target ~= &amp;#039;&amp;#039; then&lt;br /&gt;
				local title = mw.title.new(target)&lt;br /&gt;
&lt;br /&gt;
				if title and not title.isExternal and namespaces[title.namespace] then&lt;br /&gt;
					local name = title.prefixedText&lt;br /&gt;
&lt;br /&gt;
					if not seen[name] then&lt;br /&gt;
						seen[name] = true&lt;br /&gt;
						targets[#targets + 1] = name&lt;br /&gt;
					end&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return targets&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function findDomains(value)&lt;br /&gt;
	local text = stripHidden(value)&lt;br /&gt;
	local hosts = {}&lt;br /&gt;
	local seen = {}&lt;br /&gt;
&lt;br /&gt;
	for start, scheme, host in string.gmatch(text, &amp;#039;()(%a*:?)//([%w%.%-]+)&amp;#039;) do -- no need for escaping forward slash, literal&lt;br /&gt;
		local loweredScheme = string.lower(scheme)&lt;br /&gt;
		local isUrl = loweredScheme == &amp;#039;http:&amp;#039;&lt;br /&gt;
			or loweredScheme == &amp;#039;https:&amp;#039;&lt;br /&gt;
			or (scheme == &amp;#039;&amp;#039; and string.sub(text, start - 1, start - 1) == &amp;#039;[&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
		if isUrl then -- raw hosts only, MAS maps them to sites via domains list, redirect hosts are handled that way&lt;br /&gt;
			host = string.lower(host)&lt;br /&gt;
			host = string.gsub(host, &amp;#039;^www%.&amp;#039;, &amp;#039;&amp;#039;)&lt;br /&gt;
			host = string.gsub(host, &amp;#039;%.+$&amp;#039;, &amp;#039;&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
			if host ~= &amp;#039;&amp;#039; and not seen[host] then&lt;br /&gt;
				seen[host] = true&lt;br /&gt;
				hosts[#hosts + 1] = host&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return hosts&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local dataPrefix = catmark.prefix&lt;br /&gt;
&lt;br /&gt;
local function readDataAttr(value, name)&lt;br /&gt;
	if type(name) ~= &amp;#039;string&amp;#039; or not string.match(name, &amp;#039;^[%w%-]+$&amp;#039;) then&lt;br /&gt;
		devError(&lt;br /&gt;
			&amp;#039;RDA: Attribute names from CatMark need to be letters, digits, and/or hyphens. Got: &amp;#039; .. tostring(name)&lt;br /&gt;
		)&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local text = stripHidden(value)&lt;br /&gt;
	local attr = dataPrefix .. string.lower(name)&lt;br /&gt;
	local pattern = string.gsub(attr, &amp;#039;%-&amp;#039;, &amp;#039;%%-&amp;#039;) .. &amp;#039;=&amp;quot;([^&amp;quot;]*)&amp;quot;&amp;#039;&lt;br /&gt;
	local values = {}&lt;br /&gt;
&lt;br /&gt;
	for found in string.gmatch(text, pattern) do&lt;br /&gt;
		values[#values + 1] = trim(mw.text.decode(found, true))&lt;br /&gt;
	end&lt;br /&gt;
	return values&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function parseNumber(value)&lt;br /&gt;
	if value == nil then return nil end&lt;br /&gt;
&lt;br /&gt;
	local cleaned = string.gsub(trim(value), &amp;#039;[,%s]&amp;#039;, &amp;#039;&amp;#039;)&lt;br /&gt;
	if not string.match(cleaned, &amp;#039;^%-?%d+%.?%d*$&amp;#039;) then return nil end&lt;br /&gt;
	return tonumber(cleaned)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function pickBand(number, bands) -- bands = number ranges for categories, i.e. the system to handle 100-999 soybooru posts is resolved here&lt;br /&gt;
	if type(bands) ~= &amp;#039;table&amp;#039; then devError(&amp;#039;pickBand: expected a list of bands, got: &amp;#039; .. type(bands)) end&lt;br /&gt;
	if type(number) ~= &amp;#039;number&amp;#039; then return nil end&lt;br /&gt;
&lt;br /&gt;
	local best&lt;br /&gt;
&lt;br /&gt;
	for index, band in ipairs(bands) do&lt;br /&gt;
		if type(band.min) ~= &amp;#039;number&amp;#039; or type(band.name) ~= &amp;#039;string&amp;#039; then&lt;br /&gt;
			devError(&amp;#039;pickBand: band &amp;#039; .. index .. &amp;#039; needs a numeric min and a string name&amp;#039;)&lt;br /&gt;
		end&lt;br /&gt;
&lt;br /&gt;
		if number &amp;gt;= band.min and (best == nil or band.min &amp;gt; best.min) then best = band end&lt;br /&gt;
	end&lt;br /&gt;
	return best and best.name or nil&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local matchModes = { word = true, substring = true, exact = true }&lt;br /&gt;
local function prepare(text, mode, caseSensitive)&lt;br /&gt;
	local prepared = trim(text)&lt;br /&gt;
&lt;br /&gt;
	if not caseSensitive then prepared = mw.ustring.lower(prepared) end&lt;br /&gt;
&lt;br /&gt;
	if mode == &amp;#039;substring&amp;#039; then return (mw.ustring.gsub(prepared, &amp;#039;[^%w]&amp;#039;, &amp;#039;&amp;#039;)) end&lt;br /&gt;
	prepared = mw.ustring.gsub(prepared, &amp;#039;[^%w]+&amp;#039;, &amp;#039; &amp;#039;)&lt;br /&gt;
	return trim(prepared)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function normalizeDomain(domain)&lt;br /&gt;
	domain = string.lower(trim(domain))&lt;br /&gt;
	domain = string.gsub(domain, &amp;#039;^www%.&amp;#039;, &amp;#039;&amp;#039;)&lt;br /&gt;
	domain = string.gsub(domain, &amp;#039;%.+$&amp;#039;, &amp;#039;&amp;#039;)&lt;br /&gt;
	return domain&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function compileAliasSet(sets, opts)&lt;br /&gt;
	local defaultMode = (opts and opts.match) or &amp;#039;word&amp;#039;&lt;br /&gt;
	local compiled = { entries = {} }&lt;br /&gt;
	local byCategory = {}&lt;br /&gt;
	local claimedAliases = {}&lt;br /&gt;
	local claimedDomains = {}&lt;br /&gt;
&lt;br /&gt;
	if type(sets) ~= &amp;#039;table&amp;#039; then devError(&amp;#039;CAS: expected a list of alias sets, got this:&amp;#039; .. type(sets)) end&lt;br /&gt;
	if type(sets[1]) == &amp;#039;table&amp;#039; and sets[1].category ~= nil then&lt;br /&gt;
		devError(&amp;#039;CAS: got a single alias set instead of a list. Did you forget to wrap?&amp;#039;)&lt;br /&gt;
	end&lt;br /&gt;
	for setIndex = 1, table.maxn(sets) do&lt;br /&gt;
		local set = sets[setIndex]&lt;br /&gt;
		if type(set) ~= &amp;#039;table&amp;#039; then&lt;br /&gt;
			devError(&lt;br /&gt;
				&amp;#039;CAS: alias set &amp;#039;&lt;br /&gt;
					.. setIndex&lt;br /&gt;
					.. &amp;#039; is &amp;#039;&lt;br /&gt;
					.. type(set)&lt;br /&gt;
					.. &amp;#039;. Missing config key? Use config.sites or {} to fix&amp;#039;&lt;br /&gt;
			)&lt;br /&gt;
		end&lt;br /&gt;
		local setMode = set.match or defaultMode&lt;br /&gt;
		local setAliasOwners = {}&lt;br /&gt;
		local setDomainOwners = {}&lt;br /&gt;
&lt;br /&gt;
		for entryIndex, entry in ipairs(set) do&lt;br /&gt;
			local where = &amp;#039;alias set &amp;#039; .. setIndex .. &amp;#039;, entry &amp;#039; .. entryIndex&lt;br /&gt;
			local category = need(entry, &amp;#039;category&amp;#039;, where)&lt;br /&gt;
&lt;br /&gt;
			if type(category) ~= &amp;#039;string&amp;#039; or trim(category) == &amp;#039;&amp;#039; then&lt;br /&gt;
				devError(where .. &amp;#039;: category must be a noempty string&amp;#039;)&lt;br /&gt;
			end&lt;br /&gt;
&lt;br /&gt;
			category = trim(category)&lt;br /&gt;
&lt;br /&gt;
			if entry.aliases ~= nil and type(entry.aliases) ~= &amp;#039;table&amp;#039; then&lt;br /&gt;
				devError(where .. &amp;#039;: aliases must be a list&amp;#039;)&lt;br /&gt;
			end&lt;br /&gt;
			if entry.domains ~= nil and type(entry.domains) ~= &amp;#039;table&amp;#039; then&lt;br /&gt;
				devError(where .. &amp;#039;: domains must be a list&amp;#039;)&lt;br /&gt;
			end&lt;br /&gt;
&lt;br /&gt;
			local entryMode = entry.match or setMode&lt;br /&gt;
			local target = byCategory[category]&lt;br /&gt;
&lt;br /&gt;
			if not target then&lt;br /&gt;
				target = { category = category, aliases = {}, domains = {} }&lt;br /&gt;
				byCategory[category] = target&lt;br /&gt;
				compiled.entries[#compiled.entries + 1] = target&lt;br /&gt;
			end&lt;br /&gt;
&lt;br /&gt;
			for aliasIndex, alias in ipairs(entry.aliases or {}) do&lt;br /&gt;
				local text, mode, caseSensitive&lt;br /&gt;
&lt;br /&gt;
				if type(alias) == &amp;#039;string&amp;#039; then&lt;br /&gt;
					text, mode, caseSensitive = alias, entryMode, false&lt;br /&gt;
				elseif type(alias) == &amp;#039;table&amp;#039; and type(alias[1]) == &amp;#039;string&amp;#039; then&lt;br /&gt;
					text = alias[1]&lt;br /&gt;
					mode = alias.match or entryMode&lt;br /&gt;
					caseSensitive = alias.caseSensitive == true&lt;br /&gt;
				else&lt;br /&gt;
					devError(where .. &amp;#039;, alias &amp;#039; .. aliasIndex .. &amp;#039;: must be a string or { text, ... }&amp;#039;)&lt;br /&gt;
				end&lt;br /&gt;
				if not matchModes[mode] then&lt;br /&gt;
					devError(where .. &amp;#039;, alias&amp;#039; .. aliasIndex .. &amp;#039;: unknown match mode&amp;quot;&amp;#039; .. tostring(mode) .. &amp;#039;&amp;quot;&amp;#039;)&lt;br /&gt;
				end&lt;br /&gt;
&lt;br /&gt;
				local key = prepare(text, mode, caseSensitive)&lt;br /&gt;
&lt;br /&gt;
				if key ~= &amp;#039;&amp;#039; then&lt;br /&gt;
					local claimKey = mode .. &amp;#039;|&amp;#039; .. (caseSensitive and &amp;#039;c&amp;#039; or &amp;#039;i&amp;#039;) .. &amp;#039;|&amp;#039; .. key&lt;br /&gt;
					local setOwner = setAliasOwners[claimKey]&lt;br /&gt;
&lt;br /&gt;
					if setOwner ~= nil and setOwner ~= category then&lt;br /&gt;
						devError(where .. &amp;#039;, alias&amp;quot;&amp;#039; .. text .. &amp;#039;&amp;quot;: already used by&amp;quot;&amp;#039; .. setOwner .. &amp;#039;&amp;quot;in the same set&amp;#039;)&lt;br /&gt;
					end&lt;br /&gt;
					setAliasOwners[claimKey] = category&lt;br /&gt;
					if not claimedAliases[claimKey] then&lt;br /&gt;
						claimedAliases[claimKey] = true&lt;br /&gt;
						target.aliases[#target.aliases + 1] = {&lt;br /&gt;
							key = key,&lt;br /&gt;
							mode = mode,&lt;br /&gt;
							caseSensitive = caseSensitive,&lt;br /&gt;
						}&lt;br /&gt;
					end&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
&lt;br /&gt;
			for domainIndex, domain in ipairs(entry.domains or {}) do&lt;br /&gt;
				if type(domain) ~= &amp;#039;string&amp;#039; then&lt;br /&gt;
					devError(where .. &amp;#039;, domain &amp;#039; .. domainIndex .. &amp;#039;: must be a string&amp;#039;)&lt;br /&gt;
				end&lt;br /&gt;
&lt;br /&gt;
				domain = normalizeDomain(domain)&lt;br /&gt;
&lt;br /&gt;
				if domain ~= &amp;#039;&amp;#039; then&lt;br /&gt;
					local setOwner = setDomainOwners[domain]&lt;br /&gt;
&lt;br /&gt;
					if setOwner ~= nil and setOwner ~= category then&lt;br /&gt;
						devError(&lt;br /&gt;
							where .. &amp;#039;, domain&amp;quot;&amp;#039; .. domain .. &amp;#039;&amp;quot;: already used by&amp;quot;&amp;#039; .. setOwner .. &amp;#039;&amp;quot;in the same set&amp;#039;&lt;br /&gt;
						)&lt;br /&gt;
					end&lt;br /&gt;
					setDomainOwners[domain] = category&lt;br /&gt;
&lt;br /&gt;
					if not claimedDomains[domain] then&lt;br /&gt;
						claimedDomains[domain] = true&lt;br /&gt;
						target.domains[#target.domains + 1] = domain&lt;br /&gt;
					end&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return compiled&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function domainMatches(host, domain)&lt;br /&gt;
	if host == domain then return true end&lt;br /&gt;
&lt;br /&gt;
	local suffix = &amp;#039;.&amp;#039; .. domain&lt;br /&gt;
	return #host &amp;gt; #suffix and string.sub(host, -#suffix) == suffix&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function matchAliasSets(value, compiled)&lt;br /&gt;
	if compiled == nil then&lt;br /&gt;
		devError(&amp;#039;MAS: got nil instead of a set. Is the variable holding the result of CAS misspelled or not defined?&amp;#039;)&lt;br /&gt;
	elseif type(compiled) ~= &amp;#039;table&amp;#039; or type(compiled.entries) ~= &amp;#039;table&amp;#039; then&lt;br /&gt;
		if type(compiled) == &amp;#039;table&amp;#039; and type(compiled[1]) == &amp;#039;table&amp;#039; and compiled[1].category ~= nil then -- then im illiterate&lt;br /&gt;
			devError(&lt;br /&gt;
				&amp;#039;MAS: got raw alias set from config. Compile it first local sites = core.compileAliasSet({ config.sites }), then pass sites&amp;#039;&lt;br /&gt;
			)&lt;br /&gt;
		end&lt;br /&gt;
		devError(&amp;#039;MAS: expected an alias set compiled by CAS. got whatever this is:&amp;#039; .. type(compiled))&lt;br /&gt;
	end&lt;br /&gt;
	local text = clean(value)&lt;br /&gt;
	local preparedTexts = {}&lt;br /&gt;
	local hosts&lt;br /&gt;
&lt;br /&gt;
	local function preparedText(mode, caseSensitive)&lt;br /&gt;
		local cacheKey = mode .. (caseSensitive and &amp;#039;c&amp;#039; or &amp;#039;i&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
		if preparedTexts[cacheKey] == nil then preparedTexts[cacheKey] = prepare(text, mode, caseSensitive) end&lt;br /&gt;
&lt;br /&gt;
		return preparedTexts[cacheKey]&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local results = {}&lt;br /&gt;
&lt;br /&gt;
	for _, entry in ipairs(compiled.entries) do&lt;br /&gt;
		local matched = false&lt;br /&gt;
&lt;br /&gt;
		for _, alias in ipairs(entry.aliases) do&lt;br /&gt;
			local candidate = preparedText(alias.mode, alias.caseSensitive)&lt;br /&gt;
&lt;br /&gt;
			if alias.mode == &amp;#039;substring&amp;#039; then&lt;br /&gt;
				matched = string.find(candidate, alias.key, 1, true) ~= nil&lt;br /&gt;
			elseif alias.mode == &amp;#039;word&amp;#039; then&lt;br /&gt;
				matched = string.find(&amp;#039; &amp;#039; .. candidate .. &amp;#039; &amp;#039;, &amp;#039; &amp;#039; .. alias.key .. &amp;#039; &amp;#039;, 1, true) ~= nil&lt;br /&gt;
			else&lt;br /&gt;
				matched = candidate == alias.key&lt;br /&gt;
			end&lt;br /&gt;
&lt;br /&gt;
			if matched then break end&lt;br /&gt;
		end&lt;br /&gt;
		if not matched and #entry.domains &amp;gt; 0 then&lt;br /&gt;
			hosts = hosts or findDomains(value)&lt;br /&gt;
&lt;br /&gt;
			for _, host in ipairs(hosts) do&lt;br /&gt;
				for _, domain in ipairs(entry.domains) do&lt;br /&gt;
					if domainMatches(host, domain) then&lt;br /&gt;
						matched = true&lt;br /&gt;
						break&lt;br /&gt;
					end&lt;br /&gt;
				end&lt;br /&gt;
				if matched then break end&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
		if matched then results[#results + 1] = entry.category end&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	return results&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function parseYearOverride(value)&lt;br /&gt;
	local cleaned = trim(value)&lt;br /&gt;
&lt;br /&gt;
	if normalize(cleaned) == &amp;#039;undated&amp;#039; then&lt;br /&gt;
		return {}, true&lt;br /&gt;
	elseif mw.ustring.match(cleaned, &amp;#039;^%d%d%d%d$&amp;#039;) then&lt;br /&gt;
		return { cleaned }, true&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	return {}, false&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function overrideLookup(compiled)&lt;br /&gt;
	if compiled.overrideLookup then return compiled.overrideLookup end&lt;br /&gt;
&lt;br /&gt;
	local lookup = {}&lt;br /&gt;
&lt;br /&gt;
	for _, entry in ipairs(compiled.entries) do&lt;br /&gt;
		local categoryKey = normalize(entry.category)&lt;br /&gt;
&lt;br /&gt;
		if categoryKey ~= &amp;#039;&amp;#039; and lookup[categoryKey] == nil then lookup[categoryKey] = entry.category end&lt;br /&gt;
&lt;br /&gt;
		for _, alias in ipairs(entry.aliases) do&lt;br /&gt;
			local aliasKey = mw.ustring.lower((string.gsub(alias.key, &amp;#039; &amp;#039;, &amp;#039;&amp;#039;)))&lt;br /&gt;
&lt;br /&gt;
			if aliasKey ~= &amp;#039;&amp;#039; and lookup[aliasKey] == nil then lookup[aliasKey] = entry.category end&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	compiled.overrideLookup = lookup&lt;br /&gt;
	return lookup&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function parseAliasSetOverride(value, compiled) -- parse site override 2.0&lt;br /&gt;
	if type(compiled) ~= &amp;#039;table&amp;#039; or type(compiled.entries) ~= &amp;#039;table&amp;#039; then&lt;br /&gt;
		devError(&amp;#039;PASO: expected alias set compiled by CAS, got: &amp;#039; .. type(compiled))&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local cleaned = trim(value)&lt;br /&gt;
&lt;br /&gt;
	if normalize(cleaned) == &amp;#039;none&amp;#039; then return {}, true end&lt;br /&gt;
&lt;br /&gt;
	local lookup = overrideLookup(compiled)&lt;br /&gt;
	local values = {}&lt;br /&gt;
	local seen = {}&lt;br /&gt;
&lt;br /&gt;
	for _, piece in ipairs(mw.text.split(cleaned, &amp;#039;,&amp;#039;, true)) do&lt;br /&gt;
		local key = normalize(piece)&lt;br /&gt;
		local category = lookup[key]&lt;br /&gt;
&lt;br /&gt;
		if key == &amp;#039;&amp;#039; or category == nil then return {}, false end&lt;br /&gt;
&lt;br /&gt;
		if not seen[category] then&lt;br /&gt;
			seen[category] = true&lt;br /&gt;
			values[#values + 1] = category&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	return values, true&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function parseLinkTargetOverride(value, opts)&lt;br /&gt;
	local namespaces = {}&lt;br /&gt;
&lt;br /&gt;
	for _, namespace in ipairs((opts and opts.namespaces) or { 0 }) do&lt;br /&gt;
		namespaces[namespace] = true&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local target = trim(value)&lt;br /&gt;
	local inner = string.match(target, &amp;#039;^%[%[(.*)%]%]$&amp;#039;)&lt;br /&gt;
&lt;br /&gt;
	if inner then&lt;br /&gt;
		if string.find(inner, &amp;#039;[%[%]]&amp;#039;) then return {}, false end&lt;br /&gt;
&lt;br /&gt;
		target = mw.text.split(inner, &amp;#039;|&amp;#039;, true)[1]&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	target = mw.text.split(target, &amp;#039;#&amp;#039;, true)[1]&lt;br /&gt;
	target = trim((mw.ustring.gsub(target, &amp;#039;^%s*:%s*&amp;#039;, &amp;#039;&amp;#039;)))&lt;br /&gt;
&lt;br /&gt;
	if target == &amp;#039;&amp;#039; then return {}, false end&lt;br /&gt;
&lt;br /&gt;
	local title = mw.title.new(target)&lt;br /&gt;
	if title and not title.isExternal and namespaces[title.namespace] then return { title.prefixedText }, true end&lt;br /&gt;
&lt;br /&gt;
	return {}, false&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function parseExactOverride(value, map)&lt;br /&gt;
	if type(map) ~= &amp;#039;table&amp;#039; then devError(&amp;#039;PEO: expected a map of accepted values, got: &amp;#039; .. type(map)) end&lt;br /&gt;
&lt;br /&gt;
	local lookup = {}&lt;br /&gt;
&lt;br /&gt;
	for key, mapped in pairs(map) do&lt;br /&gt;
		if type(key) == &amp;#039;number&amp;#039; then key = mapped end&lt;br /&gt;
&lt;br /&gt;
		if type(key) ~= &amp;#039;string&amp;#039; or type(mapped) ~= &amp;#039;string&amp;#039; then&lt;br /&gt;
			devError(&amp;#039;PEO: map keys and values must be strings&amp;#039;)&lt;br /&gt;
		end&lt;br /&gt;
&lt;br /&gt;
		local normalizedKey = normalize(key)&lt;br /&gt;
&lt;br /&gt;
		if lookup[normalizedKey] ~= nil and lookup[normalizedKey] ~= mapped then&lt;br /&gt;
			devError(&amp;#039;PEO: &amp;quot;&amp;#039; .. key .. &amp;#039;&amp;quot; collides with another key after normalization, remove it&amp;#039;)&lt;br /&gt;
		end&lt;br /&gt;
&lt;br /&gt;
		lookup[normalizedKey] = mapped&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local mapped = lookup[normalize(value)]&lt;br /&gt;
	if trim(value) ~= &amp;#039;&amp;#039; and mapped ~= nil then return { mapped }, true end&lt;br /&gt;
&lt;br /&gt;
	return {}, false&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
-- // compiling complete, resolvers //&lt;br /&gt;
local function processValues(values, canon, filter)&lt;br /&gt;
	local result = {}&lt;br /&gt;
	local seen = {}&lt;br /&gt;
	local rejected = false&lt;br /&gt;
&lt;br /&gt;
	for _, value in ipairs(values) do&lt;br /&gt;
		local processed = value&lt;br /&gt;
&lt;br /&gt;
		if canon then processed = canon(processed) end&lt;br /&gt;
&lt;br /&gt;
		processed = processed ~= nil and trim(processed) or &amp;#039;&amp;#039;&lt;br /&gt;
&lt;br /&gt;
		if processed == &amp;#039;&amp;#039; or (filter and not filter(processed)) then&lt;br /&gt;
			rejected = true&lt;br /&gt;
		elseif not seen[processed] then&lt;br /&gt;
			seen[processed] = true&lt;br /&gt;
			result[#result + 1] = processed&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
	return result, rejected&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function resolve(spec)&lt;br /&gt;
	if type(spec) ~= &amp;#039;table&amp;#039; then devError(&amp;#039;resolver: expected table with named arguments, got: &amp;#039; .. type(spec)) end&lt;br /&gt;
&lt;br /&gt;
	local parse = spec.parse&lt;br /&gt;
	local canon = spec.canon&lt;br /&gt;
	local filter = spec.filter&lt;br /&gt;
	local max = spec.max&lt;br /&gt;
&lt;br /&gt;
	if parse ~= nil and type(parse) ~= &amp;#039;function&amp;#039; then&lt;br /&gt;
		devError(&amp;#039;resolver: parse must be a function, got: &amp;#039; .. type(parse))&lt;br /&gt;
	end&lt;br /&gt;
	if canon ~= nil and type(canon) ~= &amp;#039;function&amp;#039; then&lt;br /&gt;
		devError(&amp;#039;resolver: canon must be a function, got: &amp;#039; .. type(canon))&lt;br /&gt;
	end&lt;br /&gt;
	if filter ~= nil and type(filter) ~= &amp;#039;function&amp;#039; then&lt;br /&gt;
		devError(&amp;#039;resolver: filter must be a function, got: &amp;#039; .. type(filter))&lt;br /&gt;
	end&lt;br /&gt;
	if max ~= nil and (type(max) ~= &amp;#039;number&amp;#039; or max &amp;lt; 1 or max % 1 ~= 0) then -- necessary check to avoid hard braps&lt;br /&gt;
		devError(&amp;#039;resolver: MATH! MAX must be positive integer, got &amp;#039; .. tostring(max))&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local state = {&lt;br /&gt;
		values = {},&lt;br /&gt;
		source = &amp;#039;detected&amp;#039;,&lt;br /&gt;
		overrideProvided = trim(spec.override) ~= &amp;#039;&amp;#039;,&lt;br /&gt;
		invalid = false,&lt;br /&gt;
		undetected = false,&lt;br /&gt;
		overflow = false,&lt;br /&gt;
		customized = false,&lt;br /&gt;
	}&lt;br /&gt;
&lt;br /&gt;
	if spec.applicable == false then&lt;br /&gt;
		state.source = &amp;#039;inapplicable&amp;#039;&lt;br /&gt;
		state.invalid = state.overrideProvided&lt;br /&gt;
		state.customized = state.overrideProvided or yesno(spec.suppress, false) == true&lt;br /&gt;
		return state&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	if yesno(spec.suppress, false) then&lt;br /&gt;
		state.source = &amp;#039;suppressed&amp;#039;&lt;br /&gt;
		state.invalid = state.overrideProvided&lt;br /&gt;
		state.customized = true&lt;br /&gt;
		return state&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	state.customized = state.overrideProvided&lt;br /&gt;
&lt;br /&gt;
	if state.overrideProvided then&lt;br /&gt;
		if parse == nil then devError(&amp;#039;resolver: You provided an override, but did not provide a parse function&amp;#039;) end&lt;br /&gt;
&lt;br /&gt;
		local values, valid = parse(spec.override)&lt;br /&gt;
		if valid then&lt;br /&gt;
			if type(values) ~= &amp;#039;table&amp;#039; then&lt;br /&gt;
				devError(&amp;#039;resolver: parse must return a list of values, got &amp;#039; .. type(values))&lt;br /&gt;
			end&lt;br /&gt;
&lt;br /&gt;
			local processed, rejected = processValues(values, canon, filter)&lt;br /&gt;
			if not rejected and (max == nil or #processed &amp;lt;= max) then&lt;br /&gt;
				state.source = &amp;#039;override&amp;#039;&lt;br /&gt;
				state.values = processed&lt;br /&gt;
				return state&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
&lt;br /&gt;
		state.invalid = true&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local candidates = spec.candidates or {}&lt;br /&gt;
&lt;br /&gt;
	if type(candidates) ~= &amp;#039;table&amp;#039; then devError(&amp;#039;resolver: candidates must be a list, got &amp;#039; .. type(candidates)) end&lt;br /&gt;
&lt;br /&gt;
	local processed = processValues(candidates, canon, filter)&lt;br /&gt;
	if max and #processed &amp;gt; max then&lt;br /&gt;
		state.overflow = true&lt;br /&gt;
		for index = #processed, max + 1, -1 do&lt;br /&gt;
			processed[index] = nil&lt;br /&gt;
		end&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	state.values = processed&lt;br /&gt;
	state.undetected = #processed == 0&lt;br /&gt;
	return state&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function anyFlag(flag, ...)&lt;br /&gt;
	for index = 1, select(&amp;#039;#&amp;#039;, ...) do&lt;br /&gt;
		local state = select(index, ...)&lt;br /&gt;
		if type(state) == &amp;#039;table&amp;#039; and state[flag] then return true end&lt;br /&gt;
	end&lt;br /&gt;
	return false&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function anyCustomized(...)&lt;br /&gt;
	return anyFlag(&amp;#039;customized&amp;#039;, ...)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function anyInvalid(...)&lt;br /&gt;
	return anyFlag(&amp;#039;invalid&amp;#039;, ...)&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local function fromMap(map, opts)&lt;br /&gt;
	if type(map) ~= &amp;#039;table&amp;#039; then&lt;br /&gt;
		devError(&amp;#039;fromMap: expected a table, got &amp;#039; .. type(map) .. &amp;#039;something seriously went wrong.&amp;#039;)&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local strict = opts ~= nil and opts.strict == true&lt;br /&gt;
	return function(value)&lt;br /&gt;
		local mapped = map[value]&lt;br /&gt;
		if mapped ~= nil then&lt;br /&gt;
			return mapped&lt;br /&gt;
		elseif strict then&lt;br /&gt;
			return nil&lt;br /&gt;
		end&lt;br /&gt;
&lt;br /&gt;
		return value&lt;br /&gt;
	end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
local errorCat = &amp;#039;Pages with categorizer errors&amp;#039;&lt;br /&gt;
local missingCat = &amp;#039;Pages with nonexistent categorizer categories&amp;#039; -- module is trying to call a redlink cat, possibly requiring admin intervention at alias data&lt;br /&gt;
&lt;br /&gt;
local function makeMain(categoriesFn, namespaces)&lt;br /&gt;
	if type(categoriesFn) ~= &amp;#039;function&amp;#039; then&lt;br /&gt;
		devError(&amp;#039;M&amp;amp;Ms: expected the subject categories function, got &amp;#039; .. type(categoriesFn))&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	local allowed = {}&lt;br /&gt;
&lt;br /&gt;
	for _, namespace in ipairs(namespaces or { 0 }) do&lt;br /&gt;
		allowed[namespace] = true&lt;br /&gt;
	end&lt;br /&gt;
&lt;br /&gt;
	return function(frame)&lt;br /&gt;
		local parent = frame:getParent()&lt;br /&gt;
		if not parent then return &amp;#039;&amp;#039; end&lt;br /&gt;
		local args = parent.args&lt;br /&gt;
		local title = mw.title.getCurrentTitle()&lt;br /&gt;
&lt;br /&gt;
		if not allowed[title.namespace] or yesno(args.nocat, false) then return &amp;#039;&amp;#039; end&lt;br /&gt;
&lt;br /&gt;
		local ok, result = pcall(categoriesFn, args)&lt;br /&gt;
&lt;br /&gt;
		if not ok then -- quiet down non critical crashes, issue cat&lt;br /&gt;
			if isDevError(result) then error(result, 0) end&lt;br /&gt;
			mw.addWarning(&amp;#039;Categorizer error: &amp;#039; .. tostring(result))&lt;br /&gt;
			return &amp;#039;[[Category:&amp;#039; .. errorCat .. &amp;#039;]]&amp;#039;&lt;br /&gt;
		end&lt;br /&gt;
&lt;br /&gt;
		if getmetatable(result) == List then result = result:list() end&lt;br /&gt;
&lt;br /&gt;
		if type(result) ~= &amp;#039;table&amp;#039; then&lt;br /&gt;
			devError(&amp;#039;M&amp;amp;Ms: the categories function must return a list, but got: &amp;#039; .. type(result))&lt;br /&gt;
		end&lt;br /&gt;
&lt;br /&gt;
		local output = {}&lt;br /&gt;
		local missing = {}&lt;br /&gt;
		local failed = false&lt;br /&gt;
&lt;br /&gt;
		for _, name in ipairs(result) do&lt;br /&gt;
			name = trim(name)&lt;br /&gt;
&lt;br /&gt;
			if name ~= &amp;#039;&amp;#039; then&lt;br /&gt;
				local title = mw.title.new(name, categoryNamespace)&lt;br /&gt;
&lt;br /&gt;
				if title then&lt;br /&gt;
					output[#output + 1] = &amp;#039;[[Category:&amp;#039; .. name .. &amp;#039;]]&amp;#039;&lt;br /&gt;
					local ok, exists = pcall(function()&lt;br /&gt;
						return title.exists -- expensive call, massive value&lt;br /&gt;
					end)&lt;br /&gt;
					if ok and not exists then missing[#missing + 1] = &amp;#039;[[:Category:&amp;#039; .. name .. &amp;#039;]]&amp;#039; end -- instead of silently spamming red cats, also call an administrator/maintenance editor to review &amp;amp; handle the new red cats&lt;br /&gt;
				else&lt;br /&gt;
					failed = true&lt;br /&gt;
				end&lt;br /&gt;
			end&lt;br /&gt;
		end&lt;br /&gt;
		if failed then output[#output + 1] = &amp;#039;[[Category:&amp;#039; .. errorCat .. &amp;#039;]]&amp;#039; end&lt;br /&gt;
		if #missing &amp;gt; 0 then&lt;br /&gt;
			output[#output + 1] = &amp;#039;[[Category:&amp;#039; .. missingCat .. &amp;#039;]]&amp;#039;&lt;br /&gt;
			mw.addWarning(&lt;br /&gt;
				&amp;#039;Categorizer added nonexistent categories. If needed, create them, otherwise check subject configs or alias lists for fix. Added: &amp;#039;&lt;br /&gt;
					.. table.concat(missing, &amp;#039;, &amp;#039;)&lt;br /&gt;
			)&lt;br /&gt;
		end&lt;br /&gt;
&lt;br /&gt;
		return table.concat(output)&lt;br /&gt;
	end&lt;br /&gt;
end&lt;br /&gt;
&lt;br /&gt;
p.trim = trim&lt;br /&gt;
p.normalize = normalize&lt;br /&gt;
p.devError = devError&lt;br /&gt;
p.need = need&lt;br /&gt;
p.newList = newList&lt;br /&gt;
p.fill = fill&lt;br /&gt;
p.clean = clean&lt;br /&gt;
&lt;br /&gt;
p.findYears = findYears&lt;br /&gt;
p.findYearRanges = findYearRanges&lt;br /&gt;
p.findLinkTargets = findLinkTargets&lt;br /&gt;
p.readDataAttr = readDataAttr&lt;br /&gt;
p.parseNumber = parseNumber&lt;br /&gt;
p.pickBand = pickBand&lt;br /&gt;
p.compileAliasSet = compileAliasSet&lt;br /&gt;
p.matchAliasSets = matchAliasSets&lt;br /&gt;
&lt;br /&gt;
p.parseYearOverride = parseYearOverride&lt;br /&gt;
p.parseAliasSetOverride = parseAliasSetOverride&lt;br /&gt;
p.parseLinkTargetOverride = parseLinkTargetOverride&lt;br /&gt;
p.parseExactOverride = parseExactOverride&lt;br /&gt;
&lt;br /&gt;
p.resolve = resolve&lt;br /&gt;
p.anyCustomized = anyCustomized&lt;br /&gt;
p.anyInvalid = anyInvalid&lt;br /&gt;
p.fromMap = fromMap&lt;br /&gt;
p.makeMain = makeMain&lt;br /&gt;
&lt;br /&gt;
return p&lt;/div&gt;</summary>
		<author><name>Bulgogi2</name></author>
	</entry>
</feed>