Module:Documentation/Module

local p = {} local h = {}

local i18n = require("Module:I18n") local s = i18n.getString local lex = require("Module:Documentation/Lexer") local utilsDPL = require("Module:UtilsDPL") 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 type(module) == "table" and 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 		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 = 	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 = "\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

local snippets = {} (function	local snippetPagename = mw.getCurrentFrame:getParent:getTitle .. "/Snippets"	if not utilsDPL.pageExists(snippetPagename) then		return nil	end	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.find(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 end)

function h.case(fn, doc, case, options) local rows = {} local input, outputs if case.snippet then local snippet = snippets[doc.name .. case.snippet] input = utilsMarkup.lua(snippet.code) outputs = {snippet.fn} else input = h.printInput(doc, case.args) outputs = h.evaluateFunction(fn, case.args) 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.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, "&#", "&#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("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.", },						{							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, plus a couple flags.", allOf = { {									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  ." },									},								},								{									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: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