Module:DependencyList

Error: no local variable "shortdesc" has been set.
Module documentation[view][edit][history][purge]
This documentation is transcluded from Module:DependencyList/doc. Changes can be proposed in the talk page.
Module:DependencyList's function main is invoked by Template:DependencyList.
Module:DependencyList requires Module:DPLlua.
Module:DependencyList requires Module:Enum.
Module:DependencyList requires Module:LibraryUtil.
Module:DependencyList requires Module:Paramtest.
Module:DependencyList requires Module:Yesno.
Module:DependencyList is required by Module:Documentation.

This template is meant to be used on module documentations.

{{DependencyList|<<Module name>>}}

All parameters are optional. If Module name is omitted the page name will be used instead; /doc is automatically removed.

If category is false then no categories will be added to the page, the default value is true.

If compact is true links will be separated by commas instead of every link on a new line.

Examples

{{DependencyList|<Module:For>}}

Module:For requires Module:Arguments.
Module:For requires Module:Hatnote.
Module:For requires Module:Hatnote list.
Module:For requires Module:Yesno.

{{DependencyList|<Module:Infobox>}}

Module:Infobox's function infobox is invoked by Template:Infobox.

-- <nowiki>
local p = {}
local libraryUtil = require( 'libraryUtil' )
local enum = require( 'Module:Enum' )
local yn = require( 'Module:Yesno' )
local param = require( 'Module:Paramtest' )
local dpl = require( 'Module:DPLlua' )
local moduleIsUsed = false
local MAX_DYNAMIC_REQUIRE_LIST_LENGTH = 30
local dynamicRequireListQueryCache = {}

--- Used in case 'require( varName )' is found. Attempts to find a string value stored in 'varName'.
---@param content string    @The content of the module to look in as a string
---@param varName string
---@return string
local function substVarValue( content, varName )
    if varName:find( "%b''" ) or varName:find( '%b""' ) then -- Look for balanced quotes
        return varName -- Is already a string
    else
        local res = content:match( varName .. '%s*=%s*(%b""%s-%.*)' ) or content:match( varName .. "%s*=%s*(%b''%s-%.*)" ) or ''
        if res:find( '^(["\'])[Mm]odule:[%S]+%1' ) and not res:find( '%.%.' ) then
            return mw.text.trim( res )
        else
            return ''
        end
    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.
---@param query string
---@return string[]     @Sequence of strings
local function getDynamicRequireList( query )
    query = mw.text.split( query, '..', true )
    query = enum.map( query, function(x) return mw.text.trim(x) end )
    query = enum.map( query, function(x) return (x:match('^[\'\"]([^\'\"]+)[\'\"]$') or '%') end )
    query = table.concat( query )
    query = query:gsub( '^[Mm]odule:', '' )

    if dynamicRequireListQueryCache[ query ] then
        return dynamicRequireListQueryCache[ query ]
    end

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

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

    dynamicRequireListQueryCache[ query ] = list

    return list
end

--- Returns a list of modules loaded and required by module 'moduleName'.
---@param moduleName string
---@return string[], string[]
local function getRequireList( moduleName )
    local content = mw.title.new( moduleName ):getContent()
    local requireList = {}
    local loadDataList = {}
    local usedTemplateList = {}
    local dynamicRequirelist = {}
    local dynamicLoadDataList = {}

    assert( param.has_content( content ), string.format( '%s does not exist', moduleName ) )

    content = content:gsub( '%-%-%[(=-)%[.-%]%1%]', '' ):gsub( '%-%-[^\n]*', '' ) -- Strip comments

    local function dualGmatch( str, pat1, pat2 )
        local f1 = string.gmatch( str, pat1 )
        local f2 = string.gmatch( str, pat2 )
        return function()
            return f1() or f2()
        end
    end

    for match in dualGmatch( content, 'require%s*%(([^%)]+)', 'require%s*((["\'])%s*[Mm]odule:.-%2)' ) do
        match = mw.text.trim( match )
        match = substVarValue( content, match )

        if match:find( '%.%.' ) then
            for _, x in ipairs( getDynamicRequireList( match ) ) do
                table.insert( dynamicRequirelist, x )
            end
        elseif match ~= '' then
            match = match:gsub( '[\"\']', '' ):gsub( '_', ' ' )

            if match == 'libraryUtil' then
                match = 'Module:LibraryUtil'
            end

            table.insert( requireList, match )
        end
    end

    for match in dualGmatch( content, 'mw%.loadData%s*%(([^%)]+)', 'mw%.loadData%s*((["\'])%s*[Mm]odule:.-%2)' ) do
        match = mw.text.trim( match )
        match = substVarValue( content, match )

        if match:find( '%.%.' ) then
            for _, x in ipairs( getDynamicRequireList( match ) ) do
                table.insert( dynamicLoadDataList, x )
            end
        elseif match ~= '' then
            match = match:gsub( '[\"\']', '' ):gsub( '_', ' ' )
            table.insert( loadDataList, match )
        end
    end

    for func, match in string.gmatch( content, 'pcall%s*%(([^,]+),([^%),]+)' ) do
        func = mw.text.trim( func )
        match = mw.text.trim( match )

        if func == 'require' then
            for _, x in ipairs( getDynamicRequireList( match ) ) do
                table.insert( dynamicRequirelist, x )
            end
        elseif func == 'mw.loadData' then
            for _, x in ipairs( getDynamicRequireList( match ) ) do
                table.insert( dynamicLoadDataList, x )
            end
        end
    end

    for preprocess in string.gmatch( content, ':preprocess%s*%((.-)%)' ) do
        for template in string.gmatch( preprocess, '{{(.-)[|}]' ) do
            if template ~= '' then
                if template:find( ':' ) then
                    table.insert( usedTemplateList, template )
                else
                    table.insert( usedTemplateList, 'Template:' .. template )
                end
            end
        end
    end

    dynamicRequirelist = enum.reject( dynamicRequirelist, function(x) return (enum.contains(requireList, x) or enum.contains(loadDataList, x)) end )
    dynamicLoadDataList = enum.reject( dynamicLoadDataList, function(x) return (enum.contains(requireList, x) or enum.contains(loadDataList, x)) end )
    requireList = enum.insert( requireList, dynamicRequirelist )
    requireList = enum.unique( requireList )
    loadDataList = enum.insert( loadDataList, dynamicLoadDataList )
    loadDataList = enum.unique( loadDataList )
    usedTemplateList = enum.unique( usedTemplateList )
    table.sort( requireList )
    table.sort( loadDataList )
    table.sort( usedTemplateList )

    return requireList, loadDataList, usedTemplateList
end

--- Makes the first letter of the input string upper case
---@param str string
---@return string
local function ucfirst( str )
    return str:gsub( '^.', function(c) return c:upper() end )
end

--- Returns a list with module and function names used in all '{{#Invoke:moduleName|funcName}}' found on page 'templateName'.
---@param templateName string
---@return table<string, string>[]
local function getInvokeCallList( templateName )
    local content = mw.title.new( templateName ):getContent()
    local invokeList = {}

    assert( param.has_content( content ), string.format( '%s does not exist', templateName ) )

    for moduleName, funcName in string.gmatch( content, '{{[{|safeubt:}]-#[Ii]nvoke:([^|]+)|([^}|]+)[^}]*}}' ) do
        moduleName = ucfirst( moduleName )
        moduleName = string.format( 'Module:%s', moduleName )
        moduleName = moduleName:gsub( '_', ' ' )
        table.insert( invokeList, {moduleName=moduleName, funcName=funcName} )
    end

    invokeList = enum.unique( invokeList, function(x) return x.moduleName..x.funcName end )
    table.sort( invokeList, function(x, y) return x.moduleName..x.funcName < y.moduleName..y.funcName end )

    return invokeList
end

---@param pageName string
---@param addCategories boolean
---@return string
local function messageBoxUnused( pageName, addCategories )
    local html = mw.html.create( 'div' ):addClass( 'mbox mbox-med mbox-unusedmodule' ):attr( 'role', 'presentation')
    html:tag( 'span' )
            :addClass( 'mbox-title' )
            :tag( 'span' )
                :addClass( 'mbox-icon metadata' )
                :wikitext( '[[File:WikimediaUI-Alert.svg|14px|link=]]' )
                :done()
            :wikitext( 'This module is unused.' )
            :done()
        :tag( 'span' )
            :addClass( 'mbox-text' )
            :wikitext(
                string.format( '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.', 
                    pageName 
                )
            )
            :wikitext( addCategories and '[[Category:Unused modules]]' or '' )
            :done()
        :done()
    return tostring( html )
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.
---@param query string      @This will be in a format like 'Module:Wowee/%' or 'Module:Wowee/%/data'
---@return string
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

---@param templateName string
---@param addCategories boolean
---@param invokeList table<string, string>[]    @This is the list returned by getInvokeCallList()
---@return string
local function formatInvokeCallList( templateName, addCategories, invokeList )
    local category = addCategories and '[[Category:Lua-based templates]]' or ''
    local res = {}

    for _, item in ipairs( invokeList ) do
        table.insert( res, string.format(
            "<div role='note' class='hatnote'><div class=hatnote-container navigation-not-searchable'><span class='hatnote-icon metadata'>[[File:WikimediaUI-Code.svg|14px|link=]]</span>'''%s''' invokes function '''%s''' in [[%s]] using [[Star Citizen:Lua|Lua]].</div></div>",
            templateName,
            item.funcName,
            item.moduleName
        ) )
    end

    if #invokeList > 0 then
        table.insert( res, category )
    end

    return table.concat( res )
end

---@param moduleName string
---@param addCategories boolean
---@param whatLinksHere string    @A list generated by a dpl of pages in the Template namespace which link to moduleName.
---@return string
local function formatInvokedByList( moduleName, addCategories, whatLinksHere )
    local category = addCategories and '[[Category:Template invoked modules]]' or ''
    local templateData = enum.map( whatLinksHere, function(x) return {templateName=x, invokeList=getInvokeCallList(x)} end )
    templateData = enum.filter( templateData, function(x) return enum.any( x.invokeList, function(y) return y.moduleName==moduleName end ) end )

    if #templateData > 0 then
        moduleIsUsed = true
        local res = {}

        for _, template in ipairs( templateData ) do
            for _, invoke in ipairs( template.invokeList ) do
                table.insert( res, string.format(
                    "<div role='note' class='hatnote'><div class=hatnote-container navigation-not-searchable'><span class='hatnote-icon metadata'>[[File:WikimediaUI-Code.svg|14px|link=]]</span>'''%s's''' function '''%s''' is invoked by [[%s]].</div></div>",
                    moduleName,
                    invoke.funcName,
                    template.templateName
                ) )
            end
        end

        table.sort( res )
        table.insert( res, category )

        return table.concat( res )
    end

    return ''
end

---@param moduleName string
---@param addCategories boolean
---@param whatLinksHere string      @A list generated by a dpl of pages in the Module namespace which link to moduleName.
---@return string
local function formatRequiredByList( moduleName, addCategories, whatLinksHere )
    local childModuleData = enum.map( whatLinksHere, function ( title )
        local requireList, loadDataList = getRequireList( title )
        return {name=title, requireList=requireList, loadDataList=loadDataList}
    end )

    local requiredByList = enum.map( childModuleData, function ( item )
        if enum.any( item.requireList, function(x) return x==moduleName end ) then
            if item.name:find( '%%' ) then
                return formatDynamicQueryLink( item.name )
            else
                return '[[' .. item.name .. ']]'
            end
        end
    end )

    local loadedByList = enum.map( childModuleData, function ( item )
        if enum.any( item.loadDataList, function(x) return x==moduleName end ) then
            if item.name:find( '%%' ) then
                return formatDynamicQueryLink( item.name )
            else
                return '[[' .. item.name .. ']]'
            end
        end
    end )

    if #requiredByList > 0 or #loadedByList > 0 then
        moduleIsUsed  = true
    end

    local res = {}

    for _, requiredByModuleName in ipairs( requiredByList ) do
        table.insert( res, string.format(
            "<div role='note' class='hatnote'><div class=hatnote-container navigation-not-searchable'><span class='hatnote-icon metadata'>[[File:WikimediaUI-Code.svg|14px|link=]]</span>'''%s''' is required by %s.</div></div>",
            moduleName,
            requiredByModuleName
        ) )
    end

    if #requiredByList > 0 then
        table.insert( res, (addCategories and '[[Category:Modules required by modules]]' or '') )
    end

    for _, loadedByModuleName in ipairs( loadedByList ) do
        table.insert( res, string.format(
            "<div role='note' class='hatnote'><div class=hatnote-container navigation-not-searchable'><span class='hatnote-icon metadata'>[[File:WikimediaUI-Code.svg|14px|link=]]</span>'''%s''' is loaded by %s.</div></div>",
            moduleName,
            loadedByModuleName
        ) )
    end

    if #loadedByList > 0 then
        table.insert( res, (addCategories and '[[Category:Module data]]' or '') )
    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

---@param currentPageName string|nil
---@param addCategories boolean|string|nil
---@return string
function p._main( currentPageName, addCategories, isUsed )
    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 (
    ( title.nsText ~= 'Module'  and title.nsText ~= 'Template') or
    ( title.nsText == 'Module' and  title.text:find( '^Sandbox/' ) ) ) then
        return ''
    end

    currentPageName = param.default_to( currentPageName, title.fullText )
    currentPageName = string.gsub( currentPageName, '/[Dd]oc$', '' )
    currentPageName = string.gsub( currentPageName, '_', ' ' )
    currentPageName = ucfirst( currentPageName:gsub( ':(.)', function(c) return ':'..c:upper() end ) )
    addCategories = yn( param.default_to( addCategories, title.subpageText~='doc' ) )
    moduleIsUsed = yn( param.default_to( isUsed, false ) )

    if currentPageName:find( '^Template:' ) then
        local invokeList = getInvokeCallList( currentPageName )
        return formatInvokeCallList( currentPageName, addCategories, invokeList )
    end

    local invokedByList, requiredByList = dpl.ask( {
        namespace = 'Template',
        linksto = currentPageName,
        distinct = 'strict',
        ignorecase = true,
        ordermethod = 'title',
        allowcachedresults = true,
        cacheperiod = 604800 -- One week
    }, {
        namespace = 'Module',
        linksto = currentPageName,
        nottitlematch = '%/doc|%sandbox%|' .. currentPageName:gsub( 'Module:', '' ),
        distinct = 'strict',
        ignorecase = true,
        ordermethod = 'title',
        allowcachedresults = true,
        cacheperiod = 604800 -- One week
    } )

    invokedByList = formatInvokedByList( currentPageName, addCategories, invokedByList )
    requiredByList = formatRequiredByList( currentPageName, addCategories, requiredByList )

    local requireList, loadDataList, usedTemplateList = getRequireList( currentPageName )

    requireList = enum.map( requireList, function ( moduleName )
        if moduleName:find( '%%' ) then
            return formatDynamicQueryLink( moduleName )
        else
            return '[[' .. moduleName .. ']]'
        end
    end )

    loadDataList = enum.map( loadDataList, function ( moduleName )
        if moduleName:find( '%%' ) then
            return formatDynamicQueryLink( moduleName )
        else
            return '[[' .. moduleName .. ']]'
        end
    end )

    local res = {}

    if not moduleIsUsed then
        table.insert( res, messageBoxUnused( currentPageName:gsub( 'Module:', '' ), addCategories ) )
    end

    table.insert( res, invokedByList )

    for _, requiredModuleName in ipairs( requireList ) do
        table.insert( res, string.format(
            "<div role='note' class='hatnote'><div class=hatnote-container navigation-not-searchable'><span class='hatnote-icon metadata'>[[File:WikimediaUI-Code.svg|14px|link=]]</span>'''%s''' requires %s.</div></div>",
            currentPageName,
            requiredModuleName
        ) )
    end

    if #requireList > 0 then
        table.insert( res, (addCategories and '[[Category:Modules requiring modules]]' or '') )
    end

    for _, loadedModuleName in ipairs( loadDataList ) do
        table.insert( res, string.format(
            "<div role='note' class='hatnote'><div class=hatnote-container navigation-not-searchable'><span class='hatnote-icon metadata'>[[File:WikimediaUI-Code.svg|14px|link=]]</span>'''%s''' loads data from %s.</div></div>",
            currentPageName,
            loadedModuleName
        ) )
    end

    if #loadDataList > 0 then
        table.insert( res, (addCategories and '[[Category:Modules using data]]' or '') )
    end

    for _,  templateName in ipairs( usedTemplateList ) do
        table.insert( res, string.format(
            "<div role='note' class='hatnote'><div class=hatnote-container navigation-not-searchable'><span class='hatnote-icon metadata'>[[File:WikimediaUI-Code.svg|14px|link=]]</span>'''%s''' transcludes  [[%s]] using <samp>frame:preprocess()</samp>.</div></div>",
            currentPageName,
            templateName
        ) )

    end

    table.insert( res, requiredByList )

    return table.concat( res )
end

return p
-- </nowiki>