Module:Documentation/Module

local p = {} local h = {}

local i18n = require("Module:I18n") local s = i18n.getString local tab2 = require("Module:Tab2") 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, frame.args, 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		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 then			local tabs = utilsTable.uniqueBy("tabContent")(utilsTable.flatten(subkeys.oneOf))			if #tabs == 1 then				subkeys = utilsTable.concat(subkeys, tabs[1].tabContent) else subkeys = utilsTable.concat({tostring(tab2.Main(utilsTable.flatten(tabs), 1, "top"))}, 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, options, headerLevel) utilsSchema.validate(p.Schemas.Documentation, "Documentation", doc, "p.Documentation") local output = '' if headerLevel == 2 then output = "\n" end if doc.description then output = output .. doc.description .. "\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[functionDoc.name] output = output .. h.printFunctionSyntax(functionDoc.name, functionDoc.params, paramSchemas, functionDoc) output = output .. h.printParamsDescription(functionDoc.params, paramSchemas, functionDoc) output = output .. h.printReturnsDescription(functionDoc.returns) output = output .. h.printFunctionCases(module[functionDoc.name], functionDoc.name, functionDoc.cases, functionDoc) end if (doc.sections) then for _, section in ipairs(doc.sections) do output = output .. ("==%s=="):format(section.heading) .. "\n" if section.description then output = output .. section.description .. "\n" end output = output .. p.moduleDoc(module, section.doc or {}, schemas, options, headerLevel + 1) .. "\n" end end return output end

function h.printFunctionSyntax(functionName, params, paramSchemas, options) params = params or {} local syntax = functionName if not options.order then syntax = syntax .. h.printFunctionCall(params, paramSchemas) return utilsMarkup.code(syntax) .. "\n" end for i = 1, options.order do syntax = syntax .. h.printFunctionCall(params[i] or {}, paramSchemas) end local description = utilsMarkup.italic(s("higherOrder")) .. "\n" return utilsMarkup.code(syntax) .. "\n" .. description end function h.printFunctionCall(params, paramSchemas) local paramSyntax = {} for _, param in ipairs(params) do		local paramSchema = paramSchemas and paramSchemas[param] if not paramSchema or paramSchema.required then table.insert(paramSyntax, param) else table.insert(paramSyntax, "[" .. param .. "]") end end paramSyntax = table.concat(paramSyntax, ", ") return "(" .. paramSyntax .. ")" end

function h.printParamsDescription(params, paramSchemas, options) if not params or #params == 0 or not paramSchemas then return "" end if options.order then params = utilsTable.flatten(params) end local paramDefinitions = utilsTable.map(function(param)		return p.schema(paramSchemas[param], param)	end)(params) local definitionsList = utilsMarkup.list(paramDefinitions) -- for _, param in ipairs(params) do -- 	paramDefinitions = paramDefinitions .. p.schema(paramSchemas[param], param) .. "\n" -- end local header = ";" .. s("headers.parameters") .. "\n" return header .. definitionsList .. "\n" end

function h.printReturnsDescription(returns) if not returns then return "" end if type(returns) == "string" then returns = { returns } end local returnsList = utilsMarkup.bulletList(returns) local header = ";" .. s("headers.returns") .. "\n" local result = header .. mw.getCurrentFrame:preprocess(returnsList) .. "\n" return result end

function h.printFunctionCases(fn, fnName, fnCases, options) if not fnCases or #fnCases == 0 then return "" end local output = ";" .. 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 options.resultOnly and outputColumn or nil, 		not options.outputOnly and resultColumn or nil, 		statusColumn,	}) local tableData = { hideEmptyColumns = true, rows = { {				header = true, cells = headerCells, }		},	}	for _, case in ipairs(fnCases) do		local caseRows = h.case(fn, fnName, case, options) tableData.rows = utilsTable.mergeArrays(tableData.rows, caseRows) end output = output .. utilsMarkup.wikitable(tableData) .. "\n" return output end

function h.case(fn, fnName, case, options) local rows = {} local input = h.evaluateInput(fnName, case.args, options) local outputs = h.evaluateFunction(fn, case.args, options.order) local expected = case.expected local numReturns = type(options.returns) == "table" and #options.returns or 1 if type(expected) ~= "table" or numReturns == 1 then expected = { expected } end for i = 1, numReturns 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 = numReturns,	}) if case.description then table.insert(rows, 1, {			{				header = true,				colspan = -1,				styles = {					["text-align"] = "left"				},				content = case.description,			}		}) end return rows end

function h.evaluateInput(fnName, args, options) local args = args or {} local input = fnName local order = options.order or 1 if not options.order then args = { args } end local totalArgs = utilsTable.flatten(args) local enableLineWrap = #totalArgs == 1 and type(totalArgs[1]) == "string" for i = 1, order do input = input .. "(" .. h.printInputArgs(args[i] or {}, options.params[i], enableLineWrap) .. ")" end return utilsMarkup.lua(input, {		wrapLines = enableLineWrap	}) end function h.printInputArgs(args, params, lineWrapEnabled, multiline) argsText = utilsTable.map(function(arg)		local argText = utilsTable.print(arg)		if not(#args == 1 and type(arg) == "table") then			argText = string.gsub(argText, "\n", "\n  ")		end		return argText	end)(args) local result if multiline then result = table.concat(argsText, ",\n ") if type(args[1]) ~= "table" then result = "\n " .. result end if type(args[#args]) ~= "table" then result = result .. "\n" end return result end local result = table.concat(argsText, ", ") if not lineWrapEnabled and #result > MAX_ARGS_LENGTH then return h.printInputArgs(args, params, false, true) end return result end

function h.evaluateFunction(fn, args, order) args = args or {} if not order then return {fn(unpack(args))} end for i = 1, order - 1 do		fn = fn(unpack(args[i])) end return {fn(unpack(args[order]))} 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 = utilsMarkup.wikitable({			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 return utilsMarkup.pre(string.gsub(val, "\n", "\\n\n")) -- Show newlines 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 = success and "" or utilsMarkup.category(s("failingTestsCategory")) 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 = {		higherOrder = "This is a higher-order function.",		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 = "order", type = "number", desc = "An integer representing the arity of a higher-order function.  is order 1,   is order 2, and so on.", default = 1, },						{							name = "resultOnly", type = "boolean", desc = "When, does not display the raw wikitext output—only what is rendered on a page. Useful for functions returning strings of complex wikitext.", },						{							name = "outputOnly", type = "boolean", desc = "When, displays only the raw output of the function (opposite of resultOnly)." },						{							name = "params", required = true, type = "array", items = { desc = "An array of parameter names, or an array of arrays if the function has  > 1. Integrates with Module:Schema.", oneOf = { { type = "string" }, { type = "array", items = { type = "string" } } },							},						},						{							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.", type = "array", items = { type = "record", properties = { {										name = "description", 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, or an array of arrays if the function has  > 1", },									{										name = "expected", 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", type = "array", required = true, items = { type = "record", properties = { {									name = "heading", required = true, type = "string", desc = "Heading for the documentation section.", },								{									name = "doc", _ref = "#/definitions/functions", _hideSubkeys = true, desc = "An array of tables describing functions (see above).", },							},						},					},				},			},		}	}, }

return p