Module:Documentation/Template

From Zelda Wiki, the Zelda encyclopedia
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>", "&lt;nowiki&gt;")
		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