Module:Documentation

local p = {} local h = {}

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

local frame = mw.getCurrentFrame function p.Examples(frame) local args = frame:getParent.args return p.template(args) end

function p.Module(frame) local module = h.getModule(frame) return p.module(module, module.Documentation, frame.args, 2) end

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

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

function p.template(templateExamples) if not templateExamples.vertical then local tableData = { hideEmptyColumns = true, rows = { { s("headers.input"), s("headers.output"), s("headers.categoriesAdded"), header = true } }		}		for _, example in ipairs(templateExamples) do			local input, output, categoryList = h.templateExample(example) input = utilsMarkup.code(input) table.insert(tableData.rows, {input, output, categoryList}) end return utilsMarkup.wikitable(tableData) end local result = "" for _, example in ipairs(templateExamples) do		local input, output, categoryList = h.templateExample(example) input = utilsMarkup.pre(input) local headerStyles = { ["width"] = "5rem" -- for alignment. See Template:Letter/Documentation for an example of why this is needed }		result = result .. utilsMarkup.wikitable({			hideEmptyRows = true,			rows = {				{					{ header = true, content= s("headers.input"), styles = headerStyles}, 					input,				},				{					{ header = true, content = s("headers.output"), styles = headerStyles}, 					output				},				{					{ header = true, content = s("headers.categoriesAdded"), styles = headerStyles },					categoryList				},			}		}) .. "\n" end return result end

function p.module(module, doc, options, headerLevel) local output = '' if doc then 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 output = output .. h.printFunctionSyntax(functionDoc.name, functionDoc.params, functionDoc) output = output .. h.printParamsDescription(functionDoc.params, 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.module(module, section.doc or {}, options, headerLevel + 1) .. "\n" end end end output = output .. utilsMarkup.categories(h.getCategories) return output end

function p.schema(schemas, schemaName) if schemaName == nil then schemas = { [""] = schemas }	end local definitions = utilsSchema.getTypeDefinitions(schemas, schemaName or "", function(keyDef)		local key = keyDef.key		local symbolicType = keyDef.symbolicType		local rawType = keyDef.rawType		local desc = keyDef.desc		local subkeys = keyDef.subkeys		local parentType = keyDef.parentType		if key and key ~= "" then			if parentType == utilsSchema.TYPES.oneOf then				key = "OR"			else				key = tostring(mw.html.create("span") :css("color", "#d2d2d2") :wikitext(utilsMarkup.code(key)) )				key = utilsMarkup.link("Module:Documentation/Documentation#Schemas", key)				symbolicType = utilsMarkup.code(mw.text.nowiki(symbolicType))				key = utilsMarkup.explain(symbolicType)(key)			end		end		-- Common case such as {record}|record. Only need to render the subkeys once.		if rawType == utilsSchema.TYPES.oneOf and #subkeys == 2 then			local subkeys1 = utilsTable.tail(subkeys[1][1])			local subkeys2 = utilsTable.tail(subkeys[2][1])			if utilsTable.isEqual(subkeys1)(subkeys2) then				subkeys = subkeys1			end		end		definition = utilsTable.mergeArrays({key, desc}, subkeys)		if parentType == utilsSchema.TYPES.record or parentType == utilsSchema.TYPES.oneOf 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 h.templateExample(example) local input = mw.text.unstripNoWiki(example) local output = frame:preprocess(input) input = mw.text.trim(input) input = mw.text.nowiki(input) local output, categories = utilsMarkup.stripCategories(output) local output = utilsMarkup.killBacklinks(output) local categoryList = utilsMarkup.bulletList(categories) return input, output, categoryList end

function h.printFunctionSyntax(functionName, params, options) params = params or {} local syntax = functionName if not options.order then syntax = syntax .. h.printFunctionCall(params) return utilsMarkup.code(syntax) .. "\n" end for i = 1, options.order or 0 do syntax = syntax .. h.printFunctionCall(params[i] or {}) end local description = utilsMarkup.italic(s("higherOrder")) .. "\n" return utilsMarkup.code(syntax) .. "\n" .. description end function h.printFunctionCall(params) local paramNames if type(params[1]) == "string" then paramNames = params else paramNames = utilsTable.map(h.printParamSyntax)(params) end local paramSyntax = table.concat(paramNames, ", ") return "(" .. paramSyntax .. ")" end

function h.printParamSyntax(param) local name = param.name if param.optional then name = "[" .. name .. "]"	end return name end

function h.printParamsDescription(params, options) if not params or #params == 0 or type(params[1]) == "string" then return "" end if options.order then params = utilsTable.flatten(params) end local output = ";" .. s("headers.parameters") .. "\n" for _, param in ipairs(params) do		local parameter = (""):format(param.name, param.description or "", param.optional and "Optional" or "Required") output = output .. mw.getCurrentFrame:preprocess(parameter) .. "\n" if param.schema then output = output .. p.schema(param.schema) .. "\n" end end return output .. "\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.explain(s("explainStatusColumn"))(s("headers.status")) local headerCells = utilsTable.compactNils({		inputColumn, 		not options.resultOnly and outputColumn or nil, 		not options.outputOnly and resultColumn or nil, 		statusColumn,	}) local tableData = { 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 if not options.order then input = input .. "(" .. h.printInputArgs(args, options.params) ..")" end for i = 1, options.order or 0 do input = input .. "(" .. h.printInputArgs(args[i] or {}, options.params[i]) ..")" end return utilsMarkup.lua(input) end function h.printInputArgs(args, params) argsText = utilsTable.map(utilsTable.print)(args) -- For displaying stuff like foo(nil) in examples where required arguments are nil (e.g. Module:UtilsArg/Validate) local optionalParams = utilsTable.filter("optional")(params) local argsRequired = #params - #optionalParams local nilsToAdd = argsRequired - #utilsTable.compactNils(args) for i = 1, nilsToAdd do		table.insert(argsText, "nil") end return table.concat(argsText, ", ") 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.explain(msg)(img) local cat = success and "" or utilsMarkup.category(s("failingTestsCategory")) return img .. cat end

function h.getCategories 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 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:Pages 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 = "#/functions", }, { _ref = "#/sections", }, }	},	functions = { type = "array", desc = "An array of records describing functions", 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 the function parameters, or an array of arrays if the function has  > 1", oneOf = { {								_id = "#function", type = "record", properties = { {										name = "name", type = "string", required = true, desc = "The name of a parameter", },									{										name = "optional", type = "boolean", desc = "Set to  when the parameter is optional", },									{										name = "description", type = "string", desc = "Description of the parameter", },								},							},							{								type = "array", items = { _ref = "#function" }, },						},					},				},				{					name = "returns", _id = "#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", desc = "An array of arguments to pass to the function, or an array of arrays if the function has  > 1", oneOf = { {										type = "any" },									{										type = "array", items = { type = "any" }, },								},							},							{								name = "expected", 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.", _ref = "#returns" , },						}					},				},				{					name = "wip", type = "boolean", desc = "Tags the function doc with Template:UC." }			},		},	},	sections = { type = "record", desc = "A record representing documentation sections.", 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 = "#/functions", _showDoc = false, desc = "An array of tables describing functions (see above).", },					},				},			},		},	}, }

return p