Module:Documentation/Template

local p = {} local h = {}

local ListPages = require("Module:List Pages") local i18n = require("Module:I18n") local s = i18n.getString local utilsArg = require("Module:UtilsArg") local utilsLayout = require("Module:UtilsLayout") local utilsMarkup = require("Module:UtilsMarkup") local utilsString = require("Module:UtilsString") local utilsTable = require("Module:UtilsTable")

local FORMATS = { inline = { templateData = "", argSeparator = "|", afterLastArg = "", },	block = { templateData = "", argSeparator = "\n%s|", afterLastArg = "\n", } }

local VARARG_KEY = "..."

function p.Template(frame) local title = mw.title.getCurrentTitle local templateName = frame.args.template or title.text if title.subpageText == "Documentation" then templateName = title.baseText end local moduleName = frame.args.module or title.baseText return p.printTemplateDoc(templateName, moduleName) end

function p.Examples(frame) local args = frame:getParent.args local examples = h.examplesFromFrame(args) return p.printExamples(examples) end

function p.printTemplateDoc(templateName, moduleName) moduleName = moduleName or templateName local templateSpec = require("Module:" .. moduleName).Templates[templateName] utilsArg.schemaValidate(p.Schemas.TemplateSpec, "TemplateSpec", templateSpec, templateName) local result = "" if templateSpec.wip then result = result .. mw.getCurrentFrame:expandTemplate({title = "WIP"}) end local result = result .. "\n" if templateSpec.purpose then result = result .. utilsMarkup.heading(2, s("heading.purpose")) result = result .. mw.getCurrentFrame:preprocess(templateSpec.purpose) .. "\n" end result = result .. utilsMarkup.heading(2, s("heading.usage")) if templateSpec.usesData then local dataSource if type(templateSpec.usesData) == "table" then dataSource = ListPages.main(templateSpec.usesData) else dataSource = utilsMarkup.link(templateSpec.usesData) end result = result .. utilsMarkup.italic(s("usesData", {dataSource = dataSource})) .. "\n" end if templateSpec.usesModuleData then local dataSource = utilsMarkup.link("Module:" .. moduleName .. "/Data") result = result .. utilsMarkup.italic(s("usesModuleData", {dataSource = dataSource})) .. "\n" end if templateSpec.storesData then local dataSource = utilsMarkup.link(templateSpec.storesData) result = result .. utilsMarkup.italic(s("storesData", {dataSource = dataSource})) .. "\n" end result = result .. p.printUsage(templateName, templateSpec) if templateSpec.examples then local examples = h.examplesFromSpec(templateName, templateSpec) result = result .. utilsMarkup.heading(3, s("heading.examples")) result = result .. p.printExamples(examples) end result = result .. h.templateData(templateSpec) result = result .. h.categories(templateSpec) return result end

function p.printUsage(templateName, templateSpec) local params = {} local paramOrder = templateSpec.paramOrder local format = templateSpec.format local positionalParamCount = 0 local hasVarArg = false local repeatedParams = templateSpec.repeatedGroup and templateSpec.repeatedGroup.params or {} for k, v in pairs(templateSpec.params) 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		}) end local format = FORMATS[templateSpec.format] local result = "" local boilerplateOptions = utilsTable.merge({}, templateSpec.boilerplate or {}, {		repeatedGroupCount = templateSpec.repeatedGroup and templateSpec.repeatedGroup.count	}) if positionalParamCount > 0 or hasVarArg then result = result .. utilsLayout.tabs({			{				label = s("tabs.syntax"),				content = h.syntax(templateName, params, repeatedGroup, format, templateSpec.indent)			},			{				label = s("tabs.boilerplate"),				content = h.boilerplate(templateName, params, repeatedGroup, format, templateSpec.indent, boilerplateOptions)			},		}) else result = h.boilerplate(templateName, params, repeatedGroup, format, templateSpec.indent, boilerplateOptions) end params = utilsTable.concat(params, repeatedGroup) result = result .. h.params(params, positionalParamCount) return result end

function p.printExamples(examples) for i, example in ipairs(examples) do		local input = mw.text.trim(example.input) if string.find(input, "\n") or examples.vertical then input = utilsMarkup.pre(input, {				wrapLines = false			}) else input = utilsMarkup.code(mw.text.nowiki(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 }		else examples[i] = { input = input, desc = example.desc }		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, 						styles = {							["text-align"] = "left"						},						content = example.desc,					}				}) end table.insert(rows, {example.input, example.output, example.categoryList}) end return utilsLayout.table({			hideEmptyColumns = true,			headers = { s("header.input"), s("header.output"), s("header.categoriesAdded"), header = true },			rows = rows		}) end local result = "" for _, example in ipairs(examples) do		local headerStyles = { ["width"] = "5rem" -- for alignment. See Template:Letter/Documentation for an example of why this is needed }		result = result .. utilsLayout.table({			hideEmptyRows = true,			rows = {				{					{ header = true, content= s("header.input"), styles = headerStyles}, 					example.input,				},				{					{ header = true, content = s("header.output"), styles = headerStyles}, 					example.output				},				{					{ header = true, content = s("header.categoriesAdded"), styles = headerStyles },					example.categoryList				},			}		}) .. "\n" end return result end

function h.syntax(templateName, params, repeatedGroup, format, indent) local args = {} for i, param in ipairs(params) do		if param.param == VARARG_KEY then h.insertVarArgsSyntax(args, i, param.placeholder) 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 > 0 then for i, n in ipairs({"1", "2", "N"}) do			for _, param in ipairs(repeatedGroup) do				table.insert(args, {					param = param.param .. n,					value = ""				}) end if i ~= 3 then afterArg[#args] = "\n" end end end local result = h.printTemplateInstance(templateName, 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 = placeholder .. j		}) end table.insert(args, i+3, {		param = i+3,		value = "..."	}) table.insert(args, i+4, {		param = i+4,		value = placeholder .. "N"	}) end

function h.boilerplate(templateName, params, repeatedGroup, format, indent, options) local options = options or {} local args = {} local i = 1 for _, param in ipairs(params) do		if param.canOmit then -- do nothing elseif param.param == VARARG_KEY then h.insertVarArgBoilerplate(args, i)		else i = i + 1 table.insert(args, {				param = param.param,				value = "",				inline = param.inline,			}) end end local afterArg = {} if #args > 0 and #repeatedGroup > 0 then afterArg[#args] = "\n" end for i = 1, options.repeatedGroupCount or 0 do		for _, param in ipairs(repeatedGroup) do			if param.canOmit then -- no-op else table.insert(args, {					param = param.param .. i,					value = "",				}) end end if i ~= options.repeatedGroupCount then afterArg[#args] = "\n" end end local result = h.printTemplateInstance(templateName, args, format, {		indent = indent,		afterArg = afterArg	}) if options.list then local listArgs = { {				param = 1, value = result, },			{				param = 2, value = result, },			{				param = 3, value = result, },		}		result = h.printTemplateInstance("List", listArgs, FORMATS.block, {			indent = 1		}) 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 result = { vertical = examples.vertical }	for i, example in ipairs(examples) do		local input = mw.text.unstripNoWiki(example) local output = mw.getCurrentFrame:preprocess(mw.text.decode(input)) result[i] = { input = input, output = output }	end return result end

function h.examplesFromSpec(templateName, 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 }	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[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(templateName, args, FORMATS[templateSpec.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(name, args, format, options) local options = options or {} local afterArg = options.afterArg or {} local indent = string.rep(" ", options.indent or 0) local result = "" return result end

function h.params(params, positionalParamCount) local rows = {} for i, param in ipairs(params) do		rows[i] = h.paramRow(param, positionalParamCount) end return utilsLayout.table({		hideEmptyColumns = true,		headers = {s("header.parameter"), s("header.status"), s("header.description"), s("header.enum"), s("header.default")},		rows = rows,	}) end function h.paramRow(param, i)	if param.param == VARARG_KEY then local varArgs = utilsTable.map({"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 = param.required and utilsMarkup.bold(s("status.required")) or s("status.optional") local descriptionCell = param.desc 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 hasParamOrder = templateSpec.paramOrder local data = { description = templateSpec.purpose, params = {}, paramOrder = {}, }	local repeatedGroupParams = templateSpec.repeatedGroup and templateSpec.repeatedGroup.params or {} local repeatedGroupCount = templateSpec.repeatedGroup and templateSpec.repeatedGroup.count or 0 for k, v in pairs(templateSpec.params) do		if not utilsTable.keyOf(repeatedGroupParams, k) then data.params[k] = { label = v.name or k,				required = v.required, description = v.desc, aliases = type(k) == "number" and {v.name} or nil, 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, repeatedGroupCount 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") :attr("data-expandtext", s("templateData.show")) :attr("data-collapsetext", s("templateData.hide")) :css("float", "left") :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 return utilsMarkup.categories(categories) 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",		},		status = {			optional = "optional",			required = "required",		},		tabs = {			boilerplate = "Boilerplate", syntax = "Syntax", },		templateData = { show = "show TemplateData ▼", hide = "hide TemplateData ▲", },	} })

p.Schemas = { TemplateSpec = { type = "record", properties = { {				name = "purpose", type = "string", required = true, desc = "Purpose of the template.", },			{				name = "wip", type = "boolean", desc = "Flag to indicate the template is a work in progress.", },			{				name = "storesData", type = "string", desc = "For  templates, use this field to indicate which Data page this template is used on. If present, the template is added to Category:Cargo Storage Templates.", },			{				name = "usesData", oneOf = { { 						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. 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", required = true, enum = {"inline", "block"}, desc = "Indicates how the template should be laid out in source.  for single-line templates,   for multiline templates." },			{				name = "params", type = "map", required = true, 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 = { _ref = "#/definitions/param" }, },			{				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. (Ideally, Template:Tab2 could be using this as well.)', 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 = "count", required = true, type = "number", desc = " Roughly the maximum amount of times one can expect the params to be repeated. This is merely to decide how many repetitions to include in the boilerplate. It is always possible to repeat more times than this number. For example, Template:Trading Quest is set to 20. The longest trading quest in the series is 14 items, so 20 is likely to cover any future trading quests." }				}			},			{				name = "paramOrder", type = "array", items = { oneOf = { { type = "number" }, { type = "string" }, },				},				desc = "Array of parameter keys indicating what order they should be presented in.", },			{				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 = "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)." },						},					},					{						type = "array", items = { oneOf = { {									type = "string", },								{									type = "map", keys = { oneOf = { { type = "number" }, { type = "string" }, },									},									values = { type = "any" }, }							}						},					},				},			},		},		definitions = { param = { type = "record", properties = { {						name = "name", type = "string", desc = "Use this to assign names to positional parameters.", },					{						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 = "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 = "desc", type = "string", required = true, desc = "Wikitext description of 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.", },					{						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.", },				},			},		},	}, }

return p