Zelda Wiki

Want to contribute to this wiki?
Sign up for an account, and get started!

Come join the Zelda Wiki community Discord server!

READ MORE

Zelda Wiki
Register
mNo edit summary
No edit summary
(23 intermediate revisions by one other user not shown)
Line 26: Line 26:
   
 
function p.Schema(frame)
 
function p.Schema(frame)
local modulePage, subpage = getModulePage(frame)
+
local modulePage = frame.args.module or getModulePage(frame)
local module = require(frame.args.module or modulePage)
+
local module = require(modulePage)
local schemaName = frame.args[1] or subpage
+
local schemaName = frame.args[1]
local schemaDoc = p.schema(module.Schemas[schemaName], schemaName)
+
return p.schema(module.Schemas[schemaName], schemaName)
return schemaDoc
 
 
end
 
end
   
Line 43: Line 42:
   
 
function p.dataDoc(modulePage)
 
function p.dataDoc(modulePage)
  +
local result = "''For information on editing module data in general, see [[Guidelines:Modules/Data]].''\n"
 
local module = require(modulePage)
 
local module = require(modulePage)
 
local tabs = {}
 
local tabs = {}
Line 57: Line 57:
 
})
 
})
 
end
 
end
return utilsLayout.tabs(tabs, { collapse = true })
+
result = result .. utilsLayout.tabs(tabs, {
 
{
 
tabs = {
  +
collapse = true
  +
}
  +
}
  +
})
 
return result
 
end
 
end
   
Line 97: Line 104:
 
end
 
end
 
if subkeys and subkeys.oneOf and #subkeys.oneOf > 0 then
 
if subkeys and subkeys.oneOf and #subkeys.oneOf > 0 then
local tabData = utilsTable.uniqueBy("content")(utilsTable.flatten(subkeys.oneOf))
+
local tabData = utilsTable._uniqueBy("content")(utilsTable.flatten(subkeys.oneOf))
 
if #tabData == 1 then
 
if #tabData == 1 then
 
subkeys = utilsTable.concat(subkeys, tabData[1].content)
 
subkeys = utilsTable.concat(subkeys, tabData[1].content)
Line 142: Line 149:
 
local headingLevel = section and 3 or 2
 
local headingLevel = section and 3 or 2
 
local module, doc = h.resolveDoc(modulePage, section)
 
local module, doc = h.resolveDoc(modulePage, section)
if not doc then
 
return ""
 
end
 
 
local output = ""
 
local output = ""
 
if not section then
 
if not section then
  +
if type(module) == "table" and module.Templates then
output = "__TOC__\n"
 
  +
local templates = utilsTable.keys(module.Templates)
  +
table.sort(templates)
  +
for i in ipairs(templates) do
  +
templates[i] = utilsMarkup.link("Template:"..templates[i])
 
end
  +
local templateList = utilsMarkup.bulletList(templates)
  +
output = "This is the main module for the following templates:" .. templateList .. "\n"
  +
end
  +
if (#doc.functions > 0 or doc.sections) and type(module) == "table" and module.Templates then
  +
output = output .. "In addition, this module exports the following functions. __TOC__\n"
  +
elseif (#doc.functions > 0 or doc.sections) then
  +
output = "This module exports the following functions. __TOC__\n"
  +
end
 
end
 
end
for _, functionDoc in ipairs(doc.functions) do
+
for _, functionDoc in ipairs(doc.functions or {}) do
 
output = output .. utilsMarkup.heading(headingLevel, functionDoc.name) .. "\n"
 
output = output .. utilsMarkup.heading(headingLevel, functionDoc.name) .. "\n"
 
if functionDoc.wip then
 
if functionDoc.wip then
output = output .. frame:expandTemplate({title = "UC"}) .. "\n"
+
output = output .. mw.getCurrentFrame():expandTemplate({
  +
title = "WIP",
 
args = {
  +
align = "left",
  +
}
  +
}) .. '<div style="clear:left"/>'
 
end
 
end
 
 
Line 186: Line 208:
 
function h.resolveDoc(modulePage, section)
 
function h.resolveDoc(modulePage, section)
 
local module = require(modulePage)
 
local module = require(modulePage)
local doc
+
local doc = {}
 
if type(section) == "table" then
 
if type(section) == "table" then
 
doc = section
 
doc = section
 
elseif type(module) == "table" then
 
elseif type(module) == "table" then
doc = module.Documentation
+
doc = module.Documentation or {}
end
 
if not doc then
 
return nil
 
 
end
 
end
 
local err = utilsSchema.validate(p.Schemas.Documentation, "Documentation", doc, "p.Documentation")
 
local err = utilsSchema.validate(p.Schemas.Documentation, "Documentation", doc, "p.Documentation")
Line 201: Line 220:
 
if doc.sections then
 
if doc.sections then
 
doc.functions = {}
 
doc.functions = {}
  +
doc.snippets = h.snippets(modulePage)
 
return module, doc
 
return module, doc
 
end
 
end
Line 218: Line 238:
 
end
 
end
 
local functions = {}
 
local functions = {}
  +
doc.snippets = h.snippets(modulePage)
 
for _, functionName in ipairs(functionNames) do
 
for _, functionName in ipairs(functionNames) do
table.insert(functions, h.resolveFunctionDoc(module, functionName, doc[functionName]))
+
table.insert(functions, h.resolveFunctionDoc(module, doc, functionName))
 
doc[functionName] = nil
 
doc[functionName] = nil
 
end
 
end
Line 244: Line 265:
 
end
 
end
   
function h.resolveFunctionDoc(module, functionName, functionDoc)
+
function h.resolveFunctionDoc(module, moduleDoc, functionName)
  +
local functionDoc = moduleDoc[functionName]
 
functionDoc.name = functionName
 
functionDoc.name = functionName
 
functionDoc.fn = module[functionDoc.name]
 
functionDoc.fn = module[functionDoc.name]
 
functionDoc.cases = functionDoc.cases or {}
 
functionDoc.cases = functionDoc.cases or {}
  +
functionDoc.snippets = h.getFunctionSnippets(moduleDoc.snippets, functionDoc.name)
 
if type(functionDoc.returns) ~= "table" then
 
if type(functionDoc.returns) ~= "table" then
 
functionDoc.returns = {functionDoc.returns}
 
functionDoc.returns = {functionDoc.returns}
Line 274: Line 297:
 
end)
 
end)
 
end)
 
end)
  +
functionDoc.fp.snippets = h.getFunctionSnippets(moduleDoc.snippets, functionDoc.fp.name)
 
end
 
end
 
functionDoc.params = {resolvedParams}
 
functionDoc.params = {resolvedParams}
Line 280: Line 304:
 
end
 
end
 
return functionDoc
 
return functionDoc
 
end
  +
function h.getFunctionSnippets(moduleSnippets, functionName)
  +
local functionSnippets = {}
  +
for k, v in pairs(moduleSnippets or {}) do
  +
local s, e = k:find(functionName)
 
if e then
  +
local snippetKey = string.sub(k, e + 1)
  +
functionSnippets[snippetKey] = v
  +
end
 
end
  +
return functionSnippets
 
end
 
end
   
Line 359: Line 394:
 
local statusColumn = utilsMarkup.tooltip(s("headers.status"), s("explainStatusColumn"))
 
local statusColumn = utilsMarkup.tooltip(s("headers.status"), s("explainStatusColumn"))
 
 
local headerCells = utilsTable.compactNils({
+
local headerCells = utilsTable.compact({
 
inputColumn,
 
inputColumn,
 
not doc.cases.resultOnly and outputColumn or nil,
 
not doc.cases.resultOnly and outputColumn or nil,
Line 382: Line 417:
 
end
 
end
   
  +
h.snippets = utilsFunction.memoize(function(modulePage)
local snippets = {}
 
 
local snippetPagename = modulePage .. "/Documentation/Snippets"
(function()
 
local snippetPagename = mw.getCurrentFrame():getParent():getTitle() .. "/Snippets"
 
 
if not utilsPage.exists(snippetPagename) then
 
if not utilsPage.exists(snippetPagename) then
 
return nil
 
return nil
 
end
 
end
 
local snippets = {}
 
local snippetPage = mw.title.new(snippetPagename)
 
local snippetPage = mw.title.new(snippetPagename)
 
local module = require(snippetPagename)
 
local module = require(snippetPagename)
Line 398: Line 433:
 
if line[1] and line[1].type == "keyword" and line[1].data == "function" then
 
if line[1] and line[1].type == "keyword" and line[1].data == "function" then
 
local isOpenParens = function(token)
 
local isOpenParens = function(token)
return utilsString.startsWith("(", token.data)
+
return utilsString.startsWith(token.data, "(")
 
end
 
end
local fnName = line[utilsTable.find(line, isOpenParens) - 1].data
+
local fnName = line[utilsTable.findIndex(line, isOpenParens) - 1].data
 
table.insert(starts, i + 1)
 
table.insert(starts, i + 1)
 
table.insert(names, fnName)
 
table.insert(names, fnName)
Line 422: Line 457:
 
}
 
}
 
end
 
end
 
return snippets
end)()
+
end)
   
 
function h.case(doc, case, options)
 
function h.case(doc, case, options)
 
local rows = {}
 
local rows = {}
local input, outputs
+
local input, outputs
local snippet = case.snippet and snippets[doc.name .. case.snippet]
+
local snippet = case.snippet and doc.snippets[tostring(case.snippet)]
 
if snippet then
 
if snippet then
input = utilsMarkup.lua(snippet.code)
+
input = utilsMarkup.lua(snippet.code, { wrapLines = false })
 
outputs = {snippet.fn()}
 
outputs = {snippet.fn()}
 
elseif case.args then
 
elseif case.args then
Line 441: Line 477:
 
for i = 1, #doc.returns do
 
for i = 1, #doc.returns do
 
local outputData, resultData, statusData = h.evaluateOutput(outputs[i], expected[i])
 
local outputData, resultData, statusData = h.evaluateOutput(outputs[i], expected[i])
table.insert(rows, utilsTable.compactNils({
+
table.insert(rows, utilsTable.compact({
 
not options.resultOnly and outputData or nil,
 
not options.resultOnly and outputData or nil,
 
not options.outputOnly and resultData or nil,
 
not options.outputOnly and resultData or nil,
Line 482: Line 518:
 
local argsText = {}
 
local argsText = {}
 
for i = 1, math.max(#params, #args) do
 
for i = 1, math.max(#params, #args) do
local argText = args[i] and utilsTable.print(args[i]) or "nil"
+
local argText = args[i] == nil and "nil" or utilsTable.print(args[i])
 
if not (#args == 1 and type(args[i]) == "table") then
 
if not (#args == 1 and type(args[i]) == "table") then
 
argText = string.gsub(argText, "\n", "\n ") --ensures proper indentation of multiline table args
 
argText = string.gsub(argText, "\n", "\n ") --ensures proper indentation of multiline table args
Line 490: Line 526:
 
 
 
-- Trim nil arguments off the end so long as they're optional (but keep the first one).
 
-- Trim nil arguments off the end so long as they're optional (but keep the first one).
local argsText = utilsTable.dropRightWhile(function(argText, i)
+
local argsText = utilsTable.dropRightWhile(argsText, function(argText, i)
 
return i > 1 and argText == "nil" and params[i] and params[i].schema and not params[i].schema.required
 
return i > 1 and argText == "nil" and params[i] and params[i].schema and not params[i].schema.required
end)(argsText)
+
end)
 
 
 
local result = table.concat(argsText, ", ")
 
local result = table.concat(argsText, ", ")
Line 517: Line 553:
 
if type(output) == "string" then
 
if type(output) == "string" then
 
resultData = utilsMarkup.killBacklinks(output)
 
resultData = utilsMarkup.killBacklinks(output)
resultData, categories = utilsMarkup.stripCategories(output)
+
resultData = utilsMarkup.stripCategories(output)
if (#categories > 0) then
 
local categoryList = utilsMarkup.bold(s('headers.categoriesAdded')) .. utilsMarkup.bulletList(categories)
 
resultData = {
 
{resultData},
 
{categoryList},
 
}
 
end
 
 
end
 
end
 
if type(expected) == "string" then
 
if type(expected) == "string" then
Line 581: Line 610:
 
local moduleTitle = (isDoc or isData) and mw.title.new(title.baseText) or title
 
local moduleTitle = (isDoc or isData) and mw.title.new(title.baseText) or title
 
local isData = moduleTitle.subpageText == "Data"
 
local isData = moduleTitle.subpageText == "Data"
local isUtil = utilsString.startsWith("Utils", moduleTitle.text)
+
local isUtil = utilsString.startsWith(moduleTitle.text, "Utils")
 
local isSubmodule = moduleTitle.subpageText ~= moduleTitle.text
 
local isSubmodule = moduleTitle.subpageText ~= moduleTitle.text
 
if type == "submodule" then
 
if type == "submodule" then
Line 705: Line 734:
 
items = {
 
items = {
 
oneOf = {
 
oneOf = {
--["snippet"] = {
+
["snippet"] = {
{
 
 
type = "record",
 
type = "record",
 
properties = {
 
properties = {
Line 730: Line 758:
 
}
 
}
 
},
 
},
--["args"] = {
+
["args"] = {
{
 
 
type = "record",
 
type = "record",
 
properties = {
 
properties = {
Line 760: Line 787:
 
name = "wip",
 
name = "wip",
 
type = "boolean",
 
type = "boolean",
desc = "Tags the function doc with [[Template:UC]]."
+
desc = "Tags the function doc with [[Template:WIP]]."
 
}
 
}
 
},
 
},

Revision as of 08:24, 14 July 2021


local p = {}
local h = {}

local i18n = require("Module:I18n")
local s = i18n.getString
local lex = require("Module:Documentation/Lexer")
local utilsFunction = require("Module:UtilsFunction")
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 MAX_ARGS_LENGTH = 50

function getModulePage(frame)
	local docPage = mw.title.new(frame:getParent():getTitle())
	local modulePage = docPage.basePageTitle
	local subpageText = modulePage.subpageText
	if subpageText == "Data" then
		modulePage = modulePage.basePageTitle
	end
	return modulePage.fullText, subpageText
end

function p.Schema(frame)
	local modulePage = frame.args.module or getModulePage(frame)
	local module = require(modulePage)
	local schemaName = frame.args[1]
	return p.schema(module.Schemas[schemaName], schemaName)
end

function p.Module(frame)
	local modulePage, subpage = getModulePage(frame)
	local categories = utilsMarkup.categories(h.getCategories(frame.args.type))
	if subpage == "Data" then
		return p.dataDoc(modulePage) .. categories
	end
	return p.moduleDoc(modulePage) .. categories
end

function p.dataDoc(modulePage)
	local result = "''For information on editing module data in general, see [[Guidelines:Modules/Data]].''\n"
	local module = require(modulePage)
	local tabs = {}
	if type(module.Data) == "function" then
		table.insert(tabs, {
			label = "Data",
			content = module.Data(mw.getCurrentFrame())
		})
	end
	if module.Schemas and module.Schemas.Data then
		table.insert(tabs, {
			label = "Schema",
			content = p.schema(module.Schemas.Data, "Data")
		})
	end
	result = result .. utilsLayout.tabs(tabs, {
		{
			tabs = {
				collapse = true
			}
		}
	})
	return result
end

function p.schema(schema, schemaName)
	local definitions = utilsSchema.getTypeDefinitions(schema, schemaName or "", function(keyDef)
		local key = keyDef.key
		local symbolicType = keyDef.symbolicType
		local rawType = keyDef.rawType
		local typeLabel = keyDef.typeLabel
		local desc = keyDef.desc
		local subkeys = keyDef.subkeys
		local parentType = keyDef.parentType
		local isSubschema = keyDef.isSubschema
		
		if schemaName == "..." and not parentType then
			symbolicType = "vararg" .. symbolicType
		end
		
		symbolicType = mw.text.nowiki(symbolicType)
		
		if subkeys and subkeys.allOf then
			subkeys = utilsTable.concat(subkeys, utilsTable.flatten(subkeys.allOf))
			subkeys.allOf = nil
		end
		
		if isSubschema then
			if not desc and not subkeys then
				return nil
			end
			local definition = utilsTable.flatten(subkeys or {})
			if desc then
				table.insert(definition, 1, {nil, desc})
			end
			return {{
				label = typeLabel or symbolicType,
				tooltip = utilsMarkup.code(symbolicType),
				content = utilsMarkup.definitionList(definition)
			}}
		end
		if subkeys and subkeys.oneOf and #subkeys.oneOf > 0 then
			local tabData = utilsTable._uniqueBy("content")(utilsTable.flatten(subkeys.oneOf))
			if #tabData == 1 then
				subkeys = utilsTable.concat(subkeys, tabData[1].content)
			else
				subkeys = utilsTable.concat({utilsLayout.tabs(tabData)}, subkeys)
			end
			subkeys.oneOf = nil
		end

		if key and key ~= "" then
			key = tostring(mw.html.create("span")
				:css("color", "#f8f8f2")
				:wikitext(utilsMarkup.code(key))
			)
			key = utilsMarkup.link("Module:Documentation/Documentation#Schemas", key)
			local type = typeLabel or symbolicType
			key = utilsMarkup.tooltip(key, utilsMarkup.code(type))
		end
		
		definition = {key}
		if desc then
			table.insert(definition, desc)
		end
		if subkeys then
			definition = utilsTable.concat(definition, subkeys)
		end
		if parentType == utilsSchema.TYPES.record then
			definition = {definition}
		end
		
		return definition
	end)
	if not schemaName then
		definitions = utilsTable.flatten(utilsTable.tail(definitions))
		definitions = {{nil, definitions}}
	else
		definitions = {definitions}
	end
	local definitionsList = utilsMarkup.definitionList(definitions)
	return definitionsList
end

function p.moduleDoc(modulePage, section)
	local headingLevel = section and 3 or 2
	local module, doc = h.resolveDoc(modulePage, section)
	local output = ""
	if not section then
		if type(module) == "table" and module.Templates then
			local templates = utilsTable.keys(module.Templates)
			table.sort(templates)
			for i in ipairs(templates) do
				templates[i] = utilsMarkup.link("Template:"..templates[i])
			end
			local templateList = utilsMarkup.bulletList(templates)
			output = "This is the main module for the following templates:" .. templateList .. "\n"
		end
		if (#doc.functions > 0 or doc.sections) and type(module) == "table" and module.Templates then
			output = output .. "In addition, this module exports the following functions. __TOC__\n"
		elseif (#doc.functions > 0 or doc.sections) then
			output = "This module exports the following functions. __TOC__\n"
		end
	end
	for _, functionDoc in ipairs(doc.functions or {}) do
		output = output .. utilsMarkup.heading(headingLevel, functionDoc.name) .. "\n"
		if functionDoc.wip then
			output = output .. mw.getCurrentFrame():expandTemplate({
				title = "WIP",
				args = {
					align = "left",
				}	
			}) .. '<div style="clear:left"/>'
		end
		
		if functionDoc.fp then
			output = output .. utilsLayout.tabs({
				{
					label = functionDoc.name,
					content = h.printFunctionDoc(functionDoc)
				},
				{
					label = functionDoc.fp.name,
					content = h.printFunctionDoc(functionDoc.fp)
				}
			})
		else
			output = output .. h.printFunctionDoc(functionDoc)
		end
	end
	if doc.sections then
		for _, section in ipairs(doc.sections) do
			local sectionModule = type(section.section) == "string" and section.section or modulePage
			if section.heading then
				output = output .. utilsMarkup.heading(headingLevel, section.heading)
				output = output .. p.moduleDoc(sectionModule, section.section) .. "\n"
			else
				output = output .. p.moduleDoc(sectionModule, section.section)
			end
		end
	end
	return output
end

function h.resolveDoc(modulePage, section)
	local module = require(modulePage)
	local doc = {}
	if type(section) == "table" then
		doc = section
	elseif type(module) == "table" then
		doc = module.Documentation or {}
	end
	local err = utilsSchema.validate(p.Schemas.Documentation, "Documentation", doc, "p.Documentation")
	if err then
		mw.logObject(err)
	end
	if doc.sections then
		doc.functions = {}
		doc.snippets = h.snippets(modulePage)
		return module, doc
	end
	local functionNamesInSource = h.functionNamesInSource(modulePage)
	local functionNamesInDoc = {}
	for k, v in pairs(doc) do
		table.insert(functionNamesInDoc, k)
		if doc._params then
			table.insert(functionNamesInDoc, "_" .. k)
		end
	end
	local functionNames = utilsTable.intersection(functionNamesInSource, functionNamesInDoc)
	local undefinedFunctions = utilsTable.difference(functionNamesInDoc, functionNames)
	if #undefinedFunctions > 0 then
		local msg = string.format("Documentation references functions that do not exist: <code>%s</code>", utilsTable.print(undefinedFunctions, true))
		mw.addWarning(msg)
	end
	local functions = {}
	doc.snippets = h.snippets(modulePage)
	for _, functionName in ipairs(functionNames) do
		table.insert(functions, h.resolveFunctionDoc(module, doc, functionName))
		doc[functionName] = nil
	end
	doc.functions = functions
	return module, doc
end
function h.functionNamesInSource(modulePage)
	local source = mw.title.new(modulePage):getContent()
	local lexLines = lex(source)
	local functionNames = {}
	for _, tokens in ipairs(lexLines) do
		tokens = utilsTable.filter(tokens, function(token) 
			return token.type ~= "whitespace" 
		end)
		tokens = utilsTable.map(tokens, "data")
		if utilsTable.isEqual(
			utilsTable.slice(tokens, 1, 3),
			{"function", "p", "."}
		) then
			table.insert(functionNames, tokens[4])
		end
	end
	return functionNames
end

function h.resolveFunctionDoc(module, moduleDoc, functionName)
	local functionDoc = moduleDoc[functionName]
	functionDoc.name = functionName
	functionDoc.fn = module[functionDoc.name]
	functionDoc.cases = functionDoc.cases or {}
	functionDoc.snippets = h.getFunctionSnippets(moduleDoc.snippets, functionDoc.name)
	if type(functionDoc.returns) ~= "table" then
		functionDoc.returns = {functionDoc.returns}
		for i, case in ipairs(functionDoc.cases) do
			case.expect = {case.expect}
		end
	end
	local paramSchemas = module.Schemas and module.Schemas[functionDoc.name] or {}
	local resolvedParams = utilsTable.map(functionDoc.params, function(param)
		return { name = param, schema = paramSchemas[param] }
	end)
	if functionDoc._params then
		functionDoc.fp = mw.clone(functionDoc)
		functionDoc.fp.name = "_" .. functionDoc.name
		functionDoc.fp.fn = module[functionDoc.fp.name]
		for _, case in ipairs(functionDoc.fp.cases) do
			case.args = case.args and utilsTable.map(functionDoc._params, function(paramGroup)
				return utilsTable.map(paramGroup, function(param)
					return case.args[utilsTable.keyOf(functionDoc.params, param)]
				end)
			end)
		end
		functionDoc.fp.params = utilsTable.map(functionDoc._params, function(paramGroup)
			return utilsTable.map(paramGroup, function(param)
				return resolvedParams[utilsTable.keyOf(functionDoc.params, param)]
			end)
		end)
		functionDoc.fp.snippets = h.getFunctionSnippets(moduleDoc.snippets, functionDoc.fp.name)
	end
	functionDoc.params = {resolvedParams}
	for _, case in ipairs(functionDoc.cases) do
		case.args = {case.args}
	end
	return functionDoc
end
function h.getFunctionSnippets(moduleSnippets, functionName)
	local functionSnippets = {}
	for k, v in pairs(moduleSnippets or {}) do
		local s, e = k:find(functionName)
		if e then
			local snippetKey = string.sub(k, e + 1)
			functionSnippets[snippetKey] = v
		end
	end
	return functionSnippets
end

function h.printFunctionDoc(functionDoc)
	local result = ""
	result = result .. h.printFunctionSyntax(functionDoc)
	result = result .. h.printFunctionDescription(functionDoc)
	result = result .. h.printParamsDescription(functionDoc)
	result = result .. h.printReturnsDescription(functionDoc.returns)
	result = result .. h.printFunctionCases(functionDoc)
	return result
end

function h.printFunctionSyntax(functionDoc)
	local result = functionDoc.name
	for _, params in ipairs(functionDoc.params) do
		result = result .. "(" .. h.printParamsSyntax(params) .. ")"
	end
	return utilsMarkup.code(result) .. "\n"
end
function h.printParamsSyntax(params)
	local paramsSyntax = {}
	for _, param in ipairs(params or {}) do
		local paramSyntax = param.name
		if param.schema and not param.schema.required then
			paramSyntax = "[" .. paramSyntax .. "]"
		end
		table.insert(paramsSyntax, paramSyntax)
	end
	return table.concat(paramsSyntax, ", ") 
end

function h.printFunctionDescription(functionDoc)
	local result = ""
	if functionDoc.desc then
		result = "\n" .. mw.getCurrentFrame():preprocess(functionDoc.desc) .. "\n"
	end
	return result
end

function h.printParamsDescription(functionDoc)
	if not functionDoc.params or #functionDoc.params == 0 then
		return ""
	end
	local allParams = utilsTable.flatten(functionDoc.params)
	local paramDefinitions = {}
	for _, param in ipairs(allParams) do
		if param.schema then
			table.insert(paramDefinitions, p.schema(param.schema, param.name))
		end
	end
	if #paramDefinitions == 0 then
		return ""
	end
	local paramList = utilsMarkup.list(paramDefinitions)
	local heading = ";" .. s("headers.parameters") .. "\n"
	return heading .. paramList
end

function h.printReturnsDescription(returns)
	if not returns or #returns == 0 then
		return ""
	end
	local returnsList = utilsMarkup.bulletList(returns)
	local heading = "\n;" .. s("headers.returns") .. "\n"
	local result = heading .. mw.getCurrentFrame():preprocess(returnsList)
	return result
end

function h.printFunctionCases(doc)
	if not doc.cases or #doc.cases == 0 then
		return ""
	end
	local result = "\n;" .. s("headers.examples") .. "\n"
	
	local inputColumn = s("headers.input")
	local outputColumn = s("headers.output")
	local resultColumn = s("headers.result")
	local statusColumn = utilsMarkup.tooltip(s("headers.status"), s("explainStatusColumn"))
	
	local headerCells = utilsTable.compact({
		inputColumn, 
		not doc.cases.resultOnly and outputColumn or nil, 
		not doc.cases.outputOnly and resultColumn or nil, 
		statusColumn,
	})
	local tableData = {
		hideEmptyColumns = true,
		rows = {
			{
				header = true,
				cells = headerCells,
			}
		},
	}
	for _, case in ipairs(doc.cases) do
		local caseRows = h.case(doc, case, doc.cases)
		tableData.rows = utilsTable.concat(tableData.rows, caseRows)
	end
	result = result .. utilsLayout.table(tableData) .. "\n"
	return result
end

h.snippets = utilsFunction.memoize(function(modulePage)
	local snippetPagename = modulePage .. "/Documentation/Snippets"
	if not utilsPage.exists(snippetPagename) then
		return nil
	end
	local snippets = {}
	local snippetPage = mw.title.new(snippetPagename)
	local module = require(snippetPagename)
	local text = snippetPage:getContent()
	local lexLines = lex(text)
	local names = {}
	local starts = {}
	local ends = {}
	for i, line in ipairs(lexLines) do
		if line[1] and line[1].type == "keyword" and line[1].data == "function" then
			local isOpenParens = function(token)
				return utilsString.startsWith(token.data, "(")
			end
			local fnName = line[utilsTable.findIndex(line, isOpenParens) - 1].data
			table.insert(starts, i + 1)
			table.insert(names, fnName)
		end
		if #line == 1 and line[1].type == "keyword" and line[1].data == "end" then
			table.insert(ends, i - 1)
		end
	end
	local lines = utilsString.split(text, "\n")
	for i, fnName in ipairs(names) do
		local fnLines = utilsTable.slice(lines, starts[i], ends[i])
		fnLines = utilsTable.map(fnLines, function(line)
			line = string.gsub(line, "^\t", "")
			line = string.gsub(line, "\t", "  ")
			return line
		end)
		local fnCode = table.concat(fnLines, "\n")
		snippets[fnName] = {
			fn = module[fnName],
			code = fnCode,
		}
	end
	return snippets
end)

function h.case(doc, case, options)
	local rows = {}
	local input, outputs
	local snippet = case.snippet and doc.snippets[tostring(case.snippet)]
	if snippet then
		input = utilsMarkup.lua(snippet.code, { wrapLines = false })
		outputs = {snippet.fn()}
	elseif case.args then
		input = h.printInput(doc, case.args)
		outputs = h.evaluateFunction(doc.fn, case.args)
	else
		return {}
	end
	local expected = case.expect or {}
	
	for i = 1, #doc.returns do
		local outputData, resultData, statusData = h.evaluateOutput(outputs[i], expected[i])
		table.insert(rows, utilsTable.compact({
			not options.resultOnly and outputData or nil,
			not options.outputOnly and resultData or nil,
			statusData
		}))
	end
	
	table.insert(rows[1], 1, {
		content = input,
		rowspan = #doc.returns,
	})
	if case.desc then
		table.insert(rows, 1, {
			{
				header = true,
				colspan = -1,
				styles = {
					["text-align"] = "left"
				},
				content = case.desc,
			}
		})
	end
	return rows
end

function h.printInput(doc, argsList)
	local result = doc.name
	local allArgs = utilsTable.flatten(argsList)
	local lineWrap = #allArgs == 1 and type(allArgs[1]) == "string"
	for i, args in ipairs(argsList) do
		result = result .. "(" .. h.printInputArgs(args, doc.params[i], lineWrap) .. ")"
	end
	return utilsMarkup.lua(result, {
		wrapLines = lineWrap
	})
end
function h.printInputArgs(args, params)
	args = args or {}
	local argsText = {}
	for i = 1, math.max(#params, #args) do
		local argText = args[i] == nil and "nil" or utilsTable.print(args[i])
		if not (#args == 1 and type(args[i]) == "table") then
			argText = string.gsub(argText, "\n", "\n  ") --ensures proper indentation of multiline table args
		end
		table.insert(argsText, argText)
	end
	
	-- Trim nil arguments off the end so long as they're optional (but keep the first one).
	local argsText = utilsTable.dropRightWhile(argsText, function(argText, i)
		return i > 1 and argText == "nil" and params[i] and params[i].schema and not params[i].schema.required
	end)
	
	local result = table.concat(argsText, ", ")
	local lines = mw.text.split(result, "\n")
	-- print multiline if there's multiple args with at least one table or a line longer than the max length
	if #args > 1 and (#lines > 1 or #lines[1] > MAX_ARGS_LENGTH) then
		result = "\n  " .. table.concat(argsText, ",\n  ") .. "\n"
	end
	
	return result
end

function h.evaluateFunction(fn, args)
	for i = 1, #args - 1 do
		fn = fn(unpack(args[i]))
	end
	return {fn(unpack(args[#args]))}
end

function h.evaluateOutput(output, expected)
	local formattedOutput = h.formatValue(output)
	local outputData = formattedOutput
	local resultData = ""
	if type(output) == "string" then
		resultData = utilsMarkup.killBacklinks(output)
		resultData = utilsMarkup.stripCategories(output)
	end
	if type(expected) == "string" then
		expected = string.gsub(expected, "\t", "")
	end
	local passed = utilsTable.isEqual(expected, output)
	local statusData = (expected ~= nil or output == nil) and h.printStatus(passed) or nil
	if statusData and not passed then
		local expectedOutput = h.formatValue(expected)
		outputData = utilsLayout.table({
			hideEmptyColumns = true,
			styles = { width = "100%" },
			rows = {
				{ 
					{ 
						header = true, 
						content = "Expected", 
						styles = { width = "1em"}, -- "shrink-wraps" this column
					}, 
					{ content = expectedOutput },
				},
				{
					{ header = true, content = "Actual" }, 
					{ content = formattedOutput },
				},
			}
		})
	end
	return outputData, resultData, statusData
end

function h.formatValue(val)
	if type(val) == "string" then
		val = string.gsub(val, "&#", "&#38;#") -- show entity codes
		val = utilsTable.print(val)
		val = string.gsub(val, "\n", "\\n\n") -- Show newlines	
		return utilsMarkup.pre(val) 
	end
	return utilsMarkup.lua(val)
end

function h.printStatus(success)
	local img = success and "[[File:Green check.svg|16px|center|link=]]" or "[[File:TFH Red Link desperate.png|48px|center|link=]]"
	local msg = success and s("explainStatusGood") or s("explainStatusBad")
	img = utilsMarkup.tooltip(img, msg)
	local cat = ""
	if not success and mw.title.getCurrentTitle().subpageText ~= "Documentation" then
		cat = utilsMarkup.category(s("failingTestsCategory"))
	end
	return img .. cat
end

function h.getCategories(type)
	local title = mw.title.getCurrentTitle()
	local isDoc = title.subpageText == "Documentation"
	local moduleTitle = (isDoc or isData) and mw.title.new(title.baseText) or title
	local isData = moduleTitle.subpageText == "Data"
	local isUtil = utilsString.startsWith(moduleTitle.text, "Utils")
	local isSubmodule = moduleTitle.subpageText ~= moduleTitle.text
	if type == "submodule" then
		isSubmodule = true
	end

	if isDoc and isData then
		return {s("cat.dataDoc")}
	end
	if isDoc and isSubmodule then
		return {s("cat.submoduleDoc")}
	end
	if isDoc then
		return  {s("cat.moduleDoc")}
	end
	
	if isData then
		return {s("cat.data")}
	end
	if isSubmodule then
		return {s("cat.submodules")}
	end
	if isUtil then
		return {s("cat.modules"), s("cat.utilityModules")}
	end

	return {s("cat.modules")}
end

i18n.loadStrings({
	en = {
		failingTestsCategory = "Category:Modules with failing tests",
		explainStatusColumn = "Indicates whether a feature is working as expected",
		explainStatusGood = "This feature is working as expected",
		explainStatusBad = "This feature is not working as expected",
		headers = {
			parameters = "Parameters",
			returns = "Returns",
			examples = "Examples",
			input = "Input",
			output = "Output",
			result = "Result",
			categories = "Categories",
			categoriesAdded = "Categories added",
			status = "Status",
		},
		cat = {
			modules = "Category:Modules",
			submodules = "Category:Submodules",
			utilityModules = "Category:Utility Modules",
			moduleDoc = "Category:Module Documentation",
			submoduleDoc = "Category:Submodule Documentation",
			data = "Category:Module Data",
			dataDoc = "Category:Module Data Documentation",
		}
	}
})

p.Schemas = {
	Documentation = {
		oneOf = {
			{ _ref = "#/definitions/functions" },
			{ _ref = "#/definitions/sections" },
		},
		definitions = {
			functions = {
				desc = "Map of function names to function documentation. Functions are printed in the order in which they appear in the source code.",
				type = "map",
				keys = { type = "string" },
				values = {
					type = "record",
					properties = {
						{
							name = "desc",
							type = "string",
							desc = "Description of the function. Use only when clarification is needed—usually the param/returns/cases doc speaks for itself.",
						},
						{
							name = "params",
							required = true,
							type = "array",
							items = { type = "string" },
							desc = "An array of parameter names. Integrates with [[Module:Schema#Functions|Module:Schema]].",
						},
						{
							name = "_params",
							type = "array",
							items = {
								type = "array", 
								items = { type = "string" },
							},
							desc = "To be specified for functions with an alternative [[Guidelines:Modules#Higher Order Function|higher-order function]]."
						},
						{
							name = "returns",
							desc = "A string describing the return value of the function, or an array of such strings if the function returns multiple values",
							oneOf = { 
								{ type = "string" },
								{ type = "array", items = { type = "string" } },
							},
						},
						{
							name = "cases",
							desc = "A collection of use cases that double as test cases, plus a couple flags.",
							allOf = {
								{
									type = "record",
									properties = {
										{
											name = "resultOnly",
											type = "boolean",
											desc = "When <code>true</code>, displays only rendered wikitext as opposed to raw function output. Useful for functions returning strings of complex wikitext.",
										},
										{
											name = "outputOnly",
											type = "boolean",
											desc = "When <code>true</code>, displays only the raw output of the function (opposite of <code>resultOnly</code>). Enabled by default for functions returning data of type other than <code>string</code>."
										},
									},
								},
								{
									type = "array",
									items = {
										oneOf = {
											["snippet"] = {
												type = "record",
												properties = {
													{
														name = "desc",
														type = "string",
														desc = "A description of the use case.",
													},
													{
														name = "snippet",
														required = true,
														oneOf = {
															{ type = "number" },
															{ type = "string" }
														},
														desc = "See [[Module:UtilsTable]] for examples of usage.",
													},
													{
														name = "expect",
														type = "any",
														desc = "The expected return value, which is deep-compared against the actual value to determine pass/fail status. Or, an array of such items if there are multiple return values.",
													},
												}
											},
											["args"] = {
												type = "record",
												properties = {
													{
														name = "desc",
														type = "string",
														desc = "A description of the use case.",
													},
													{
														name = "args",
														type = "array",
														items = { type = "any" },
														desc = "An array of arguments to pass to the function.",
													},
													{
														name = "expect",
														type = "any",
														desc = "The expected return value, which is deep-compared against the actual value to determine pass/fail status. Or, an array of such items if there are multiple return values.",
													},
												},
											},
										}
									},
								}
							},
						},
						{
							name = "wip",
							type = "boolean",
							desc = "Tags the function doc with [[Template:WIP]]."
						}
					},
				},
			},
			sections = {
				type = "record",
				properties = {
					{
						name = "sections",
						type = "array",
						required = true,
						items = {
							type = "record",
							properties = {
								{
									name = "heading",
									type = "string",
								},
								{
									name = "section",
									required = true,
									oneOf = {
										{ type = "string" },
										{
											_ref = "#/definitions/functions",
											required = true,
											_hideSubkeys = true,
										},
									},
								}
							}
						},
					},
				},
			},
		}
	},
}

return p