Module:Documentation/Module
Jump to navigation
Jump to search
local p = {}
local h = {}
local styles = mw.getCurrentFrame() and mw.getCurrentFrame():extensionTag({
name = "templatestyles",
args = { src = "Module:Documentation/Styles.css" }
})
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 styles..p.schema(module.Schemas()[schemaName], schemaName)
end
function p.Spec(frame)
return p.ModuleSpec(frame)
end
function p.Main(frame)
return p.Module(frame)
end
function p.Module(frame, hasSpecPage)
local modulePage, subpage, docPage = getModulePage(frame, hasSpecPage)
local specPage = hasSpecPage and (modulePage.."/Documentation/Spec") or modulePage
local categories = utilsMarkup.categories(h.getCategories(frame.args.type))
local result
if utilsString.endsWith(docPage, "/Documentation/Snippets/Documentation") then
local category = mw.title.getCurrentTitle().subpageText == "Documentation" and "[[Category:Module Snippets Documentation]]" or "[[Category:Module Snippets]]"
result = string.format("This module contains snippets used to [[Module:Documentation|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 "[[Category:Template Data Documentation]]" or "[[Category:Template Data]]"
result = string.format("This data is used to [[Module:Documentation|auto-generate documentation]] and [[Module:UtilsArg|validate input]] for templates backed by [[Module:%s]]. %s", mw.title.getCurrentTitle().rootText, category)
elseif subpage == "Data" and modulePage ~= "Module:Data" then -- documenting data
result = p.dataDoc(modulePage, specPage) .. categories
elseif utilsString.startsWith(modulePage, "Module:Util/") then -- documentating new style of utils which export one function per page
result = p.moduleDocUtil(modulePage, specPage) .. categories
else --documenting a module
result = p.moduleDoc(modulePage, specPage) .. categories
end
return styles..result
end
function p.ModuleSpec(frame)
return p.Module(frame, true)
end
function p.dataDoc(modulePage, specPage)
local result = "''For information on editing module data in general, see [[Guidelines:Modules/Data]].''\n"
local spec = require(specPage)
local tabs = {}
if type(spec.Data) == "function" then
table.insert(tabs, {
label = "Data",
content = spec.Data(mw.getCurrentFrame())
})
end
local schemas = spec.Schemas and spec.Schemas()
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.."[[Category:Modules with invalid data]]"
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("[[Module:Documentation#Schemas|<code><%s></code>]]", 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 .."="..tostring(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.moduleDocUtil(modulePage, specPage)
local title = mw.title.new(modulePage)
local fn = title.subpageText -- function name
fn = string.gsub(fn, "^ ", "_")
local module = { [fn] = require(modulePage) }
local spec = require(specPage)
local doc = {
[fn] = spec.Documentation(),
snippets = h.snippets(modulePage),
}
local schemas = spec.Schemas and { [fn] = spec.Schemas() } or {}
local fnDoc = h.resolveFunctionDoc(module, doc, schemas, fn)
local result = h.printFunctionDoc(fnDoc, modulePage)
if string.find(fn, "^_") then
-- Prevents the _ from being shown as a blank space in the h1 header for pages like Module:Util/strings/_startsWith
local displayTitle = string.gsub(mw.title.getCurrentTitle().fullText, "/ ", "/_")
result = result .. mw.getCurrentFrame():preprocess("{{DISPLAYTITLE:"..displayTitle.."}}")
local nonCurriedFnPage = string.gsub(modulePage, "/ ", "/")
local nonCurriedFnName = string.gsub(fn, "^_", "")
local curryMsg = string.format("<p>A [[Module:Util/tables#Currying|curried]] version of [[%s]].</p>", nonCurriedFnPage, nonCurriedFnName)
result = curryMsg .. result
end
return result
end
function p.moduleDoc(modulePage, specPage, section)
local headingLevel = section and 3 or 2
headingLevel = mw.getCurrentFrame().args.functionHeadingLevel or headingLevel -- hack for [[Module:Error/Documentation]]
local doc, templates = h.resolveDoc(modulePage, specPage, 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. __TOC__\n"
end
elseif #doc.functions > 0 or doc.sections then
output = "This module exports the following functions. __TOC__\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",
}
}) .. '<div style="clear: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, sectionModule, section.section) .. "\n"
else
output = output .. p.moduleDoc(sectionModule, sectionModule, section.section)
end
end
end
return output
end
function h.resolveDoc(modulePage, specPage, section)
local spec = require(specPage)
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(spec) == "table" then
doc = spec.Documentation and spec.Documentation()
schemas = spec.Schemas and spec.Schemas()
templates = templates or module.Templates
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 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: <code>%s</code>", 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 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("{{#invoke:%s|%s%s%s}}", 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 '<pre class="module-documentation__invoke-syntax">'..result..'</pre>'
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 = "" .. '<p class="module-documentation__function-subheading">' .. s("headers.parameters") .. "</p>\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" .. '<p class="module-documentation__function-subheading">' .. s("headers.returns") .. "</p>\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 = '<p class="module-documentation__function-subheading">' .. s("headers.parameters") .. "</p>\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" .. '<p class="module-documentation__function-subheading">' .. s("headers.examples") .. "</p>\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
local orderedParams = h.sortFrameParams(doc, doc.frameParams)
input, rawinput = h.printFrameInput(moduleName, doc.name, orderedParams, 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
-- 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
if options.outputOnly == nil then
-- showing result for any type other than string or nil is pretty much useless compared to showing output
options.outputOnly = outputCount == 1 and type(outputs[1]) ~= "string" and outputs[1] ~= nil
end
for i = 1, outputCount do
local outputData, resultData, statusData, categories = h.evaluateOutput(outputs[i], case.expect and case.expect[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,
case.expect and statusData,
}))
end
rows[1] = rows[1] or {}
table.insert(rows[1], 1, {
content = input,
rowspan = outputCount,
styles = {
["word-break"] = "break-all",
},
})
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, params, args)
local result = string.format("{{#invoke:%s|%s", moduleName, functionName)
local seenArgs = {}
-- First, arguments of known parameters
for i, param in ipairs(params) do
local paramKey = param.param
local argValue = args[paramKey]
if argValue then
seenArgs[paramKey] = true
result = result..h.printFrameArg(paramKey, argValue)
end
end
-- Then arguments of unknown parameters
-- This was used by Module:Franchise List - unclear if still needed
for argKey, argValue in pairs(args) do
if not seenArgs[argKey] then
result = result..h.printFrameArg(argKey, argValue)
end
end
result = result.."}}"
return utilsMarkup.pre(result), result
end
function h.printFrameArg(param, arg)
local result = "|"
if type(param) == "string" then
if arg ~= "" then
arg = " "..arg
end
result = result..param.."="..arg
else
result = result..arg
end
return 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
local argsText = utilsTable.dropRightWhile(argsText, function(argText, i)
return 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)
elseif output == nil then
resultData = " " -- #invoke treats nil as empty space, so we want to show blank as the output
else
resultData = tostring(output)
end
if type(expected) == "string" then
expected = string.gsub(expected, "\t", "")
end
local passed, expectedOutput
if type(expected) == "function" then
local assertionOutput = expected(output)
passed = assertionOutput == nil
expectedOutput = assertionOutput and assertionOutput.expected
else
passed = utilsTable.isEqual(expected, output)
expectedOutput = expected
end
local statusData = (expected ~= nil or output == nil) and h.printStatus(passed) or nil
if statusData and not passed then
outputData = utilsLayout.table({
hideEmptyColumns = true,
styles = { width = "100%" },
rows = {
{
{
header = true,
content = "Expected",
styles = { width = "1em"}, -- "shrink-wraps" this column
},
{ content = h.formatValue(expectedOutput) },
},
{
{ header = true, content = "Actual" },
{ content = formattedOutput },
},
}
})
end
return outputData, resultData, statusData, categories
end
function h.formatValue(val)
if type(val) ~= "string" then
return utilsMarkup.lua(val)
elseif string.find(val, "UNIQ--") then
-- If the return value has strip markers the string itself probably won't have much meaning
-- Might as well save an expensive parser function call to SyntaxHighlight
return utilsMarkup.pre(val)
else
val = string.gsub(val, "&#", "&#") -- show entity codes
val = utilsTable.print(val)
val = string.gsub(val, "\n", "\\n\n") -- Show newlines
return utilsMarkup.lua(val)
end
end
function h.printStatus(success)
local img = success and "[[File:Green check.svg|16px|center|link=]]" or "[[File:ZW Bad.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 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
-- Note that the new [[Module:Util]] subpages are a separate concept than the old Utils* modules
-- They require a different approach. Not sure how best to categorize them yet.
if title.rootText == "Util" and not isDoc then
return {}
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",
}
}
})
function p.Schemas()
return {
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 [[Module:Documentation#Schemas|Schemas]].",
},
{
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 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 = "<p>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.</p><p>It is also possible to specify a function here to perform a custom assertion other than a simple equality check. See [[Module:Util/args/parse/Documentation/Spec]] for example.</p>",
},
}
},
["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 ="<p>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.</p><p>It is also possible to specify a function here to perform a custom assertion other than a simple equality check. See [[Module:Util/args/parse/Documentation/Spec]] for example.</p>",
},
},
},
}
},
}
},
},
},
},
{
_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 <code>frameParams</code> should appear in.",
type = "array",
items = {
type = "string",
},
},
{
name = "frameParams",
desc = "Use this instead of <code>params</code> 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 = "enum",
type = "array",
items = { type = "string" },
desc = "Indicates which string values are allowed for this 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 <code>frameParamsFormat</code> is set to <code>multiLine</code>. 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.",
},
},
},
},
},
},
}
},
},
},
},
},
},
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 <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>."
},
},
},
}
},
}
end
return p