模組:Class

-- This module implements [[Template:Class]], [[Template:Class/icon]] and
-- [[Template:Class/colour]].

local mArguments -- lazily loaded
local definitions = mw.loadJsonData('Module:Class/definition.json')
local yesno = require("Module:Yesno")

local p = {}

local class_always_enable = {
	"fa", "fl", "fm", "a", "ga", "b", "c", "start", "stub", "list", "sia", "na", "file", "unassessed"
}
--------------------------------------------------------------------------------
-- Local configuration and messages
--------------------------------------------------------------------------------

local cfg = {
	defaultCode = 'DEFAULT',
	classPrefix = 'assess-',
	globalClass = 'assess',
	defaultClassSuffix = 'default',
	unboldClassSuffix = 'unbold',
	catRootFormat = '%s%s',
	catTopicFormat = '%s%s级%s',
	catBasicFormat = '%s级%s',
	categoryFormat = '[[:Category:%s|%s]]',
	templateLocation = 'Template:Class',
	iconTemplateLocation = 'Template:Class/icon',
	colourTemplateLocation = 'Template:Class/colour',
	IDTemplateLocation = 'Template:Class/ID',
	stylesLocation = 'Module:Class/styles.css',
	baseColourPath = {'colour', 'base'},
	iconPath = {"icon", "file"},
	iconDefaultPath = {"icon", "default"},
	iconAttribPath = {"icon", "requiresAttribution"},
	fullLabelPath = {"labels", "full"},
	shortLabelPath = {"labels", "short"},
	categoryRootPath = {"categoryRoot"},
	tooltipPath = {"labels", "tooltip"},
	yes = "yes",
	no = "no", 
	argumentNames = {
		class = "class",
		style = "style"
	},
	getOptions = {
		--First item is localized argument name, second is case-sensitivity
		bold = {"bold", false},
		header = {"header", false},
		image = {"image", false},
		rowspan = {"rowspan", false},
		fullcategory = {"fullcategory", true},
		category = {"category", true},
		topic = {"topic", true}
	}
}

--------------------------------------------------------------------------------
-- Argument helper functions
--------------------------------------------------------------------------------

local function getRawArgs(frame, wrapper)
	--Retrieves the arguments from the frame
	mArguments = mArguments or require('Module:Arguments')
	return mArguments.getArgs(frame, {
		wrappers = wrapper,
		trim = false,
		removeBlanks = false
	})
end

local function makeInvokeFunction(func, wrapper)
	--Wraps a general function into an invokable version
	return function (frame)
		local args = getRawArgs(frame, wrapper)
		return func(args)
	end
end

--------------------------------------------------------------------------------
-- String helper functions
--------------------------------------------------------------------------------

local function trim(str)
	--Trims strings, passes through non-strings without modification
	return (type(str) == 'string') and mw.text.trim(str) or str
end

local function normalizeValue(val)
	--Normalizes strings, particularly class codes
	if type(val) == 'string' then val = trim(val):lower() end
	if val == '' then val = nil end
	return val
end

local function ucfirst(str)
	--Capitalizes the first character of a string
	return mw.ustring.upper(mw.ustring.sub(str, 1, 1)) .. mw.ustring.sub(str, 2)
end

--------------------------------------------------------------------------------
-- Definition helper functions
--------------------------------------------------------------------------------

local function getDefinition(code)
	--Retrieves the definition and canonical class code for a given code.
	--Returns two values: the definition object and the canonical class code
	--string.
	local canonicalCode = normalizeValue(code)
	if code == cfg.defaultCode then canonicalCode = code end
	local class = definitions[canonicalCode]
	while class and class.alias do
		canonicalCode = class.alias
		class = definitions[class.alias]
	end
	if not class then
		return nil, nil
	end
	return class, canonicalCode
end

local function getDefault()
	--Shortcut function for retrieving the default definition
	return getDefinition(cfg.defaultCode) end

local function getProperty(class, default, map)
	--Retrieves a given property from a string given a class definition, a
	--default class definition, and a map for the path to traverse through the
	--class object. The map should be a sequential table of string property
	--names, e.g. {"colour", "base"} would retrieve someClass.colour.base
	local prop, dProp = class, default
	for k, v in ipairs(map) do
		prop = ((type(prop) == 'table') or nil) and prop[v]
		dProp = ((type(dProp) == 'table') or nil) and dProp[v]
	end
	if prop == nil then prop = dProp end
	return prop
end

--------------------------------------------------------------------------------
-- Color functions
--------------------------------------------------------------------------------

function p._colour(code)
	--Retrieves the base colour for a given code
	return getProperty(getDefinition(code), getDefault(), cfg.baseColourPath)
end

function p.colour(frame)
	--Retrieves the base colour for a given code; is invokable
	local args = getRawArgs(frame, cfg.colourTemplateLocation)
	-- Nowiki tags prevent output beginning with "#" from triggering bug 14974.
	return frame:extensionTag('nowiki', p._colour(args[1]))
end

--------------------------------------------------------------------------------
-- ID functions
--------------------------------------------------------------------------------
function p._ID(code)
	local _, id = getDefinition(code)
	return id
end

function p.ID(frame)
	--Retrieves the ID for a given code; is invokable
	local args = getRawArgs(frame, cfg.IDTemplateLocation)
	return p._ID(args[1])
end

--------------------------------------------------------------------------------
-- Icon functions
--------------------------------------------------------------------------------

function p._icon(args)
	--Retrieves an icon image and formats it as wikitext
	local class = getDefinition(args[cfg.argumentNames.class] or args[1])
	local default = getDefault()
	local file = getProperty(class, default, cfg.iconPath)
	local label = 
		getProperty(class, default, cfg.tooltipPath) or
		ucfirst(getProperty(class, default, cfg.fullLabelPath))
	local attrib = getProperty(class, default, cfg.iconAttribPath)
	local size = args.size or '16px'
	local span = mw.html.create('span')

	span
		:cssText(args[cfg.argumentNames.style])
		:attr('title', label)
		:wikitext(
			string.format(
				'[[File:%s|%s|' .. size .. '%s|class=noviewer|alt=]]',
				file,
				label,
				attrib and '' or '|link='
			)
		)
	return tostring(span)
end

p.icon = makeInvokeFunction(p._icon, cfg.iconTemplateLocation)
--Invokable version of p._icon

--------------------------------------------------------------------------------
-- Class functions
--------------------------------------------------------------------------------

function p._class(args)
	--Parses its arguments into a table cell with an optional icon, a name
	--linked to an appropriate category, and appropriate colour styling
	local classDef, classCode =
		getDefinition(args[cfg.argumentNames.class] or args[1])
	local default = getDefault()
	local iconDefault = getProperty(classDef, default, cfg.iconDefaultPath)
	local shortLabel = getProperty(classDef, default, cfg.shortLabelPath)
	local categoryRoot = getProperty(classDef, default, cfg.categoryRootPath)
	--o is short for "options", go for "get options". Bool true → case-sensitive
	local o, go = {}, cfg.getOptions
	for k, v in pairs(go) do
		o[k] = v[2] and trim(args[v[1]]) or normalizeValue(args[v[1]])
	end

	local cell = mw.html.create(o.header and 'th' or 'td')
	--image=yes forces icon, image=no disables it, otherwise checks default
	local icon = iconDefault and (o.image ~= cfg.no) or (o.image == cfg.yes)
	icon = icon and p.icon(args) .. ' ' or ''
	local page_type = require("Module:PJBSClass/main").getPageType(classCode)
	local category
	if o.fullcategory then
		category = o.fullcategory
	elseif o.category then
		category = string.format(cfg.catRootFormat, categoryRoot, o.category)
	elseif o.topic then
		category = string.format(cfg.catTopicFormat, categoryRoot, o.topic, page_type)
	else
		category = string.format(cfg.catBasicFormat, categoryRoot, page_type)
	end
	local text = string.format(cfg.categoryFormat, category, shortLabel)
	cell
		:addClass(cfg.globalClass)
		:addClass(
			o.bold == cfg.no and cfg.classPrefix .. cfg.unboldClassSuffix or nil
		)
		:addClass(cfg.classPrefix .. (classCode or cfg.defaultClassSuffix))
		:attr('rowspan', tonumber(o.rowspan))
		:wikitext(mw.getCurrentFrame():extensionTag{ name = 'templatestyles', args = {src = cfg.stylesLocation} }, icon, text)

	return tostring(cell)
end

p.class = makeInvokeFunction(p._class, cfg.templateLocation)
--Invokable version of p._class

function p.class_mask(input_data)
	local class = input_data or ""
	if type((input_data or {}).args) ~= type(nil) then --input_data is a frame
		local inner_args = input_data.args
		local outer_args = {}
		if type(input_data.getParent) == type(tostring) then outer_args = input_data:getParent().args or {} end
		class = outer_args['1'] or outer_args[1] or outer_args.class or outer_args.CLASS or 
			inner_args['1'] or inner_args[1] or inner_args.class or inner_args.CLASS or ""
	end
	local class_cfg = {}
	for _, _class_key in ipairs(class_always_enable) do
		local class_key = p._ID(tostring(_class_key))
		class_cfg[class_key] = true
	end
	local args = ((type(input_data) == type(0)) and {} or input_data).args or {}
	local b_class = definitions.b.name[1]
	local file_image = definitions.file.name[1]
	local forceNA = yesno(args.forceNA, true)
	for _class_key, class_flag in pairs(args) do
		if _class_key == "file-image" then
			file_image = class_flag
		end
		local class_key = p._ID(tostring(_class_key))
		if type(class_key) == type("string") then
			if class_key == 'b' and yesno(class_flag, "is_para") == "is_para" then
				b_class = tostring(class_flag)
				class_cfg[class_key] = true
			else
				class_cfg[class_key] = yesno(class_flag, false)
			end
		end
	end
	class = p._ID(tostring(class))
	if forceNA and class == "na" then
		class = require("Module:PJBSClass/main").getClassAuto() or ''
		if class == '' then class = "na" end
		class = p._ID(tostring(class))
	end
	local masked_class, i = class, 0
	while not yesno(class_cfg[masked_class], false) do
		if definitions[masked_class] then
			local fallback = definitions[masked_class].fallback
			if type(fallback) == type("string") then
				masked_class = fallback
			else
				masked_class = "unassessed"
				break
			end
		else
			masked_class = "unassessed"
			break
		end
		i = i + 1 if i > 100 then
			masked_class = "unassessed"
			break
		end
	end
	if masked_class == 'b' then return b_class end
	if masked_class == 'file' then return file_image end
	return definitions[masked_class].name[1] or definitions["unassessed"].name[1] 
end

return p