PhantomCaleb (talk | contribs) mNo edit summary |
MannedTooth (talk | contribs) No edit summary |
||
(23 intermediate revisions by one other user not shown) | |||
Line 26: | Line 26: | ||
function p.Schema(frame) |
function p.Schema(frame) |
||
− | local modulePage |
+ | local modulePage = frame.args.module or getModulePage(frame) |
− | local module = require( |
+ | local module = require(modulePage) |
− | local schemaName = frame.args[1] |
+ | local schemaName = frame.args[1] |
− | + | return p.schema(module.Schemas[schemaName], schemaName) |
|
⚫ | |||
end |
end |
||
Line 43: | Line 42: | ||
function p.dataDoc(modulePage) |
function p.dataDoc(modulePage) |
||
+ | local result = "''For information on editing module data in general, see [[Guidelines:Modules/Data]].''\n" |
||
local module = require(modulePage) |
local module = require(modulePage) |
||
local tabs = {} |
local tabs = {} |
||
Line 57: | Line 57: | ||
}) |
}) |
||
end |
end |
||
− | + | result = result .. utilsLayout.tabs(tabs, { |
|
⚫ | |||
⚫ | |||
+ | collapse = true |
||
+ | } |
||
+ | } |
||
+ | }) |
||
⚫ | |||
end |
end |
||
Line 97: | Line 104: | ||
end |
end |
||
if subkeys and subkeys.oneOf and #subkeys.oneOf > 0 then |
if subkeys and subkeys.oneOf and #subkeys.oneOf > 0 then |
||
− | local tabData = utilsTable. |
+ | local tabData = utilsTable._uniqueBy("content")(utilsTable.flatten(subkeys.oneOf)) |
if #tabData == 1 then |
if #tabData == 1 then |
||
subkeys = utilsTable.concat(subkeys, tabData[1].content) |
subkeys = utilsTable.concat(subkeys, tabData[1].content) |
||
Line 142: | Line 149: | ||
local headingLevel = section and 3 or 2 |
local headingLevel = section and 3 or 2 |
||
local module, doc = h.resolveDoc(modulePage, section) |
local module, doc = h.resolveDoc(modulePage, section) |
||
⚫ | |||
⚫ | |||
⚫ | |||
local output = "" |
local output = "" |
||
if not section then |
if not section then |
||
+ | if type(module) == "table" and module.Templates then |
||
− | output = "__TOC__\n" |
||
+ | local templates = utilsTable.keys(module.Templates) |
||
+ | table.sort(templates) |
||
+ | for i in ipairs(templates) do |
||
+ | templates[i] = utilsMarkup.link("Template:"..templates[i]) |
||
⚫ | |||
+ | local templateList = utilsMarkup.bulletList(templates) |
||
+ | output = "This is the main module for the following templates:" .. templateList .. "\n" |
||
+ | end |
||
+ | if (#doc.functions > 0 or doc.sections) and type(module) == "table" and module.Templates then |
||
+ | output = output .. "In addition, this module exports the following functions. __TOC__\n" |
||
+ | elseif (#doc.functions > 0 or doc.sections) then |
||
+ | output = "This module exports the following functions. __TOC__\n" |
||
+ | end |
||
end |
end |
||
− | for _, functionDoc in ipairs(doc.functions) do |
+ | for _, functionDoc in ipairs(doc.functions or {}) do |
output = output .. utilsMarkup.heading(headingLevel, functionDoc.name) .. "\n" |
output = output .. utilsMarkup.heading(headingLevel, functionDoc.name) .. "\n" |
||
if functionDoc.wip then |
if functionDoc.wip then |
||
− | output = output .. |
+ | output = output .. mw.getCurrentFrame():expandTemplate({ |
+ | title = "WIP", |
||
⚫ | |||
+ | align = "left", |
||
+ | } |
||
+ | }) .. '<div style="clear:left"/>' |
||
end |
end |
||
Line 186: | Line 208: | ||
function h.resolveDoc(modulePage, section) |
function h.resolveDoc(modulePage, section) |
||
local module = require(modulePage) |
local module = require(modulePage) |
||
− | local doc |
+ | local doc = {} |
if type(section) == "table" then |
if type(section) == "table" then |
||
doc = section |
doc = section |
||
elseif type(module) == "table" then |
elseif type(module) == "table" then |
||
− | doc = module.Documentation |
+ | doc = module.Documentation or {} |
⚫ | |||
− | if not doc then |
||
− | return nil |
||
end |
end |
||
local err = utilsSchema.validate(p.Schemas.Documentation, "Documentation", doc, "p.Documentation") |
local err = utilsSchema.validate(p.Schemas.Documentation, "Documentation", doc, "p.Documentation") |
||
Line 201: | Line 220: | ||
if doc.sections then |
if doc.sections then |
||
doc.functions = {} |
doc.functions = {} |
||
+ | doc.snippets = h.snippets(modulePage) |
||
return module, doc |
return module, doc |
||
end |
end |
||
Line 218: | Line 238: | ||
end |
end |
||
local functions = {} |
local functions = {} |
||
+ | doc.snippets = h.snippets(modulePage) |
||
for _, functionName in ipairs(functionNames) do |
for _, functionName in ipairs(functionNames) do |
||
− | table.insert(functions, h.resolveFunctionDoc(module, |
+ | table.insert(functions, h.resolveFunctionDoc(module, doc, functionName)) |
doc[functionName] = nil |
doc[functionName] = nil |
||
end |
end |
||
Line 244: | Line 265: | ||
end |
end |
||
− | function h.resolveFunctionDoc(module, |
+ | function h.resolveFunctionDoc(module, moduleDoc, functionName) |
+ | local functionDoc = moduleDoc[functionName] |
||
functionDoc.name = functionName |
functionDoc.name = functionName |
||
functionDoc.fn = module[functionDoc.name] |
functionDoc.fn = module[functionDoc.name] |
||
functionDoc.cases = functionDoc.cases or {} |
functionDoc.cases = functionDoc.cases or {} |
||
+ | functionDoc.snippets = h.getFunctionSnippets(moduleDoc.snippets, functionDoc.name) |
||
if type(functionDoc.returns) ~= "table" then |
if type(functionDoc.returns) ~= "table" then |
||
functionDoc.returns = {functionDoc.returns} |
functionDoc.returns = {functionDoc.returns} |
||
Line 274: | Line 297: | ||
end) |
end) |
||
end) |
end) |
||
+ | functionDoc.fp.snippets = h.getFunctionSnippets(moduleDoc.snippets, functionDoc.fp.name) |
||
end |
end |
||
functionDoc.params = {resolvedParams} |
functionDoc.params = {resolvedParams} |
||
Line 280: | Line 304: | ||
end |
end |
||
return functionDoc |
return functionDoc |
||
⚫ | |||
+ | function h.getFunctionSnippets(moduleSnippets, functionName) |
||
+ | local functionSnippets = {} |
||
+ | for k, v in pairs(moduleSnippets or {}) do |
||
+ | local s, e = k:find(functionName) |
||
⚫ | |||
+ | local snippetKey = string.sub(k, e + 1) |
||
+ | functionSnippets[snippetKey] = v |
||
+ | end |
||
⚫ | |||
+ | return functionSnippets |
||
end |
end |
||
Line 359: | Line 394: | ||
local statusColumn = utilsMarkup.tooltip(s("headers.status"), s("explainStatusColumn")) |
local statusColumn = utilsMarkup.tooltip(s("headers.status"), s("explainStatusColumn")) |
||
− | local headerCells = utilsTable. |
+ | local headerCells = utilsTable.compact({ |
inputColumn, |
inputColumn, |
||
not doc.cases.resultOnly and outputColumn or nil, |
not doc.cases.resultOnly and outputColumn or nil, |
||
Line 382: | Line 417: | ||
end |
end |
||
+ | h.snippets = utilsFunction.memoize(function(modulePage) |
||
⚫ | |||
⚫ | |||
− | (function() |
||
⚫ | |||
if not utilsPage.exists(snippetPagename) then |
if not utilsPage.exists(snippetPagename) then |
||
return nil |
return nil |
||
end |
end |
||
⚫ | |||
local snippetPage = mw.title.new(snippetPagename) |
local snippetPage = mw.title.new(snippetPagename) |
||
local module = require(snippetPagename) |
local module = require(snippetPagename) |
||
Line 398: | Line 433: | ||
if line[1] and line[1].type == "keyword" and line[1].data == "function" then |
if line[1] and line[1].type == "keyword" and line[1].data == "function" then |
||
local isOpenParens = function(token) |
local isOpenParens = function(token) |
||
− | return utilsString.startsWith( |
+ | return utilsString.startsWith(token.data, "(") |
end |
end |
||
− | local fnName = line[utilsTable. |
+ | local fnName = line[utilsTable.findIndex(line, isOpenParens) - 1].data |
table.insert(starts, i + 1) |
table.insert(starts, i + 1) |
||
table.insert(names, fnName) |
table.insert(names, fnName) |
||
Line 422: | Line 457: | ||
} |
} |
||
end |
end |
||
⚫ | |||
− | end |
+ | end) |
function h.case(doc, case, options) |
function h.case(doc, case, options) |
||
local rows = {} |
local rows = {} |
||
− | local input, outputs |
+ | local input, outputs |
− | local snippet = case.snippet and snippets[ |
+ | local snippet = case.snippet and doc.snippets[tostring(case.snippet)] |
if snippet then |
if snippet then |
||
− | input = utilsMarkup.lua(snippet.code) |
+ | input = utilsMarkup.lua(snippet.code, { wrapLines = false }) |
outputs = {snippet.fn()} |
outputs = {snippet.fn()} |
||
elseif case.args then |
elseif case.args then |
||
Line 441: | Line 477: | ||
for i = 1, #doc.returns do |
for i = 1, #doc.returns do |
||
local outputData, resultData, statusData = h.evaluateOutput(outputs[i], expected[i]) |
local outputData, resultData, statusData = h.evaluateOutput(outputs[i], expected[i]) |
||
− | table.insert(rows, utilsTable. |
+ | table.insert(rows, utilsTable.compact({ |
not options.resultOnly and outputData or nil, |
not options.resultOnly and outputData or nil, |
||
not options.outputOnly and resultData or nil, |
not options.outputOnly and resultData or nil, |
||
Line 482: | Line 518: | ||
local argsText = {} |
local argsText = {} |
||
for i = 1, math.max(#params, #args) do |
for i = 1, math.max(#params, #args) do |
||
− | local argText = args[i] and utilsTable.print(args[i]) |
+ | local argText = args[i] == nil and "nil" or utilsTable.print(args[i]) |
if not (#args == 1 and type(args[i]) == "table") then |
if not (#args == 1 and type(args[i]) == "table") then |
||
argText = string.gsub(argText, "\n", "\n ") --ensures proper indentation of multiline table args |
argText = string.gsub(argText, "\n", "\n ") --ensures proper indentation of multiline table args |
||
Line 490: | Line 526: | ||
-- Trim nil arguments off the end so long as they're optional (but keep the first one). |
-- Trim nil arguments off the end so long as they're optional (but keep the first one). |
||
− | local argsText = utilsTable.dropRightWhile(function(argText, i) |
+ | 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 |
return i > 1 and argText == "nil" and params[i] and params[i].schema and not params[i].schema.required |
||
− | end |
+ | end) |
local result = table.concat(argsText, ", ") |
local result = table.concat(argsText, ", ") |
||
Line 517: | Line 553: | ||
if type(output) == "string" then |
if type(output) == "string" then |
||
resultData = utilsMarkup.killBacklinks(output) |
resultData = utilsMarkup.killBacklinks(output) |
||
− | resultData |
+ | resultData = utilsMarkup.stripCategories(output) |
− | if (#categories > 0) then |
||
− | local categoryList = utilsMarkup.bold(s('headers.categoriesAdded')) .. utilsMarkup.bulletList(categories) |
||
⚫ | |||
− | {resultData}, |
||
− | {categoryList}, |
||
⚫ | |||
⚫ | |||
end |
end |
||
if type(expected) == "string" then |
if type(expected) == "string" then |
||
Line 581: | Line 610: | ||
local moduleTitle = (isDoc or isData) and mw.title.new(title.baseText) or title |
local moduleTitle = (isDoc or isData) and mw.title.new(title.baseText) or title |
||
local isData = moduleTitle.subpageText == "Data" |
local isData = moduleTitle.subpageText == "Data" |
||
− | local isUtil = utilsString.startsWith( |
+ | local isUtil = utilsString.startsWith(moduleTitle.text, "Utils") |
local isSubmodule = moduleTitle.subpageText ~= moduleTitle.text |
local isSubmodule = moduleTitle.subpageText ~= moduleTitle.text |
||
if type == "submodule" then |
if type == "submodule" then |
||
Line 705: | Line 734: | ||
items = { |
items = { |
||
oneOf = { |
oneOf = { |
||
− | + | ["snippet"] = { |
|
⚫ | |||
type = "record", |
type = "record", |
||
properties = { |
properties = { |
||
Line 730: | Line 758: | ||
} |
} |
||
}, |
}, |
||
− | + | ["args"] = { |
|
− | { |
||
type = "record", |
type = "record", |
||
properties = { |
properties = { |
||
Line 760: | Line 787: | ||
name = "wip", |
name = "wip", |
||
type = "boolean", |
type = "boolean", |
||
− | desc = "Tags the function doc with [[Template: |
+ | desc = "Tags the function doc with [[Template:WIP]]." |
} |
} |
||
}, |
}, |
Revision as of 08:24, 14 July 2021
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 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
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 = getModulePage(frame)
local categories = utilsMarkup.categories(h.getCategories(frame.args.type))
if subpage == "Data" then
return p.dataDoc(modulePage) .. categories
end
return p.moduleDoc(modulePage) .. categories
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
if module.Schemas and module.Schemas.Data then
table.insert(tabs, {
label = "Schema",
content = p.schema(module.Schemas.Data, "Data")
})
end
result = result .. utilsLayout.tabs(tabs, {
{
tabs = {
collapse = true
}
}
})
return result
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 {{
label = typeLabel or symbolicType,
tooltip = utilsMarkup.code(symbolicType),
content = utilsMarkup.definitionList(definition)
}}
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
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 = {{nil, definitions}}
else
definitions = {definitions}
end
local definitionsList = utilsMarkup.definitionList(definitions)
return definitionsList
end
function p.moduleDoc(modulePage, section)
local headingLevel = section and 3 or 2
local module, doc = h.resolveDoc(modulePage, section)
local output = ""
if not section then
if type(module) == "table" and module.Templates then
local templates = utilsTable.keys(module.Templates)
table.sort(templates)
for i in ipairs(templates) do
templates[i] = utilsMarkup.link("Template:"..templates[i])
end
local templateList = utilsMarkup.bulletList(templates)
output = "This is the main module for the following templates:" .. templateList .. "\n"
end
if (#doc.functions > 0 or doc.sections) and type(module) == "table" and module.Templates then
output = output .. "In addition, this module exports the following functions. __TOC__\n"
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)
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 = {}
if type(section) == "table" then
doc = section
elseif type(module) == "table" then
doc = module.Documentation or {}
end
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
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, functionName))
doc[functionName] = nil
end
doc.functions = functions
return module, doc
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, 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 = module.Schemas and module.Schemas[functionDoc.name] or {}
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
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}
for _, case in ipairs(functionDoc.cases) do
case.args = {case.args}
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)
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)
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)
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,
statusColumn,
})
local tableData = {
hideEmptyColumns = true,
rows = {
{
header = true,
cells = headerCells,
}
},
}
for _, case in ipairs(doc.cases) do
local caseRows = h.case(doc, case, 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, options)
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 case.args then
input = h.printInput(doc, case.args)
outputs = h.evaluateFunction(doc.fn, case.args)
else
return {}
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.compact({
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] == 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 = ""
if type(output) == "string" then
resultData = utilsMarkup.killBacklinks(output)
resultData = 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
end
function h.formatValue(val)
if type(val) == "string" then
val = string.gsub(val, "&#", "&#") -- 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 "[[File:Green check.svg|16px|center|link=]]" or "[[File:TFH Red Link desperate.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 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 = {
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.",
type = "map",
keys = { type = "string" },
values = {
type = "record",
properties = {
{
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#Functions|Module:Schema]].",
},
{
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, plus a couple flags.",
allOf = {
{
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>."
},
},
},
{
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:WIP]]."
}
},
},
},
sections = {
type = "record",
properties = {
{
name = "sections",
type = "array",
required = true,
items = {
type = "record",
properties = {
{
name = "heading",
type = "string",
},
{
name = "section",
required = true,
oneOf = {
{ type = "string" },
{
_ref = "#/definitions/functions",
required = true,
_hideSubkeys = true,
},
},
}
}
},
},
},
},
}
},
}
return p