Toggle menu
Toggle preferences menu
Toggle personal menu
Not logged in
Your IP address will be publicly visible if you make any edits.

Module:DependencyList: Difference between revisions

From Deepspace Lore
No edit summary
No edit summary
Line 2: Line 2:
--- @see https://runescape.wiki/w/Module:DependencyList
--- @see https://runescape.wiki/w/Module:DependencyList


require("strict")
-- <nowiki>
require('strict')
 
local p = {}
local p = {}
local libraryUtil = require('libraryUtil')
local libraryUtil = require('libraryUtil')
Line 8: Line 10:
local yn = require('Module:Yesno')
local yn = require('Module:Yesno')
local param = require('Module:Paramtest')
local param = require('Module:Paramtest')
local dpl = require('Module:DPLlua')
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 tooltip = require('Module:Tooltip')
local hatnote = require( 'Module:Hatnote' )._hatnote
 
local mHatlist = require( 'Module:Hatnote list' )
local dpl -- lazy loaded
local mbox = require( 'Module:Mbox' )._mbox
 
local COLLAPSE_LIST_LENGTH_THRESHOLD = 5
local COLLAPSE_LIST_LENGTH_THRESHOLD = 5
local MAX_DYNAMIC_REQUIRE_LIST_LENGTH = 30
local MAX_DYNAMIC_REQUIRE_LIST_LENGTH = 30
local dynamicRequireListQueryCache = {}
local dynamicRequireListQueryCache = {}
local NS_MODULE_NAME = mw.site.namespaces[828].name
local NS_TEMPLATE_NAME = mw.site.namespaces[10].name


local builtins = {
local builtins = {
["libraryUtil"] = {
    ['libraryUtil'] = {
link = "mw:Special:MyLanguage/Extension:Scribunto/Lua reference manual#libraryUtil",
        link = 'mw:Special:MyLanguage/Extension:Scribunto/Lua reference manual#libraryUtil',
categories = {},
        categories = {},
},
    },
["strict"] = {
    ['strict'] = {
link = "mw:Special:MyLanguage/Extension:Scribunto/Lua reference manual#strict",
        link = 'mw:Special:MyLanguage/Extension:Scribunto/Lua reference manual#strict',
categories = { "[[Category:Strict mode modules]]" },
        categories = { '[[Category:Strict mode modules]]' },
},
    },
}
}


-- Used in case 'require( varName )' is found. Attempts to find a string value stored in 'varName'.
-- ============================================================
local function substVarValue( moduleContent, varName )
-- String / name utilities
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 )
local function substVarValue(moduleContent, varName)
else
    local res = moduleContent:match(varName .. '%s*=%s*(%b""%s-%.*)') or
return ''
                moduleContent:match(varName .. "%s*=%s*(%b''%s-%.*)")or ''
end
    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
end


local function extractModuleName( capture, moduleContent )
local function extractModuleName(capture, moduleContent)
capture = capture:gsub( '^%(%s*(.-)%s*%)$', '%1' )
    capture = capture:gsub('^%(%s*(.-)%s*%)$', '%1')
 
    if capture:find('^(["\']).-%1$') then
if capture:find( '^(["\']).-%1$' ) then -- Check if it is already a pure string
        return capture
return capture
    elseif capture:find('^[%a_][%w_]*$') then
elseif capture:find( '^[%a_][%w_]*$' ) then -- Check if if is a single variable
        return substVarValue(moduleContent, capture)
return substVarValue( moduleContent, capture )
    end
end
    return capture
 
return capture
end
end


local function formatPageName( str )
local function formatPageName(str)
local name = mw.text.trim(str)
    return mw.text.trim(str)
:gsub( '^(["\'])(.-)%1$', '%2' ) -- Only remove quotes at start and end of string if both are the same type
        :gsub('^(["\'])(.-)%1$', '%2')
:gsub( '_', ' ' )
        :gsub('_', ' ')
:gsub( '^.', string.upper )
        :gsub('^.', string.upper)
:gsub( '^([^:]-:)(.)', function(a,b) return a..string.upper(b) end )
        :gsub('^([^:]-:)(.)', function(a, b) return a .. string.upper(b) end)
 
return name
end
end


local function formatModuleName( str, allowBuiltins )
local function formatModuleName(str, allowBuiltins)
if allowBuiltins then
    if allowBuiltins then
local name = mw.text.trim(str)
        local name = mw.text.trim(str):gsub('^(["\'])(.-)%1$', '%2')
-- Only remove quotes at start and end of string if both are the same type
        if builtins[name] then return name end
:gsub('^(["\'])(.-)%1$', '%2')
    end
 
    local module = formatPageName(str)
if builtins[name] then
    if not module:find('^[Mm]odule:') then
return name
        module = NS_MODULE_NAME .. ':' .. module
end
    end
end
    return module
 
local module = formatPageName( str )
 
if not string.find( module, '^[Mm]odule:' ) then
module = 'Module:' .. module
end
 
return module
end
end


local function multiGmatch( str, ... )
local function isDynamicPath(str)
local generators = {}
    return str:find('%.%.') or str:find('%%%a')
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
end


local function isDynamicPath( str )
local function multiGmatch(str, ...)
return string.find( str, '%.%.' ) or string.find( str, '%%%a' )
    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
end


-- Used in case a construct like 'require( "Module:wowee/" .. isTheBest )' is found.
-- ============================================================
-- Will return a list of pages which satisfy this pattern where 'isTheBest' can take any value.
-- 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', '%%' ) -- Replace lua string.format specifiers with a dpl wildcard
end
query = query:gsub( '^[Mm]odule:', '' )


query = mw.language.getContentLanguage():ucfirst( query )
local function getDynamicRequireList(query)
if query:find( '^Exchange/' ) or query:find( '^Data/' ) then
    if query:find('%.%.') then
return { 'Module:' .. query }  -- This format will later be used by formatDynamicQueryLink()
        query = mw.text.split(query, '..', true)
end
        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
    if dynamicRequireListQueryCache[query] then
return dynamicRequireListQueryCache[ query ]
        return dynamicRequireListQueryCache[query]
end
    end


local list = dpl.ask{
    local list = dpl.ask{
namespace = 'Module',
        namespace = NS_MODULE_NAME,
titlematch = query,
        titlematch = query,
nottitlematch = '%/doc|'..query..'/%',
        nottitlematch = '%/doc|' .. query .. '/%',
distinct = 'strict',
        distinct = 'strict',
ordermethod = 'title',
        ordermethod = 'title',
count = MAX_DYNAMIC_REQUIRE_LIST_LENGTH + 1,
        count = MAX_DYNAMIC_REQUIRE_LIST_LENGTH + 1,
skipthispage = 'no',
        skipthispage = 'no',
allowcachedresults = true,
        allowcachedresults = true,
cacheperiod = 604800 -- One week
        cacheperiod = 604800,
}
    }


if #list > MAX_DYNAMIC_REQUIRE_LIST_LENGTH then
    if #list > MAX_DYNAMIC_REQUIRE_LIST_LENGTH then
list = { 'Module:' .. query }
        list = {'Module:' .. query}
end
    end


dynamicRequireListQueryCache[ query ] = list
    dynamicRequireListQueryCache[query] = list
 
    return list
return list
end
end


--- Returns a list of modules loaded and required by module 'moduleName'.
-- ============================================================
local function getRequireLists( moduleContent )
-- Require / loadData list parsing
local requireList = arr{}
-- ============================================================
local loadDataList = arr{}
local extraCategories = arr{}


local function getList( list, patterns )
local function getRequireLists(moduleContent)
for match in multiGmatch( moduleContent, unpack( patterns ) ) do
    local requireList = arr{}
match = mw.text.trim( match )
    local loadDataList = arr{}
local name = extractModuleName( match, moduleContent )
    local extraCategories = arr{}


if isDynamicPath( name ) then
    local function getList(list, patterns)
list:insert( getDynamicRequireList( name ), true )
        for match in multiGmatch(moduleContent, unpack(patterns)) do
elseif name ~= '' then
            match = mw.text.trim(match)
name = formatModuleName( name, true )
            local name = extractModuleName(match, moduleContent)
table.insert( list, name )
            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


if builtins[name] then
    getList(requireList, {
extraCategories = extraCategories:insert( builtins[name].categories, true )
        'require%s*(%b())',
end
        'require%s*((["\'])%s*[Mm]odule:.-%2)',
end
        'pcall%s*%(%s*require%s*,([^%),]+)',
end
    })
end
    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*,([^%),]+)',
    })


local requirePatterns = {
    requireList = requireList:unique()
'require%s*(%b())',
    loadDataList = loadDataList:unique()
'require%s*((["\'])%s*[Mm]odule:.-%2)',
    extraCategories = extraCategories:unique()
'pcall%s*%(%s*require%s*,([^%),]+)'
    table.sort(requireList)
}
    table.sort(loadDataList)
    table.sort(extraCategories)


local loadDataPatterns = {
    return {require = requireList, loadData = loadDataList, extraCategories = extraCategories}
'mw%.loadData%s*(%b())',
end
'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*,([^%),]+)'
}
 
getList( requireList, requirePatterns )
getList( loadDataList, loadDataPatterns )
 
requireList = requireList:unique()
loadDataList = loadDataList:unique()
extraCategories = extraCategories:unique()
table.sort( requireList )
table.sort( loadDataList )
table.sort( extraCategories )


return {
-- ============================================================
require = requireList,
-- TemplateStyles / used-template detection
loadData = loadDataList,
-- ============================================================
extraCategories = extraCategories,
}
end


local function insertTemplateStyle( styleName, templateStylesList )
local function insertTemplateStyle(styleName, list)
styleName = formatPageName( styleName )
    styleName = formatPageName(styleName)
if not styleName:find( ':' ) then
    if not styleName:find(':') then styleName = 'Template:' .. styleName end
styleName = "Template:" .. styleName
    if isDynamicPath(styleName) then
end
        list:insert(getDynamicRequireList(styleName), true)
if isDynamicPath(styleName) then
    else
templateStylesList:insert( getDynamicRequireList( styleName ), true )
        list:insert(styleName)
else
    end
templateStylesList:insert( styleName )
end
end
end


local function extractTemplateStyles( pageContent, templateStylesList )
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
    for _, styleName in string.gmatch(
styleName = formatPageName( styleName )
        pageContent,
if styleName ~= '' then
        '<[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee][Ss][Tt][Yy][Ll][Ee][Ss]%s+[Ss][Rr][Cc]=(["\'])(.-)%1'
insertTemplateStyle( styleName, templateStylesList )
    ) do
end
        styleName = formatPageName(styleName)
end
        if styleName ~= '' then insertTemplateStyle(styleName, list) end
    end
end
end


local function recursiveGMatch( str, pat )
local function recursiveGMatch(str, pat)
local list = {}
    local list = {}
local i = 0
    local i = 0
repeat
    repeat
for match in string.gmatch( list[i] or str, pat ) do
        for match in string.gmatch(list[i] or str, pat) do
table.insert( list, match )
            table.insert(list, match)
end
        end
i = i + 1
        i = i + 1
until i > #list or i > 100
    until i > #list or i > 100
 
    i = 0
i = 0
    return function()
return function()
        i = i + 1
i = i + 1
        return list[i]
return list[i]
    end
end
end
end


local function formatTemplate( name )
local function formatTemplate(name)
if name:find( ':' ) then
    if name:find(':') then
local ns = name:match( '^(.-):' )
        local ns = name:match('^(.-):')
if arr.contains( {'', 'template', 'calculator', 'user'}, ns:lower() ) then
        if arr.contains({'', 'template', 'user'}, ns:lower()) then
return name
            return name
elseif ns == ns:upper() then
        elseif ns == ns:upper() then
return ns -- Probably a magic word
            return ns
end
        end
else
    else
if name:match( '^%u+$' ) or name == '!' then
        if name:match('^%u+$') or name == '!' then
return name -- Probably a magic word
            return name
else
        else
return 'Template:'..name
            return 'Template:' .. name
end
        end
end
    end
end
end


local function getUsedTemplatesList( moduleContent )
local function getUsedTemplatesList(moduleContent)
local usedTemplateList = arr{}
    local usedTemplateList = arr{}
local templateStylesList = 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 )
    for preprocess in string.gmatch(moduleContent, ':preprocess%s*(%b())') do
end
        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
    for capture in string.gmatch(moduleContent, 'expandTemplate%s*%(?%s*{%s*title%s*=%s*((["\'])%s*.-%2)') do
local name = formatPageName( capture )
        local name = formatPageName(capture)
if name ~= '' then
        if name ~= '' then usedTemplateList:insert(formatTemplate(name)) end
usedTemplateList:insert( formatTemplate( name ) )
    end
end
end


for _, capture in multiGmatch(
    for _, capture in multiGmatch(
moduleContent,
        moduleContent,
'extensionTag%s*%(%s*'
        'extensionTag%s*%(%s*'
.. '(["\'])[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee][Ss][Tt][Yy][Ll][Ee][Ss]%1%s*,'
            .. '(["\'])[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee][Ss][Tt][Yy][Ll][Ee][Ss]%1%s*,'
.. '.-,'
            .. '.-,'
.. '%s*{%s*src%s*=%s*((["\'])%s*.-%3)',
            .. '%s*{%s*src%s*=%s*((["\'])%s*.-%3)',
'extensionTag%s*%(?%s*{%s*'
        'extensionTag%s*%(?%s*{%s*'
             .. 'name%s*=%s*(["\'])[Tt][Ee][Mm][Pp][Ll][Aa][Tt][Ee][Ss][Tt][Yy][Ll][Ee][Ss]%1'
             .. '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)'
             .. 'args%s*=%s*{%s*src%s*=%s*((["\'])%s*.-%3)'
) do
    ) do
local name = formatPageName( capture )
        local name = formatPageName(capture)
if name ~= '' then
        if name ~= '' then insertTemplateStyle(name, templateStylesList) end
insertTemplateStyle( name, templateStylesList )
    end
end
end


usedTemplateList = usedTemplateList:unique()
    usedTemplateList = usedTemplateList:unique()
templateStylesList = templateStylesList:unique()
    templateStylesList = templateStylesList:unique()
table.sort( usedTemplateList )
    table.sort(usedTemplateList)
table.sort( templateStylesList )
    table.sort(templateStylesList)


return {
    return {usedTemplateList = usedTemplateList, templateStylesList = templateStylesList}
usedTemplateList = usedTemplateList,
templateStylesList = templateStylesList,
}
end
end


-- Returns all dependencies of the template found on page 'pageName'
-- ============================================================
local function getTemplateDependencyList( pageName )
-- Template dependency list (invoke + TemplateStyles)
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 ) )
local function getTemplateDependencyList(pageName)
    local pageContent = mw.title.new(pageName):getContent()
    local invokeList = {}
    local templateStylesList = arr{}


for moduleName, funcName in string.gmatch( pageContent, '{{[{|safeubt:}]-#[Ii]nvoke:([^|]+)|([^}|]+)[^}]*}}' ) do
    assert(pageContent, string.format('Failed to retrieve text content of page "%s"', pageName))
moduleName = formatModuleName( moduleName )
funcName = mw.text.trim( funcName )
if string.find( funcName, '^{{{' ) then
funcName = funcName ..  '}}}'
end
table.insert( invokeList, {moduleName=moduleName, funcName=funcName} )
end


-- For form calcs invoking the module directly
    for moduleName, funcName in string.gmatch(
for config in multiGmatch( pageContent, '<[pd][ri][ev]%s+class%s*=%s*["\']jcConfig["\'](.-)</[pd][ri][ev]>', '{{[Ff]orm calculator%s*|(.+)}}' ) do
        pageContent,
local moduleName = string.match( config, 'module%s*=%s*(.-)[\n|]' )
        '{{[{|safeubt:}]-#[Ii]nvoke:([^|]+)|([^}|]+)[^}]*}}'
if param.has_content( moduleName ) then
    ) do
moduleName = formatModuleName( moduleName )
        moduleName = formatModuleName(moduleName)
local funcName = string.match( config, 'modulefunc%s*=%s*(.-)[\n|]' ) or 'main'
        funcName = mw.text.trim(funcName)
table.insert( invokeList, {moduleName=moduleName, funcName=funcName} )
        if funcName:find('^{{{') then funcName = funcName .. '}}}' end
end
        table.insert(invokeList, {moduleName = moduleName, funcName = funcName})
end
    end


extractTemplateStyles( pageContent, templateStylesList )
    extractTemplateStyles(pageContent, templateStylesList)


invokeList = arr.unique( invokeList, function(x) return (x.moduleName .. '#' .. x.funcName) end )
    invokeList = arr.unique(invokeList, function(x) return x.moduleName .. '#' .. x.funcName end)
templateStylesList = templateStylesList:unique()
    templateStylesList = templateStylesList:unique()
table.sort( invokeList, function(x, y) return (x.moduleName .. '#' .. x.funcName) < (y.moduleName .. '#' .. y.funcName) end )
    table.sort(invokeList, function(x, y)
table.sort( templateStylesList )
        return (x.moduleName .. '#' .. x.funcName) < (y.moduleName .. '#' .. y.funcName)
    end)
    table.sort(templateStylesList)


return {
    return {invokeList = invokeList, templateStylesList = templateStylesList}
invokeList = invokeList,
templateStylesList = templateStylesList,
}
end
end


-- Returns a list with module and function names used in all '{{#Invoke:moduleName|funcName}}' found on page 'templateName'.
local function getInvokeCallList(pageName)
local function getInvokeCallList( pageName )
    return getTemplateDependencyList(pageName).invokeList
return getTemplateDependencyList( pageName ).invokeList
end
end


local function getInvokedByList( moduleName )
-- ============================================================
local whatTemplatesLinkHere = dpl.ask( {
-- DPL-based "what links here" lookups (from RuneScape module)
namespace = 'Template|Calculator',
-- ============================================================
linksto = moduleName,
distinct = 'strict',
ordermethod = 'title',
allowcachedresults = true,
cacheperiod = 604800 -- One week
} )
 
local function lcfirst( str )
return string.gsub( str, '^[Mm]odule:.', string.lower )
end
 
local invokedByList = {}
 
for _, templateName in ipairs( whatTemplatesLinkHere ) do
local invokeList = getInvokeCallList( templateName )


for _, invokeData in ipairs( invokeList ) do
local function getWhatLinksHere(pageName, namespace)
if lcfirst( invokeData.moduleName ) == lcfirst( moduleName ) then
    return dpl.ask{
table.insert( invokedByList, { templateName=templateName, funcName=invokeData.funcName } )
        namespace = namespace,
end
        linksto = pageName,
end
        distinct = 'strict',
end
        ignorecase = true,
 
        ordermethod = 'title',
return invokedByList
        allowcachedresults = true,
        cacheperiod = 604800,
    }
end
end


local function messageBoxUnused()
local function getInvokedByList(moduleName)
local html = mw.html.create( 'table' ):addClass( 'messagebox obsolete plainlinks' )
    local whatTemplatesLinkHere = getWhatLinksHere(moduleName, NS_TEMPLATE_NAME)
html:tag( 'td' )
:attr( 'width', '40xp' )
:wikitext( '[[File:Iron full helm detail old.png|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. If this is in error, make sure to add <code>{{[[Template:Documentation|Documentation]]}}</code>/<code>{{[[Template:No documentation|No&nbsp;documentation]]}}</code> to the calling template\'s or parent\'s module documentation.' )
:done()
:done()
 
return tostring( html )
end


local function collapseList( list, id, listType )
    local function lcfirst(str)
local text = string.format( '%d %s', #list, listType )
        return str:gsub('^[Mm]odule:.', string.lower)
local button = tooltip._span{ name=id, alt=text }
    end
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 ) }
    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
end


-- Creates a link to [[Special:Search]] showing all pages found by getDynamicRequireList() in case it found more than MAX_DYNAMIC_REQUIRE_LIST_LENGTH pages.
local function getRequiredByLists(moduleName)
-- Input query uses DPL % wildcards like 'Module:Wowee/%' or 'Module:Wowee/%/data'
    local whatModulesLinkHere = getWhatLinksHere(moduleName, NS_MODULE_NAME)
local function formatDynamicQueryLink( query )
local prefix = query:match( '^([^/]+)' )
local linkText = query:gsub( '%%', '&lt; ... &gt;' )


query = query:gsub( '^Module:',  '' )
    local requiredByList = arr{}
    local loadedByList = arr{}


query = query:gsub( '([^/]+)/?', function ( match )
    for _, callerName in ipairs(whatModulesLinkHere) do
if match == '%' then
        if callerName:lower() ~= moduleName:lower() then
return '\\/[^\\/]+'
            local lists = getRequireLists(
else
                (mw.title.new(callerName):getContent() or '')
return '\\/"' .. match .. '"'
                    :gsub('%-%-%[(=-)%[.-%]%1%]', '')
end
                    :gsub('%-%-[^\n]*', '')
end )
            )
            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


query = query:gsub( '^\\/', '' )
    requiredByList = requiredByList:unique()
    loadedByList = loadedByList:unique()
    table.sort(requiredByList)
    table.sort(loadedByList)


query = string.format(
    return {require = requiredByList, loadData = loadedByList}
'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
end


local function formatModuleLinks( pages )
-- ============================================================
local links = arr{}
-- Formatting helpers
-- ============================================================


for _, moduleName in ipairs(pages) do
local function collapseList(list, id, listType)
if moduleName:find( '%%' ) then
    local text = string.format('%d %s', #list, listType)
links:insert( formatDynamicQueryLink( moduleName ) )
    local button = tooltip._span{name = id, alt = text}
elseif builtins[moduleName] then
    list = arr.map(list, function(x) return '\n# ' .. x end)
links:insert( '[[' .. builtins[moduleName].link .. '|' .. moduleName .. ']]' )
    local content = tooltip._div{name = id, content = '\n' .. table.concat(list) .. '\n\n'}
else
    return {tostring(button) .. tostring(content)}
links:insert( '[[' .. moduleName .. ']]' )
end
end
end


return links
local function formatDynamicQueryLink(query)
    local prefix = query:match('^([^/]+)')
    local linkText = query:gsub('%%', '&lt; ... &gt;')
    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
end


local function formatTemplateLinks( pages )
local function formatModuleLinks(pages)
local links = arr{}
    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


for _, templateName in ipairs(pages) do
local function formatTemplateLinks(pages)
if string.find( templateName, ':' ) then -- Real templates are prefixed by a namespace, magic words are not
    local links = arr{}
links:insert( '[['..templateName..']]' )
    for _, templateName in ipairs(pages) do
else
        if templateName:find(':') then
links:insert( "'''&#123;&#123;"..templateName.."&#125;&#125;'''" ) -- Magic words don't have a page so make them bold instead
            links:insert('[[' .. templateName .. ']]')
end
        else
end
            links:insert("'''&#123;&#123;" .. templateName .. "&#125;&#125;'''")
 
        end
return links
    end
    return links
end
end


local function formatTemplateStyleLinks( pages, dynamic )
local function formatTemplateStyleLinks(pages, dynamic)
local links = arr{}
    local links = arr{}
 
    for _, stylesName in ipairs(pages) do
for _, stylesName in ipairs( pages ) do
        if dynamic and stylesName:find('%%') then
if dynamic and stylesName:find( '%%' ) then
            links:insert(formatDynamicQueryLink(stylesName))
links:insert( formatDynamicQueryLink( stylesName ) )
        else
else
            links:insert('[[' .. stylesName .. ']]')
links:insert( '[[' .. stylesName .. ']]' )
        end
end
    end
end
    return links
 
return links
end
end


--- @param callerName string
local function formatTemplateStylesList(callerName, templateStylesList, forModule)
--- @param templateStylesList string[]
    templateStylesList = formatTemplateStyleLinks(templateStylesList, forModule)
--- @param forModule? boolean
    local res = {}
--- @return string
    if #templateStylesList > COLLAPSE_LIST_LENGTH_THRESHOLD then
local function formatTemplateStylesList( callerName, templateStylesList, forModule )
        templateStylesList = collapseList(templateStylesList, 'templateStyles', 'styles')
templateStylesList = formatTemplateStyleLinks( templateStylesList, forModule )
    end
local res = {}
    for _, item in ipairs(templateStylesList) do
 
        table.insert(res, string.format(
if #templateStylesList > COLLAPSE_LIST_LENGTH_THRESHOLD then
            "<div class='seealso'>'''%s''' uses styles from %s using [[mw:Special:MyLanguage/Help:TemplateStyles|TemplateStyles]].</div>",
templateStylesList = collapseList( templateStylesList, 'templateStyles', 'styles' )
            callerName, item
end
        ))
 
    end
for _, item in ipairs( templateStylesList ) do
    return table.concat(res)
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
end


local function formatInvokeCallList( templateName, invokeList )
local function formatInvokeCallList(templateName, invokeList)
local res = {}
    local res = {}
 
    for _, item in ipairs(invokeList) do
for _, item in ipairs( invokeList ) do
        table.insert(res, string.format(
table.insert( res, string.format(
            "<div class='seealso'>'''%s''' invokes function '''%s''' in [[%s]] using [[mw:Special:MyLanguage/Extension:Scribunto|Lua]].</div>",
"<div class='seealso'>'''%s''' invokes function '''%s''' in [[%s]] using [[RuneScape:Lua|Lua]].</div>",
            templateName, item.funcName, item.moduleName
templateName,
        ))
item.funcName,
    end
item.moduleName
    return table.concat(res)
) )
end
 
return table.concat( res )
end
end


local function formatInvokedByList( moduleName, invokedByList )
local function formatInvokedByList(moduleName, invokedByList)
for i, invoke in ipairs( invokedByList ) do
    local items = {}
invokedByList[i] = string.format( "function '''%s''' is invoked by [[%s]]", invoke.funcName, invoke.templateName )
    for _, invoke in ipairs(invokedByList) do
end
        table.insert(items, string.format("function '''%s''' is invoked by [[%s]]", invoke.funcName, invoke.templateName))
    end
    table.sort(items)


table.sort( invokedByList)
    local res = {}
 
    if #items > COLLAPSE_LIST_LENGTH_THRESHOLD then
local res = {}
        table.insert(res, string.format(
 
            "<div class='seealso'>'''%s''' is invoked by %s.</div>",
if #invokedByList > COLLAPSE_LIST_LENGTH_THRESHOLD then
            moduleName, collapseList(items, 'invokedBy', 'templates')[1]
table.insert( res, string.format(
        ))
"<div class='seealso'>'''%s''' is invoked by %s.</div>",
    else
moduleName,
        for _, item in ipairs(items) do
collapseList( invokedByList, 'invokedBy', 'templates' )[1]
            table.insert(res, string.format(
) )
                "<div class='seealso'>'''%s's''' %s.</div>",
else
                moduleName, item
for _, item in ipairs( invokedByList ) do
            ))
table.insert( res, string.format(
        end
"<div class='seealso'>'''%s's''' %s.</div>",
    end
moduleName,
    return table.concat(res)
item
) )
end
end
 
return table.concat( res )
end
end


local function formatRequiredByList( moduleName, requiredByLists )
local function formatRequiredByList(moduleName, requiredByLists)
local requiredByList = formatModuleLinks( requiredByLists.require )
    local requiredByList = formatModuleLinks(requiredByLists.require)
local loadedByList = formatModuleLinks( requiredByLists.loadData )
    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
    if #requiredByList > COLLAPSE_LIST_LENGTH_THRESHOLD then
loadedByList = collapseList( loadedByList, 'loadedBy', 'modules' )
        requiredByList = collapseList(requiredByList, 'requiredBy', 'modules')
end
    end
    if #loadedByList > COLLAPSE_LIST_LENGTH_THRESHOLD then
        loadedByList = collapseList(loadedByList, 'loadedBy', 'modules')
    end


local res = {}
    local res = {}
 
    for _, name in ipairs(requiredByList) do
for _, requiredByModuleName in ipairs( requiredByList ) do
        table.insert(res, string.format("<div class='seealso'>'''%s''' is required by %s.</div>", moduleName, name))
table.insert( res, string.format(
    end
"<div class='seealso'>'''%s''' is required by %s.</div>",
    for _, name in ipairs(loadedByList) do
moduleName,
        table.insert(res, string.format("<div class='seealso'>'''%s''' is loaded by %s.</div>", moduleName, name))
requiredByModuleName
    end
) )
    return table.concat(res)
end
 
for _, loadedByModuleName in ipairs( loadedByList ) do
table.insert( res, string.format(
"<div class='seealso'>'''%s''' is loaded by %s.</div>",
moduleName,
loadedByModuleName
) )
end
 
return table.concat( res )
end
end


local function formatImportList( currentPageName, moduleList, id, message )
local function formatImportList(currentPageName, moduleList, id, message)
moduleList = formatModuleLinks( moduleList )
    moduleList = formatModuleLinks(moduleList)
 
    if #moduleList > COLLAPSE_LIST_LENGTH_THRESHOLD then
if #moduleList > COLLAPSE_LIST_LENGTH_THRESHOLD then
        moduleList = collapseList(moduleList, id, 'modules')
moduleList = collapseList( moduleList, id, 'modules' )
    end
end
    local res = arr.map(moduleList, function(moduleName)
 
        return '<div class="seealso">' .. string.format(message, currentPageName, moduleName) .. '</div>'
local res = arr.map( moduleList, function( moduleName )
    end)
return '<div class="seealso">' .. string.format( message, currentPageName, moduleName ) .. '</div>'
    return table.concat(res)
end )
 
return table.concat( res )
end
end


local function formatUsedTemplatesList( currentPageName, usedTemplateList )
local function formatUsedTemplatesList(currentPageName, usedTemplateList)
usedTemplateList = formatTemplateLinks( usedTemplateList )
    usedTemplateList = formatTemplateLinks(usedTemplateList)
local res = {}
    local res = {}
 
    if #usedTemplateList > COLLAPSE_LIST_LENGTH_THRESHOLD then
if #usedTemplateList > COLLAPSE_LIST_LENGTH_THRESHOLD then
        usedTemplateList = collapseList(usedTemplateList, 'usedTemplates', 'templates')
usedTemplateList = collapseList( usedTemplateList, 'usedTemplates', 'templates' )
    end
end
    for _, templateName in ipairs(usedTemplateList) do
 
        table.insert(res, string.format(
for _, templateName in ipairs( usedTemplateList ) do
            "<div class='seealso'>'''%s''' transcludes %s using <samp>frame:preprocess()</samp> or <samp>frame:expandTemplate()</samp>.</div>",
table.insert( res, string.format(
            currentPageName, templateName
"<div class='seealso'>'''%s''' transcludes %s using <samp>frame:preprocess()</samp> or <samp>frame:expandTemplate()</samp>.</div>",
        ))
currentPageName,
    end
templateName
    return table.concat(res)
) )
end
 
return table.concat( res )
end
end


local function setBucketFields( requireLists )
local function messageBoxUnused()
if mw.title.getCurrentTitle().subpageText ~= 'doc' and (#requireLists.require > 0 or #requireLists.loadData > 0) then
    local html = mw.html.create('table'):addClass('messagebox obsolete plainlinks')
bucket( 'dependency_list' ).put{
    html:tag('td')
require = requireLists.require,
        :attr('width', '40px')
load_data = requireLists.loadData
        :wikitext('[[File:WikimediaUI-Alert.svg|center|30px|link=]]')
}
        :done()
end
        :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
end


local function getRequiredByLists( currentPageName )
-- ============================================================
local requiredByListRaw = bucket( 'dependency_list' ).select( 'page_name' ).where( 'require', currentPageName ).run()
-- Main entry points
local loadedByListRaw = bucket( 'dependency_list' ).select( 'page_name' ).where( 'load_data', currentPageName ).run()
-- ============================================================
local requiredByList = {}
local loadedByList = {}
 
for _, bucketItem in ipairs( requiredByListRaw ) do
table.insert( requiredByList, bucketItem.page_name )
end
for _, bucketItem in ipairs( loadedByListRaw ) do
table.insert( loadedByList, bucketItem.page_name )
end
 
requiredByList = arr.unique( requiredByList )
loadedByList = arr.unique( loadedByList )
table.sort( requiredByList )
table.sort( loadedByList )
 
return {
require = requiredByList,
loadData = loadedByList
}
end


local function templateDependencyList( currentPageName, addCategories )
local function templateDependencyList(currentPageName, addCategories)
local dependencyList = getTemplateDependencyList( currentPageName )
    local dependencyList = getTemplateDependencyList(currentPageName)
local res = arr{}
    local res = arr{}


res:insert( formatInvokeCallList( currentPageName, dependencyList.invokeList ) )
    res:insert(formatInvokeCallList(currentPageName, dependencyList.invokeList))
res:insert( formatTemplateStylesList( currentPageName, dependencyList.templateStylesList ) )
    res:insert(formatTemplateStylesList(currentPageName, dependencyList.templateStylesList))


if addCategories then
    if addCategories then
if #dependencyList.templateStylesList > 0 then
        if #dependencyList.templateStylesList > 0 then
res:insert( '[[Category:Templates using TemplateStyles]]' )
            res:insert('[[Category:Templates using TemplateStyles]]')
end
        end
if #dependencyList.invokeList > 0 then
        if #dependencyList.invokeList > 0 then
res:insert( '[[Category:Lua-based templates]]' )
            res:insert('[[Category:Lua-based templates]]')
end
        end
end
    end


return table.concat( res )
    return table.concat(res)
end
end


local function moduleDependencyList( currentPageName, addCategories, isUsed )
local function moduleDependencyList(currentPageName, addCategories, isUsed)
local moduleContent = mw.title.new( currentPageName ):getContent()
    local moduleContent = mw.title.new(currentPageName):getContent()
assert( moduleContent, string.format( 'Failed to retrieve text content of page "%s"', currentPageName ) )
    assert(moduleContent, string.format('Failed to retrieve text content of page "%s"', currentPageName))
moduleContent = moduleContent:gsub( '%-%-%[(=-)%[.-%]%1%]', '' ):gsub( '%-%-[^\n]*', '' ) -- Strip comments
    moduleContent = moduleContent:gsub('%-%-%[(=-)%[.-%]%1%]', ''):gsub('%-%-[^\n]*', '')


local requireLists = getRequireLists( moduleContent )
    local requireLists = getRequireLists(moduleContent)
local usedTemplateList = getUsedTemplatesList( moduleContent )
    local usedTemplateList = getUsedTemplatesList(moduleContent)
local requiredByLists = getRequiredByLists( currentPageName )
    local requiredByLists = getRequiredByLists(currentPageName)
local invokedByList = getInvokedByList( currentPageName )
    local invokedByList = getInvokedByList(currentPageName)


setBucketFields( requireLists )
    local res = arr{}


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))


res:insert( formatInvokedByList( currentPageName, invokedByList ) )
    if addCategories then
res:insert( formatImportList( currentPageName, requireLists.require, 'require', "'''%s''' requires %s." ) )
        res:insert(requireLists.extraCategories, true)
res:insert( formatImportList( currentPageName, requireLists.loadData, 'loadData', "'''%s''' loads data from %s." ) )
        if #usedTemplateList.templateStylesList > 0 then res:insert('[[Category:Modules using TemplateStyles]]') end
res:insert( formatUsedTemplatesList( currentPageName, usedTemplateList.usedTemplateList ) )
        if #requireLists.require > 0 then res:insert('[[Category:Modules requiring modules]]') end
res:insert( formatTemplateStylesList( currentPageName, usedTemplateList.templateStylesList, true ) )
        if #requireLists.loadData > 0 then res:insert('[[Category:Modules using data]]') end
res:insert( formatRequiredByList( currentPageName, requiredByLists ) )
        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 addCategories then
    if not (
res:insert( requireLists.extraCategories, true )
        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


if #usedTemplateList.templateStylesList > 0 then
    return table.concat(res)
res:insert( '[[Category:Modules using TemplateStyles]]' )
end
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
function p.main(frame)
not (
    local args = frame:getParent().args
yn( isUsed )
    return p._main(args[1], args.category, args.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
end


function p.main( frame )
function p._main(currentPageName, addCategories, isUsed)
local args = frame:getParent().args
    libraryUtil.checkType('Module:DependencyList._main', 1, currentPageName, 'string', true)
return p._main( args[1], args.category, args.isUsed )
    libraryUtil.checkTypeMulti('Module:DependencyList._main', 2, addCategories, {'boolean', 'string', 'nil'})
end
    libraryUtil.checkTypeMulti('Module:DependencyList._main', 3, isUsed, {'boolean', 'string', 'nil'})


function p._main( currentPageName, addCategories, isUsed )
    local title = mw.title.getCurrentTitle()
libraryUtil.checkType( 'Module:RequireList._main', 1, currentPageName, 'string', true )
libraryUtil.checkTypeMulti( 'Module:RequireList._main', 2, addCategories, {'boolean', 'string', 'nil'} )
libraryUtil.checkTypeMulti( 'Module:RequireList._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


-- Leave early if not in module, template or calculator namespace or if module is part of exchange or data groups
    currentPageName = param.default_to(currentPageName, title.fullText)
if param.is_empty( currentPageName ) and (
    currentPageName = currentPageName:gsub('/[Dd]oc$', '')
( not arr.contains( {'Module', 'Template', 'Calculator'}, title.nsText ) ) or
    currentPageName = formatPageName(currentPageName)
( title.nsText == 'Module' and ( arr.contains( {'Exchange', 'Exchange historical', 'Data'}, title.text:match( '^(.-)/' ) ) ) )
) then
return ''
end


currentPageName = param.default_to( currentPageName, title.fullText )
    if addCategories == nil then
currentPageName = string.gsub( currentPageName, '/[Dd]oc$', '' )
        addCategories = title.subpageText ~= 'doc'
currentPageName = formatPageName( currentPageName )
    end
    addCategories = yn(addCategories)


if (addCategories == nil) then
    dpl = require('Module:DPLlua')
addCategories = title.subpageText~='doc'
end
addCategories = yn(addCategories)


if currentPageName:find( '^Template:' ) or currentPageName:find( '^Calculator:' ) then
    if currentPageName:find('^' .. NS_TEMPLATE_NAME .. ':') then
return templateDependencyList( currentPageName, addCategories )
        return templateDependencyList(currentPageName, addCategories)
end
    end


return moduleDependencyList( currentPageName, addCategories, isUsed )
    return moduleDependencyList(currentPageName, addCategories, isUsed)
end
end


return p
return p
-- </nowiki>

Revision as of 16:31, 24 April 2026

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('%%', '&lt; ... &gt;')
    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("'''&#123;&#123;" .. templateName .. "&#125;&#125;'''")
        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>