Module:TemplateData
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(' ') -- 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