Module:TemplateData

From Coral Island Wiki
Jump to navigation Jump to search

Documentation for this module may be created at Module:TemplateData/doc

--- Module copied from Genshin Impact wiki.

---  A library used to process other modules' arguments and to output matching 
--  [[mw:Extension:TemplateData|template data]].
--  @script TemplateData

local p = {}

-- general helper functions
local function quote(s)
	return string.format('"%s"', s)
end

local function ensureTable(v)
	if type(v) == "table" then return v end
	return {v}
end

-- metatable helper functions

--- Try getting from nonDefaults, then from defaults.
local function getWithDefault(tbl, k)
	local value = tbl.nonDefaults[k]
	if value ~= nil then return value end
	return tbl.defaults[k]
end

--- Main generator function for pairs.
local function pairsWithDefaultGenerator(data)
	local tbl, params, i = unpack(data)
	i = i + 1
	if i > #params then
		return
	end
	local k = params[i]
	local v = getWithDefault(tbl, k)
	data[3] = i
	return k, v
end

--- Main generator function for ipairs.
local function ipairsWithDefaultGenerator(tbl, i)
	i = i + 1
    local v = getWithDefault(tbl, i)
    if v ~= nil then
    	return i, v
	end
end

local METATABLE = {
	__name = "ArgsWithDefault",
	__index = getWithDefault,
	__newindex = function(self, k, v)
		self.nonDefaults[k] = v
	end,
	__pairs = function(self)
		local paramSet = {}
		for k, _ in pairs(self.nonDefaults) do
			paramSet[k] = true
		end
		for k, _ in pairs(self.defaults) do
			paramSet[k] = true
		end
		
		local params = {}
		for k, _ in pairs(paramSet) do
			table.insert(params, k)
		end
		
		return pairsWithDefaultGenerator, {self, params, 0}
	end,
	__ipairs = function(self)
		return ipairsWithDefaultGenerator, self, 0
	end,
}


--[[
	A table with configurable default values for properties.
	@{TemplateData.processArgs} returns objects of this type.
	
	In addition to acting as a regular map-style table, these tables include two
	sub-tables that allow access to default value and explicitly-set values, respectively.
	@type TableWithDefaults
]]

--[[
	@factory TableWithDefaults
	@return {TableWithDefaults} A new table with defaults.
--]]
function p.createObjectWithDefaults()
	-- @export
	local out = {
		--[[
			A table of default entries.
			Initially, defaults come from the argument configuration, but they can be
			changed retroactively after the table has been created.
			
			Not included in `pairs(TableWithDefaults)` or `ipairs(TableWithDefaults)`.
		]]
		defaults={},
		--[[
			A table of manually-assigned entries.
			Useful for checking whether a property was set explicitly.
			(Assinging to the `TableWithDefaults` directly will update this table.)
			
			Not included in `pairs(TableWithDefaults)` or `ipairs(TableWithDefaults)`.
		]]
		nonDefaults={}
	}
	setmetatable(out, METATABLE)
	return out
end
--- @type end

local CONFIG_DEFAULTS = {}
local CONFIG_TRANSFORMS = {}
-- note: self.name is set in `p.wrapConfig`, so it doesn't need a default
function CONFIG_DEFAULTS:displayName()
	return self.name
end
function CONFIG_TRANSFORMS:displayName(value)
	return tostring(value)
end
function CONFIG_TRANSFORMS:alias(value)
	return ensureTable(value)
end
function CONFIG_DEFAULTS:displayAlias()
	if self.alias then
		local name = tostring(self.name)
		if self.displayName ~= name then
			-- swap name and displayName if displayName is an alias
			local output = {}
			for i, v in ipairs(self.alias) do
				v = tostring(v)
				if v == self.displayName then
					output[i] = name
				else
					output[i] = v
				end
			end
			return output
		else
			return self.alias
		end
	end
end
function CONFIG_DEFAULTS:description()
	if self.type == "1" then
		return "Set to 1 to " .. self.config.trueDescription
	end
end
function CONFIG_DEFAULTS:summary()
	if self.description then
		-- take everything up to (and including) the first period
		return self.description:match('^.-%.') or self.description
	end
end
function CONFIG_DEFAULTS:example()
	if self.type == "1" then
		return 1
	end
end
function CONFIG_TRANSFORMS:example(value)
	if type(value) == "table" then
		return quote(table.concat(value, '", "'))
	else
		return quote(value)
	end
end
function CONFIG_DEFAULTS:displayDefault()
	if self.default then
		return quote(self.default)
	end
end
function CONFIG_DEFAULTS:displayType()
	if self.type == "1" then
		return "boolean"
	end
	if self.type == "number" then
		return "number"
	end
	return "line"
end

local CONFIG_WRAPPER_METATABLE = {
	__index = function(self, key)
		-- use value from config if present
		local value = self.config[key]
		
		-- else try to generate a default value
		if not value then
			local getter = CONFIG_DEFAULTS[key]
			if getter then
				value = getter(self)
			end
		end
		
		if not value then
			return nil
		end
		
		-- if value is present (and not false), run transform if needed
		local transform = CONFIG_TRANSFORMS[key]
		if transform then
			value = transform(self, value)
		end
		-- memoize and return
		self[key] = value
		return value
	end
}

function p.wrapConfig(config, key)
	local output = {config = config, name = config.name or key}
	setmetatable(output, CONFIG_WRAPPER_METATABLE)
	return output
end

--- Processes arguments based on given argument configs.
--  @param      {table} args The table of arguments to process.
--  @param      {ArgumentConfigTable} argConfigs The argument configs to use when processing `args`.
--  @param[opt] {table} options Extra options
--  @param[opt] {boolean} options.preserveDefaults Set true to treat `args` as a `TableWithDefaults`
--              and use its `defaults` and `nonDefaults` to set default/non-default values for
--              arguments that use the same name in both `args` and `argConfigs`
--              (i.e., this currently does not work with aliases).
--  @return     {TableWithDefaults} The processed version of the arguments.
function p.processArgs(args, paramConfigs, options)
	local preserveDefaults = false
	if type(options) == "table" and options.preserveDefaults and type(args.defaults) == "table" and type(args.nonDefaults) == "table" then
		preserveDefaults = true
	end
	
	local out = p.createObjectWithDefaults()
	for key, config in pairs(paramConfigs) do
		key = config.name or key
		local value, default
		if preserveDefaults then
			value = args.nonDefaults[key]
			default = args.defaults[key] or config.default
		else
			value = args[key]
			default = config.default
		end
		if value == nil and config.alias then
			for _, alias in ipairs(ensureTable(config.alias)) do
				value = args[alias]
				if value ~= nil then break end
			end
		end
		
		if config.type == "1" then
			if value ~= nil then
				value = value == "1" or value == 1 or value == true
			end
			default = default or false
		elseif config.type == "number" then
			value = tonumber(value)
		end
		out.nonDefaults[key] = value
		-- note that default values will not be processed by type
		out.defaults[key] = default
	end
	
	-- note that multiple layers of defaultFrom are not supported
	for key, config in pairs(paramConfigs) do
		key = config.name or key
		if config.defaultFrom then
			local to = key
			local from = config.defaultFrom
			-- allow defaulting from params that won't appear in output
			-- use explicit default if param not found
			out.defaults[to] = out.nonDefaults[from] or out.defaults[from] or args[from] or out.defaults[to]
		end
		if config.error and out[key] == nil then
			local message = config.error
			if type(message) == "boolean" then
				message = "Please specify a value for the required argument \"" .. key .. "\""
			end
			error(message, -1)
		end
	end
	
	return out
end

--- Creates and returns template data for display on a page.
--  @param      {ArgumentConfigList} argConfigs Argument configs to base the template data on.
--              Must use list form in order for parameter order to be predictable.
--  @param      {table} options Any other top-level keys to add to the template data.
--              Usually used to add `description` and `format`.
--  @param[opt] {Frame} frame Current frame. If not supplied, template data will be returned as a raw string,
--              which is useful for previewing in console, but will not display properly when output to wiki articles.
--  @return     {string} The template data.
function p.templateData(paramConfigs, options, frame)
	local json = require("Module:JSON")
	
	local params = {}
	local paramOrder = {}
	local output = {
		params=params,
		paramOrder=paramOrder
	}
	for key, val in pairs(options) do
		output[key] = val
	end
	
	for key, config in ipairs(paramConfigs) do
		config = p.wrapConfig(config)
		params[config.displayName] = { -- allow false to behave the same as nil for all properties
			aliases=config.displayAlias,
			label=config.label,
			description=config.description,
			type=config.displayType,
			default=config.displayDefault,
			example=config.example,
			auto=config.auto,
			required=config.status=="required" or nil,
			suggested=config.status=="suggested" or nil,
			deprecated=config.status=="deprecated" or nil,
		}
		table.insert(paramOrder, config.displayName)
	end
	
	if frame then
		return frame:extensionTag("templatedata", json.encode(output))
	else
		return "<templatedata>"..json.encode(output).."</templatedata>"
	end
end

local function addLine(o)
	-- note: putting divs into code elements is not valid HTML,
	-- so instead use a span with CSS
	return o:tag('span'):css('display', 'block')
end

function p.syntax(paramConfigs, frame)
	frame = frame or mw.getCurrentFrame()
	local templateName = frame.args[1] or mw.title.new(frame:getTitle()).text
	
	local params = {}
	local maxNameLength = 0
	for key, config in ipairs(paramConfigs) do
		config = p.wrapConfig(config)
		local param = {
			name = config.displayName,
			nameLength = #config.displayName,
			summary = config.summary,
			aliases = config.displayAlias,
			status = config.status, -- (not currently displayed)
		}
		maxNameLength = math.max(maxNameLength, param.nameLength)
		
		table.insert(params, param)
	end
	
	local indent = maxNameLength + 4 -- 1ch from '|' + 3ch from ' = '
	local output = mw.html.create('code'):css({
		display='inline-block',
		['line-height']='1.5',
		['white-space']='pre-wrap',
		['text-indent']=-indent..'ch',
		['padding-left']='calc('..indent..'ch + 4px)', -- 4px from default padding
	})
	addLine(output):wikitext("{{"):tag('b'):wikitext(templateName)
	for _, param in ipairs(params) do
		local line = addLine(output)
		line:wikitext('|'):tag('b'):wikitext(param.name)
		for i = 1, maxNameLength - param.nameLength do
			line:wikitext('&nbsp;') -- regular spaces get collapsed on mobile
		end
		line:wikitext(' = ', param.summary)
		if param.aliases then
			line:wikitext(' (', table.concat(param.aliases, '/'), ')')
		end
	end
	addLine(output):wikitext("}}")
	return output
end

--- A table (either list or map form) of @{ArgumentConfig} configurations for arguments.
--  
--  Modules will create such tables and pass them to @{TemplateData.processArgs}
--  and @{TemplateData.templateData} to control how arguments are processed and
--  what template data gets output, respectively.
--  @see @{ArgumentConfigList} and @{ArgumentConfigMap}
--  @see @{ArgumentConfig} for details on each argument's configuration
--  @type ArgumentConfigTable

--- An @{ArgumentConfigTable} that acts as a list of @{ArgumentConfig}.
--  @type ArgumentConfigList

--- An @{ArgumentConfigTable} that acts as a map from argument name keys to @{ArgumentConfig} values.
--  @type ArgumentConfigMap

--[[
Configuration for a single argument.
Modules will create tables that match this type for their @{ArgumentConfigTable}s.
@type ArgumentConfig
]]

	--[[
	Argument name. If not provided,
	defaults to the key of this `ArgumentConfig` in the table that contains it.
	@property[opt]  {string} ArgumentConfig.name
	--]]
	
	--[[
	Display name for documentation.
	Typically used to mark one of the aliases (e.g., "1") to use as the main
	name in the documentation while using a more descriptive name in code
	(e.g., `args.item_name`).
	@property[opt]  {string] ArgumentConfig.displayName
	]]
	
	--[[
	An alias or list of aliases, any of which clients can use instead of `name`
	in their module arguments.
	(`name` will take precedence if it appears along with an alias in the args,
	and if multiple aliases apppear, the first one in the list will take precedence.)
	@property[opt]  {string|table} ArgumentConfig.aliases
	--]]

	--[[
	Display name to be used in template data.
	Has no effect on argument processing.
	@property[opt]  {string} ArgumentConfig.label
	--]]
	
	--[[
	Type of argument for automatic conversions.
	Supported values:
	* `"number"`: uses @{tonumber} on the argument value
	* `"1"`: converts argument value a boolean based on whether it's the string `"1"`,
	  the number `1`, or the boolean `true`.
	@property[opt]  {string} ArgumentConfig.type
	--]]
	
	--[[
	Display type to be used in template data.
	Has no effect on argument processing.
	Overrides `ArgumentConfig.type` in template data.
	@property[opt]  {string} ArgumentConfig.displayType
	--]]
	
	--[[
	Default value to use for the argument if the user does not provide any
	(or if the `type` conversion produces a nil result).
	@property[opt]  {any} ArgumentConfig.default
	--]]
	
	--[[
	Display for default value to be used in template data.
	Has no effect on argument processing.
	Overrides `ArgumentConfig.default` in template data.
	@property[opt]  {any} ArgumentConfig.displayDefault
	--]]
	
	--[[
	Error message to use if argument is not specified.
	Set to `true` to use a default error message.
	(Note: setting this does NOT automatically set `status` to `"required"`.)
	@property[opt]  {string|boolean} ArgumentConfig.error
	--]]
	
	--[[
	Defaults to "optional".
	Set to "required", "suggested", or "deprecated" to set corresponding flags
	in the template data.
	Has no effect on argument processing.
	@property[opt]  {string} ArgumentConfig.status
	--]]
	
	--[[
	Default value used in the Visual Editor.
	Has no effect on argument processing.
	@property[opt]  {any} ArgumentConfig.auto
	--]]
	
	--[[
	A description of the argument's purpose.
	Has no effect on argument processing.
	@property[opt]  {string} ArgumentConfig.description
	--]]
	
	--[[
	Only used for arguments with a `type` of `"1"`.
	Describes what happens when the argument value is set to 1.
	Used to reduce boilerplate in descriptions for boolean args.
	@usage "output text without a link"
	@property[opt]  {string} ArgumentConfig.trueDescription
	--]]
	
	--[[
	Example value(s) for the argument.
	The value(s) will be surrounded by quotes in the template data.
	@property[opt]  {string|table} ArgumentConfig.example
	--]]

return p