Module:Modifier compatibility: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
No edit summary
(the unveiled mod is in unveiled domain now. Crafted version does not have spawn weight and probably out of scope of to list those)
Line 385: Line 385:
                     AND mods.id LIKE "%%Master%%"
                     AND mods.id LIKE "%%Master%%"
                 ]],
                 ]],
                 cfg.mod_domains.crafted,
                 cfg.mod_domains.unveiled,
                 cfg.mod_generation_types.prefix,
                 cfg.mod_generation_types.prefix,
                 table.concat(tpl_args.item_tags, '","')
                 table.concat(tpl_args.item_tags, '","')
Line 402: Line 402:
                     AND mods.id LIKE "%%Master%%"
                     AND mods.id LIKE "%%Master%%"
                 ]],
                 ]],
                 cfg.mod_domains.crafted,
                 cfg.mod_domains.unveiled,
                 cfg.mod_generation_types.suffix,
                 cfg.mod_generation_types.suffix,
                 table.concat(tpl_args.item_tags, '","')
                 table.concat(tpl_args.item_tags, '","')

Revision as of 15:32, 26 November 2022

Module documentation[view] [edit] [history] [purge]


Implements {{Item modifier compatibility}}.

-------------------------------------------------------------------------------
-- 
--                           Module:Modifier compatibility
-- 
-- This module implements Template:Item modifier compatibility
-------------------------------------------------------------------------------

require('Module:No globals')
local m_util = require('Module:Util')
local m_cargo = require('Module:Cargo')

-- Should we use the sandbox version of our submodules?
local use_sandbox = m_util.misc.maybe_sandbox('Modifier compatibility')

local m_game = mw.loadData('Module:Game')

-- Lazy loading
local f_item_link -- require('Module:Item link').item_link

-- The cfg table contains all localisable strings and configuration, to make it
-- easier to port this module to another wiki.
local cfg = use_sandbox and mw.loadData('Module:Modifier compatibility/config/sandbox') or mw.loadData('Module:Modifier compatibility/config')

local i18n = cfg.i18n

-- ----------------------------------------------------------------------------
-- Helper functions
-- ----------------------------------------------------------------------------

local h = {}

-- Lazy loading for Module:Item link
function h.item_link(args)
    if not f_item_link then
        f_item_link = require('Module:Item link').item_link
    end
    return f_item_link(args)
end

function h.header(str)
    --[[
    This function replaces specific numbers with a generic #.
    ]]

    local s = table.concat(m_util.string.split(str, '%(%d+%.*%d*%-%d+%.*%d*%)'), '#')
    s = table.concat(m_util.string.split(s, '%d+%.*%d*'), '#')
    s = table.concat(m_util.string.split(s, '<br>'), ', ')
    s = table.concat(m_util.string.split(s, '<br />'), ' ')

   return s
end

h.tbl = {}
h.tbl.display = {}
h.tbl.display.factory = {}
function h.tbl.display.factory.value(args)
    -- Format options for each field:
    args.options = args.options or {}

    -- Separator between fields:
    args.delimiter = args.delimiter or ', '

    return function(tpl_args, tr, data, fields)
        local values = {}
        local fmt_values = {}

        for index, field in ipairs(fields) do
            local value = {
                min=data[field],
                max=data[field],
                base=data[field],
            }
            if value.min then
                values[#values+1] = value.max
                local opts = args.options[index] or {}
                -- Global colour is set, no overrides.
                if args.color ~= nil or opts.color == nil then
                    opts.no_color = true
                end
                fmt_values[#fmt_values+1] = m_util.html.format_value(nil, value, opts)
            end
        end

        if #values == 0 then
            tr
                :wikitext(m_util.html.td.na())
        else
            local td = tr:tag('td')
            td
                :attr('data-sort-value', table.concat(values, args.delimiter))
                :wikitext(table.concat(fmt_values, args.delimiter))
            if args.color then
                td:attr('class', 'tc -' .. args.color)
            end
        end
    end
end

-- ----------------------------------------------------------------------------
-- Additional configuration
-- ----------------------------------------------------------------------------

local c = {}

c.item_mods = {}

c.item_mods.sections = {
    {
        header = i18n.item_mods.prefix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    mods.domain = %s
                    AND mods.generation_type = %s
                    AND mods.stat_text > ''
                    AND mod_spawn_weights.tag IN ("%s")
                ]],
                item_data['items.domain'],
                cfg.mod_generation_types.prefix,
                table.concat(tpl_args.item_tags, '","')
            )
        end,
    },
    {
        header = i18n.item_mods.suffix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    mods.domain = %s
                    AND mods.generation_type = %s
                    AND mods.stat_text > ''
                    AND mod_spawn_weights.tag IN ("%s")
                ]],
                item_data['items.domain'],
                cfg.mod_generation_types.suffix,
                table.concat(tpl_args.item_tags, '","')
            )
        end,
    },
    {
        tags = {'elder'},
        header = i18n.item_mods.elder_prefix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    (mod_spawn_weights.tag="%s"
                    AND mods.generation_type=%s
                    AND mods.domain=%s
                    AND mods.stat_text > '')
                ]],
                item_data.tags_cfg.elder,
                cfg.mod_generation_types.prefix,
                item_data['items.domain']
            )
        end,
    },
    {
        tags = {'elder'},
        header = i18n.item_mods.elder_suffix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    (mod_spawn_weights.tag="%s"
                    AND mods.generation_type=%s
                    AND mods.domain=%s
                    AND mods.stat_text > '')
                ]],
                item_data.tags_cfg.elder,
                cfg.mod_generation_types.suffix,
                item_data['items.domain']
            )
        end,
    },
    {
        tags = {'shaper'},
        header = i18n.item_mods.shaper_prefix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    (mod_spawn_weights.tag="%s"
                    AND mods.generation_type=%s
                    AND mods.domain=%s
                    AND mods.stat_text > '')
                ]],
                item_data.tags_cfg.shaper,
                cfg.mod_generation_types.prefix,
                item_data['items.domain']
            )
        end,
    },
    {
        tags = {'shaper'},
        header = i18n.item_mods.shaper_suffix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    (mod_spawn_weights.tag="%s"
                    AND mods.generation_type=%s
                    AND mods.domain=%s
                    AND mods.stat_text > '')
                ]],
                item_data.tags_cfg.shaper,
                cfg.mod_generation_types.suffix,
                item_data['items.domain']
            )
        end,
    },
    {
        header = i18n.item_mods.delve_prefix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    mods.domain = %s
                    AND mods.generation_type = %s
                    AND mods.stat_text > ''
                    AND mod_spawn_weights.tag IN ("%s")
                ]],
                cfg.mod_domains.delve,
                cfg.mod_generation_types.prefix,
                table.concat(tpl_args.item_tags, '","')
            )
        end,
    },
    {
        header = i18n.item_mods.delve_suffix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    mods.domain = %s
                    AND mods.generation_type = %s
                    AND mods.stat_text > ''
                    AND mod_spawn_weights.tag IN ("%s")
                ]],
                cfg.mod_domains.delve,
                cfg.mod_generation_types.suffix,
                table.concat(tpl_args.item_tags, '","')
            )
        end,
    },
    {
        tags = {'crusader'},
        header = i18n.item_mods.crusader_prefix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    (mod_spawn_weights.tag="%s"
                    AND mods.generation_type=%s
                    AND mods.domain=%s
                    AND mods.stat_text > '')
                ]],
                item_data.tags_cfg.crusader,
                cfg.mod_generation_types.prefix,
                item_data['items.domain']
            )
        end,
    },
    {
        tags = {'crusader'},
        header = i18n.item_mods.crusader_suffix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    (mod_spawn_weights.tag="%s"
                    AND mods.generation_type=%s
                    AND mods.domain=%s
                    AND mods.stat_text > '')
                ]],
                item_data.tags_cfg.crusader,
                cfg.mod_generation_types.suffix,
                item_data['items.domain']
            )
        end,
    },
    {
        tags = {'eyrie'},
        header = i18n.item_mods.eyrie_prefix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    (mod_spawn_weights.tag="%s"
                    AND mods.generation_type=%s
                    AND mods.domain=%s
                    AND mods.stat_text > '')
                ]],
                item_data.tags_cfg.eyrie,
                cfg.mod_generation_types.prefix,
                item_data['items.domain']
            )
        end,
    },
    {
        tags = {'eyrie'},
        header = i18n.item_mods.eyrie_suffix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    (mod_spawn_weights.tag="%s"
                    AND mods.generation_type=%s
                    AND mods.domain=%s
                    AND mods.stat_text > '')
                ]],
                item_data.tags_cfg.eyrie,
                cfg.mod_generation_types.suffix,
                item_data['items.domain']
            )
        end,
    },
    {
        tags = {'basilisk'},
        header = i18n.item_mods.basilisk_prefix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    (mod_spawn_weights.tag="%s"
                    AND mods.generation_type=%s
                    AND mods.domain=%s
                    AND mods.stat_text > '')
                ]],
                item_data.tags_cfg.basilisk,
                cfg.mod_generation_types.prefix,
                item_data['items.domain']
            )
        end,
    },
    {
        tags = {'basilisk'},
        header = i18n.item_mods.basilisk_suffix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    (mod_spawn_weights.tag="%s"
                    AND mods.generation_type=%s
                    AND mods.domain=%s
                    AND mods.stat_text > '')
                ]],
                item_data.tags_cfg.basilisk,
                cfg.mod_generation_types.suffix,
                item_data['items.domain']
            )
        end,
    },
    {
        tags = {'adjudicator'},
        header = i18n.item_mods.adjudicator_prefix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    (mod_spawn_weights.tag="%s"
                    AND mods.generation_type=%s
                    AND mods.domain=%s
                    AND mods.stat_text > '')
                ]],
                item_data.tags_cfg.adjudicator,
                cfg.mod_generation_types.prefix,
                item_data['items.domain']
            )
        end,
    },
    {
        tags = {'adjudicator'},
        header = i18n.item_mods.adjudicator_suffix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    (mod_spawn_weights.tag="%s"
                    AND mods.generation_type=%s
                    AND mods.domain=%s
                    AND mods.stat_text > '')
                ]],
                item_data.tags_cfg.adjudicator,
                cfg.mod_generation_types.suffix,
                item_data['items.domain']
            )
        end,
    },
    {
        header = i18n.item_mods.veiled_prefix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    mods.domain = %s
                    AND mods.generation_type = %s
                    AND mods.stat_text > ''
                    AND mod_spawn_weights.tag IN ("%s")
                    AND mods.id LIKE "%%Master%%"
                ]],
                cfg.mod_domains.unveiled,
                cfg.mod_generation_types.prefix,
                table.concat(tpl_args.item_tags, '","')
            )
        end,
    },
    {
        header = i18n.item_mods.veiled_suffix,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    mods.domain = %s
                    AND mods.generation_type = %s
                    AND mods.stat_text > ''
                    AND mod_spawn_weights.tag IN ("%s")
                    AND mods.id LIKE "%%Master%%"
                ]],
                cfg.mod_domains.unveiled,
                cfg.mod_generation_types.suffix,
                table.concat(tpl_args.item_tags, '","')
            )
        end,
    },
    {
        header = i18n.item_mods.corrupted,
        where = function(tpl_args, item_data)
            return string.format(
                [[
                    mods.domain = %s
                    AND mods.generation_type = %s
                    AND mods.stat_text > ''
                    AND mod_spawn_weights.tag IN ("%s")
                ]],
                item_data['items.domain'],
                cfg.mod_generation_types.corrupted,
                table.concat(tpl_args.item_tags, '","')
            )
        end,
        is_implicit = true,
    },
}

-- ----------------------------------------------------------------------------
-- Main functions
-- ----------------------------------------------------------------------------

local function _item_modifier_compatibility(tpl_args)
    --[[
    This function queries mods that can spawn on an item. It compares
    the item tags and the spawn weight tags. If there's a match and
    the spawn weight is larger than zero, then that mod is added to a
    drop down list.

    To Do:
    * Add support to:
        * Forsaken masters
        * Bestiary
    * Add a proper expand/collapse toggle for the entire header row so
        it reacts together with mw-collapsible.
    * Show Mod group in a better way perhaps:
        Mod group [Collapsible, default=Expanded]
            # to Damage [Collapsible, default=Collapsed]
                3 to Damage
                5 to Damage
    * Add a where condition that somehow filters out mods that obviously
      wont match with the item. mod_spawn_weights.value>0 isn't enough due
      to possible edge cases.


    Examples:
    Weapons
    = p.drop_down_table{item='Rusted Hatchet', header='One Handed Axes'}
    = p.drop_down_table{item='Stone Axe', header='Two Handed Axes'}

    Accessories
    = p.drop_down_table{item='Amber Amulet', header='Amulets'}

    Jewels
    = p.drop_down_table{item='Cobalt Jewel', header='Jewels'}

    Armour
    = p.drop_down_table{item='Plate Vest', header='Body armours'}

    Boots
    = p.drop_down_table{item='Iron Greaves', header='Boots'}

    = p.drop_down_table{
        item='Fishing Rod',
        header='FISH PLEASE',
        item_tags='fishing_rod',
        extra_fields='mods.tags'
    }

    = p.drop_down_table{
        item='Fishing Rod',
        item_tags='axe, one_hand_weapon, onehand, weapon, default',
        extra_item_tags='fishing_rod'
    }

    = p.drop_down_table{
        item='Vaal Blade',
    }

    ]]

    -- Format the cargo query:
    local where
    for _,v in ipairs({
            {tpl_args.page, 'items._pageName = "%s"'},
            {tpl_args.item, 'items.name = "%s"'},
    }) do
        if v[1] ~= nil then
            where = string.format(v[2], v[1])
            break
        end
    end

    local item_data = m_cargo.query(
        {'items'},
        {
            'items.name',
            'items.tags',
            'items.class_id',
            'items.inventory_icon',
            'items.html',
            'items.release_version',
            'items._pageName'
        },
        {
            where = where,
            groupBy = 'items._pageID',
            orderBy = 'items.name, items.release_version DESC',
        }
    )[1]

    -- Set the item class as key and the corresponding mod domain as
    -- value:

    -- Get the domain, if it's not defined in the table assume it's
    -- in the item domain.
    item_data['items.domain'] = tonumber(tpl_args.domain) or cfg.mod_domains_by_item_class[item_data['items.class_id']] or cfg.mod_domains.item

    -- Convert the mod domain number to understandable text:
    item_data['items.domain_text'] = m_game.constants.mod.domains[item_data['items.domain']]['short_lower']

    -- Format item tags:
    tpl_args.item_tags = m_util.string.split_args(
        tpl_args.item_tags or item_data['items.tags'],
        {sep=',%s*'}
    )
    for _,v in ipairs(m_util.string.split_args(tpl_args.extra_item_tags, {sep=',%s*'})) do
        tpl_args.item_tags[#tpl_args.item_tags+1] = v
    end

    -- Format extra fields:
    local extra_fields = m_util.string.split_args(tpl_args.extra_fields, {sep=',%s*'})


    -- Get tags that are appended to special items:
    item_data.tags_cfg = m_game.constants.item.classes[item_data['items.class_id']].tags or {}

    -- Save the original tag format:
    local item_tags_orig = {}
    for i,v in ipairs(tpl_args.item_tags) do
        item_tags_orig[i] = v
    end

    local item_mods = {}
    local mod_group_counter = {}
    mod_group_counter['all'] = {}
    local extra_fieldss = {}
    local table_index_base = -1
    for _, sctn in ipairs(c.item_mods.sections) do
        item_mods[sctn['header']] = {}

        -- Preallocate the mod group counter, implicit and explicit mods
        -- are counted separetely because they can spawn together:
        mod_group_counter[sctn['header']] = {}
        local adj = 'explicit'
        if sctn['is_implicit'] then
            adj = 'implicit'
        end
        for _, header in ipairs({sctn['header'], 'all'}) do
            if mod_group_counter[header][adj] == nil then
                mod_group_counter[header][adj] = {}
            end
        end

        local continue = true
        local current_tags = {}
        if sctn['tags'] then
            -- some item classes do not have shaper/elder items, so the table
            -- will not contain any tags:
            if #sctn['tags'] == 0 or m_util.table.length(item_data.tags_cfg) == 0 then
                continue = false
            else
                for _, tag in ipairs(sctn['tags']) do
                    current_tags[#current_tags+1] = item_data.tags_cfg[tag]
                end
            end
        else
            -- Reset to original tags:
            for i,v in ipairs(item_tags_orig) do
                current_tags[i] = v
            end
        end

        if continue then
            -- Cargo preparation:
            local q_fields = {
                'mods._pageName',
                'mods.name',
                'mods.id',
                'mods.required_level',
                'mods.generation_type',
                'mods.domain',
                'mods.mod_group',
                'mods.mod_type',
                'mods.stat_text',
                'mods.stat_text_raw',
                'mods.tags',
                'mod_stats.id',
                'mod_spawn_weights.tag',
                'mod_spawn_weights.value',
                'mod_spawn_weights.ordinal',
                'mod_spawn_weights._pageName'
            }
            for i, v in ipairs(extra_fields) do
                q_fields[#q_fields+1] = v
            end

            -- Query mods and map the results to the pagename:
            local results = m_cargo.map_results_to_id{
                results=m_cargo.query(
                    {'mods', 'mod_spawn_weights', 'mod_stats'},
                    q_fields,
                    {
                        join = [[
                            mods._pageID=mod_spawn_weights._pageID,
                            mods._pageID=mod_stats._pageID
                        ]],
                        where = sctn['where'](tpl_args, item_data),
                        groupBy = [[
                            mods._pageName,
                            mod_spawn_weights.tag,
                            mod_spawn_weights.value
                        ]],
                        orderBy = [[
                            mods.generation_type,
                            mods.mod_group,
                            mods.mod_type,
                            mods.required_level,
                            mods._pageName,
                            mod_spawn_weights.ordinal
                        ]],
                    }
                ),
                field='mods._pageName',
                keep_id_field=true,
                append_id_field=true,
            }

            if #results > 0 then
                -- Loop through all found modifiers:
                local last
                for _, id in ipairs(results) do
                    -- Loop through all the modifier tags until they match
                    -- the item tags:
                    local j = 0
                    local tag_match_stop
                    repeat
                        j = j+1
                        local mod_tag = results[id][j]['mod_spawn_weights.tag']
                        local mod_tag_weight = tonumber(
                            results[id][j]['mod_spawn_weights.value']
                        )

                        -- Loop through the item tags until it matches the
                        -- spawn weight tag and the mod tag has a value larger than
                        -- zero:
                        local y = 0
                        local tag_match_add = false
                        repeat
                            y = y+1
                            tag_match_stop = ((mod_tag == current_tags[y]) and ((mod_tag_weight or -1) >= 0)) or (results[id][j] == nil)
                            tag_match_add  =  (mod_tag == current_tags[y]) and ((mod_tag_weight or -1) > 0)
                        until tag_match_stop or y == #current_tags

                        -- If there's a match then save that mod and other
                        -- interesting information:
                        if tag_match_add then

                            -- Assume that the mod is global then go through
                            -- all the stat ids and check if any of the
                            -- stats are local:
                            local mod_scope = 'Global'
                            for _, vv in ipairs(results[id]) do
                                if vv['mod_stats.id']:find('.*local.*') ~= nil then
                                    mod_scope = 'Local'
                                end
                            end

                            -- Save the matching modifier tag:
                            local a = #item_mods[sctn['header']]
                            item_mods[sctn['header']][a+1] = results[id][j]

                            -- Save other interesting fields:
                            item_mods[sctn['header']][a+1]['mods.scope'] = mod_scope
                            item_mods[sctn['header']][a+1]['spawn_weight.idx_match'] = j
                            item_mods[sctn['header']][a+1]['mods.add'] = tag_match_add
                            item_mods[sctn['header']][a+1]['mods.stop'] = tag_match_stop

                            -- Count the mod groups:
                            local group = item_mods[sctn['header']][a+1]['mods.mod_group'] or 'nil_group'
                            for _, header in ipairs({sctn['header'], 'all'}) do
                                if mod_group_counter[header][adj][group] == nil then
                                    mod_group_counter[header][adj][group] = {}
                                end
                                local tp = results[id][j]['mods.mod_type']
                                local bef = mod_group_counter[header][adj][group][tp] or 0
                                mod_group_counter[header][adj][group][tp] = 1 + bef
                            end
                        end
                    until tag_match_stop
                end

                extra_fieldss[sctn['header']] = extra_fields
            end
        end
    end


    --
    -- Display the item mods
    --

    -- Introductory text:
    local out = {}
    out[#out+1] = string.format(
        '==%s== \n',
        tpl_args['header'] or table.concat(tpl_args.item_tags, ', ')
    )
    local expand_button = string.format(
        '<div style="float: right; text-align:center"><div class="mw-collapsible-collapse-all" style="cursor:pointer;">[%s]</div><hr><div class="mw-collapsible-expand-all" style="cursor:pointer;">[%s]</div></div>',
        i18n.item_mods.collapse_all,
        i18n.item_mods.expand_all
    )
    out[#out+1] = expand_button
    out[#out+1] = string.format('%s %s.<br><br><br>',
        i18n.item_mods.table_intro,
        h.item_link{
            page=item_data['items._pageName'],
            name=item_data['items.name'],
            inventory_icon=item_data['items.inventory_icon'] or '',
            html=item_data['items.html'] or '',
            skip_query=true
        }
    )

    -- Loop through the sections:
    for _, sctn in ipairs(c.item_mods.sections) do
        local extra_fields = extra_fieldss[sctn['header']]
        local adj = 'explicit'
        if sctn['is_implicit'] then
            adj = 'implicit'
        end

        -- Create html container:
        local container = mw.html.create('div')
            :attr('style', 'vertical-align:top; display:inline-block;')

        -- Create the drop down table with <table></table>:
        if #item_mods[sctn['header']] > 0 then
            container
                :tag('h3')
                    :wikitext(string.format('%s', sctn['header']))
                    :done()
                :done()
        end

        local total_mod_groups = 0
        for _ in pairs(mod_group_counter[sctn['header']][adj]) do
            total_mod_groups = 1+total_mod_groups
        end

        -- Loop through and add all matching mods to the <table>.
        local tbl, last_group, last_type, table_index
        for _, rows in ipairs(item_mods[sctn['header']]) do

            -- If the last mod group is different to the current
            -- mod group then assume the mod isn't related and start
            -- a new drop down list, if there's only one mod group
            -- then use mod type instead:
            if rows['mods.mod_group'] ~= last_group or (total_mod_groups == 1 and rows['mods.mod_type'] ~= last_type) then
                -- Check through all the mods and see if there are
                -- multiple mod types within the same mod group:
                local count = {}
                for _, n in ipairs(item_mods[sctn['header']]) do

                    -- If the mod has the same mod group, then add
                    -- the mod type to the counter. Only unique mod
                    -- types matter so the number is just a dummy
                    -- value:
                    if n['mods.mod_group'] == rows['mods.mod_group'] then
                        count[n['mods.mod_type']] = 1
                    end
                end

                -- Calculate how many unique mod types with the
                -- same mod group there are for all explicit or implicit
                -- sections since a mod with the same mod group can't
                -- spawn. Doesn't matter if it's prefix or suffix.
                local number_of_mod_types = 0
                for _ in pairs(mod_group_counter['all'][adj][rows['mods.mod_group']]) do
                    number_of_mod_types = 1 + number_of_mod_types
                end

                -- If there are multiple unique mod types with the
                -- same mod group then change the style of the drop
                -- down list to indicate it, if there's only one
                -- mod group in the generation type then ignore it:
                local table_index_mod_group
                local tbl_caption
                if number_of_mod_types > 1 and total_mod_groups > 1 then
                    table_index_mod_group = table.concat(
                        {string.byte(rows['mods.mod_group'], 1, #rows['mods.mod_group'])},
                        ''
                    )

                    tbl_caption = string.format(
                        '%s',
                        m_util.html.poe_color(
                            'stat',
                            string.format(
                                '%s %s',
                                i18n.item_mods.mod_group,
                                rows['mods.mod_group']
                            )
                        ) or ''
                    )

                else
                    tbl_caption = string.format(
                        '%s (%s)',
                        m_util.html.poe_color(
                            'mod',
                            h.header(rows['mods.stat_text_raw'])
                        ) or '',
                        rows['mods.scope']
                    )
                end

                -- Create a table index for handling the collapsible:
                table_index_base = table_index_base+1
                if table_index_mod_group ~= nil then
                    table_index = table_index_mod_group
                else
                    table_index = table_index_base
                end

                -- Add class and style to the <table>:
                tbl = container:tag('table')
                tbl
                    :attr('class', 'mw-collapsible mw-collapsed')
                    :attr('style',
                        'text-align:left; line-height:1.60em; width:810px;'
                    )
                    :tag('th')
                        :attr('class',
                            string.format('mw-customtoggle-%s', table_index)
                        )
                        :attr('style',
                            'text-align:left; line-height:1.40em; border-bottom:1pt solid dimgrey;'
                        )
                        :attr('colspan', '3' .. #extra_fields)
                        :wikitext(tbl_caption)
                        :done()
                    :done()
            end

            -- If the mod has no name then use the mod type:
            local mod_name = rows['mods.name'] or ''
            if  mod_name == '' then
                mod_name = rows['mods.mod_type']
            end

            -- Check if there are any extra properties to show in
            -- the drop down list and then add a cell for that,
            -- add this node at the end of the table row:
            local td = mw.html.create('td')
            if extra_fields ~= nil then
                for _, extra_field in ipairs(extra_fields) do
                    td
                        :attr('width', '*')
                        :wikitext(string.format(
                            '%s:&nbsp;%s ',
                            extra_field,
                            rows[extra_field] or ''
                            )
                        )
                        :done()
                end
            end

            -- Style mods.tags:
            local mods_tags = table.concat(
                m_util.string.split_args(rows['mods.tags'], {sep=',%s*'}),
                ', '
            )
            if mods_tags ~= '' then
                mods_tags = m_util.html.tooltip('*', mods_tags)
            end

            -- Add a table row with the interesting properties that
            -- modifier has:
            if table_index ~= nil then
                tbl
                    :tag('tr')
                        :attr('class', 'mw-collapsible mw-collapsed')
                        :attr(
                            'id',
                            string.format('mw-customcollapsible-%s', table_index)
                        )
                        :tag('td')
                            :attr('width', '160')
                            :wikitext(
                                string.format(
                                    '&nbsp;&nbsp;&nbsp;[[%s|%s]]',
                                    rows['mods._pageName'],
                                    mod_name:gsub('%s', '&nbsp;')
                                )
                            )
                            :done()
                        :tag('td')
                            :attr('width', '1')
                            :wikitext(
                                string.format(
                                    '%s&nbsp;%s',
                                    m_game.level_requirement['short_upper']
                                        :gsub('%s', '&nbsp;'),
                                    rows['mods.required_level']
                                )
                            )
                            :done()
                        :tag('td')
                            :attr('width', '*')
                            :wikitext(
                                string.format(
                                    '%s%s',
                                    m_util.html.poe_color(
                                        'mod',
                                        rows['mods.stat_text']
                                            :gsub('<br>', ', ')
                                            :gsub('<br />', ' ')
                                    ) or '',
                                    mods_tags
                                )
                            )
                            :done()
                        :node(td)
                        :done()
                    :done()
            end

            -- Save the last mod group for later comparison:
            last_group = rows['mods.mod_group']
            last_type = rows['mods.mod_type']
        end
        out[#out+1] = tostring(container)
    end

    -- Outro text:
    out[#out+1] = '<br>'
    out[#out+1] = m_util.html.poe_color(
        'normal',
        string.format('[[#Top|%s]]', i18n.item_mods.back_to_top)
    )

    return table.concat(out,'')
end

-- ----------------------------------------------------------------------------
-- Exported functions
-- ----------------------------------------------------------------------------

local p = {}

-- 
-- Template:Item modifier compatibility
-- 
p.item_modifier_compatibility = m_util.misc.invoker_factory(_item_modifier_compatibility, {
    wrappers = cfg.wrappers.item_mods,
})

return p