Module:Area: Difference between revisions

From Path of Exile Wiki
Jump to navigation Jump to search
>Illviljan
m (Reverted edits by OmegaK2 (talk) to last revision by Illviljan)
>Illviljan
m (Reverted edits by Illviljan (talk) to last revision by OmegaK2)
Line 7: Line 7:
-- spawn_weight* values
-- spawn_weight* values
-- spawnchacne values
-- spawnchacne values
--
-- i18n for properties


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
Line 76: Line 74:
         main_page_is_invalid = 'main_page argument got "%s" which is not a valid wiki page',  
         main_page_is_invalid = 'main_page argument got "%s" which is not a valid wiki page',  
         main_page_does_not_exist = 'main_page argument requires the specified page "%s" to exist in the main wiki namespace',
         main_page_does_not_exist = 'main_page argument requires the specified page "%s" to exist in the main wiki namespace',
    },
    intro = {
        text_with_name = "'''%s''' is the internal id for the [[%s|%s]] [[area]]. ",
        text_without_name = "'''%s''' is the internal id of an unnamed [[area]]. ",
        connections = 'It is connected to the following areas:',
     },
     },
     tooltips = {
     tooltips = {
Line 107: Line 110:
     },
     },
}
}
function m_util.args.spawn_weight_list (argtbl, args)
    -- Parses a weighted pair of lists and sets properties
    --
    -- argtbl: argument table to work with
    -- args:
    --  output_argument - if set, set arguments to this value
    --  frame - if set, automtically set subobjects
    --  input_argument - input prefix for parsing the arguments from the argtbl
    --  weight_property - name of the weight property on the subobject
    --  subobject_name - name of the subobject
    args = args or {}
    args.input_argument = 'spawn_weight'
    args.output_argument = 'spawn_weights'
    args.weight_property = 'Has spawn weight'
    args.subobject_name = 'spawn weight'
    args.cargo_table = 'spawn_weights'
    local i = 0
    local id = nil
    local value = nil
   
    if args.output_argument then
        argtbl[args.output_argument] = {}
    end
    repeat
        i = i + 1
        id = {
            tag = string.format('%s%s_tag', args.input_argument, i),
            value = string.format('%s%s_value', args.input_argument, i),
        }
   
        value = {
            tag = argtbl[id.tag],
            value = argtbl[id.value],
        }
       
        if value.tag ~= nil and value.value ~= nil then
            if args.output_argument then
                argtbl[args.output_argument][i] = value
            end
           
            if args.frame then
                if args.cargo_table then
                    m_util.cargo.store(args.frame, {
                        _table = args.cargo_table,
                        ordinal = i,
                        tag = value.tag,
                        weight = m_util.cast.number(value.value, {min=0}),
                    })
                end
            end
        elseif not (value.tag == nil and value.value == nil) then
            error(string.format(i18n.errors.invalid_weight, id.tag, id.value))
        end
    until value.tag == nil
end


-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
Line 180: Line 124:


local factory = {}
local factory = {}
function factory.arg_list(k, args)
    return function (tpl_args, frame)
        if tpl_args[k] ~= nil then
            tpl_args[k] = m_util.string.split(tpl_args[k], ', ')
        end
    end
end


function factory.display_value(k, args)
function factory.display_value(k, args)
Line 219: Line 156:
-- Argument & display mapping
-- Argument & display mapping
-- ----------------------------------------------------------------------------
-- ----------------------------------------------------------------------------
local display  = {}


local argument_map = {
local argument_map = {
     table = 'areas',
     table = 'areas',
   
    order = {
        'main_page',
        'id',
        'name',
        'act',
        'area_level',
        'level_restriction_max',
        'area_type_tags',
        'tags',
        'loading_screen',
        'connection_ids',
        'parent_area_id',
        'modifier_ids',
        -- populated via modifier ids
        'stat_text',
        'boss_monster_ids',
        'monster_ids',
        'entry_text',
        'entry_npc',
        'flavour_text',
        'screenshot_ext',
        'screenshot',
        'vaal_area_spawn_chance',
        'vaal_area_ids',
        'strongbox_spawn_chance',
        'strongbox_max_count',
        'strongbox_rarity_weight',
        -- those four are parsed via the argument above
        'strongbox_weight_normal',
        'strongbox_weight_magic',
        'strongbox_weight_rare',
        'strongbox_weight_unique',
        'is_map_area',
        'is_unique_map_area',
        'is_town_area',
        'is_hideout_area',
        'is_vaal_area',
        'is_master_daily_area',
        'is_labyrinth_area',
        'is_labyrinth_airlock_area',
        'is_labyrinth_boss_area',
        'has_waypoint',
        -- parsed by m_util.args.version:
        'mainpage_categories',
       
        -- Non argument, but passed to the script
        'areas',
    },
   
     --
     --
     -- User supplied arguments
     -- User supplied arguments
Line 242: Line 230:
                     end
                     end
                 end
                 end
                return tpl_args.main_page
             end
             end
         },
         },
Line 261: Line 250:
             field = 'act',
             field = 'act',
             type = 'Integer',
             type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.act, {key_out='act'}),
         },
         },
         area_level = {
         area_level = {
             field = 'area_level',
             field = 'area_level',
             type = 'Integer',
             type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.area_level, {key_out='area_level'}),
         },
         },
         level_restriction_max = {
         level_restriction_max = {
             field = 'level_restriction_max',
             field = 'level_restriction_max',
             type = 'Integer',
             type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.level_restriction_max, {key_out='level_restriction_max'}),
             default = 100,
             default = 100,
         },
         },
Line 298: Line 284:
                 local loading_id = tpl_args[i18n.args.loading_screen]
                 local loading_id = tpl_args[i18n.args.loading_screen]
                 if loading_id ~= nil then
                 if loading_id ~= nil then
                     tpl_args.loading_screen = string.format(i18n.images.loading_screen, loading_id)
                     tpl_args.loading_screen_infobox = string.format(i18n.images.loading_screen_infobox, loading_id)
                     tpl_args.loading_screen_infobox = string.format(i18n.images.loading_screen_infobox, loading_id)  
                      
                    return string.format(i18n.images.loading_screen, loading_id)
                 end
                 end
             end,
             end,
Line 306: Line 293:
             field = 'connection_ids',
             field = 'connection_ids',
             type = 'List (,) of String',
             type = 'List (,) of String',
            func = factory.arg_list(i18n.args.connection_ids, {key_out='connection_ids'}),
             default = {},
             default = {},
         },
         },
Line 316: Line 302:
             field = 'modifier_ids',
             field = 'modifier_ids',
             type = 'List (,) of String',
             type = 'List (,) of String',
             func = function(tpl_args, frame)
             func = function(tpl_args, frame, value)
                factory.arg_list(i18n.args.modifier_ids, {key_out='modifier_ids'})(tpl_args, frame)
                 if value == nil then
               
                 if tpl_args.modifier_ids == nil then
                     return
                     return
                 end
                 end
                tpl_args.mods = m_util.cargo.array_query{
                    tables={'mods'},
                    fields={'mods.stat_text'},
                    id_field='mods.id',
                    id_array=tpl_args.modifier_ids
                }
                  
                  
                 local query_ids = {}
                 return tpl_args.modifier_ids
                for i, mod_id in ipairs(tpl_args.modifier_ids) do
            end,
                    query_ids[#query_ids+1] = string.format('mods.id="%s"', mod_id)
            default = {},
        },
        stat_text = {
            field = 'stat_text',
            type = 'Text',
            func = function (tpl_args, frame)
                if tpl_args.mods == nil then
                    return
                 end
                 end
               
                local results = cargo.query(
                    'mods',
                    'mods.id, mods.stat_text',
                    {
                        where=table.concat(query_ids, ' OR '),
                    }
                )
               
                 local text = {}
                 local text = {}
                 for page, row in pairs(results) do
                 for page, row in pairs(tpl_args.mods) do
                     if row['mods.stat_text'] ~= '' then
                     if row['mods.stat_text'] ~= '' then
                         text[#text+1] = row['mods.stat_text']
                         text[#text+1] = row['mods.stat_text']
                     end
                     end
                 end
                 end
                  
                 return table.concat(text, '<br>')
                tpl_args.stat_text = table.concat(text, '<br>')
             end,
             end,
            default = {},
         },
         },
         monster_ids = {
         monster_ids = {
             field = 'monster_ids',
             field = 'monster_ids',
             type = 'List (,) of String',
             type = 'List (,) of String',
            func = factory.arg_list(i18n.args.monster_ids, {key_out='monster_ids'}),
             default = {},
             default = {},
         },
         },
Line 356: Line 341:
             field = 'boss_monster_ids',
             field = 'boss_monster_ids',
             type = 'List (,) of String',
             type = 'List (,) of String',
            func = factory.arg_list(i18n.args.boss_monster_ids, {key_out='boss_monster_ids'}),
             default = {},
             default = {},
         },
         },
Line 392: Line 376:
             field = 'vaal_area_ids',
             field = 'vaal_area_ids',
             type = 'List (,) of String',
             type = 'List (,) of String',
            func = factory.arg_list(i18n.args.vaal_area_ids, {key_out='vaal_area_ids'}),
             default = {},
             default = {},
         },
         },
Line 398: Line 381:
             field = 'vaal_area_spawn_chance',
             field = 'vaal_area_spawn_chance',
             type = 'Integer',
             type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.vaal_area_spawn_chance, {key_out='vaal_area_spawn_chance'}),
             default = 0,
             default = 0,
         },
         },
Line 404: Line 386:
             field = 'strongbox_spawn_chance',
             field = 'strongbox_spawn_chance',
             type = 'Integer',
             type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.strongbox_spawn_chance, {key_out='strongbox_spawn_chance'}),
             default = 0,
             default = 0,
         },
         },
Line 410: Line 391:
             field = 'strongbox_max_count',
             field = 'strongbox_max_count',
             type = 'Integer',
             type = 'Integer',
            func = m_util.cast.factory.number(i18n.args.strongbox_max_count, {key_out='strongbox_max_count'}),
             default = 0,
             default = 0,
         },
         },
Line 422: Line 402:
                 for index, data in ipairs(m_game.constants.item.rarity) do
                 for index, data in ipairs(m_game.constants.item.rarity) do
                     local value = tonumber(weights[index]) or 0
                     local value = tonumber(weights[index]) or 0
                    -- will be read later
                     tpl_args.strongbox_rarity_weight[data.long_lower] = value
                     tpl_args.strongbox_rarity_weight[data.long_lower] = value
                    tpl_args._properties[string.format('strongbox_weight_%s', data.long_lower)] = value
                 end
                 end
             end,
             end,
        },
        strongbox_weight_normal = {
            field = 'strongbox_weight_normal',
            type = 'Integer',
        },
        strongbox_weight_magic = {
            field = 'strongbox_weight_magic',
            type = 'Integer',
        },
        strongbox_weight_rare = {
            field = 'strongbox_weight_rare',
            type = 'Integer',
        },
        strongbox_weight_unique = {
            field = 'strongbox_weight_unique',
            type = 'Integer',
         },
         },
         --
         --
Line 433: Line 429:
             field = 'is_map_area',
             field = 'is_map_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_map_area, {key_out='is_map_area'}),
             default = false,
             default = false,
         },
         },
Line 439: Line 434:
             field = 'is_unique_map_area',
             field = 'is_unique_map_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_unique_map_area, {key_out='is_unique_map_area'}),
             default = false,
             default = false,
         },
         },
Line 445: Line 439:
             field = 'is_town_area',
             field = 'is_town_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_town_area, {key_out='is_town_area'}),
             default = false,
             default = false,
         },
         },
Line 451: Line 444:
             field = 'is_hideout_area',
             field = 'is_hideout_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_hideout_area, {key_out='is_hideout_area'}),
             default = false,
             default = false,
         },
         },
Line 457: Line 449:
             field = 'is_vaal_area',
             field = 'is_vaal_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_vaal_area, {key_out='is_vaal_area'}),
             default = false,
             default = false,
         },
         },
Line 463: Line 454:
             field = 'is_is_master_daily_area',
             field = 'is_is_master_daily_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_master_daily_area, {key_out='is_master_daily_area'}),
             default = false,
             default = false,
         },
         },
Line 469: Line 459:
             field = 'is_labyrinth_area',
             field = 'is_labyrinth_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_labyrinth_area, {key_out='is_labyrinth_area'}),
             default = false,
             default = false,
         },
         },
Line 475: Line 464:
             field = 'is_labyrinth_airlock_area',
             field = 'is_labyrinth_airlock_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_labyrinth_airlock_area, {key_out='is_labyrinth_airlock_area'}),
             default = false,
             default = false,
         },
         },
Line 481: Line 469:
             field = 'is_labyrinth_boss_area',
             field = 'is_labyrinth_boss_area',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.is_labyrinth_boss_area, {key_out='is_labyrinth_boss_area'}),
             default = false,
             default = false,
         },
         },
Line 487: Line 474:
             field = 'has_waypoint',
             field = 'has_waypoint',
             type = 'Boolean',
             type = 'Boolean',
            func = m_util.cast.factory.boolean(i18n.args.has_waypoint, {key_out='has_waypoint'}),
             default = false,
             default = false,
        },
        mainpage_categories = {
            field = 'mainpage_categories',
            type = 'List (,) of String',
            func = function (tpl_args, frame)
                -- Category handling for main page only by adding the categories to a cargo field:
                -- Do notice the plural form.
                -- -- Areas, Act X areas/Map areas, Unique map areas
                local cats = {
                    'Category:Areas',
                }
                local cats_ini_len = #cats
                for _, key in ipairs(display.area_type) do
                    if tpl_args[key] == true then
                        cats[#cats+1] = string.format('Category:%ss', i18n.tooltips[key])
                    end
                end
                if #cats == cats_ini_len then
                    cats[#cats+1] = string.format('Category:Act %s areas', tpl_args.act)
                end
                return cats
            end
         },
         },
         --
         --
         -- Handled elsewhere in the old
         -- Handled elsewhere
         --
         --
         release_version = {
         release_version = {
             field = 'release_version',
             field = 'release_version',
             type = 'String'
             type = 'String',
            skip = true,
         },
         },
         removal_version = {
         removal_version = {
             field = 'removal_version',
             field = 'removal_version',
             type = 'String',
             type = 'String',
        },
             skip = true,
        mainpage_categories = {
            field = 'mainpage_categories',
             type = 'List (,) of String',
         },
         },
         infobox_html = {
         infobox_html = {
             field = 'infobox_html',
             field = 'infobox_html',
             type = 'Text',
             type = 'Text',
        },
             skip = true,
        strongbox_weight_normal = {
            field = 'strongbox_weight_normal',
            type = 'Integer',
        },
        strongbox_weight_magic = {
            field = 'strongbox_weight_magic',
            type = 'Integer',
        },
        strongbox_weight_rare = {
            field = 'strongbox_weight_rare',
            type = 'Integer',
        },
        strongbox_weight_unique = {
            field = 'strongbox_weight_unique',
            type = 'Integer',
        },
        stat_text = {
            field = 'stat_text',
             type = 'Text',
         },
         },
         --
         --
Line 574: Line 561:
}
}


local argument_order = {
    'main_page',
    'id',
    'name',
    'act',
    'area_level',
    'level_restriction_max',
    'area_type_tags',
    'tags',
    'loading_screen',
    'connection_ids',
    'parent_area_id',
    'modifier_ids',
    'boss_monster_ids',
    'monster_ids',
    'entry_text',
    'entry_npc',
    'flavour_text',
    'screenshot_ext',
    'screenshot',
    'vaal_area_spawn_chance',
    'vaal_area_ids',
    'strongbox_spawn_chance',
    'strongbox_max_count',
    'strongbox_rarity_weight',
    'is_map_area',
    'is_unique_map_area',
    'is_town_area',
    'is_hideout_area',
    'is_vaal_area',
    'is_master_daily_area',
    'is_labyrinth_area',
    'is_labyrinth_airlock_area',
    'is_labyrinth_boss_area',
    'has_waypoint',
    -- parsed by m_util.args.version:
    -- 'release_version',
    -- 'removal_version',
   
    -- Non argument, but passed to the script
    'areas',
}
local display  = {}
display.area_type = {
display.area_type = {
     'is_map_area',
     'is_map_area',
Line 778: Line 721:
     if tpl_args['name'] then
     if tpl_args['name'] then
         out[#out+1] = string.format(
         out[#out+1] = string.format(
             "'''%s''' is the internal id for the [[%s|%s]] area. ",  
             i18n.intro.text_with_name,  
             tpl_args['id'],  
             tpl_args['id'],  
             tpl_args['main_page'] or tostring(mw.title.getCurrentTitle()),  
             tpl_args['main_page'] or tostring(mw.title.getCurrentTitle()),  
Line 785: Line 728:
     else  
     else  
         out[#out+1] = string.format(
         out[#out+1] = string.format(
             "'''%s''' is the internal id of an unnamed area. ",
             i18n.intro.text_without_name,
             tpl_args['id']
             tpl_args['id']
         )
         )
Line 807: Line 750:
     if #connected_areas > 0 then  
     if #connected_areas > 0 then  
         out[#out+1] = string.format(
         out[#out+1] = string.format(
             'It is connected to the following areas:<ul>%s</ul>',  
             i18n.intro.connections .. '<ul>%s</ul>',  
             table.concat(connected_areas)
             table.concat(connected_areas)
         )
         )
Line 1,032: Line 975:
     -- Shared args
     -- Shared args
     --
     --
    tpl_args._properties = {
        _table = argument_map.table,
    }
   
     -- Handle release_version and removal_version
     -- Handle release_version and removal_version
     m_util.args.version(tpl_args, {frame=frame})
     m_util.args.version(tpl_args, {frame=frame})
      
      
     -- Parse args
     local cargo_values = m_util.args.from_cargo_map{
    for _, k in ipairs(argument_order) do
         tpl_args=tpl_args,
        local data = argument_map.fields[k]
         frame=frame,
        if data == nil then
         table_map=argument_map,
            error('Missing data in argument_map: ' .. k)
         rtr=true,
         end
     }
        if data.func ~= nil then
            data.func(tpl_args, frame)
         end
       
        if data.default ~= nil and tpl_args[k] == nil then
            tpl_args[k] = data.default
        end
    end
   
    -- This should include all values as long they're set, regardless
    -- of whether they are parsed above
    for key, data in pairs(argument_map.fields) do
         local v = tpl_args[key]
         if data.field ~= nil and v ~= nil then
            tpl_args._properties[data.field] = v
        end
     end
      
      
     -- Parse spawn weights
     -- Parse spawn weights
Line 1,068: Line 989:
         frame=frame,  
         frame=frame,  
     })
     })
   
    -- Category handling for main page only by adding the categories in a property:
    -- Do notice the plural form.
    -- -- Areas, Act X areas/Map areas, Unique map areas
    local cats = {
        'Category:Areas',
    }
    local cats_ini_len = #cats
    for _, key in ipairs(display.area_type) do
        if tpl_args[key] == true then
            cats[#cats+1] = string.format('Category:%ss', i18n.tooltips[key])
        end
    end
    if #cats == cats_ini_len then
        cats[#cats+1] = string.format('Category:Act %s areas', tpl_args.act)
    end
    tpl_args._properties['mainpage_categories'] = table.concat(cats, ',')
      
      
     -- Category handling for the local data page:
     -- Category handling for the local data page:
Line 1,097: Line 1,000:
      
      
     -- Property to store what's output to main pages:
     -- Property to store what's output to main pages:
     tpl_args._properties['infobox_html'] = out[1]
     mw.logObject(cargo_values)
    cargo_values['infobox_html'] = out[1]
      
      
     -- Set all semantic properties:
     -- Set all semantic properties:
     mw.logObject('Cargo:' .. m_util.cargo.store(frame, tpl_args._properties))
     mw.logObject('Cargo:' .. m_util.cargo.store(frame, cargo_values))
    mw.logObject(tpl_args._properties)
      
      
     -- Display only on data page:
     -- Display only on data page:

Revision as of 04:20, 16 May 2018

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


This module is used on 4,000+ pages.

To avoid major disruption and server load, do not make unnecessary edits to this module. Test changes to this module first using its /sandbox and /testcases subpages . All of the changes can then be applied to this module in a single edit.

Consider discussing changes on the talk page or on Discord before implementing them.

The item module provides functionality for various area-related templates.

Overview

This module is responsible for creating area boxes, other area-related tasks. In the process a lot of the input data is verified and also added as semantic property to pages; as such, any templates deriving from this module should not be used on user pages other then for temporary testing purposes.

This template is also backed by an export script in PyPoE (pypoe_exporter wiki area ...) which can be used to export item data from the game files which then can be used on the wiki. Use the export when possible.

Implemented templates

Core templates

-- SMW powered area module

-- ----------------------------------------------------------------------------
-- TODO
-- ----------------------------------------------------------------------------
-- invalid tags -> utl?
-- spawn_weight* values
-- spawnchacne values

-- ----------------------------------------------------------------------------
-- Imports
-- ----------------------------------------------------------------------------

local m_util = require('Module:Util')
local getArgs = require('Module:Arguments').getArgs
local m_game = require('Module:Game')
local f_infocard = require('Module:Infocard')._main

local cargo = mw.ext.cargo

-- ----------------------------------------------------------------------------
-- Localization
-- ----------------------------------------------------------------------------

-- Strings
local i18n = {
    images = {
        waypoint_no = '[[File:No waypoint area icon.png|link=|No Waypoint]]',
        waypoint_yes = '[[File:Waypoint area icon.png|link=|No Waypoint]]',
        waypoint_town = '[[File:Town area icon.png|link=|Town Hub]]',
        loading_screen = 'File:%s loading screen.png',
        loading_screen_infobox = '[[File:%s loading screen.png|250px]]',
        screenshot = 'File:%s area screenshot.%s',
        screenshot_infobox = '[[File:%s area screenshot.%s|250px]]',
    },

    args = {
        main_page = 'main_page',
        id = 'id',
        name = 'name',
        act = 'act',
        area_level = 'level',
        level_restriction_max = 'level_restriction_max',
        area_type_tags = 'area_type_tags',
        tags = 'tags',
        loading_screen = 'loading_screen',
        connection_ids = 'connection_ids',
        parent_area_id = 'parent_area_id',
        modifier_ids = 'modifier_ids',
        boss_monster_ids = 'boss_monster_ids',
        monster_ids = 'monster_ids',
        entry_text = 'entry_text',
        entry_npc = 'entry_npc',
        flavour_text = 'flavour_text',
        screenshot_ext = 'screenshot_ext',
        vaal_area_spawn_chance = 'vaal_area_spawn_chance',
        vaal_area_ids = 'vaal_area_ids',
        strongbox_spawn_chance = 'strongbox_spawn_chance',
        strongbox_max_count = 'strongbox_max',
        strongbox_rarity_weight = 'strongbox_rarity_weight',
        is_map_area = 'is_map_area',
        is_unique_map_area = 'is_unique_map_area',
        is_town_area = 'is_town_area',
        is_hideout_area = 'is_hideout_area',
        is_vaal_area = 'is_vaal_area',
        is_master_daily_area = 'is_master_daily_area',
        is_labyrinth_area = 'is_labyrinth_area',
        is_labyrinth_airlock_area = 'is_labyrinth_airlock_area',
        is_labyrinth_boss_area = 'is_labyrinth_boss_area',
        has_waypoint = 'has_waypoint',
    },
    errors = {
        invalid_tag = '%s is not a valid tag',
        main_page_is_invalid = 'main_page argument got "%s" which is not a valid wiki page', 
        main_page_does_not_exist = 'main_page argument requires the specified page "%s" to exist in the main wiki namespace',
    },
    intro = {
        text_with_name = "'''%s''' is the internal id for the [[%s|%s]] [[area]]. ",
        text_without_name = "'''%s''' is the internal id of an unnamed [[area]]. ",
        connections = 'It is connected to the following areas:',
    },
    tooltips = {
        -- boolean tooltips. Use singular form here, categories makes these plural.
        is_map_area = 'Map area',
        is_unique_map_area = 'Unique Map area',
        is_town_area = 'Town area',
        is_hideout_area = 'Hideout area',
        is_vaal_area = 'Vaal area',
        is_master_daily_area = 'Master daily spawn area',
        is_labyrinth_area = 'Labyrinth area',
        is_labyrinth_airlock_area = 'Labyrinth airlock area',
        is_labyrinth_boss_area = 'Labyrinth boss area',
        area = 'area',
        
        --
        entry_message = '%s: %s',
    },
    headers = {
        id = 'Id',
        act = 'Act',
        area_level = 'Area level',
        level_restriction_max = m_util.html.abbr('Max level', 'Characters above this level can not enter this zone.'),
        area_type_tags = 'Area type tags',
        tags = 'Tags',
        parent_area = m_util.html.abbr('Parent Town', 'Portals will lead to the parent town'),
        connections = 'Connections',
        -- monsters
        -- boss monsters
        vaal_areas = 'Vaal Areas',
    },
}

-- ----------------------------------------------------------------------------
-- Constats
-- ----------------------------------------------------------------------------

local c = {}
-- TODO: test
c.max_area_query = 8

-- ----------------------------------------------------------------------------
-- Utility & helper functions
-- ----------------------------------------------------------------------------

local factory = {}

function factory.display_value(k, args)
    return function(tpl_args, frame)
        return tpl_args[k]
    end
end

local util = {}
util.display = {}
function util.display.multiple_areas(tpl_args, frame, area_ids)
    local out = {}
    for _, area_id in ipairs(area_ids) do
        out[#out+1] = util.display.single_area(tpl_args, frame, area_id)
    end
    return table.concat(out, '<br>')
end

function util.display.single_area(tpl_args, frame, area_id)
    if tpl_args.areas[area_id] then
        if tpl_args.areas[area_id]['areas.main_page'] ~= '' then
            return string.format('[[%s|%s]]', tpl_args.areas[area_id]['areas.main_page'], tpl_args.areas[area_id]['areas.name'])
        else
            return string.format('%s ([[%s|%s]])', tpl_args.areas[area_id]['areas.name'], tpl_args.areas[area_id]['areas._pageName'], area_id)
        end
    else
        return area_id
    end
end

-- ----------------------------------------------------------------------------
-- Argument & display mapping
-- ----------------------------------------------------------------------------
local display  = {}

local argument_map = {
    table = 'areas',
    
    order = {
        'main_page',
        'id',
        'name',
        'act',
        'area_level',
        'level_restriction_max',
        'area_type_tags',
        'tags',
        'loading_screen',
        'connection_ids',
        'parent_area_id',
        'modifier_ids',
        -- populated via modifier ids
        'stat_text',
        'boss_monster_ids',
        'monster_ids',
        'entry_text',
        'entry_npc',
        'flavour_text',
        'screenshot_ext',
        'screenshot',
        'vaal_area_spawn_chance',
        'vaal_area_ids',
        'strongbox_spawn_chance',
        'strongbox_max_count',
        'strongbox_rarity_weight',
        -- those four are parsed via the argument above
        'strongbox_weight_normal',
        'strongbox_weight_magic',
        'strongbox_weight_rare',
        'strongbox_weight_unique',
        'is_map_area',
        'is_unique_map_area',
        'is_town_area',
        'is_hideout_area',
        'is_vaal_area',
        'is_master_daily_area',
        'is_labyrinth_area',
        'is_labyrinth_airlock_area',
        'is_labyrinth_boss_area',
        'has_waypoint',
        -- parsed by m_util.args.version:
        'mainpage_categories',
        
        -- Non argument, but passed to the script
        'areas',
    },
    
    --
    -- User supplied arguments
    -- 
    fields = {
        main_page = {
            field = 'main_page', 
            type = 'Page',
            func = function(tpl_args, frame)
                local page = tpl_args.main_page 
                if page ~= nil then
                    page = mw.title.new(tpl_args.main_page)
                    if page == nil then
                        error(string.format(i18n.errors.main_page_is_invalid, tpl_args.main_page))
                    elseif not page.exists then
                        error(string.format(i18n.errors.main_page_does_not_exist, tpl_args.main_page))
                    else
                        -- dont need the title object anymore
                        --tpl_args.main_page = page
                    end
                end
                return tpl_args.main_page
            end
        },
        
        --
        -- Can be populated by PyPoE
        --
        id = {
            field = 'id',
            type = 'String',
            func = nil,
        },
        name = {
            field = 'name',
            type = 'String',
            func = nil,
        },
        act = {
            field = 'act',
            type = 'Integer',
        },
        area_level = {
            field = 'area_level',
            type = 'Integer',
        },
        level_restriction_max = {
            field = 'level_restriction_max',
            type = 'Integer',
            default = 100,
        },
        area_type_tags = {
            field = 'area_type_tags',
            type = 'List (,) of String',
            func = m_util.cast.factory.assoc_table(i18n.args.area_type_tags, {
                tbl = m_game.constants.tags,
                errmsg = i18n.errors.invalid_tag,
                key_out = 'area_type_tags',
            }),
        },
        tags = {
            field = 'tags',
            type = 'List (,) of String',
            func = m_util.cast.factory.assoc_table(i18n.args.tags, {
                tbl = m_game.constants.tags,
                errmsg = i18n.errors.invalid_tag,
                key_out = 'tags',
            }),
        },
        loading_screen = {
            field = 'loading_screen',
            type = 'Page',
            func = function (tpl_args, frame)
                local loading_id = tpl_args[i18n.args.loading_screen]
                if loading_id ~= nil then
                    tpl_args.loading_screen_infobox = string.format(i18n.images.loading_screen_infobox, loading_id)
                    
                    return string.format(i18n.images.loading_screen, loading_id)
                end
            end,
        },
        connection_ids = {
            field = 'connection_ids',
            type = 'List (,) of String',
            default = {},
        },
        parent_area_id = {
            field = 'parent_area_id',
            type = 'String',
        },
        modifier_ids = {
            field = 'modifier_ids',
            type = 'List (,) of String',
            func = function(tpl_args, frame, value)
                if value == nil then
                    return
                end
                tpl_args.mods = m_util.cargo.array_query{
                    tables={'mods'},
                    fields={'mods.stat_text'},
                    id_field='mods.id',
                    id_array=tpl_args.modifier_ids
                }
                
                return tpl_args.modifier_ids
            end,
            default = {},
        },
        stat_text = {
            field = 'stat_text',
            type = 'Text',
            func = function (tpl_args, frame)
                if tpl_args.mods == nil then
                    return
                end
                local text = {}
                for page, row in pairs(tpl_args.mods) do
                    if row['mods.stat_text'] ~= '' then
                        text[#text+1] = row['mods.stat_text']
                    end
                end
                return table.concat(text, '<br>')
            end,
        },
        monster_ids = {
            field = 'monster_ids',
            type = 'List (,) of String',
            default = {},
        },
        boss_monster_ids = {
            field = 'boss_monster_ids',
            type = 'List (,) of String',
            default = {},
        },
        entry_text = {
            field = 'entry_text',
            type = 'Text',
        },
        entry_npc = {
            field = 'entry_npc',
            type = 'String',
        },
        flavour_text = {
            field = 'flavour_text',
            type = 'Text',
        },
        screenshot_ext = {
            func = nil,
            default = 'jpg',
        },
        screenshot = {
            field = 'screenshot',
            type = 'Page',
            func = function(tpl_args, frame)
                if tpl_args.name ~= nil then
                    local name = tpl_args.screenshot or tpl_args.main_page or tpl_args.name
                    tpl_args.screenshot = string.format(i18n.images.screenshot, name, tpl_args.screenshot_ext)
                    tpl_args.screenshot_infobox = string.format(i18n.images.screenshot_infobox, name, tpl_args.screenshot_ext)
                end
            end,
        },
        --
        -- Spawn chances
        --
        vaal_area_ids = {
            field = 'vaal_area_ids',
            type = 'List (,) of String',
            default = {},
        },
        vaal_area_spawn_chance = {
            field = 'vaal_area_spawn_chance',
            type = 'Integer',
            default = 0,
        },
        strongbox_spawn_chance = {
            field = 'strongbox_spawn_chance',
            type = 'Integer',
            default = 0,
        },
        strongbox_max_count = {
            field = 'strongbox_max_count',
            type = 'Integer',
            default = 0,
        },
        -- fields are handled for this below
        strongbox_rarity_weight = {
            func = function (tpl_args, frame)
                local weights = m_util.string.split(tpl_args[i18n.args.strongbox_rarity_weight] or '', ', ')
                
                tpl_args.strongbox_rarity_weight = {}
                
                for index, data in ipairs(m_game.constants.item.rarity) do
                    local value = tonumber(weights[index]) or 0
                    -- will be read later
                    tpl_args.strongbox_rarity_weight[data.long_lower] = value
                end
            end,
        },
        strongbox_weight_normal = {
            field = 'strongbox_weight_normal',
            type = 'Integer',
        },
        strongbox_weight_magic = {
            field = 'strongbox_weight_magic',
            type = 'Integer',
        },
        strongbox_weight_rare = {
            field = 'strongbox_weight_rare',
            type = 'Integer',
        },
        strongbox_weight_unique = {
            field = 'strongbox_weight_unique',
            type = 'Integer',
        },
        --
        -- Area flags
        --
        is_map_area = {
            field = 'is_map_area',
            type = 'Boolean',
            default = false,
        },
        is_unique_map_area = {
            field = 'is_unique_map_area',
            type = 'Boolean',
            default = false,
        },
        is_town_area = {
            field = 'is_town_area',
            type = 'Boolean',
            default = false,
        },
        is_hideout_area = {
            field = 'is_hideout_area',
            type = 'Boolean',
            default = false,
        },
        is_vaal_area = {
            field = 'is_vaal_area',
            type = 'Boolean',
            default = false,
        },
        is_master_daily_area = {
            field = 'is_is_master_daily_area',
            type = 'Boolean',
            default = false,
        },
        is_labyrinth_area = {
            field = 'is_labyrinth_area',
            type = 'Boolean',
            default = false,
        },
        is_labyrinth_airlock_area = {
            field = 'is_labyrinth_airlock_area',
            type = 'Boolean',
            default = false,
        },
        is_labyrinth_boss_area = {
            field = 'is_labyrinth_boss_area',
            type = 'Boolean',
            default = false,
        },
        has_waypoint = {
            field = 'has_waypoint',
            type = 'Boolean',
            default = false,
        },
        mainpage_categories = {
            field = 'mainpage_categories',
            type = 'List (,) of String',
            func = function (tpl_args, frame)
                -- Category handling for main page only by adding the categories to a cargo field:
                -- Do notice the plural form.
                -- -- Areas, Act X areas/Map areas, Unique map areas 
                local cats = {
                    'Category:Areas', 
                }
                local cats_ini_len = #cats
                for _, key in ipairs(display.area_type) do
                    if tpl_args[key] == true then
                        cats[#cats+1] = string.format('Category:%ss', i18n.tooltips[key])
                    end
                end
                if #cats == cats_ini_len then 
                    cats[#cats+1] = string.format('Category:Act %s areas', tpl_args.act)
                end
                return cats
            end
        },
        --
        -- Handled elsewhere
        --
        release_version = {
            field = 'release_version',
            type = 'String',
            skip = true,
        },
        removal_version = {
            field = 'removal_version',
            type = 'String',
            skip = true,
        },
        infobox_html = {
            field = 'infobox_html',
            type = 'Text',
            skip = true,
        },
        --
        -- Not argument to the template, but still parsing arguments
        --
        areas = {
            func = function(tpl_args, frame)
                local query_ids = {}
                for _, arg in ipairs({'parent_area_id', 'connection_ids', 'vaal_area_ids'}) do 
                    if type(tpl_args[arg]) == 'table' then
                        for _, tbl_id in ipairs(tpl_args[arg]) do
                            query_ids[tbl_id] = {}
                        end
                    elseif tpl_args[arg] then
                        query_ids[tpl_args[arg]] = {}
                    end
                end
                
                local query_ids_trimmed = {}
                for k, _ in pairs(query_ids) do
                    if type(k) ~= 'number' then
                        query_ids_trimmed[#query_ids_trimmed+1] = string.format('areas.id="%s"', k)
                    end
                end
                
                if #query_ids_trimmed == 0 then
                    tpl_args.areas = {}
                else
                    local result = cargo.query(
                        'areas',
                        'areas._pageName, areas.id, areas.name, areas.main_page',
                        {
                            where=table.concat(query_ids_trimmed, ' OR '),
                        }
                    )
                    
                    tpl_args.areas = {}
                    for _, row in ipairs(result) do
                        tpl_args.areas[row['areas.id']] = row
                    end
                end
                -- TODO: Error/Warning for missing areas?
            end,
        },
    },
}

display.area_type = {
    'is_map_area',
    'is_unique_map_area',
    'is_town_area',
    'is_hideout_area',
    'is_vaal_area',
    'is_master_daily_area',
    'is_labyrinth_area',
    'is_labyrinth_airlock_area',
    'is_labyrinth_boss_area',
}

display.table_map = {
    {
        args = {
            id = {
            },
        },
        header = i18n.headers.id,
        func = function(tpl_args, frame)
            return string.format('[[%s|%s]]', mw.title.getCurrentTitle().fullText, tpl_args.id)
        end,
    },
    {
        args = {
            act = {
            },
        },
        header = i18n.headers.act,
        func = factory.display_value('act'),
    }, 
    {
        args = {
            area_level = {
            },
        },
        header = i18n.headers.area_level,
        func = factory.display_value('area_level'),
    },
    {
        args = {
            level_restriction_max = {
                hide = 100,
            },
        },
        header = i18n.headers.level_restriction_max,
        func = factory.display_value('level_restriction_max'),
    },
    {
        args = {
            area_type_tags = {
            },
        },
        header = i18n.headers.area_type_tags,
        func = function(tpl_args, frame)
            return table.concat(tpl_args.area_type_tags, ', ')
        end,
    },
    {
        args = {
            tags = {
            },
        },
        header = i18n.headers.tags,
        func = function(tpl_args, frame)
            return table.concat(tpl_args.tags, ', ')
        end,
    },
    {
        args = {
            entry_text = {},
            entry_npc = {},
        },
        header = i18n.headers.entry_messsage,
        func = function(tpl_args, frame)
            return m_util.html.poe_color('quest',  -- Any other alternatives for spoken text?
                string.format(i18n.tooltips.entry_message, tpl_args.entry_npc, tpl_args.entry_text) 
            )
        end,
    },
    {
        args = {
            parent_area_ids = {},
        },
        header = i18n.headers.parent_area,
        func = function(tpl_args, frame)
            return util.display.single_area(tpl_args, frame, tpl_args.parent_area)
        end,
    },
    {
        args = {
            connection_ids = {},
        },
        header = i18n.headers.connections,
        func = function(tpl_args, frame)
            return util.display.multiple_areas(tpl_args, frame, tpl_args.connection_ids)
        end,
    },
    {
        args = {
            vaal_area_ids = {},
        },
        header = i18n.headers.vaal_areas,
        func = function(tpl_args, frame)
            return util.display.multiple_areas(tpl_args, frame, tpl_args.vaal_area_ids)
        end,
    },
}

display.list_map = {
    {
        args = {
            flavour_text = {},
        },
        func = function(tpl_args, frame)
            return m_util.html.poe_color('flavour', tpl_args.flavour_text)
        end,
    },
    {
        args = {
            loading_screen_infobox = {},
        },
        func = factory.display_value('loading_screen_infobox'),
    },
    {
        args = {
            screenshot_infobox = {},
        },
        func = factory.display_value('screenshot_infobox'),
    },
    {
        args = {
            stat_text = {},
        },
        func = function(tpl_args, frame)
            return m_util.html.poe_color('mod', tpl_args.stat_text)
        end,
    },
}

-- ----------------------------------------------------------------------------
-- display functions
-- ----------------------------------------------------------------------------

local d = {}

function d.intro_text(tpl_args, frame)
    --[[
    Display an introductory text about the area.
    ]]
    local out = {}
    if mw.ustring.find(tpl_args['id'], '_') then
        out[#out+1] = frame:expandTemplate{
            title='Incorrect title', 
            args = {title=tpl_args['id']} 
        }
    end
    
    if tpl_args['name'] then
        out[#out+1] = string.format(
            i18n.intro.text_with_name, 
            tpl_args['id'], 
            tpl_args['main_page'] or tostring(mw.title.getCurrentTitle()), 
            tpl_args['name']
        )
    else 
        out[#out+1] = string.format(
            i18n.intro.text_without_name,
            tpl_args['id']
        )
    end 

    local connected_areas = {}
    for _, arg in ipairs({'connection_ids', 'vaal_area_ids'}) do  
        if tpl_args[arg] then
            for _, id in ipairs(tpl_args[arg]) do
                if tpl_args.areas[id] then 
                    connected_areas[#connected_areas+1] = string.format(
                        '<li>[[%s|%s]] (%s)</li>', 
                        tostring(tpl_args.areas[id]['areas._pageName']), 
                        id, 
                        tostring(tpl_args.areas[id]['areas.name'])
                    )
                end
            end
        end
    end
    if #connected_areas > 0 then 
        out[#out+1] = string.format(
            i18n.intro.connections .. '<ul>%s</ul>', 
            table.concat(connected_areas)
        )
    end
    
    return table.concat(out)
end

function d._check_args(tpl_args, frame, data)
    local continue = true
    if data.args ~= nil then
        for key, key_data in pairs(data.args) do
            if tpl_args[key] == nil or (type(tpl_args[key]) == 'table' and #tpl_args[key] == 0) then
                continue = false
                break
            elseif type(key_data.hide) == 'table' then
                local br = false
                for _, value in ipairs(key_data.hide) do
                    if tpl_args[key] == value then
                        br = true
                        break
                    end
                end
                if br then
                    continue = false
                    break
                end
            elseif key_data.hide ~= nil and tpl_args[key] == key_data.hide then
                continue = false
                break
            end
        end
    end
    return continue
end

function d.area_box(tpl_args, frame)
    --[[
    Display the area info box.
    ]]
    
    local infocard_args = {}
    
    infocard_args.header = tpl_args.name
    
    -- Subheader
    local out = {}
    for _, key in ipairs(display.area_type) do
        if tpl_args[key] == true then
            out[#out+1] = i18n.tooltips[key]
        end
    end

    if #out > 0 then
        infocard_args.subheader = table.concat(out, ', ')
    else
        infocard_args.subheader = i18n.tooltips.area
    end
    
    -- Side header
    if tpl_args.is_town_area then
        infocard_args.headerright = i18n.images.waypoint_town
    elseif tpl_args.has_waypoint then
        infocard_args.headerright = i18n.images.waypoint_yes
    else
        infocard_args.headerright = i18n.images.waypoint_no
    end
    
    -- Main sections, loop through
    local tbl = mw.html.create('table')
    for _, data in ipairs(display.table_map) do
        if d._check_args(tpl_args, frame, data) then
            tbl
                :tag('tr')
                    :tag('th')
                        :wikitext(data.header or '')
                        :done()
                    :tag('td')
                        :wikitext(data.func(tpl_args, frame) or '')
                        :done()
                    :done()
        end
    end
    
    infocard_args[1] = tostring(tbl)
    
    local i = 2
    for _, data in ipairs(display.list_map) do
        if d._check_args(tpl_args, frame, data) then
            infocard_args[i] = data.func(tpl_args, frame)
            i = i + 1
        end
    end

    return f_infocard(infocard_args)
end

function d.subobject_box(tpl_args, frame)
    --[[
    Display the subobject box.
    ]]
    local container = mw.html.create('div')
    container
        :attr('class', 'modbox floatright')
        
    -- spawn weight table 
    tbl = container:tag('table')
    tbl
        :attr('class', 'wikitable sortable')
        -- :attr('style', 'style="width: 100%;"')
        :tag('tr')
            :tag('th')
                :attr('colspan', 3)
                :wikitext('Spawn Weights')
                :done()
            :done()
        :tag('tr')
            :tag('th')
                :wikitext('#')
                :done()
            :tag('th')
                :wikitext('Tag')
                :done()
            :tag('th')
                :wikitext('Has spawn weight')
                :done()
            :done()
        :done()
        
    local i = 0
    local value = nil
    repeat
        i = i + 1
        value = {
            tag = tpl_args[string.format('spawn_weight%s_tag', i)],
            value = tpl_args[string.format('spawn_weight%s_value', i)],
        }
        
        if value.tag then
            tbl
                :tag('tr')
                    :tag('td')
                        :wikitext(i)
                        :done()
                    :tag('td')
                        :wikitext(value.tag)
                        :done()
                    :tag('td')
                        :wikitext(value.value)
                        :done()
                    :done()
                :done()
        end
    until value.tag == nil
    
    return tostring(container)
end

-- ----------------------------------------------------------------------------
-- Page functions
-- ----------------------------------------------------------------------------

local p = {}

p.table_areas = m_util.cargo.declare_factory{data=argument_map}

function p.area(frame)
    --[[
    This function adds cargo tables and displays information about the 
    area.
    
    Examples
    --------
    = p.area{
        id = '1_1_1', 
        name = 'The Twilight Strand', 
        act = 1, 
        level = 1, 
        tags = 'no_tempests, area_with_water', 
        loading_screen = 'Act1', 
        connection_ids = '1_1_town',
        parent_area_id = '1_1_town',  
        boss_monster_ids = 'Metadata/Monsters/ZombieBoss/ZombieBossHillockNormal', 
        flavour_text = 'Hope was drowned here.', 
        main_page = 'The Twilight Strand (Act 1)'
    }  
    = p.area{
        id = '1_1_1', 
        name = 'The Twilight Strand', 
        act = 1, 
        level = 1, 
        tags = 'no_tempests, area_with_water', 
        loading_screen = 'Act1', 
        connection_ids = '1_1_town',
        parent_area_id = '1_1_town',  
        boss_monster_ids = 'Metadata/Monsters/ZombieBoss/ZombieBossHillockNormal', 
        flavour_text = 'Hope was drowned here.', main_page = 'The Twilight Strand (Act 1)'
    }
    = p.area{
        id = '1_4_2', 
        name = 'The Dried Lake', 
        act = '4', 
        level = '34', 
        area_type_tags = 'shore', 
        tags = 'act_boss_area', 
        loading_screen = 'Act4', 
        connection_ids = '1_4_town', 
        parent_area_id = '1_4_town', 
        boss_monster_ids = 'Metadata/Monsters/Voll/VollBoss, Metadata/Monsters/SkeletonSoldier/SkeletonSoldierRangedBoss', 
        vaal_area_spawn_chance = '18', 
        vaal_area_ids = '1_SideArea4_2, 1_SideArea4_4', 
        strongbox_spawn_chance = '30', 
        strongbox_max = '2', strongbox_rarity_weight = '50, 50, 50, 1', 
        flavour_text = 'Bones of betrayal, ashes of purity.', main_page = 'The Dried Lake'}
    ]]
    
    
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
    
    --
    -- Shared args
    --
    -- Handle release_version and removal_version
    m_util.args.version(tpl_args, {frame=frame})
    
    local cargo_values = m_util.args.from_cargo_map{
        tpl_args=tpl_args,
        frame=frame,
        table_map=argument_map,
        rtr=true,
    }
    
    -- Parse spawn weights
    m_util.args.spawn_weight_list(tpl_args, {
        frame=frame, 
    })
    
    -- Category handling for the local data page:
    local page_cats = {
        'Area data',
    }
    
    -- Display only on main pages:
    local out = {}
    out[#out+1] = d.area_box(tpl_args, frame)
    
    -- Property to store what's output to main pages:
    mw.logObject(cargo_values)
    cargo_values['infobox_html'] = out[1]
    
    -- Set all semantic properties:
    mw.logObject('Cargo:' .. m_util.cargo.store(frame, cargo_values))
    
    -- Display only on data page:
    out[#out+1] = d.subobject_box(tpl_args, frame)
    out[#out+1] = d.intro_text(tpl_args, frame)
    
    -- Output of function
    return table.concat(out) .. m_util.misc.add_category(page_cats)
end

function p.query_area_info(frame)
    --[[
    Queries and displays the area infobox. 
    
    ]]
    -- = p.query_area_info{conditions = '[[Is area id::test]]', cats='yes'}
    local tpl_args = getArgs(frame, {
        parentFirst = true
    })
    frame = m_util.misc.get_frame(frame)
    
    if tpl_args.where == nil then
        return
    end
    
    tpl_args.cats = m_util.cast.boolean(tpl_args.cats)
    
    local results = cargo.query(
        'areas',
        'areas.infobox_html, areas.mainpage_categories',
        {
            where=tpl_args.where,
            orderBy=tpl_args.order_by,
            -- Temporary fix for cargo duplicating entries
            groupBy='areas._pageID',
        }
    )
    
    local out = {}
    local cats = {}
    
    for _, row in ipairs(results) do
        out[#out+1] = row['areas.infobox_html']
        if row['areas.mainpage_categories'] ~= '' then
            for _, cat in ipairs(m_util.string.split(row['areas.mainpage_categories'], ',')) do
                cats[#cats+1] = string.gsub(cat, '[Cc]ategory:', '')
            end
        end
    end
    if tpl_args.cats then
        return table.concat(out) .. m_util.misc.add_category(cats)
    else
        return table.concat(out)
    end
end

return p