Module:Documentation/Module

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 utilsVar = require("Module:UtilsVar")

local caseCounter = utilsVar.counter("testCases")

local DocumentationTemplate = require("Module:Documentation/Template")

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, docPage.fullText 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, docPage = getModulePage(frame) local categories = utilsMarkup.categories(h.getCategories(frame.args.type)) if utilsString.endsWith(docPage, "/Documentation/Data/Documentation") then -- documenting documentation data local category = mw.title.getCurrentTitle.subpageText == "Documentation" and "" or "" return string.format("This data is used to auto-generate documentation for Module:%s. %s", mw.title.getCurrentTitle.rootText, category) elseif utilsString.endsWith(docPage, "/Documentation/Snippets/Documentation") then local category = mw.title.getCurrentTitle.subpageText == "Documentation" and "" or "" return string.format("This module contains snippets used to generate documentation and unit tests for Module:%s. %s", mw.title.getCurrentTitle.rootText, category) elseif utilsString.endsWith(docPage, "/TemplateData/Documentation") then local category = mw.title.getCurrentTitle.subpageText == "Documentation" and "" or "" return string.format("This data is used to auto-generate documentation and validate input for templates backed by Module:%s. %s", mw.title.getCurrentTitle.rootText, category) elseif subpage == "Data" then -- documenting data return p.dataDoc(modulePage) .. categories else --documenting a module return p.moduleDoc(modulePage) .. categories end 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 local schemas = module.Schemas if type(schemas) == "function" then schemas = schemas end local dataSchema = schemas and schemas.Data if dataSchema then table.insert(tabs, {			label = "Schema",			content = p.schema(dataSchema, "Data")		}) local data = mw.loadData(modulePage.."/Data") local err = utilsSchema.validate(dataSchema, "Data", data, "data") if err then result = result.."" end end result = result .. utilsLayout.tabs(tabs, {		{			tabs = {				collapse = true			}		}	}) return result end

function p.schema(schema, schemaName) local referenceDefinitions = p.collectReferenceDefinitions(schema) local dt, dds = p.printSchemaNode(referenceDefinitions, schema, schemaName) local schemaDef = utilsTable.flatten({dt, dds}) schemaDef = utilsMarkup.definitionList({schemaDef}) return schemaDef end function p.collectReferenceDefinitions(schema) local defs = schema.definitions or {} for k, v in pairs(schema) do		if k == "_id" then defs[v] = schema elseif type(v) == "table" then defs = utilsTable.merge({}, defs, p.collectReferenceDefinitions(v)) end end return defs end

local seenRefs = {} function p.printSchemaNode(definitions, schemaNode, schemaName, isCollectionItem) local dds = {} local nodeType local refName = schemaNode._ref and string.gsub(schemaNode._ref, "#/definitions/", "") if schemaNode._hideSubkeys then nodeType = refName elseif refName then local ref = definitions[refName]

if not ref and not seenRefs[refName] then error(string.format("Definition '%s' not found", refName)) end if not ref and seenRefs[refName] then nodeType = refName else schemaNode = utilsTable.merge({}, schemaNode, ref) end -- prevents infinite recursion when definitions reference themselves seenRefs[refName] = true definitions = utilsTable.clone(definitions) definitions[refName] = nil end nodeType = nodeType or schemaNode.type

if schemaNode.desc then table.insert(dds, schemaNode.desc) end if nodeType == "record" then local dl = {} for i, property in ipairs(schemaNode.properties or {}) do			local subdt, subdds = p.printSchemaNode(definitions, property, property.name) table.insert(dl, utilsTable.flatten({subdt, subdds})) end dl = utilsMarkup.definitionList(dl) table.insert(dds, dl) end if nodeType == "map" then local dt, subdds, valueSubtype = p.printSchemaNode(definitions, schemaNode.values, nil, true) local _, keydds, keySubtype = p.printSchemaNode(definitions, schemaNode.keys, nil, true) local keyPlaceholder = string.format(" ", schemaNode.keyPlaceholder or keySubtype) if #subdds > 0 then local dl = utilsMarkup.definitionList({utilsTable.flatten({keyPlaceholder, keydds, subdds})}) table.insert(dds, dl) end nodeType = string.format("map<%s, %s>", keySubtype, valueSubtype) end if nodeType == "array" then local dt, subdds, subtype = p.printSchemaNode(definitions, schemaNode.items, nil, true) dds = utilsTable.concat(dds, subdds) nodeType = "{ "..(subtype).." }" end if schemaNode.allOf then local subtypes = {} for i, node in ipairs(schemaNode.allOf) do			local dt, subdds, subtype = p.printSchemaNode(definitions, node) dds = utilsTable.concat(dds, subdds) table.insert(subtypes, subtype) end nodeType = table.concat(subtypes, "&") end if schemaNode.oneOf then local tabs = {} local subtypes = {} for k, node in pairs(schemaNode.oneOf) do			local subdt, subdds, subtype = p.printSchemaNode(definitions, node, nil, true) if subtype == "record" and subtypes[#subtypes] == "record" then -- no-op: don't bother showing record|record else table.insert(subtypes, subtype) end

local tabname = tostring(k) if type(k) == "number" and node._tabName then tabname = node._tabName elseif type(k) == "number" and node._ref then tabname = string.gsub(node._ref, "#/definitions/", "") elseif type(k) == "number" then tabname = subtype end if #subdds > 0 then table.insert(subdds, 1, nil) table.insert(tabs, {					label = tabname,					content = utilsMarkup.definitionList({subdds})				}) end end if #tabs > 0 then tabs = utilsLayout.tabs(tabs, {				tabOptions = {					collapse = true,				},			}) table.insert(dds, tabs) end nodeType = table.concat(subtypes, "|") end if schemaNode.required and not isCollectionItem then nodeType = nodeType and nodeType.."!" elseif not isCollectionItem then nodeType = nodeType and "["..nodeType.."]" if schemaNode.default then schemaName = schemaName .."="..mw.dumpObject(schemaNode.default) end schemaName = schemaName and "["..schemaName.."]" end local dt = schemaName and utilsMarkup.inline(schemaName, {		code = true,		tooltip = nodeType,	}) dt = utilsMarkup.link("Module:Documentation#Schemas", dt)

return dt, dds, nodeType end

function p.moduleDoc(modulePage, section) local headingLevel = section and 3 or 2 local module, doc, templates = h.resolveDoc(modulePage, section) local output = "" if not section then if templates ~= nil then local templates = utilsTable.keys(templates) table.sort(templates) local templateLinks = {} for i, templateName in ipairs(templates) do				local templatePage = "Template:"..templateName if utilsPage.exists(templatePage) then local templateLink = utilsMarkup.link(templatePage) table.insert(templateLinks, templateLink) end end if #templateLinks > 0 then local templateList = utilsMarkup.bulletList(templateLinks) output = "This is the main module for the following templates:" .. templateList .. "\n" end if #templateLinks > 0 and #doc.functions > 0 or doc.sections then output = output .. "In addition, this module exports the following functions. \n" end elseif #doc.functions > 0 or doc.sections then output = "This module exports the following functions. \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",				}				}) .. ' '		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, modulePage) 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 local schemas local templates local templateDataPage = modulePage .. "/TemplateData" -- For performance reasons, template data may be placed on a separate page -- We try to load it from there first. If it doesn't exist, we load it directly from the module page itself if utilsPage.exists(templateDataPage) then local templateData = require(templateDataPage) if type(templateData) == "table" then templates = templateData end end if type(module) == "table" then doc = module.Documentation schemas = module.Schemas templates = templates or module.Templates end -- Documentation used to be exported as tables but should now be exported as functions returning tables -- So that the documentation object (which can be quite big) is initalized only on the documentation page that uses it, ot every page that uses the module. if type(doc) == "function" then doc = doc end if type(schemas) == "function" then schemas = schemas end if type(section) == "table" then doc = section end doc = doc or {} schemas = schemas or {} 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, templates 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: ", 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, schemas, functionName)) doc[functionName] = nil end doc.functions = functions return module, doc, templates 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, schemas, 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 = schemas[functionDoc.name] or {} local resolvedParams = utilsTable.map(functionDoc.params or {}, 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} if not functionDoc.frameParams then for _, case in ipairs(functionDoc.cases) do			case.args = {case.args} end 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, modulePage) local result = "" local moduleName = modulePage and string.gsub(modulePage, "Module:", "") result = result .. h.printFunctionSyntax(functionDoc, moduleName) result = result .. h.printFunctionDescription(functionDoc) result = result .. h.printParamsDescription(functionDoc) result = result .. h.printReturnsDescription(functionDoc.returns) result = result .. h.printFrameParams(functionDoc) result = result .. h.printFunctionCases(functionDoc, moduleName) return result end

function h.printFunctionSyntax(functionDoc, moduleName) local result = "" if functionDoc.frameParams then result = string.format("", moduleName, functionDoc.name, h.printFrameParamsSyntax(functionDoc), functionDoc.frameParamsFormat == "multiLine" and "\n" or "") else for _, params in ipairs(functionDoc.params) do result = functionDoc.name .. "(" .. h.printParamsSyntax(params) .. ")" end end if functionDoc.frameParams then return ''..result..' ' else return utilsMarkup.code(result) .. "\n" end 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.printFrameParamsSyntax(functionDoc) local paramsSyntax = "" local orderedParams = h.sortFrameParams(functionDoc, functionDoc.frameParams)

for i, param in ipairs(orderedParams) do		if functionDoc.frameParamsFormat == "multiLine" and not param.inline then paramsSyntax = paramsSyntax .. "\n" end if functionDoc.frameParamsFormat == "multiLine" and param.spaceBefore then paramsSyntax = paramsSyntax .. "\n" end paramsSyntax = paramsSyntax.."|" if type(param.param) == "string" then paramsSyntax = paramsSyntax .. param.param .. "="		else paramsSyntax = paramsSyntax.."<"..(param.name or "")..">" end end return paramsSyntax end function h.sortFrameParams(functionDoc, frameParams) local orderedParams = {} local seenParams = {} -- First the positional parameters for i, param in ipairs(frameParams) do		param = utilsTable.merge({}, param, {			param = i -- needs to have "param" key for Module:Documentation/Template to print the parameter table		}) table.insert(orderedParams, param) seenParams[i] = true end -- Then, according to frameParamsOrder for i, paramKey in ipairs(functionDoc.frameParamsOrder or {}) do		local param = frameParams[paramKey] if param then param = utilsTable.merge({}, param, {				param = paramKey			}) table.insert(orderedParams, param) end seenParams[paramKey] = true end -- Then, any remaining params local paramKeys = utilsTable.keys(frameParams or {}) local seenParamKeys = utilsTable.keys(seenParams) local remainingParamNames = utilsTable.difference(paramKeys, seenParamKeys) for i, paramKey in ipairs(remainingParamNames) do		local param = frameParams[paramKey] param = utilsTable.merge({}, param, {			param = paramKey		}) table.insert(orderedParams, param) end

return orderedParams 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.printFrameParams(doc) if not doc.frameParams then return "" end local params = h.sortFrameParams(doc, doc.frameParams)

local heading = '' .. s("headers.parameters") .. " \n" local paramTable = DocumentationTemplate.params(params) return heading..paramTable end

function h.printFunctionCases(doc, moduleName) 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,		doc.frameParams and "Categories added" 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, moduleName, 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, moduleName, options) local caseId = caseCounter.increment 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 doc.frameParams and case.input then input = utilsMarkup.pre(case.input) outputs = {mw.getCurrentFrame:preprocess(case.input)} elseif doc.frameParams and case.args then input, rawinput = h.printFrameInput(moduleName, doc.name, case.args) local output = mw.getCurrentFrame:preprocess(rawinput) if output == "" then output = " " -- forces output column to always show for #invoke examples end outputs = {output} 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 {} -- Raw output doesn't make as much sense for #invoke functions so we disable it by default if doc.frameParams and options.resultOnly == nil then options.resultOnly = true end local outputCount = doc.frameParams and 1 or #doc.returns for i = 1, outputCount do		local outputData, resultData, statusData, categories = h.evaluateOutput(outputs[i], expected[i]) local categoryList = categories and utilsMarkup.bulletList(categories) table.insert(rows, utilsTable.compact({ not options.resultOnly and outputData or nil, not options.outputOnly and resultData or nil, doc.frameParams and categoryList or nil, statusData, }))	end rows[1] = rows[1] or {} table.insert(rows[1], 1, {		content = input,		rowspan = outputCount,	}) table.insert(rows[1], 1, {		content = utilsMarkup.anchor("case-"..tostring(caseId), caseId),		rowspan = outputCount,	}) 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.printFrameInput(moduleName, functionName, args) local result = string.format(""	return utilsMarkup.pre(result), result 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 = nil local categories if type(output) == "string" then resultData = utilsMarkup.killBacklinks(output) resultData, categories = 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, categories 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 "" or "" 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 = { required = true, 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. A function's documentation object has the following properties, depending on whether it is a template function or a module function.", type = "map", keyPlaceholder = "function name", keys = { type = "string" }, values = { oneOf = { {							_tabName = "Module function", type = "record", desc = "A function which is to be invoked by other modules.", properties = { {									name = "wip", type = "boolean", desc = "Tags the function doc with Template:WIP." },								{									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", type = "array", items = { type = "string" }, desc = "An array of parameter names. Integrates with Schemas.", },								{									name = "_params", type = "array", items = { type = "array", items = { type = "string" }, },									desc = "To be specified for functions with an alternative 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 and documented examples.", allOf = { {											_ref = "#/definitions/casesOptions" },										{											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 and Module:UtilsTable/Documentation/Snippets 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", _ref = "#/definitions/args", },															{																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.", },														},													},												}											},										}								},							},							},						},						{							_tabName = "Template function", type = "record", desc = "A function which is to be invoked by templates using the #invoke parser function.", properties = { {									name = "wip", type = "boolean", desc = "Tags the function doc with Template:WIP." },								{									name = "desc", type = "string", desc = "Description of the function - which templates should use it and when.", },								{									name = "frameParamsFormat", type = "string", enum = {"singleLine", "multiLine"}, desc = "Indicates how the #invoke parameters should be laid out.", },								{									name = "frameParamsOrder", desc = "Determines the order that  should appear in.", type = "array", items = { type = "string", },								},								{									name = "frameParams", desc = "Use this instead of  when documenting a template-based function. See Module:Error for example.", type = "map", keys = { oneOf = { { type = "number" }, { type = "string" } },									},									values = { type = "record", properties = { {												name = "name", type = "string", desc = "Use this to assign names to positional parameters.", },											{												name = "required", type = "boolean", desc = "Indicates a required parameter.", },											{												name = "default", type = "string", desc = "Default value for the parameter." },											{												name = "desc", type = "string", desc = "Description of the parameter." },											{												name = "inline", type = "boolean", desc = 'If true, then the parameter will be printed on the same line as the previous parameter, even if  is set to. See Module:Comment for example.', },											{												name = "spaceBefore", type = "boolean", desc = "If true, adds an extra newline before printing the parameter. See Module:Comment for a usage example.", },										}									},								},								{									name = "cases", desc = "A collection of use cases that double as test cases and documented examples.", allOf = { {											_ref = "#/definitions/casesOptions" },										{											type = "array", items = { oneOf = { {														_tabName = "args", type = "record", properties = { {																name = "desc", type = "string", desc = "A description of the example/test case.", },															{																name = "args", _ref = "#/definitions/args", },														},													},													{														_tabName = "input", type = "record", properties = { {																name = "desc", type = "string", desc = "A description of the example/test case.", },															{																name = "input", type = "string", desc = "Raw input for the example. See Module:Franchise for usage.", },														},													},												},											},										},									}								},							},						},					},				},			},			sections = { type = "record", properties = { {						desc = "Divides the documentation into sections. See Module:UtilsTable for a usage example.", name = "sections", type = "array", required = true, items = { type = "record", properties = { {									name = "heading", type = "string", desc = "Section heading" },								{									name = "section", required = true, oneOf = { { type = "string" }, {											_ref = "#/definitions/functions", required = true, },									},								}							}						},					},				},			},			args = { name = "args", allOf = { {						type = "array", items = { type = "any" }, },					{						type = "map", keys = { oneOf = { {									type = "string" },								{									type = "number" },							},						},						values = { type = "string" },					},				},				desc = "An array of arguments to pass to the function.", },			casesOptions = { type = "record", properties = { {						name = "resultOnly", type = "boolean", desc = "When, displays only rendered wikitext as opposed to raw function output. Useful for functions returning strings of complex wikitext.", },					{						name = "outputOnly", type = "boolean", desc = "When, displays only the raw output of the function (opposite of  ). Enabled by default for functions returning data of type other than  ." },				},			},		}	}, }

return p