Module:DependencyList
From Deepspace Lore
More actions
Documentation for this module may be created at Module:DependencyList/doc
--- Based on Module:DependencyList from RuneScape Wiki and Star Citizen Wiki
--- @see https://runescape.wiki/w/Module:DependencyList
-- <nowiki>
require('strict')
local p = {}
local libraryUtil = require('libraryUtil')
local arr = require('Module:Array')
local yn = require('Module:Yesno')
local param = require('Module:Paramtest')
local userError = require('Module:User error')
local hatnote = require('Module:Hatnote')._hatnote
local mHatlist = require('Module:Hatnote list')
local mbox = require('Module:Mbox')._mbox
local tooltip = require('Module:Tooltip')
local dpl -- lazy loaded
local COLLAPSE_LIST_LENGTH_THRESHOLD = 5
local MAX_DYNAMIC_REQUIRE_LIST_LENGTH = 30
local dynamicRequireListQueryCache = {}
local NS_MODULE_NAME = mw.site.namespaces[828].name
local NS_TEMPLATE_NAME = mw.site.namespaces[10].name
local builtins = {
['libraryUtil'] = {
link = 'mw:Special:MyLanguage/Extension:Scribunto/Lua reference manual#libraryUtil',
categories = {},
},
['strict'] = {
link = 'mw:Special:MyLanguage/Extension:Scribunto/Lua reference manual#strict',
categories = { '[[Category:Strict mode modules]]' },
},
}
-- ============================================================
-- String / name utilities
-- ============================================================
local function substVarValue(moduleContent, varName)
local res = moduleContent:match(varName .. '%s*=%s*(%b""%s-%.*)') or
moduleContent:match(varName .. "%s*=%s*(%b''%s-%.*)")or ''
if res:find('^(["\'])[Mm]odule:[%S]+%1') and not res:find('%.%.') and not res:find('%%%a') then
return mw.text.trim(res)
else
return ''
end
end
local function extractModuleName(capture, moduleContent)
capture = capture:gsub('^%(%s*(.-)%s*%)$', '%1')
if capture:find('^(["\']).-%1$') then
return capture
elseif capture:find('^[%a_][%w_]*$') then
return substVarValue(moduleContent, capture)
end
return capture
end
local function formatPageName(str)
return mw.text.trim(str)
:gsub('^(["\'])(.-)%1$', '%2')
:gsub('_', ' ')
:gsub('^.', string.upper)
:gsub('^([^:]-:)(.)', function(a, b) return a .. string.upper(b) end)
end
local function formatModuleName(str, allowBuiltins)
if allowBuiltins then
local name = mw.text.trim(str):gsub('^(["\'])(.-)%1$', '%2')
if builtins[name] then return name end
end
local module = formatPageName(str)
if not module:find('^[Mm]odule:') then
module = NS_MODULE_NAME .. ':' .. module
end
return module
end
local function isDynamicPath(str)
return str:find('%.%.') or str:find('%%%a')
end
local function multiGmatch(str, ...)
local generators = {}
for i, pat in ipairs({...}) do
generators[i] = string.gmatch(str, pat)
end
local function nextCaptures()
local captures = {generators[1]()}
if #captures > 0 then
return unpack(captures)
elseif #generators > 1 then
table.remove(generators, 1)
return nextCaptures()
end
end
return nextCaptures
end
-- ============================================================
-- Dynamic require list (DPL wildcard queries)
-- ============================================================
local function getDynamicRequireList(query)
if query:find('%.%.') then
query = mw.text.split(query, '..', true)
query = arr.map(query, function(x) return (x:match('^%s*[\'\"](.-)[\'\"]%s*$') or '%') end)
query = table.concat(query)
else
local _, _query = query:match('(["\'])(.-)%1')
query = _query:gsub('%%%a', '%%')
end
query = query:gsub('^[Mm]odule:', '')
query = mw.language.getContentLanguage():ucfirst(query)
if dynamicRequireListQueryCache[query] then
return dynamicRequireListQueryCache[query]
end
local list = dpl.ask{
namespace = NS_MODULE_NAME,
titlematch = query,
nottitlematch = '%/doc|' .. query .. '/%',
distinct = 'strict',
ordermethod = 'title',
count = MAX_DYNAMIC_REQUIRE_LIST_LENGTH + 1,
skipthispage = 'no',
allowcachedresults = true,
cacheperiod = 604800,
}
if #list > MAX_DYNAMIC_REQUIRE_LIST_LENGTH then
list = {'Module:' .. query}
end
dynamicRequireListQueryCache[query] = list
return list
end
-- ============================================================
-- Require / loadData list parsing
-- ============================================================
local function getRequireLists(moduleContent)
local requireList = arr{}
local loadDataList = arr{}
local extraCategories = arr{}
local function getList(list, patterns)
for match in multiGmatch(moduleContent, unpack(patterns)) do
match = mw.text.trim(match)
local name = extractModuleName(match, moduleContent)
if isDynamicPath(name) then
list:insert(getDynamicRequireList(name), true)
elseif name ~= '' then
name = formatModuleName(name, true)
table.insert(list, name)
if builtins[name] then
extraCategories = extraCategories:insert(builtins[name].categories, true)
end
end
end
end
getList(requireList, {
'require%s*(%b())',
'require%s*((["\'])%s*[Mm]odule:.-%2)',
'pcall%s*%(%s*require%s*,([^%),]+)',
})
getList(loadDataList, {
'mw%.loadData%s*(%b())',
'mw%.loadData%s*((["\'])%s*[Mm]odule:.-%2)',
'pcall%s*%(%s*mw%.loadData%s*,([^%),]+)',
'mw%.loadJsonData%s*(%b())',
'mw%.loadJsonData%s*((["\'])%s*[Mm]odule:.-%2)',
'pcall%s*%(%s*mw%.loadJsonData%s*,([^%),]+)',
})
requireList = requireList:unique()
loadDataList = loadDataList:unique()
extraCategories = extraCategories:unique()
table.sort(requireList)
table.sort(loadDataList)
table.sort(extraCategories)
return {require = requireList, loadData = loadDataList, extraCategories = extraCategories}
end
-- ============================================================
-- TemplateStyles / used-template detection
-- ============================================================
local function insertTemplateStyle(styleName, list)
styleName = formatPageName(styleName)
if not styleName:find(':') then styleName = 'Template:' .. styleName end
if isDynamicPath(styleName) then
list:insert(getDynamicRequireList(styleName), true)
else
list:insert(styleName)
end
end
local function extractTemplateStyles(pageContent, list)
for _, styleName in string.gmatch(
pageContent,
'<[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee][Ss][Tt][Yy][Ll][Ee][Ss]%s+[Ss][Rr][Cc]=(["\'])(.-)%1'
) do
styleName = formatPageName(styleName)
if styleName ~= '' then insertTemplateStyle(styleName, list) end
end
end
local function recursiveGMatch(str, pat)
local list = {}
local i = 0
repeat
for match in string.gmatch(list[i] or str, pat) do
table.insert(list, match)
end
i = i + 1
until i > #list or i > 100
i = 0
return function()
i = i + 1
return list[i]
end
end
local function formatTemplate(name)
if name:find(':') then
local ns = name:match('^(.-):')
if arr.contains({'', 'template', 'user'}, ns:lower()) then
return name
elseif ns == ns:upper() then
return ns
end
else
if name:match('^%u+$') or name == '!' then
return name
else
return 'Template:' .. name
end
end
end
local function getUsedTemplatesList(moduleContent)
local usedTemplateList = arr{}
local templateStylesList = arr{}
for preprocess in string.gmatch(moduleContent, ':preprocess%s*(%b())') do
for template in recursiveGMatch(preprocess, '{(%b{})}') do
local name = string.match(template, '{(.-)[|{}]')
if name ~= '' then usedTemplateList:insert(formatTemplate(name)) end
end
extractTemplateStyles(preprocess, templateStylesList)
end
for capture in string.gmatch(moduleContent, 'expandTemplate%s*%(?%s*{%s*title%s*=%s*((["\'])%s*.-%2)') do
local name = formatPageName(capture)
if name ~= '' then usedTemplateList:insert(formatTemplate(name)) end
end
for _, capture in multiGmatch(
moduleContent,
'extensionTag%s*%(%s*'
.. '(["\'])[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee][Ss][Tt][Yy][Ll][Ee][Ss]%1%s*,'
.. '.-,'
.. '%s*{%s*src%s*=%s*((["\'])%s*.-%3)',
'extensionTag%s*%(?%s*{%s*'
.. 'name%s*=%s*(["\'])[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee][Ss][Tt][Yy][Ll][Ee][Ss]%1'
.. '.-'
.. 'args%s*=%s*{%s*src%s*=%s*((["\'])%s*.-%3)'
) do
local name = formatPageName(capture)
if name ~= '' then insertTemplateStyle(name, templateStylesList) end
end
usedTemplateList = usedTemplateList:unique()
templateStylesList = templateStylesList:unique()
table.sort(usedTemplateList)
table.sort(templateStylesList)
return {usedTemplateList = usedTemplateList, templateStylesList = templateStylesList}
end
-- ============================================================
-- Template dependency list (invoke + TemplateStyles)
-- ============================================================
local function getTemplateDependencyList(pageName)
local pageContent = mw.title.new(pageName):getContent()
local invokeList = {}
local templateStylesList = arr{}
assert(pageContent, string.format('Failed to retrieve text content of page "%s"', pageName))
for moduleName, funcName in string.gmatch(
pageContent,
'{{[{|safeubt:}]-#[Ii]nvoke:([^|]+)|([^}|]+)[^}]*}}'
) do
moduleName = formatModuleName(moduleName)
funcName = mw.text.trim(funcName)
if funcName:find('^{{{') then funcName = funcName .. '}}}' end
table.insert(invokeList, {moduleName = moduleName, funcName = funcName})
end
extractTemplateStyles(pageContent, templateStylesList)
invokeList = arr.unique(invokeList, function(x) return x.moduleName .. '#' .. x.funcName end)
templateStylesList = templateStylesList:unique()
table.sort(invokeList, function(x, y)
return (x.moduleName .. '#' .. x.funcName) < (y.moduleName .. '#' .. y.funcName)
end)
table.sort(templateStylesList)
return {invokeList = invokeList, templateStylesList = templateStylesList}
end
local function getInvokeCallList(pageName)
return getTemplateDependencyList(pageName).invokeList
end
-- ============================================================
-- DPL-based "what links here" lookups (from RuneScape module)
-- ============================================================
local function getWhatLinksHere(pageName, namespace)
return dpl.ask{
namespace = namespace,
linksto = pageName,
distinct = 'strict',
ignorecase = true,
ordermethod = 'title',
allowcachedresults = true,
cacheperiod = 604800,
}
end
local function getInvokedByList(moduleName)
local whatTemplatesLinkHere = getWhatLinksHere(moduleName, NS_TEMPLATE_NAME)
local function lcfirst(str)
return str:gsub('^[Mm]odule:.', string.lower)
end
local invokedByList = {}
for _, templateName in ipairs(whatTemplatesLinkHere) do
local invokeList = getInvokeCallList(templateName)
for _, invokeData in ipairs(invokeList) do
if lcfirst(invokeData.moduleName) == lcfirst(moduleName) then
table.insert(invokedByList, {templateName = templateName, funcName = invokeData.funcName})
end
end
end
return invokedByList
end
local function getRequiredByLists(moduleName)
local whatModulesLinkHere = getWhatLinksHere(moduleName, NS_MODULE_NAME)
local requiredByList = arr{}
local loadedByList = arr{}
for _, callerName in ipairs(whatModulesLinkHere) do
if callerName:lower() ~= moduleName:lower() then
local lists = getRequireLists(
(mw.title.new(callerName):getContent() or '')
:gsub('%-%-%[(=-)%[.-%]%1%]', '')
:gsub('%-%-[^\n]*', '')
)
if arr.any(lists.require, function(x) return x:lower() == moduleName:lower() end) then
requiredByList:insert(callerName)
end
if arr.any(lists.loadData, function(x) return x:lower() == moduleName:lower() end) then
loadedByList:insert(callerName)
end
end
end
requiredByList = requiredByList:unique()
loadedByList = loadedByList:unique()
table.sort(requiredByList)
table.sort(loadedByList)
return {require = requiredByList, loadData = loadedByList}
end
-- ============================================================
-- Formatting helpers
-- ============================================================
local function collapseList(list, id, listType)
local text = string.format('%d %s', #list, listType)
local button = tooltip._span{name = id, alt = text}
list = arr.map(list, function(x) return '\n# ' .. x end)
local content = tooltip._div{name = id, content = '\n' .. table.concat(list) .. '\n\n'}
return {tostring(button) .. tostring(content)}
end
local function formatDynamicQueryLink(query)
local prefix = query:match('^([^/]+)')
local linkText = query:gsub('%%', '< ... >')
query = query:gsub('^Module?:', '')
query = query:gsub('([^/]+)/?', function(match)
if match == '%' then return '\\/[^\\/]+' else return '\\/"' .. match .. '"' end
end)
query = query:gsub('^\\/', '')
query = string.format(
'intitle:/%s%s/i -intitle:/%s\\/""/i -intitle:doc prefix:"%s"',
query, query:find('"$') and '' or '""', query, prefix
)
return string.format('<span class="plainlinks">[%s %s]</span>',
tostring(mw.uri.fullUrl('Special:Search', {search = query})), linkText)
end
local function formatModuleLinks(pages)
local links = arr{}
for _, moduleName in ipairs(pages) do
if moduleName:find('%%') then
links:insert(formatDynamicQueryLink(moduleName))
elseif builtins[moduleName] then
links:insert('[[' .. builtins[moduleName].link .. '|' .. moduleName .. ']]')
else
links:insert('[[' .. moduleName .. ']]')
end
end
return links
end
local function formatTemplateLinks(pages)
local links = arr{}
for _, templateName in ipairs(pages) do
if templateName:find(':') then
links:insert('[[' .. templateName .. ']]')
else
links:insert("'''{{" .. templateName .. "}}'''")
end
end
return links
end
local function formatTemplateStyleLinks(pages, dynamic)
local links = arr{}
for _, stylesName in ipairs(pages) do
if dynamic and stylesName:find('%%') then
links:insert(formatDynamicQueryLink(stylesName))
else
links:insert('[[' .. stylesName .. ']]')
end
end
return links
end
local function formatTemplateStylesList(callerName, templateStylesList, forModule)
templateStylesList = formatTemplateStyleLinks(templateStylesList, forModule)
local res = {}
if #templateStylesList > COLLAPSE_LIST_LENGTH_THRESHOLD then
templateStylesList = collapseList(templateStylesList, 'templateStyles', 'styles')
end
for _, item in ipairs(templateStylesList) do
table.insert(res, string.format(
"<div class='seealso'>'''%s''' uses styles from %s using [[mw:Special:MyLanguage/Help:TemplateStyles|TemplateStyles]].</div>",
callerName, item
))
end
return table.concat(res)
end
local function formatInvokeCallList(templateName, invokeList)
local res = {}
for _, item in ipairs(invokeList) do
table.insert(res, string.format(
"<div class='seealso'>'''%s''' invokes function '''%s''' in [[%s]] using [[mw:Special:MyLanguage/Extension:Scribunto|Lua]].</div>",
templateName, item.funcName, item.moduleName
))
end
return table.concat(res)
end
local function formatInvokedByList(moduleName, invokedByList)
local items = {}
for _, invoke in ipairs(invokedByList) do
table.insert(items, string.format("function '''%s''' is invoked by [[%s]]", invoke.funcName, invoke.templateName))
end
table.sort(items)
local res = {}
if #items > COLLAPSE_LIST_LENGTH_THRESHOLD then
table.insert(res, string.format(
"<div class='seealso'>'''%s''' is invoked by %s.</div>",
moduleName, collapseList(items, 'invokedBy', 'templates')[1]
))
else
for _, item in ipairs(items) do
table.insert(res, string.format(
"<div class='seealso'>'''%s's''' %s.</div>",
moduleName, item
))
end
end
return table.concat(res)
end
local function formatRequiredByList(moduleName, requiredByLists)
local requiredByList = formatModuleLinks(requiredByLists.require)
local loadedByList = formatModuleLinks(requiredByLists.loadData)
if #requiredByList > COLLAPSE_LIST_LENGTH_THRESHOLD then
requiredByList = collapseList(requiredByList, 'requiredBy', 'modules')
end
if #loadedByList > COLLAPSE_LIST_LENGTH_THRESHOLD then
loadedByList = collapseList(loadedByList, 'loadedBy', 'modules')
end
local res = {}
for _, name in ipairs(requiredByList) do
table.insert(res, string.format("<div class='seealso'>'''%s''' is required by %s.</div>", moduleName, name))
end
for _, name in ipairs(loadedByList) do
table.insert(res, string.format("<div class='seealso'>'''%s''' is loaded by %s.</div>", moduleName, name))
end
return table.concat(res)
end
local function formatImportList(currentPageName, moduleList, id, message)
moduleList = formatModuleLinks(moduleList)
if #moduleList > COLLAPSE_LIST_LENGTH_THRESHOLD then
moduleList = collapseList(moduleList, id, 'modules')
end
local res = arr.map(moduleList, function(moduleName)
return '<div class="seealso">' .. string.format(message, currentPageName, moduleName) .. '</div>'
end)
return table.concat(res)
end
local function formatUsedTemplatesList(currentPageName, usedTemplateList)
usedTemplateList = formatTemplateLinks(usedTemplateList)
local res = {}
if #usedTemplateList > COLLAPSE_LIST_LENGTH_THRESHOLD then
usedTemplateList = collapseList(usedTemplateList, 'usedTemplates', 'templates')
end
for _, templateName in ipairs(usedTemplateList) do
table.insert(res, string.format(
"<div class='seealso'>'''%s''' transcludes %s using <samp>frame:preprocess()</samp> or <samp>frame:expandTemplate()</samp>.</div>",
currentPageName, templateName
))
end
return table.concat(res)
end
local function messageBoxUnused()
local html = mw.html.create('table'):addClass('messagebox obsolete plainlinks')
html:tag('td')
:attr('width', '40px')
:wikitext('[[File:WikimediaUI-Alert.svg|center|30px|link=]]')
:done()
:tag('td')
:wikitext("'''This module is unused.'''")
:tag('div')
:css{['font-size'] = '0.85em', ['line-height'] = '1.45em'}
:wikitext('This module is neither invoked by a template nor required/loaded by another module.')
:done()
:done()
return tostring(html)
end
-- ============================================================
-- Main entry points
-- ============================================================
local function templateDependencyList(currentPageName, addCategories)
local dependencyList = getTemplateDependencyList(currentPageName)
local res = arr{}
res:insert(formatInvokeCallList(currentPageName, dependencyList.invokeList))
res:insert(formatTemplateStylesList(currentPageName, dependencyList.templateStylesList))
if addCategories then
if #dependencyList.templateStylesList > 0 then
res:insert('[[Category:Templates using TemplateStyles]]')
end
if #dependencyList.invokeList > 0 then
res:insert('[[Category:Lua-based templates]]')
end
end
return table.concat(res)
end
local function moduleDependencyList(currentPageName, addCategories, isUsed)
local moduleContent = mw.title.new(currentPageName):getContent()
assert(moduleContent, string.format('Failed to retrieve text content of page "%s"', currentPageName))
moduleContent = moduleContent:gsub('%-%-%[(=-)%[.-%]%1%]', ''):gsub('%-%-[^\n]*', '')
local requireLists = getRequireLists(moduleContent)
local usedTemplateList = getUsedTemplatesList(moduleContent)
local requiredByLists = getRequiredByLists(currentPageName)
local invokedByList = getInvokedByList(currentPageName)
local res = arr{}
res:insert(formatInvokedByList(currentPageName, invokedByList))
res:insert(formatImportList(currentPageName, requireLists.require, 'require', "'''%s''' requires %s."))
res:insert(formatImportList(currentPageName, requireLists.loadData, 'loadData', "'''%s''' loads data from %s."))
res:insert(formatUsedTemplatesList(currentPageName, usedTemplateList.usedTemplateList))
res:insert(formatTemplateStylesList(currentPageName, usedTemplateList.templateStylesList, true))
res:insert(formatRequiredByList(currentPageName, requiredByLists))
if addCategories then
res:insert(requireLists.extraCategories, true)
if #usedTemplateList.templateStylesList > 0 then res:insert('[[Category:Modules using TemplateStyles]]') end
if #requireLists.require > 0 then res:insert('[[Category:Modules requiring modules]]') end
if #requireLists.loadData > 0 then res:insert('[[Category:Modules using data]]') end
if #requiredByLists.require > 0 then res:insert('[[Category:Modules required by modules]]') end
if #requiredByLists.loadData > 0 then res:insert('[[Category:Module data]]') end
if #invokedByList > 0 then res:insert('[[Category:Template invoked modules]]') end
end
if not (
yn(isUsed)
or currentPageName:lower():find('sandbox')
or #requiredByLists.require > 0
or #requiredByLists.loadData > 0
or #invokedByList > 0
) then
table.insert(res, 1, messageBoxUnused())
if addCategories then res:insert('[[Category:Unused modules]]') end
end
return table.concat(res)
end
function p.main(frame)
local args = frame:getParent().args
return p._main(args[1], args.category, args.isUsed)
end
function p._main(currentPageName, addCategories, isUsed)
libraryUtil.checkType('Module:DependencyList._main', 1, currentPageName, 'string', true)
libraryUtil.checkTypeMulti('Module:DependencyList._main', 2, addCategories, {'boolean', 'string', 'nil'})
libraryUtil.checkTypeMulti('Module:DependencyList._main', 3, isUsed, {'boolean', 'string', 'nil'})
local title = mw.title.getCurrentTitle()
if param.is_empty(currentPageName) and
not arr.contains({NS_MODULE_NAME, NS_TEMPLATE_NAME}, title.nsText) then
return ''
end
currentPageName = param.default_to(currentPageName, title.fullText)
currentPageName = currentPageName:gsub('/[Dd]oc$', '')
currentPageName = formatPageName(currentPageName)
if addCategories == nil then
addCategories = title.subpageText ~= 'doc'
end
addCategories = yn(addCategories)
dpl = require('Module:DPLlua')
if currentPageName:find('^' .. NS_TEMPLATE_NAME .. ':') then
return templateDependencyList(currentPageName, addCategories)
end
return moduleDependencyList(currentPageName, addCategories, isUsed)
end
return p
-- </nowiki>