Module:Documentation/Template

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 = "", 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 .. "\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.keyOf(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 local boilerplateOptions = templateSpec.boilerplate or {}

if boilerplateOptions.disable then -- do nothing elseif positionalParamCount > 0 or hasVarArg then result = result .. utilsLayout.tabs({			{				label = s("tabs.syntax"),				content = h.syntax(params, repeatedGroup, format, templateSpec.indent, boilerplateOptions)			},			{				label = s("tabs.boilerplate"),				content = h.boilerplates(params, repeatedGroup, format, templateSpec.indent, repeatedParamsCounts, boilerplateOptions)			},		}) else result = h.boilerplates(params, repeatedGroup, format, templateSpec.indent, repeatedParamsCounts, boilerplateOptions) 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, " ", "&lt;nowiki&gt;") if (string.find(input, "\n") or string.find(input, " ") 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, "/", "/ ") -- 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(params, repeatedGroup, format, indent, boilerplateOptions) boilerplateOptions = boilerplateOptions or {} 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(params, repeatedGroup, format, indent, repeatedParamsCounts, options) local tabData = {} local requiredParams = utilsTable.filter(params, "required") local requiredRepeatedParams = utilsTable.filter(repeatedGroup, "required") local allRequired = #params == #requiredParams and #repeatedGroup == #requiredRepeatedParams

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 = 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 = "" 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 = elseif param.name and param.name ~= param.param then paramCell = else paramCell = utilsMarkup.code(param.param) end local statusCell = "optional" if param.required then statusCell = "required" 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 param.type == "number" then defaultValue = tonumber(param.default) end local defaultCell = defaultValue and utilsMarkup.code(defaultValue) or "" return {paramCell, statusCell, descriptionCell, enumCell, defaultCell} 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) 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		if not utilsTable.keyOf(repeatedGroupParams, k) then 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 repeatedGroupParams and templateSpec.paramOrder then data.paramOrder = utilsTable.difference(templateSpec.paramOrder, repeatedGroupParams) -- start by removing repeated params. Those will be added later. 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(templateSpec.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 = " Corresponds to the  property of . This description is displayed in the popup that appears when editing a template in .  Must be in plain text, without any wikitext markup. " },					{						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  templates, use this field to indicate which Data page this template is used on, or simply set to   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  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 /Data pages in the Module namespace.", },					{						name = "format", type = "string", enum = {"inline", "block"}, desc = "Indicates how the template should be laid out in source.  for single-line templates,   for multiline templates. Defaults to  .", },					{						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 . The option exists and is true by default because it's often useful (e.g. Template:Tabs) but not always (e.g. Template:Trading Quest). Works only for block templates . Inline templates have the   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:Trading Quest.', 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 ), 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 .", utilsTable.print(DEFAULT_REPEATED_PARAM_COUNTS)), }						}					},					{						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 = " Map of the template parameters. Numerical keys indicate positional parameters. String keys indicate named parameters. A special key named  indicates a variadic parameter (i.e. a template with a variable number of trailing arguments, such as Template:List).  This data can be used by Module:UtilsArg to parse template arguments. ", 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", required = true, desc = "Wikitext description of the parameter.", },								{									name = "placeholder", type = "string", desc = "Placeholder to use for argument value when demonstrating template usage. Defaults to  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 = "", },									desc = "One of the 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  key can be added to the Lua table which links to a page listing all the allowed values (see Module:Franchise 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  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:Letter for an example.", },								{									name = "default", oneOf = { { type = "string" }, { type = "number" }, },									desc = "Default value for the parameter.", },								{									name = "canOmit", type = "boolean", desc = " 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. Works only for inline templates, as block templates by default have two separate boilerplates for minimal required parameters vs. the full set of parameters. See the   option. ", },								{									name = "inline", type = "boolean", desc = 'If true, then the parameter will be printed on the same line as the previous parameter, even if  is set to. See Template:Sequence/Store for example.', },								{									name = "trim", type = "boolean", desc = "Indicator to utilsArg.parse that this template argument should be trimmed using utilsString.trim.", },								{									name = "nilIfEmpty", type = "boolean", desc = "Indicator to utilsArg.parse that this template argument should be made nil if it is an empty string, using utilsString.nilIfEmpty.", },								{									name = "split", oneOf = { { type = "string" }, { type = "boolean" }, },									desc = "Indicator to utilsArg.parse. If set to, the template argument is treated as a list of commma-separated values, to be turned into an array using utilsString.split. If set to a string, that string will be used as the splitting pattern.", },								{									name = "sortAndRemoveDuplicates", type = "boolean", desc = "Indicator to utilsArg.parse that can be used together with  and  . If true, then the split array will be sorted according to the order of items in  . Duplicates and invalid values are be removed.", },							}						},					},					{						name = "examples", desc = " Array of argument tables representing different invocations of the template + an optional  key. It is possible to add descriptions to specific examples as well. See Template:List for examples.  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:Letter)." },									{										name = "wrapLines", type = "boolean", default = "false", desc = "If true, line wrapping is enabled for all template input shown in  tags. Otherwise, line wrapping is enabled only if the input contains newlines or   is enabled.", },								},							},							{								type = "array", items = { oneOf = { {											type = "string", },										{											type = "map", keys = { oneOf = { { type = "number" }, { type = "string" }, },											},											values = { type = "any" }, }									}								},							},						},					},				},			},		},	} end

return p