Module:Documentation/Template
Jump to navigation
Jump to search
local p = {}
local h = {}
local ListPages = require("Module:List Pages")
local i18n = require("Module:I18n")
local s = i18n.getString
local utilsLayout = require("Module:UtilsLayout")
local utilsMarkup = require("Module:UtilsMarkup")
local utilsPage = require("Module:UtilsPage")
local utilsSchema = require("Module:UtilsSchema")
local utilsString = require("Module:UtilsString")
local utilsTable = require("Module:UtilsTable")
local utilsVar = require("Module:UtilsVar")
local styles = mw.getCurrentFrame() and mw.getCurrentFrame():extensionTag({
name = "templatestyles",
args = { src = "Module:Documentation/Styles.css" }
})
local examplesCounter = utilsVar.counter("examples")
local DEFAULT_FORMAT = "inline"
local FORMATS = {
inline = {
type = "inline",
canOmitParams = true,
templateData = "{{_|_= _}}",
argSeparator = "|",
afterLastArg = "",
},
block = {
type = "block",
canOmitParams = false,
templateData = "{{_\n|_= _\n}}",
argSeparator = "\n%s|",
afterLastArg = "\n",
}
}
-- Chose Fibonacci numbers a on a whim, though the increments do seem about right for most cases so far
local DEFAULT_REPEATED_PARAM_COUNTS = {2, 3, 5, 8, 13, 21}
local VARARG_KEY = "..."
function p.Template(frame)
local moduleName = frame.args.module or mw.title.getCurrentTitle().rootText
local templateName = frame.args.template or p.getTemplateName()
local templateSpec = p.loadTemplateSpec(moduleName, templateName)
return p.printTemplateDoc(moduleName, templateName, templateSpec, false)
end
-- Prints only template synax, boilerplate and parameter descriptions
-- Useful when multiple templates share parameters but need their own purpose description or examples
function p.TemplateUsage(frame)
local moduleName = frame.args.module or mw.title.getCurrentTitle().rootText
local templateName = frame.args.template or p.getTemplateName()
local templateSpec = p.loadTemplateSpec(moduleName, templateName)
return p.printTemplateDoc(moduleName, templateName, templateSpec, true)
end
-- Prints only TemplateData, when customized usage information is needed (see [[Template:Gallery List]])
function p.TemplateData(frame)
local moduleName = frame.args.module or mw.title.getCurrentTitle().rootText
local templateName = frame.args.template or p.getTemplateName()
local templateSpec = p.loadTemplateSpec(moduleName, templateName)
return h.templateData(templateSpec)
end
function p.Examples(frame)
local args = frame:getParent().args
local examples = h.examplesFromFrame(args)
return styles, p.printExamples(examples)
end
function p.getTemplateName()
local title = mw.title.getCurrentTitle()
if title.subpageText == "Documentation" then
return title.baseText
else
return title.text
end
end
function p.loadTemplateSpec(moduleName, templateName)
-- The template specs may be on the module page itself (convenient) or on a subpage (perfomant)
local templates = {}
local docDataPage = "Module:"..moduleName.."/TemplateData"
local modulePage = "Module:"..moduleName
local docDataPageExists = utilsPage.exists(docDataPage)
if docDataPageExists then
templates = require(docDataPage)
else
templates = require(modulePage).Templates
end
local templateSpec = templates[templateName]
if not templateSpec then
error(string.format("No template '%s' defined at [[%s]]. For help with auto-generated template documentation, see [[Module:Documentation/Documentation]]", templateName, docDataPageExists and docDataPage or modulePage))
end
return templateSpec
end
function p.printTemplateDoc(moduleName, templateName, templateSpec, usageOnly)
utilsSchema.validate(p.Schemas().Templates.values, "Templates.values", templateSpec, templateName)
local result = ""
if templateSpec.wip then
result = result .. mw.getCurrentFrame():expandTemplate({title = "WIP"})
end
if not usageOnly then
result = result .. "__TOC__\n"
end
if not usageOnly and templateSpec.purpose then
result = result .. utilsMarkup.heading(2, s("heading.purpose"))
result = result .. mw.getCurrentFrame():preprocess(templateSpec.purpose) .. "\n"
end
if not usageOnly then
result = result .. utilsMarkup.heading(2, s("heading.usage"))
end
if templateSpec.usage then
result = result .. mw.getCurrentFrame():preprocess(templateSpec.usage)
end
if templateSpec.usesData then
local dataSource
if type(templateSpec.usesData) == "table" then
dataSource = ListPages.main(templateSpec.usesData)
elseif type(templateSpec.usesData) == "string" then
dataSource = utilsMarkup.link(templateSpec.usesData)
end
if dataSource then
result = result .. utilsMarkup.italic(s("usesData", {dataSource = dataSource})) .. "\n"
end
end
if templateSpec.usesModuleData then
local dataSource = utilsMarkup.link("Module:" .. moduleName .. "/Data")
result = result .. utilsMarkup.italic(s("usesModuleData", {dataSource = dataSource})) .. "\n"
end
if type(templateSpec.storesData) == "string" then
local dataSource = utilsMarkup.link(templateSpec.storesData)
result = result .. utilsMarkup.italic(s("storesData", {dataSource = dataSource})) .. "\n"
end
result = result .. p.printUsage(templateSpec)
result = result .. h.templateData(templateSpec)
if templateSpec.examples then
local examples = h.examplesFromSpec(templateSpec)
result = result .. utilsMarkup.heading(3, s("heading.examples"))
result = result .. p.printExamples(examples)
end
result = result .. h.categories(templateSpec)
return result
end
function p.printUsage(templateSpec)
local params = {}
local paramOrder = templateSpec.paramOrder
local positionalParamCount = 0
local hasVarArg = false
local repeatedParams = templateSpec.repeatedGroup and templateSpec.repeatedGroup.params or {}
for k, v in pairs(templateSpec.params or {}) do
if type(k) == "number" then
positionalParamCount = positionalParamCount + 1
elseif k == VARARG_KEY then
hasVarArg = true
end
if utilsTable.includes(repeatedParams, k) then
-- do nothing, repeated params processed separately
else
local i = paramOrder and utilsTable.keyOf(paramOrder, k) or (#params + 1)
params[i] = utilsTable.merge({}, v, {
param = k
})
end
end
local repeatedGroup = {}
for _, param in ipairs(repeatedParams) do
repeatedGroup[#repeatedGroup + 1] = utilsTable.merge({}, templateSpec.params[param], {
param = param,
isRepeated = true,
})
end
local format = FORMATS[templateSpec.format or DEFAULT_FORMAT]
local result = ""
local repeatedParamsCounts = templateSpec.repeatedGroup and templateSpec.repeatedGroup.counts
if templateSpec.boilerplate and templateSpec.boilerplate.disable then
-- do nothing
elseif positionalParamCount > 0 or hasVarArg then
result = result .. utilsLayout.tabs({
{
label = s("tabs.syntax"),
content = h.syntax(templateSpec, params, repeatedGroup, format)
},
{
label = s("tabs.boilerplate"),
content = h.boilerplates(templateSpec, params, repeatedGroup, format, repeatedParamsCounts)
},
})
else
result = h.boilerplates(templateSpec, params, repeatedGroup, format, repeatedParamsCounts)
end
-- If there are parameters after the repeated ones, we insert them in the empty gap. Otherwise they are inserted at the end.
local i = #(utilsTable.takeWhile(params, function(param) return param ~= nil end))
for j, repeatedParam in ipairs(repeatedGroup) do
table.insert(params, i+j, repeatedParam)
end
params = utilsTable.compact(params)
result = result .. p.params(params, positionalParamCount)
return styles..result
end
function p.printExamples(examples)
for i, example in ipairs(examples) do
local exampleId = examplesCounter.increment() -- we use this instead of i in case Template:Examples is invoked more than once on a page
exampleId = utilsMarkup.anchor("example-"..tostring(exampleId), exampleId)
local input = mw.text.trim(example.input)
input = mw.text.decode(example.input)
input = string.gsub(input, "<nowiki>", "<nowiki>")
if (string.find(input, "\n") or string.find(input, "<p>") or examples.vertical) then
input = utilsMarkup.pre(input, {
wrapLines = examples.vertical or examples.wrapLines ~= nil
})
else
input = mw.text.nowiki(input)
input = string.gsub(input, "/", "/<wbr>") -- add word break opportunities for long urls - see [[Template:Cite Magazine]] for exam
input = utilsMarkup.code(input)
end
if example.output then
local output, categories = utilsMarkup.stripCategories(example.output)
local output = utilsMarkup.killBacklinks(output)
local categoryList = utilsMarkup.bulletList(categories)
examples[i] = {
input = input,
output = output,
categoryList = categoryList,
desc = example.desc,
id = exampleId,
}
else
examples[i] = {
input = input,
desc = example.desc,
id = exampleId,
}
end
end
if not examples.vertical then
local rows = {}
for _, example in ipairs(examples) do
if example.desc then
table.insert(rows, {
{
header = true,
colspan = -1,
content = example.desc,
class = "template-examples__description-header",
}
})
end
table.insert(rows, {
{
class = "template-examples__example-id-cell",
content = example.id,
},
{
class = "template-examples__input-cell",
content = example.input
},
{
class = "template-examples__output-cell",
content = example.output and "\n"..example.output -- \n needed for Template:Stub
},
{
class = "template-examples__category-cell",
content = example.categoryList
}
})
end
local headers = {
s("header.input"),
s("header.output"),
s("header.categoriesAdded"),
}
if examples.showNumbers ~= false then
table.insert(headers, 1, "#")
end
return utilsLayout.table({
class = "wikitable template-examples",
hideEmptyColumns = true,
headers = {
"#",
s("header.input"),
s("header.output"),
s("header.categoriesAdded"),
},
rows = rows,
caption = examples.caption,
})
end
local exampleGrid = mw.html.create("div")
:addClass("template-examples__grid")
for i, example in ipairs(examples) do
if i ~= 1 then
exampleGrid:tag("hr"):addClass("template-examples__grid-example-separator")
end
exampleGrid
:tag("div")
:addClass("template-examples__grid-example-desc")
:wikitext(example.desc)
:done()
if examples.showNumbers ~= false then
exampleGrid:tag("div")
:addClass("template-examples__grid-example-id")
:wikitext(example.id)
:done()
end
exampleGrid
:tag("div")
:addClass("template-examples__grid-example-header")
:wikitext("Input")
:done()
:tag("div")
:addClass("template-examples__grid-example-input")
:wikitext(example.input)
:done()
:tag("div")
:addClass("template-examples__grid-example-header")
:wikitext("Output")
:done()
:tag("div")
:addClass("template-examples__grid-example-output")
:wikitext(example.output)
:done()
if utilsString.notEmpty(example.categoryList) then
exampleGrid
:tag("div")
:addClass("template-examples__grid-example-header")
:wikitext("Categories Added")
:done()
:tag("div")
:addClass("template-examples__grid-example-categories")
:wikitext(example.categoryList)
:done()
end
end
return styles..tostring(exampleGrid)
end
function h.syntax(templateSpec, params, repeatedGroup, format)
local boilerplateOptions = templateSpec.boilerplate or {}
local indent = templateSpec.indent
local hiddenParams = utilsTable.invert(boilerplateOptions.hiddenParams or {})
local args = {}
for i, param in ipairs(params) do
if hiddenParams[param.param] then
-- do nothing
elseif param.param == VARARG_KEY then
h.insertVarArgsSyntax(args, i, param.placeholder or param.name)
elseif type(param.param) == "number" then
args[i] = {
param = param.param,
value = param.placeholder or string.format("<%s>", param.name or param.param),
inline = param.inline,
}
else
args[i] = {
param = param.param,
inline = param.inline,
value = ""
}
end
end
-- Add syntax for repeated group
local afterArg = {}
if #repeatedGroup > 1 then
for i, n in ipairs({"1", "2", "N"}) do
for _, param in ipairs(repeatedGroup) do
if not hiddenParams[param.param] then
table.insert(args, {
param = param.param .. n,
value = ""
})
end
end
if i ~= 3 then
afterArg[#args] = "\n"
end
end
end
local result = h.printTemplateInstance(args, format, {
indent = indent,
afterArg = afterArg,
})
return utilsMarkup.pre(result)
end
function h.insertVarArgsSyntax(args, i, placeholder)
for j = 1, 3 do
table.insert(args, i+j-1, {
param = j,
value = string.format("<%s>", placeholder .. j)
})
end
table.insert(args, i+3, {
param = i+3,
value = "..."
})
table.insert(args, i+4, {
param = i+4,
value = string.format("<%s>", placeholder.."N")
})
end
function h.boilerplates(templateSpec, params, repeatedGroup, format, repeatedParamsCounts)
local tabData = {}
local requiredParams = utilsTable.filter(params, "required")
local requiredRepeatedParams = utilsTable.filter(repeatedGroup, "required")
local allRequired = #params == #requiredParams and #repeatedGroup == #requiredRepeatedParams
local indent = templateSpec.indent
local options = templateSpec.boilerplate or {}
if options.tabs then
for i, tab in ipairs(options.tabs) do
local tabParams = utilsTable.filter(params, function(param)
return utilsTable.invert(tab.params)[param.param]
end)
local tabDescription = mw.getCurrentFrame():preprocess(tab.desc or "")
table.insert(tabData, {
label = tab.label,
content = tabDescription..h.boilerplate(tabParams, {}, format, indent, {}, options),
})
end
-- For block templates, provide two boilerplates, one simple one with only required parameters, one advanced with all parameters
-- We don't do this for inline templates out of the unwritten(?) rule that inline templates should not omit optional parameters
-- unless they're rarely used, which we cover with the canOmit option
--
-- This feature can be turned on or off via boilerplate options - it is on by default
elseif format.type == "block" and options.separateRequiredParams ~= false and not allRequired then
tabData = {
{
label = "Required Parameters",
content = h.boilerplate(requiredParams, requiredRepeatedParams, format, indent, repeatedParamsCounts, options),
},
{
label = "All Parameters",
content = h.boilerplate(params, repeatedGroup, format, indent, repeatedParamsCounts, options)
}
}
else
tabData = {
{
label = "Parameters",
content = h.boilerplate(params, repeatedGroup, format, indent, repeatedParamsCounts, options)
}
}
end
return utilsLayout.tabs(tabData, {
tabOptions = {
collapse = true -- don't show tabs if template has only required parameters
}
})
end
function h.boilerplate(params, repeatedGroup, format, indent, repeatedParamsCounts, options)
if #repeatedGroup == 0 then
return h._boilerplate(params, repeatedGroup, format, indent, 0, options)
else
repeatedParamsCounts = repeatedParamsCounts or DEFAULT_REPEATED_PARAM_COUNTS
local tabData = utilsTable.map(repeatedParamsCounts, function(repeatCount)
return {
label = repeatCount,
content = h._boilerplate(params, repeatedGroup, format, indent, repeatCount, options)
}
end)
local tabOptions = { collapse = true }
return utilsLayout.tabs(tabData, tabOptions)
end
end
function h._boilerplate(params, repeatedGroup, format, indent, repeatedParamsCount, options)
local options = options or {}
local args = {}
local hiddenParams = utilsTable.invert(options.hiddenParams or {})
for k, param in pairs(params) do
-- No need to omit from boilerplate for block templates, which already have separate boilerplate for minimal vs. advanced usage
-- Optional positional parameters are omitted by default
local canOmit = format.canOmitParams and (param.canOmit or type(param.param) == "number" and not param.required)
if canOmit or hiddenParams[param.param] then
-- do nothing
elseif param.param == VARARG_KEY and not param.deprecated then
h.insertVarArgBoilerplate(args, k)
else
args[k] = {
param = param.param,
value = "",
inline = param.inline,
deprecated = param.deprecated,
}
end
end
-- there may be more args than this number if there are non-repeating params after the repeated goup - in this case there's a "hole" in the middle of the args table
-- We insert the repeated parameters in this gap
local i = #(utilsTable.takeWhile(args, function(arg) return arg ~= nil end))
local afterArg = {}
if #args > 0 and #repeatedGroup > 1 then
afterArg[i] = "\n"
end
local j = 1
for k = 1, repeatedParamsCount do
for _, param in ipairs(repeatedGroup) do
if param.canOmit and format.canOmitParams or hiddenParams[param.param] then
-- no-op
else
table.insert(args, i+j, {
param = param.param .. k,
value = "",
})
j = j + 1
end
end
if k ~= repeatedParamsCount and #repeatedGroup > 1 then
afterArg[i+j-1] = "\n"
end
end
args = utilsTable.compact(args)
if #args > (i+j-1) then
afterArg[i+j-1] = "\n" -- add newline if there are params after the repeated groups
end
local result = h.printTemplateInstance(args, format, {
indent = indent,
afterArg = afterArg,
hideDeprecated = true,
})
if options.list then
local listArgs = {
{
param = 1,
value = result,
},
{
param = 2,
value = result,
},
{
param = 3,
value = result,
},
}
result = h.printTemplateInstance(listArgs, FORMATS.block, {
indent = 1,
name = "List",
})
end
if options.before then
result = options.before .. result
end
if options.after then
result = result .. options.after
end
return utilsMarkup.pre(result)
end
function h.insertVarArgBoilerplate(args, i)
for j = 1, 3 do
table.insert(args, i+j-1, {
param = j,
value = ""
})
end
end
function h.examplesFromFrame(examples)
local showNumbers = examples.showNumbers and utilsString.trim(examples.showNumbers)
local result = {
caption = examples.caption,
vertical = examples.vertical,
wrapLines = examples.wrapLines,
showNumbers = showNumbers ~= "false",
}
local desc
for i, example in ipairs(examples) do
if utilsString.startsWith(example, "-") then
desc = string.sub(example, 2)
else
local input = utilsString.trim(mw.text.unstripNoWiki(example))
local output = mw.getCurrentFrame():preprocess(mw.text.decode(input))
table.insert(result, {
input = input,
output = output,
desc = desc,
})
desc = nil
end
end
return result
end
function h.examplesFromSpec(templateSpec)
local paramOrder = templateSpec.paramOrder
local repeatedParams = templateSpec.repeatedGroup and templateSpec.repeatedGroup.params or {}
local repeatedParamsMap = utilsTable.invert(repeatedParams)
local isRepeated = h.isRepeated(repeatedParamsMap)
local examples = templateSpec.examples
local result = {
vertical = examples.vertical,
wrapLines = examples.wrapLines,
}
for i, example in ipairs(examples) do
result[i] = {}
if type(example) == "string" then
result[i].input = example
elseif type(example) == "table" then
local args = {}
local unsortedArgs = {}
local repeatedGroupArgs = {}
for k, v in pairs(example.args or example) do
local arg = {
param = k,
value = v,
inline = templateSpec.params and templateSpec.params[k] and templateSpec.params[k].inline
}
local isRepeated, index, param = isRepeated(k)
if isRepeated then
repeatedGroupArgs[index] = repeatedGroupArgs[index] or {}
utilsTable.merge(repeatedGroupArgs[index], {
[param] = arg
})
else
local idx = paramOrder and utilsTable.keyOf(paramOrder, k)
if idx then
args[idx] = arg
else
table.insert(unsortedArgs, arg)
end
end
end
args = utilsTable.compact(args) -- needed when template has a mix of named parameters and optional anonymous parameters (e.g. Template:Cite)
args = utilsTable.concat(args, unsortedArgs)
-- repeated groups go last
local afterArg = {}
if #args > 0 and #repeatedParams > 0 then
afterArg[#args] = "\n"
end
for i, group in ipairs(repeatedGroupArgs) do
for _, param in ipairs(repeatedParams) do
table.insert(args, group[param])
end
if i ~= #repeatedGroupArgs then
afterArg[#args] = "\n"
end
end
result[i].input = h.printTemplateInstance(args, FORMATS[templateSpec.format or DEFAULT_FORMAT], {
indent = templateSpec.indent,
afterArg = afterArg,
})
result[i].desc = example.desc
end
if not templateSpec.storesData then
-- It's important not to use frame:expandTemplate here.
-- We've had a bug in the past that we didn't catch because expandTemplate can accept args with "=" in them, while actual template calls cannot.
result[i].output = mw.getCurrentFrame():preprocess(result[i].input)
end
end
return result
end
function h.isRepeated(repeatedParamsMap)
-- @param param a template parameter e.g. "tab1"
-- @return boolean indicating whether parameter is part of a repeated group
-- @return number index of the repition, e.g. 1 for "tab1", 2 for "tab2", etc.
-- @return name of the parameter without the index, e.g. "tab" for "tab1", "tab2", etc.
return function(param)
if type(param) == "number" then
return false
end
local name = utilsString.trim(param, "0-9")
local index = tonumber(string.sub(param, #name + 1))
if not repeatedParamsMap[name] or not index then
return false
end
return true, index, name
end
end
function h.printTemplateInstance(args, format, options)
local name = options.name or p.getTemplateName()
local options = options or {}
local afterArg = options.afterArg or {}
local indent = string.rep(" ", options.indent or 0)
local result = "{{" .. name
for i, arg in ipairs(args) do
if not options.hideDeprecated or not arg.deprecated then
local printedArg
if type(arg.param) == "number" then
printedArg = arg.value
else
printedArg = string.format("%s= %s", arg.param, arg.value)
end
if arg.inline then
result = result .. FORMATS.inline.argSeparator .. printedArg
else
result = result .. string.format(format.argSeparator, indent) .. printedArg
end
if afterArg[i] then
result = result .. afterArg[i]
end
end
end
result = result .. format.afterLastArg .. "}}"
return result
end
function p.params(params, positionalParamCount)
local rows = {}
for i, param in ipairs(params) do
rows[i] = h.paramRow(param, positionalParamCount)
end
if #rows == 0 then
return ""
else
return utilsLayout.table({
class = "wikitable template-parameters",
hideEmptyColumns = true,
headers = {s("header.parameter"), s("header.status"), s("header.description"), s("header.enum"), s("header.default")},
rows = rows,
})
end
end
function h.paramRow(param, i)
if param.isRepeated then
param.param = param.param.."N"
end
if param.param == VARARG_KEY then
local varArgs = utilsTable.map({tostring(i+1), ".", ".", "N"}, utilsMarkup.code)
varArgs = utilsMarkup.list(varArgs)
paramCell = {{varArgs, utilsMarkup.code(param.name)}}
elseif param.name and param.name ~= param.param then
paramCell = {{utilsMarkup.code(param.param), utilsMarkup.code(param.name)}}
else
paramCell = utilsMarkup.code(param.param)
end
local statusCell = "optional"
if param.required then
statusCell = "<b>required</b>"
elseif param.suggested then
statusCell = "suggested"
end
local descriptionCell = param.desc
if param.deprecated then
local deprecatedMarker = utilsMarkup.bold("Deprecated")
if param.desc then
descriptionCell = deprecatedMarker.." – "..param.desc
else
descriptionCell = deprecatedMarker
end
end
local enumCell = param.enum and h.printEnum(param) or ""
local defaultValue = param.default
if defaultValue and not string.find(defaultValue, "[<%[]") then -- check for markup
defaultValue = utilsMarkup.code(defaultValue)
end
return {paramCell, statusCell, descriptionCell, enumCell, defaultValue or ""}
end
function h.printEnum(param)
local enum = param.enum
local dependsOn = param.enumDependsOn
local enumReference = type(enum) == "table" and enum.reference or nil
if dependsOn then
return s("enum.depends", { arg = utilsMarkup.code(dependsOn) })
elseif enumReference then
return s("enum.ref", { dataSource = enumReference })
else
local values = utilsTable.map(enum, utilsMarkup.code)
return utilsMarkup.bulletList(values)
end
end
function h.templateData(templateSpec)
-- Something's wrong with the TemplateData for Template:Term/Store that's preventing even null edits to that template.
-- This mitigation is in place until we get to the root cause.
local title = mw.title.getCurrentTitle()
if title.prefixedText == "Template:Term/Store" or title.prefixedText == "Template:Term/Store/Documentation" then
return ""
end
local params = templateSpec.params or {}
if utilsTable.size(params) == 0 then
return ""
end
local hasParamOrder = templateSpec.paramOrder
local data = {
description = templateSpec.description or templateSpec.purpose,
params = {},
paramOrder = {},
}
local repeatedGroup = templateSpec.repeatedGroup
local repeatedGroupParams = repeatedGroup and repeatedGroup.params or {}
local repeatedParamsCount = repeatedGroup and repeatedGroup.counts and repeatedGroup.counts[#repeatedGroup.counts] or 5
for k, v in pairs(params) do
local repeatedParam = utilsTable.includes(repeatedGroupParams, k)
if repeatedParam and not repeatedGroup.allowSingle then
-- do nothing, repeated params proccessed separately
else
data.params[k] = {
label = v.name or k,
required = v.required,
suggested = v.suggested,
description = v.desc,
aliases = v.aliases,
type = v.type,
}
if not hasParamOrder then
table.insert(data.paramOrder, k)
end
end
end
if repeatedGroup and not repeatedGroup.allowSingle and templateSpec.paramOrder then
data.paramOrder = utilsTable.difference(templateSpec.paramOrder, repeatedGroupParams) -- start by removing repeated params. Those will be added later.
elseif templateSpec.paramOrder then
data.paramOrder = utilsTable.clone(templateSpec.paramOrder)
end
for i = 1, repeatedParamsCount do
for _, paramName in ipairs(repeatedGroupParams) do
local param = templateSpec.params[paramName]
local paramKey = paramName .. i
data.params[paramKey] = {
label = l,
required = param.required,
description = param.desc,
type = param.type
}
table.insert(data.paramOrder, paramKey)
end
end
for _, param in pairs(data.params) do
if type(param.required) == "string" then
param.required = true
end
end
-- The following workaround is ugly, but necessary to ensure that data.params is encoded as a JSON object rather than an array.
if utilsTable.isArray(data.params) then
data.params["_"] = {
label = "_",
description = "Dummy parameter. Do not use.",
}
table.insert(data.paramOrder, "_")
end
local templateData = mw.getCurrentFrame():extensionTag("templatedata", mw.text.jsonEncode(data))
local html = mw.html.create("div")
:addClass("mw-collapsible mw-collapsed template-data")
:attr("data-expandtext", s("templateData.show"))
:attr("data-collapsetext", s("templateData.hide"))
:wikitext(templateData)
return tostring(html)
end
function h.categories(templateSpec)
local categories = {s("categories.all")}
if templateSpec.usesData then
table.insert(categories, s("categories.usesData"))
end
if templateSpec.storesData then
table.insert(categories, s("categories.storesData"))
end
if templateSpec.categories then
categories = utilsTable.concat(categories, templateSpec.categories)
end
if mw.title.getCurrentTitle().subpageText ~= "Documentation" then
return utilsMarkup.categories(categories)
else
return ""
end
end
i18n.loadStrings({
en = {
categories = {
all = "Category:Templates",
usesData = "Category:Cargo query templates",
storesData = "Category:Cargo storage templates",
},
usesData = "This template relies on the centralized data stored at ${dataSource}.",
usesModuleData = "This template relies on configuration data stored at ${dataSource}.",
storesData = "This template is used to store centralized data at ${dataSource}.",
enum = {
ref = "See ${dataSource}",
depends = "Depends on ${arg}",
},
header = {
description = "Description",
parameter = "Parameter",
status = "Status",
enum = "Accepted values",
default = "Default value",
categoriesAdded = "Categories added",
input = "Input",
output = "Output",
},
heading = {
examples = "Examples",
purpose = "Purpose",
usage = "Usage",
},
tabs = {
boilerplate = "Boilerplate",
syntax = "Syntax",
},
templateData = {
show = "show TemplateData ▼",
hide = "hide TemplateData ▲",
},
}
})
function p.Schemas()
return {
Templates = {
required = true,
type = "map",
keyPlaceholder = "template name",
keys = { type = "string" },
desc = "A map of template names to template specs.",
values = {
type = "record",
properties = {
{
name = "wip",
type = "boolean",
desc = "Flag to indicate the template is a work in progress.",
},
{
name = "description",
type = "string",
desc = "<p>Corresponds to the <code>description</code> property of {{MediaWiki|Extension:TemplateData|TemplateData}}. This description is displayed in the popup that appears when editing a template in {{MediaWiki|VisualEditor}}.</p><p>Must be in plain text, without any wikitext markup.</p>"
},
{
name = "purpose",
type = "string",
desc = "Purpose of the template. Displayed in the Purpose section of the template documentation page.",
},
{
name = "categories",
desc = "Categories to add to the template page.",
type = "array",
items = { type = "string" },
},
{
name = "usage",
type = "string",
desc = "General usage information, such as instructions on where to put this template on articles. Placed at the top of the Usage section of the template documentation page.",
},
{
name = "storesData",
oneOf = {
{
type = "string"
},
{
type = "boolean",
},
},
desc = "For <code>/Store</code> templates, use this field to indicate which Data page this template is used on, or simply set to <code>true</code> if the storage is not centralized. If present, the template is added to [[:Category:Cargo storage templates]].",
},
{
name = "usesData",
oneOf = {
{
type = "boolean",
},
{
type = "string"
},
{
type = "array",
items = { type = "string" }
},
},
desc = "For templates that use Cargo data, use this field to indicate which page(s) the data is stored on, or simply set to <code>true</code> if the storage is not centralized. If present, the template is added to [[:Category:Cargo query templates]].",
},
{
name = "usesModuleData",
type = "boolean",
desc = "Set to true for templates that rely on [[:Category:Module data|/Data]] pages in the Module namespace.",
},
{
name = "format",
type = "string",
enum = {"inline", "block"},
desc = "Indicates how the template should be laid out in source. <code>inline</code> for single-line templates, <code>block</code> for multiline templates. Defaults to <code>inline</code> unless <code>tableParams</code> is present.",
},
{
name = "indent",
type = "number",
default = 0,
desc = "Number of spaces to indent block-format parameters.",
},
{
name = "boilerplate",
type = "record",
desc = "Directives for how generate boilerplate for the template.",
properties = {
{
name = "before",
type = "string",
desc = "Any wikitext entered here will be prepended to the boilerplate",
},
{
name = "after",
type = "string",
desc = "Any wikitext entered here will be appended to the boilerplate",
},
{
name = "list",
type = "boolean",
desc = "If true, boilerplate includes [[Template:List]]. See [[Template:Game Rating]] for example.",
},
{
name = "separateRequiredParams",
type = "boolean",
desc = "If true, two boilerplate tabs will be generated: one with only default parameters, and one with all parameters. Default value is <code>true</code>. The option exists and is true by default because it's often useful (e.g. [[Template:Tabs]]) but not always (e.g. [[Template:Armor/Store]]). Works only for block templates (<code>format = \"block\"</code>). Inline templates have the <code>canOmit</code> option instead."
},
{
name = "hiddenParams",
type = "array",
items = { type = "string" },
desc = "Parameters to exclude from boilerplate. See [[Module:Navbox]] for example usage.",
},
{
name = "disable",
type = "boolean",
desc = "If true, boilerplate generation is disabled completely. See [[Template:Cite Book/Documentation]] for usage.",
},
{
name = "tabs",
desc = "To generate multiple tabs of boilerplate, each with a different set of parameters.",
type = "array",
items = {
type = "record",
properties = {
{
name = "label",
required = true,
desc = "Tab name",
type = "string",
},
{
name = "params",
required = true,
desc = "Parameters to include in the boilerplate.",
type = "array",
items = { type = "string" },
},
{
name = "desc",
desc = "Description of the use case for this set of parameters.",
type = "string",
},
},
}
},
},
},
{
name = "repeatedGroup",
type = "record",
desc = 'This is used to make templates that accept "arrays of objects" or "rows" of input. For an example, see [[Template:Tabs]].',
properties = {
{
name = "name",
required = true,
type = "string",
desc = "The key that the resulting array will be assigned to in the returned argument table.",
},
{
name = "params",
required = true,
type = "array",
items = { type = "string" },
desc = "An array of parameter keys (similar to <code>paramOrder</code>), indicating which parameters are repeated."
},
{
name = "counts",
type = "array",
items = {
type = "number"
},
desc = string.format("Determines how many times the parameters are repeated in the boilerplate. Each number causes a new tab of boilerplate to be created with that many repetitions. The default value for this field is <code>%s</code>.", utilsTable.print(DEFAULT_REPEATED_PARAM_COUNTS)),
},
{
name = "allowSingle",
type = "boolean",
desc = "Allows a repeated parameter to also be specified as a regular parameter. See [[Module:Wares]] for example usage.",
},
}
},
{
name = "paramOrder",
type = "array",
items = {
oneOf = {
{ type = "number" },
{ type = "string" },
},
},
desc = "Array of parameter keys indicating what order they should be presented in.",
},
{
name = "params",
type = "map",
desc = "<p>Map of the template parameters. Numerical keys indicate positional parameters. String keys indicate named parameters. A special key named <code>" .. VARARG_KEY .. "</code> indicates a variadic parameter (i.e. a template with a variable number of trailing arguments, such as [[Template:List]]).</p><p>This data can be used by [[Module:UtilsArg]] to parse template arguments.</p>",
keys = {
oneOf = {
{ type = "string" },
{ type = "number" },
},
},
values = {
type = "record",
properties = {
{
name = "name",
type = "string",
desc = "Use this to assign names to positional parameters.",
},
{
name = "aliases",
type = "array",
items = { type = "string" },
desc = "Alternative names for the parameter. Used when renaming a parameter in a high-usage template.",
},
{
name = "desc",
type = "string",
desc = "Wikitext description of the parameter.",
},
{
name = "placeholder",
type = "string",
desc = "Placeholder to use for argument value when demonstrating template usage. Defaults to <code><name></code> for positional parameters; empty string for named parameters.",
},
{
name = "type",
type = "string",
enum = {
"unknown",
"number",
"string",
"line",
"boolean",
"date",
"url",
"wiki-page-name",
"wiki-file-name",
"wiki-template-name",
"wiki-user-name",
"content",
"unbalanced-wikitext",
reference = "{{Mediawiki|Extension:TemplateData}}",
},
desc = "One of the {{Mediawiki|Extension:TemplateData}} types."
},
{
name = "required",
oneOf = {
{ type = "boolean" },
{ type = "string" },
},
desc = "Indicates a required parameter.",
},
{
name = "suggested",
type = "boolean",
desc = "Indicates a suggested parameter."
},
{
name = "deprecated",
type = "boolean",
desc = "Indicates a parameter that should no longer be used.",
},
{
name = "enum",
type = "any",
desc = "An array of allowed values. Optionally, a <code>reference</code> key can be added to the Lua table which links to a page listing all the allowed values (see [[Module:Franchise#enum]] for example).",
},
{
name = "enumDependsOn",
type = "string",
desc = "If the allowed values for this parameter depends on the value of another parameter, specify that parameter name here. Then, instead of <code>enum</code> being an array, it can be a function that returns an array. The value of the dependency argument is passed to the function. See [[Module:Scale]] for an example.",
},
{
name = "default",
oneOf = {
{ type = "string" },
{ type = "number" },
},
desc = "Default value for the parameter.",
},
{
name = "canOmit",
type = "boolean",
desc = "<p>Omit parameter from generated boilerplate. Use for parameters that cover edge cases and should thefore not be used in normal circumstances. See [[Template:Game Rating]] for example.</p><p>Works only for inline templates (<code>format = \"inline\"</code>), as block templates by default have two separate boilerplates for minimal required parameters vs. the full set of parameters. See the <code>separateRequiredParams</code> option.</p>",
},
{
name = "inline",
type = "boolean",
desc = 'If true, then the parameter will be printed on the same line as the previous parameter, even if <code>format</code> is set to <code>"block"</code>. See [[Template:Sequence/Store]] for example.',
},
{
name = "trim",
type = "boolean",
desc = "Indicator to [[Module:UtilsArg#parse|utilsArg.parse]] that this template argument should be trimmed using [[Module:UtilsString#trim|utilsString.trim]].",
},
{
name = "nilIfEmpty",
type = "boolean",
desc = "Indicator to [[Module:UtilsArg#parse|utilsArg.parse]] that this template argument should be made nil if it is an empty string, using [[Module:UtilsString#nilIfEmpty|utilsString.nilIfEmpty]].",
},
{
name = "split",
oneOf = {
{ type = "string" },
{ type = "boolean" },
},
desc = "Indicator to [[Module:UtilsArg#parse|utilsArg.parse]]. If set to <code>true</code>, the template argument is treated as a list of commma-separated values, to be turned into an array using [[Module:UtilsString#split|utilsString.split]]. If set to a string, that string will be used as the splitting pattern.",
},
{
name = "sortAndRemoveDuplicates",
type = "boolean",
desc = "Indicator to [[Module:UtilsArg#parse|utilsArg.parse]] that can be used together with <code>split</code> and <code>enum</code>. If true, then the split array will be sorted according to the order of items in <code>enum</code>. Duplicates and invalid values are be removed.",
},
}
},
},
{
name = "examples",
desc = "<p>Array of argument tables representing different invocations of the template + an optional <code>vertical</code> key. It is possible to add descriptions to specific examples as well. See [[Template:List]] for examples.</p><p>It is also possible to write examples as wikitext strings. This is useful when the example requires some context outside the template itself. (At the time of writing there are no longer uses for this – please add an example here if that changes.)",
allOf = {
{
type = "record",
properties = {
{
name = "vertical",
type = "boolean",
default = "false",
desc = "If false, examples are laid out in a single table (e.g. [[Template:List]]). If true, examples are listed one after the other (e.g. [[Template:Data Table]])."
},
{
name = "wrapLines",
type = "boolean",
default = "false",
desc = "If true, line wrapping is enabled for all template input shown in <nowiki><pre></nowiki> tags. Otherwise, line wrapping is enabled only if the input contains newlines or <code>vertical</code> is enabled.",
},
},
},
{
type = "array",
items = {
oneOf = {
{
type = "string",
},
{
type = "map",
keys = {
oneOf = {
{ type = "number" },
{ type = "string" },
},
},
values = { type = "any" },
}
}
},
},
},
},
},
},
},
}
end
return p