Module:Category handler
--------------------------------------------------------------------------------
 --                                                                            --
 --                              CATEGORY HANDLER                              --
 --                                                                            --
 --      This module implements the {{category handler}} template in Lua,      --
 --      with a few improvements: all namespaces and all namespace aliases     --
 --      are supported, and namespace names are detected automatically for     --
 --      the local wiki. This module requires [[Module:Namespace detect]]      --
 --      and [[Module:Yesno]] to be available on the local wiki. It can be     --
 --      configured for different wikis by altering the values in              --
 --      [[Module:Category handler/config]], and pages can be blacklisted      --
 --      from categorisation by using [[Module:Category handler/blacklist]].   --
 --                                                                            --
 --------------------------------------------------------------------------------
 
 -- Load required modules
 local yesno = require('Module:Yesno')
 
 -- Lazily load things we don't always need
 local mShared, mappings
 
 local p = {}
 
 --------------------------------------------------------------------------------
 -- Helper functions
 --------------------------------------------------------------------------------
 
 local function trimWhitespace(s, removeBlanks)
 	if type(s) ~= 'string' then
 		return s
 	end
 	s = s:match('^%s*(.-)%s*$')
 	if removeBlanks then
 		if s ~= '' then
 			return s
 		else
 			return nil
 		end
 	else
 		return s
 	end
 end
 
 --------------------------------------------------------------------------------
 -- CategoryHandler class
 --------------------------------------------------------------------------------
 
 local CategoryHandler = {}
 CategoryHandler.__index = CategoryHandler
 
 function CategoryHandler.new(data, args)
 	local obj = setmetatable({ _data = data, _args = args }, CategoryHandler)
 	
 	-- Set the title object
 	do
 		local pagename = obj:parameter('demopage')
 		local success, titleObj
 		if pagename then
 			success, titleObj = pcall(mw.title.new, pagename)
 		end
 		if success and titleObj then
 			obj.title = titleObj
 			if titleObj == mw.title.getCurrentTitle() then
 				obj._usesCurrentTitle = true
 			end
 		else
 			obj.title = mw.title.getCurrentTitle()
 			obj._usesCurrentTitle = true
 		end
 	end
 
 	-- Set suppression parameter values
 	for _, key in ipairs{'nocat', 'categories'} do
 		local value = obj:parameter(key)
 		value = trimWhitespace(value, true)
 		obj['_' .. key] = yesno(value)
 	end
 	do
 		local subpage = obj:parameter('subpage')
 		local category2 = obj:parameter('category2')
 		if type(subpage) == 'string' then
 			subpage = mw.ustring.lower(subpage)
 		end
 		if type(category2) == 'string' then
 			subpage = mw.ustring.lower(category2)
 		end
 		obj._subpage = trimWhitespace(subpage, true)
 		obj._category2 = trimWhitespace(category2) -- don't remove blank values
 	end
 	return obj
 end
 
 function CategoryHandler:parameter(key)
 	local parameterNames = self._data.parameters[key]
 	local pntype = type(parameterNames)
 	if pntype == 'string' or pntype == 'number' then
 		return self._args[parameterNames]
 	elseif pntype == 'table' then
 		for _, name in ipairs(parameterNames) do
 			local value = self._args[name]
 			if value ~= nil then
 				return value
 			end
 		end
 		return nil
 	else
 		error(string.format(
 			'invalid config key &quot;%s&quot;',
 			tostring(key)
 		), 2)
 	end
 end
 
 function CategoryHandler:isSuppressedByArguments()
 	return
 		-- See if a category suppression argument has been set.
 		self._nocat == true
 		or self._categories == false
 		or (
 			self._category2
 			and self._category2 ~= self._data.category2Yes
 			and self._category2 ~= self._data.category2Negative
 		)
 
 		-- Check whether we are on a subpage, and see if categories are
 		-- suppressed based on our subpage status.
 		or self._subpage == self._data.subpageNo and self.title.isSubpage
 		or self._subpage == self._data.subpageOnly and not self.title.isSubpage
 end
 
 function CategoryHandler:shouldSkipBlacklistCheck()
 	-- Check whether the category suppression arguments indicate we
 	-- should skip the blacklist check.
 	return self._nocat == false
 		or self._categories == true
 		or self._category2 == self._data.category2Yes
 end
 
 function CategoryHandler:matchesBlacklist()
 	if self._usesCurrentTitle then
 		return self._data.currentTitleMatchesBlacklist
 	else
 		mShared = mShared or require('Module:Category handler/shared')
 		return mShared.matchesBlacklist(
 			self.title.prefixedText,
 			mw.loadData('Module:Category handler/blacklist')
 		)
 	end
 end
 
 function CategoryHandler:isSuppressed()
 	-- Find if categories are suppressed by either the arguments or by
 	-- matching the blacklist.
 	return self:isSuppressedByArguments()
 		or not self:shouldSkipBlacklistCheck() and self:matchesBlacklist()
 end
 
 function CategoryHandler:getNamespaceParameters()
 	if self._usesCurrentTitle then
 		return self._data.currentTitleNamespaceParameters
 	else
 		if not mappings then
 			mShared = mShared or require('Module:Category handler/shared')
 			mappings = mShared.getParamMappings(true) -- gets mappings with mw.loadData
 		end
 		return mShared.getNamespaceParameters(
 			self.title,
 			mappings
 		)
 	end
 end
 
 function CategoryHandler:namespaceParametersExist()
 	-- Find whether any namespace parameters have been specified.
 	-- We use the order &quot;all&quot; --&gt; namespace params --&gt; &quot;other&quot; as this is what
 	-- the old template did.
 	if self:parameter('all') then
 		return true
 	end
 	if not mappings then
 		mShared = mShared or require('Module:Category handler/shared')
 		mappings = mShared.getParamMappings(true) -- gets mappings with mw.loadData
 	end
 	for ns, params in pairs(mappings) do
 		for i, param in ipairs(params) do
 			if self._args[param] then
 				return true
 			end
 		end
 	end
 	if self:parameter('other') then
 		return true
 	end
 	return false
 end
 
 function CategoryHandler:getCategories()
 	local params = self:getNamespaceParameters()
 	local nsCategory
 	for i, param in ipairs(params) do
 		local value = self._args[param]
 		if value ~= nil then
 			nsCategory = value
 			break
 		end
 	end
 	if nsCategory ~= nil or self:namespaceParametersExist() then
 		-- Namespace parameters exist - advanced usage.
 		if nsCategory == nil then
 			nsCategory = self:parameter('other')
 		end
 		local ret = {self:parameter('all')}
 		local numParam = tonumber(nsCategory)
 		if numParam and numParam &gt;= 1 and math.floor(numParam) == numParam then
 			-- nsCategory is an integer
 			ret[#ret + 1] = self._args[numParam]
 		else
 			ret[#ret + 1] = nsCategory
 		end
 		if #ret &lt; 1 then
 			return nil
 		else
 			return table.concat(ret)
 		end
 	elseif self._data.defaultNamespaces[self.title.namespace] then
 		-- Namespace parameters don't exist, simple usage.
 		return self._args[1]
 	end
 	return nil
 end
 
 --------------------------------------------------------------------------------
 -- Exports
 --------------------------------------------------------------------------------
 
 local p = {}
 
 function p._exportClasses()
 	-- Used for testing purposes.
 	return {
 		CategoryHandler = CategoryHandler
 	}
 end
 
 function p._main(args, data)
 	data = data or mw.loadData('Module:Category handler/data')
 	local handler = CategoryHandler.new(data, args)
 	if handler:isSuppressed() then
 		return nil
 	end
 	return handler:getCategories()
 end
 
 function p.main(frame, data)
 	data = data or mw.loadData('Module:Category handler/data')
 	local args = require('Module:Arguments').getArgs(frame, {
 		wrappers = data.wrappers,
 		valueFunc = function (k, v)
 			v = trimWhitespace(v)
 			if type(k) == 'number' then
 				if v ~= '' then
 					return v
 				else
 					return nil
 				end
 			else
 				return v
 			end
 		end
 	})
 	return p._main(args, data)
 end