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
mNo edit summary
mNo edit summary
Line 226: Line 226:
 
local result = ""
 
local result = ""
 
if functionDoc.desc then
 
if functionDoc.desc then
result = "\n" .. mw.getCurrentFrame():preprocess(functionDoc.desc) .. "\n\n"
+
result = "\n" .. mw.getCurrentFrame():preprocess(functionDoc.desc) .. "\n"
 
end
 
end
 
return result
 
return result

Revision as of 19:53, 19 April 2020


local p = {}
local h = {}

local i18n = require("Module:I18n")
local s = i18n.getString
local utilsLayout = require("Module:UtilsLayout")
local utilsMarkup = require("Module:UtilsMarkup")
local utilsSchema = require("Module:UtilsSchema")
local utilsString = require("Module:UtilsString")
local utilsTable = require("Module:UtilsTable")

local MAX_ARGS_LENGTH = 50

function getModule(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
	local module = require(frame.args.module or modulePage.fullText)
	return module, subpageText
end

function p.Schema(frame)
	local module, subpageName = getModule(frame)
	local schemaName = frame.args[1] or subpageName
	local schemaDoc = p.schema(module.Schemas[schemaName], schemaName)
	return schemaDoc
end

function p.Module(frame)
	local module, subpageName = getModule(frame)
	local categories = utilsMarkup.categories(h.getCategories(frame.args.type))
	if subpageName == "Data" and type(module.Data) == "function" then
		return module.Data(frame) .. categories
	end
	if module.Documentation then
		return p.moduleDoc(module, module.Documentation, module.Schemas, 2) .. categories
	end
	return categories
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
				tabData = utilsTable.flatten(tabData)
				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(module, doc, schemas, headerLevel)
	utilsSchema.validate(p.Schemas.Documentation, "Documentation", doc, "p.Documentation")
	local output = ""
	if headerLevel == 2 then
		output = "__TOC__\n"
	end
	for _, functionDoc in ipairs(doc) do
		output = output .. utilsMarkup.heading(headerLevel)(functionDoc.name) .. "\n"
		if functionDoc.wip then
			output = output .. frame:expandTemplate({title = "UC"}) .. "\n"
		end
		local paramSchemas = schemas and schemas[functionDoc.name]
		functionDoc = h.resolveFunctionDoc(functionDoc, paramSchemas)
		if functionDoc.fp then
			output = output .. utilsLayout.tabs({
				{
					label = functionDoc.name,
					content = h.printFunctionDoc(functionDoc, module[functionDoc.name])
				},
				{
					label = functionDoc.fp.name,
					content = h.printFunctionDoc(functionDoc.fp, module[functionDoc.fp.name])
				}
			})
		else
			output = output .. h.printFunctionDoc(functionDoc, module[functionDoc.name])
		end
	end
	if doc.sections then
		for _, section in ipairs(doc.sections) do
			output = output .. ("==%s=="):format(section.heading) .. "\n"
			output = output .. p.moduleDoc(module, section.doc, schemas, headerLevel + 1) .. "\n"
		end
	end
	return output
end

function h.resolveFunctionDoc(functionDoc, paramSchemas)
	paramSchemas = paramSchemas or {}
	functionDoc.cases = functionDoc.cases or {}
	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 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
		for _, case in ipairs(functionDoc.fp.cases) do
			case.args = 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)
	end
	functionDoc.params = {resolvedParams}
	for _, case in ipairs(functionDoc.cases) do
		case.args = {case.args}
	end
	return functionDoc
end

function h.printFunctionDoc(functionDoc, fn)
	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, fn)
	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, fn)
	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.compactNils({
		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(fn, doc, case, doc.cases)
		tableData.rows = utilsTable.concat(tableData.rows, caseRows)
	end
	result = result .. utilsLayout.table(tableData) .. "\n"
	return result
end

function h.case(fn, doc, case, options)
	local rows = {}
	
	local input = h.printInput(doc, case.args)
	local outputs = h.evaluateFunction(fn, case.args)
	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.compactNils({
			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] and utilsTable.print(args[i]) or "nil"
		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(function(argText, i)
		return i > 1 and argText == "nil" and params[i] and params[i].schema and not params[i].schema.required
	end)(argsText)
	
	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, categories = utilsMarkup.stripCategories(output)
		if (#categories > 0) then
			local categoryList = utilsMarkup.bold(s('headers.categoriesAdded')) .. utilsMarkup.bulletList(categories) 
			resultData = {
				{resultData},
				{categoryList},
			}
		end
	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, "&#", "&#") -- 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("Utils", moduleTitle.text)
	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 = {
		desc = "Either an array of function docs, or an array of sections of function docs.",
		oneOf = {
			{ _ref = "#/definitions/functions" },
			{ _ref = "#/definitions/sections" },
		},
		definitions = {
			functions = {
				type = "array",
				items = {
					type = "record",
					properties = {
						{ 
							name = "name",
							type = "string", 
							required = true, 
							desc = "The name of a function in the module.",
						},
						{
							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 = {
										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:UC]]."
						}
					},
				},
			},
			sections = {
				type = "record",
				additionalProperties = true,
				properties = {
					{
						name = "sections",
						required = true,
						type = "array",
						items = {
							type = "record",
							properties = {
								{
									name = "heading",
									required = true,
									type = "string",
									desc = "Heading for the documentation section.",
								},
								{
									name = "doc",
									_ref = "#/definitions/functions",
									required = true,
									_hideSubkeys = true,
									desc = "An array of tables describing functions (see above).",
								},
							},
						},
					},
				},
			},
		}
	},
}

return p