Jump to content

Module:ItemTooltip: Difference between revisions

From Apogea Wiki
Jayarrowz (talk | contribs)
Item toolip module
 
Jayarrowz (talk | contribs)
No edit summary
 
(4 intermediate revisions by the same user not shown)
Line 23: Line 23:
}
}


-- Format a stat line with positive/negative coloring
-- Helper to get args from either direct invoke or template
local function formatStat(label, value)
local function getArgs(frame)
    local args = {}
   
    local parent = frame:getParent()
    if parent and parent.args then
        for k, v in pairs(parent.args) do
            args[k] = v
        end
    end
   
    if frame.args then
        for k, v in pairs(frame.args) do
            args[k] = v
        end
    end
   
    return args
end
 
-- Format a stat line with positive/negative coloring and optional bonus
local function formatStat(label, value, bonus)
     if not value or value == "" then
     if not value or value == "" then
         return nil
         return nil
Line 38: Line 58:
         else
         else
             colorClass = "color-coral"
             colorClass = "color-coral"
             prefix = "" -- negative sign already included
             prefix = ""
        end
       
        -- Check for bonus value
        local bonusStr = ""
        if bonus and bonus ~= "" then
            local numBonus = tonumber(bonus)
            if numBonus then
                if numBonus >= 0 then
                    bonusStr = string.format('<span class="color-columbia">+%s</span>', bonus)
                else
                    bonusStr = string.format('<span class="color-coral">%s</span>', bonus)
                end
            end
         end
         end
         return string.format('<p>%s: <span class="%s">%s%s</span></p>', label, colorClass, prefix, value)
       
         return string.format('<p>%s: <span class="%s">%s%s</span>%s</p>', label, colorClass, prefix, value, bonusStr)
     else
     else
        -- Non-numeric value (like "2/10" for size)
         return string.format('<p>%s: %s</p>', label, value)
         return string.format('<p>%s: %s</p>', label, value)
     end
     end
Line 61: Line 94:
-- Main display function
-- Main display function
function p.display(frame)
function p.display(frame)
     local args = frame.args
     local args = getArgs(frame)
      
      
     -- Get parameters
     -- Get parameters
Line 69: Line 102:
     local description = args.description or ""
     local description = args.description or ""
     local spriteSize = args.spriteSize or "64"
     local spriteSize = args.spriteSize or "64"
     local float = args.float or "right" -- Default to right side
     local float = args.float or "right"
      
      
     -- Stats
     -- Stats (base values)
     local damage = args.damage
     local damage = args.damage
     local defense = args.defense
     local defense = args.defense
     local range = args.range
     local range = args.range
     local attackspeed = args.attackspeed
     local attackspeed = args.attackspeed
    local armor = args.armor
     local movespeed = args.movespeed
     local movespeed = args.movespeed
     local hpRegen = args.hpRegen
     local hpRegen = args.hpRegen
Line 81: Line 115:
     local size = args.size
     local size = args.size
     local weight = args.weight
     local weight = args.weight
   
    -- Bonus stats (e.g., from enchantments)
    local damageBonus = args.damageBonus
    local defenseBonus = args.defenseBonus
    local rangeBonus = args.rangeBonus
    local attackspeedBonus = args.attackspeedBonus
    local movespeedBonus = args.movespeedBonus
    local armorBonus = args.armorBonus
    local hpRegenBonus = args.hpRegenBonus
    local mpRegenBonus = args.mpRegenBonus
      
      
     -- Special properties
     -- Special properties
     local special = args.special -- e.g., "Fills you for 340 seconds"
     local special = args.special
     local action = args.action   -- e.g., "Right-click to eat"
     local action = args.action
     local category = args.category -- e.g., "Special Foods"
     local category = args.category
      
      
     -- Get rarity color
     -- Get rarity color
Line 95: Line 139:
      
      
     -- Container with float
     -- Container with float
     table.insert(html, string.format('<div class="tooltip-panel font-apogea-long" style="float:%s; margin-left:%s; margin-bottom:10px; clear:%s;">',  
     table.insert(html, string.format('<div class="tooltip-panel font-apogea-long" style="float:%s; margin-%s:15px; margin-bottom:10px;">',  
         float,  
         float,  
         float == "right" and "15px" or "0",
         float == "right" and "left" or "right"))
        float))
      
      
     -- Sprite
     -- Sprite
Line 107: Line 150:
      
      
     -- Description (jade color)
     -- Description (jade color)
     if description ~= "" then
     if description and description ~= "" then
         table.insert(html, formatLine(description, "color-jade"))
         table.insert(html, formatLine(description, "color-jade"))
     end
     end
      
      
     -- Stats (in typical order)
     -- Stats (in typical order)
     local statLines = {
     if range and range ~= "" then
         formatStat("Range", range),
         table.insert(html, formatStat("Range", range, rangeBonus))
         formatStat("Damage", damage),
    end   
         formatStat("Attackspeed", attackspeed),
    if armor and armor ~= "" then
         formatStat("Defense", defense),
        table.insert(html, formatStat("Armor", armor, armorBonus))
         formatStat("Movespeed", movespeed),
    end
         formatStat("Hp Regen", hpRegen),
    if damage and damage ~= "" then
        formatStat("Mp Regen", mpRegen)
         table.insert(html, formatStat("Damage", damage, damageBonus))
     }
    end
      
    if attackspeed and attackspeed ~= "" then
    for _, line in ipairs(statLines) do
         table.insert(html, formatStat("Attackspeed", attackspeed, attackspeedBonus))
        if line then
    end
            table.insert(html, line)
    if defense and defense ~= "" then
        end
         table.insert(html, formatStat("Defense", defense, defenseBonus))
    end
    if movespeed and movespeed ~= "" then
         table.insert(html, formatStat("Movespeed", movespeed, movespeedBonus))
    end
    if hpRegen and hpRegen ~= "" then
         table.insert(html, formatStat("Hp Regen", hpRegen, hpRegenBonus))
     end
     if mpRegen and mpRegen ~= "" then
        table.insert(html, formatStat("Mp Regen", mpRegen, mpRegenBonus))
     end
     end
      
      
Line 164: Line 216:
     table.insert(html, '</div>')
     table.insert(html, '</div>')
      
      
    -- Return as wikitext (the Sprite template will be processed by MediaWiki)
     return frame:preprocess(table.concat(html, '\n'))
     return frame:preprocess(table.concat(html, '\n'))
end
end
Line 170: Line 221:
-- Quick display for common items
-- Quick display for common items
function p.common(frame)
function p.common(frame)
     frame.args.rarity = "common"
     local args = getArgs(frame)
     return p.display(frame)
    args.rarity = "common"
     return p._display(frame, args)
end
end


function p.uncommon(frame)
function p.uncommon(frame)
     frame.args.rarity = "uncommon"
     local args = getArgs(frame)
     return p.display(frame)
    args.rarity = "uncommon"
     return p._display(frame, args)
end
end


function p.rare(frame)
function p.rare(frame)
     frame.args.rarity = "rare"
     local args = getArgs(frame)
     return p.display(frame)
    args.rarity = "rare"
     return p._display(frame, args)
end
end


function p.epic(frame)
function p.epic(frame)
     frame.args.rarity = "epic"
     local args = getArgs(frame)
     return p.display(frame)
    args.rarity = "epic"
     return p._display(frame, args)
end
end


function p.legendary(frame)
function p.legendary(frame)
     frame.args.rarity = "legendary"
     local args = getArgs(frame)
     return p.display(frame)
    args.rarity = "legendary"
     return p._display(frame, args)
end
 
-- Internal display with pre-fetched args
function p._display(frame, args)
    local name = args.name or "Unknown Item"
    local rarity = string.lower(args.rarity or "common")
    local itemType = args.type or ""
    local description = args.description or ""
    local spriteSize = args.spriteSize or "64"
    local float = args.float or "right"
   
    local damage = args.damage
    local defense = args.defense
    local armor = args.armor
    local range = args.range
    local attackspeed = args.attackspeed
    local movespeed = args.movespeed
    local hpRegen = args.hpRegen
    local mpRegen = args.mpRegen
    local size = args.size
    local weight = args.weight
   
    local damageBonus = args.damageBonus
    local defenseBonus = args.defenseBonus
    local rangeBonus = args.rangeBonus
    local armorBonus = args.armorBonus
    local attackspeedBonus = args.attackspeedBonus
    local movespeedBonus = args.movespeedBonus
    local hpRegenBonus = args.hpRegenBonus
    local mpRegenBonus = args.mpRegenBonus
   
    local special = args.special
    local action = args.action
    local category = args.category
   
    local rarityColor = rarityColors[rarity] or "silver"
    local rarityText = rarityNames[rarity] or ""
   
    local html = {}
   
    table.insert(html, string.format('<div class="tooltip-panel font-apogea-long" style="float:%s; margin-%s:15px; margin-bottom:10px;">',
        float,
        float == "right" and "left" or "right"))
   
    table.insert(html, string.format('{{Sprite|%s|%s}}', name, spriteSize))
    table.insert(html, string.format('<p class="font-bitcell color-%s">%s</p>', rarityColor, name))
   
    if description and description ~= "" then
        table.insert(html, formatLine(description, "color-jade"))
    end
   
    if range and range ~= "" then
        table.insert(html, formatStat("Range", range, rangeBonus))
    end
    if armor and armor  ~= "" then
        table.insert(html, formatStat("Armor", armor, armorBonus))
    end
    if damage and damage ~= "" then
        table.insert(html, formatStat("Damage", damage, damageBonus))
    end
    if attackspeed and attackspeed ~= "" then
        table.insert(html, formatStat("Attackspeed", attackspeed, attackspeedBonus))
    end
    if defense and defense ~= "" then
        table.insert(html, formatStat("Defense", defense, defenseBonus))
    end
    if movespeed and movespeed ~= "" then
        table.insert(html, formatStat("Movespeed", movespeed, movespeedBonus))
    end
    if hpRegen and hpRegen ~= "" then
        table.insert(html, formatStat("Hp Regen", hpRegen, hpRegenBonus))
    end
    if mpRegen and mpRegen ~= "" then
        table.insert(html, formatStat("Mp Regen", mpRegen, mpRegenBonus))
    end
   
    if size and size ~= "" then
        table.insert(html, string.format('<p>Size: %s</p>', size))
    end
   
    if weight and weight ~= "" then
        table.insert(html, string.format('<p>It weighs %s oz.</p>', weight))
    end
   
    if special and special ~= "" then
        table.insert(html, formatLine(special, "color-columbia"))
    end
   
    if rarity ~= "common" then
        table.insert(html, formatLine(rarityText, "color-" .. rarityColor))
    end
   
    if action and action ~= "" then
        table.insert(html, formatLine("[" .. action .. "]", "color-silver"))
    end
   
    if itemType ~= "" then
        table.insert(html, formatLine("[" .. itemType .. "]", "color-silver"))
    end
   
    if category and category ~= "" then
        table.insert(html, formatLine("[" .. category .. "]", "color-silver"))
    end
   
    table.insert(html, '</div>')
   
    return frame:preprocess(table.concat(html, '\n'))
end
end


return p
return p

Latest revision as of 22:41, 27 January 2026

Module:ItemTooltip

[edit source]

This module displays styled item tooltip panels for wiki pages.

Basic Usage

[edit source]
{{#invoke:ItemTooltip|display
|name=Item Name
|rarity=common
|damage=10
|weight=5
}}

Parameters

[edit source]
Parameter Required Description
name Yes The item's display name (also used for sprite)
rarity No common, uncommon, rare, epic, or legendary (default: common)
type No Item type shown in brackets, e.g., "Dagger", "Large Weapons"
description No Flavor text (shown in green/jade color)
float No "right" or "left" (default: right)
spriteSize No Sprite size in pixels (default: 64)
Parameter Bonus Parameter Description
damage damageBonus Damage value
armor armorBonus Armor value
defense defenseBonus Defense value
range rangeBonus Range value
attackspeed attackspeedBonus Attack speed (negative values show in red)
movespeed movespeedBonus Movement speed
hpRegen hpRegenBonus HP regeneration
mpRegen mpRegenBonus MP regeneration
size - Size value (e.g., "8/10")
weight - Weight in oz (just the number)

Special Properties

[edit source]
Parameter Description
special Special effect text (shown in blue), e.g., "Fills you for 340 seconds"
action Action hint, e.g., "right-click to eat"
category Additional category, e.g., "Special Foods"

Rarity Colors

[edit source]
Rarity Color Example
common Silver Template:Color
uncommon Mint/Green Template:Color
rare Sky Blue Template:Color
epic Pink Template:Color
legendary Gold Template:Color

Rarity Shortcuts

[edit source]

You can use shortcut functions instead of specifying rarity:

{{#invoke:ItemTooltip|common|name=Basic Sword|...}}
{{#invoke:ItemTooltip|uncommon|name=Silver Dagger|...}}
{{#invoke:ItemTooltip|rare|name=Crossbow|...}}
{{#invoke:ItemTooltip|epic|name=Battle Axe|...}}
{{#invoke:ItemTooltip|legendary|name=Excalibur|...}}

Examples

[edit source]

Common Food Item

[edit source]
{{#invoke:ItemTooltip|display
|name=Blueberry Muffin
|rarity=common
|damage=2
|movespeed=1
|hpRegen=12
|mpRegen=8
|weight=10.5
|special=Fills you for 340 seconds
|action=right-click to eat
|category=Special Foods
}}

Weapon with Bonus Stats

[edit source]
{{#invoke:ItemTooltip|display
|name=Broadsword
|rarity=common
|range=2
|damage=27
|damageBonus=4
|attackspeed=-3
|defense=5
|size=8/10
|weight=75
|type=Large Weapons
}}

Uncommon Weapon

[edit source]
{{#invoke:ItemTooltip|uncommon
|name=Silver Dagger
|type=Dagger
|description=For rituals.
|damage=4
|attackspeed=3
|size=2/10
|weight=5.6
}}

Rare Weapon

[edit source]
{{#invoke:ItemTooltip|rare
|name=Crossbow
|type=Crossbow
|range=61
|damage=4
|attackspeed=-3
|defense=2
|size=9/10
|weight=22.4
}}

Epic Weapon

[edit source]
{{#invoke:ItemTooltip|epic
|name=Battle Axe
|type=Large Axe
|description=A nhordic axe.
|range=4
|damage=44
|attackspeed=-1
|defense=4
|size=8/10
|weight=45
}}

Legendary Tool

[edit source]
{{#invoke:ItemTooltip|legendary
|name=Shovel
|type=Tools
|description=Used to open holes.
|damage=11
|defense=5
|size=6/10
|weight=13
}}

Stat Colors

[edit source]

Float Position

[edit source]

By default, tooltips float to the right. Use float=left to position on the left side:

{{#invoke:ItemTooltip|display
|name=Item Name
|float=left
|...
}}

-- Module:ItemTooltip
-- Displays game item tooltips with stats and rarity styling
-- Usage: {{#invoke:ItemTooltip|display|name=Item Name|...}}

local p = {}

-- Rarity color mapping
local rarityColors = {
    common = "silver",
    uncommon = "mint",
    rare = "sky",
    epic = "pink",
    legendary = "gold"
}

-- Rarity display names
local rarityNames = {
    common = "Common item.",
    uncommon = "Uncommon item.",
    rare = "Rare item.",
    epic = "Epic item.",
    legendary = "Legendary item."
}

-- Helper to get args from either direct invoke or template
local function getArgs(frame)
    local args = {}
    
    local parent = frame:getParent()
    if parent and parent.args then
        for k, v in pairs(parent.args) do
            args[k] = v
        end
    end
    
    if frame.args then
        for k, v in pairs(frame.args) do
            args[k] = v
        end
    end
    
    return args
end

-- Format a stat line with positive/negative coloring and optional bonus
local function formatStat(label, value, bonus)
    if not value or value == "" then
        return nil
    end
    
    local numValue = tonumber(value)
    local colorClass, prefix
    
    if numValue then
        if numValue >= 0 then
            colorClass = "color-conifer"
            prefix = "+"
        else
            colorClass = "color-coral"
            prefix = ""
        end
        
        -- Check for bonus value
        local bonusStr = ""
        if bonus and bonus ~= "" then
            local numBonus = tonumber(bonus)
            if numBonus then
                if numBonus >= 0 then
                    bonusStr = string.format('<span class="color-columbia">+%s</span>', bonus)
                else
                    bonusStr = string.format('<span class="color-coral">%s</span>', bonus)
                end
            end
        end
        
        return string.format('<p>%s: <span class="%s">%s%s</span>%s</p>', label, colorClass, prefix, value, bonusStr)
    else
        return string.format('<p>%s: %s</p>', label, value)
    end
end

-- Format a plain text line
local function formatLine(text, colorClass)
    if not text or text == "" then
        return nil
    end
    if colorClass then
        return string.format('<p class="%s">%s</p>', colorClass, text)
    else
        return string.format('<p>%s</p>', text)
    end
end

-- Main display function
function p.display(frame)
    local args = getArgs(frame)
    
    -- Get parameters
    local name = args.name or "Unknown Item"
    local rarity = string.lower(args.rarity or "common")
    local itemType = args.type or ""
    local description = args.description or ""
    local spriteSize = args.spriteSize or "64"
    local float = args.float or "right"
    
    -- Stats (base values)
    local damage = args.damage
    local defense = args.defense
    local range = args.range
    local attackspeed = args.attackspeed
    local armor = args.armor
    local movespeed = args.movespeed
    local hpRegen = args.hpRegen
    local mpRegen = args.mpRegen
    local size = args.size
    local weight = args.weight
    
    -- Bonus stats (e.g., from enchantments)
    local damageBonus = args.damageBonus
    local defenseBonus = args.defenseBonus
    local rangeBonus = args.rangeBonus
    local attackspeedBonus = args.attackspeedBonus
    local movespeedBonus = args.movespeedBonus
    local armorBonus = args.armorBonus
    local hpRegenBonus = args.hpRegenBonus
    local mpRegenBonus = args.mpRegenBonus
    
    -- Special properties
    local special = args.special
    local action = args.action
    local category = args.category
    
    -- Get rarity color
    local rarityColor = rarityColors[rarity] or "silver"
    local rarityText = rarityNames[rarity] or ""
    
    -- Build the tooltip HTML
    local html = {}
    
    -- Container with float
    table.insert(html, string.format('<div class="tooltip-panel font-apogea-long" style="float:%s; margin-%s:15px; margin-bottom:10px;">', 
        float, 
        float == "right" and "left" or "right"))
    
    -- Sprite
    table.insert(html, string.format('{{Sprite|%s|%s}}', name, spriteSize))
    
    -- Item name with rarity color
    table.insert(html, string.format('<p class="font-bitcell color-%s">%s</p>', rarityColor, name))
    
    -- Description (jade color)
    if description and description ~= "" then
        table.insert(html, formatLine(description, "color-jade"))
    end
    
    -- Stats (in typical order)
    if range and range ~= "" then
        table.insert(html, formatStat("Range", range, rangeBonus))
    end    
    if armor and armor ~= "" then
        table.insert(html, formatStat("Armor", armor, armorBonus))
    end
    if damage and damage ~= "" then
        table.insert(html, formatStat("Damage", damage, damageBonus))
    end
    if attackspeed and attackspeed ~= "" then
        table.insert(html, formatStat("Attackspeed", attackspeed, attackspeedBonus))
    end
    if defense and defense ~= "" then
        table.insert(html, formatStat("Defense", defense, defenseBonus))
    end
    if movespeed and movespeed ~= "" then
        table.insert(html, formatStat("Movespeed", movespeed, movespeedBonus))
    end
    if hpRegen and hpRegen ~= "" then
        table.insert(html, formatStat("Hp Regen", hpRegen, hpRegenBonus))
    end
    if mpRegen and mpRegen ~= "" then
        table.insert(html, formatStat("Mp Regen", mpRegen, mpRegenBonus))
    end
    
    -- Size
    if size and size ~= "" then
        table.insert(html, string.format('<p>Size: %s</p>', size))
    end
    
    -- Weight
    if weight and weight ~= "" then
        table.insert(html, string.format('<p>It weighs %s oz.</p>', weight))
    end
    
    -- Special property (columbia blue)
    if special and special ~= "" then
        table.insert(html, formatLine(special, "color-columbia"))
    end
    
    -- Rarity text
    if rarity ~= "common" then
        table.insert(html, formatLine(rarityText, "color-" .. rarityColor))
    end
    
    -- Action hint
    if action and action ~= "" then
        table.insert(html, formatLine("[" .. action .. "]", "color-silver"))
    end
    
    -- Item type/category
    if itemType ~= "" then
        table.insert(html, formatLine("[" .. itemType .. "]", "color-silver"))
    end
    
    if category and category ~= "" then
        table.insert(html, formatLine("[" .. category .. "]", "color-silver"))
    end
    
    table.insert(html, '</div>')
    
    return frame:preprocess(table.concat(html, '\n'))
end

-- Quick display for common items
function p.common(frame)
    local args = getArgs(frame)
    args.rarity = "common"
    return p._display(frame, args)
end

function p.uncommon(frame)
    local args = getArgs(frame)
    args.rarity = "uncommon"
    return p._display(frame, args)
end

function p.rare(frame)
    local args = getArgs(frame)
    args.rarity = "rare"
    return p._display(frame, args)
end

function p.epic(frame)
    local args = getArgs(frame)
    args.rarity = "epic"
    return p._display(frame, args)
end

function p.legendary(frame)
    local args = getArgs(frame)
    args.rarity = "legendary"
    return p._display(frame, args)
end

-- Internal display with pre-fetched args
function p._display(frame, args)
    local name = args.name or "Unknown Item"
    local rarity = string.lower(args.rarity or "common")
    local itemType = args.type or ""
    local description = args.description or ""
    local spriteSize = args.spriteSize or "64"
    local float = args.float or "right"
    
    local damage = args.damage
    local defense = args.defense
    local armor = args.armor
    local range = args.range
    local attackspeed = args.attackspeed
    local movespeed = args.movespeed
    local hpRegen = args.hpRegen
    local mpRegen = args.mpRegen
    local size = args.size
    local weight = args.weight
    
    local damageBonus = args.damageBonus
    local defenseBonus = args.defenseBonus
    local rangeBonus = args.rangeBonus
    local armorBonus = args.armorBonus
    local attackspeedBonus = args.attackspeedBonus
    local movespeedBonus = args.movespeedBonus
    local hpRegenBonus = args.hpRegenBonus
    local mpRegenBonus = args.mpRegenBonus
    
    local special = args.special
    local action = args.action
    local category = args.category
    
    local rarityColor = rarityColors[rarity] or "silver"
    local rarityText = rarityNames[rarity] or ""
    
    local html = {}
    
    table.insert(html, string.format('<div class="tooltip-panel font-apogea-long" style="float:%s; margin-%s:15px; margin-bottom:10px;">', 
        float, 
        float == "right" and "left" or "right"))
    
    table.insert(html, string.format('{{Sprite|%s|%s}}', name, spriteSize))
    table.insert(html, string.format('<p class="font-bitcell color-%s">%s</p>', rarityColor, name))
    
    if description and description ~= "" then
        table.insert(html, formatLine(description, "color-jade"))
    end
    
    if range and range ~= "" then
        table.insert(html, formatStat("Range", range, rangeBonus))
    end
    if armor and armor  ~= "" then
        table.insert(html, formatStat("Armor", armor, armorBonus))
    end
    if damage and damage ~= "" then
        table.insert(html, formatStat("Damage", damage, damageBonus))
    end
    if attackspeed and attackspeed ~= "" then
        table.insert(html, formatStat("Attackspeed", attackspeed, attackspeedBonus))
    end
    if defense and defense ~= "" then
        table.insert(html, formatStat("Defense", defense, defenseBonus))
    end
    if movespeed and movespeed ~= "" then
        table.insert(html, formatStat("Movespeed", movespeed, movespeedBonus))
    end
    if hpRegen and hpRegen ~= "" then
        table.insert(html, formatStat("Hp Regen", hpRegen, hpRegenBonus))
    end
    if mpRegen and mpRegen ~= "" then
        table.insert(html, formatStat("Mp Regen", mpRegen, mpRegenBonus))
    end
    
    if size and size ~= "" then
        table.insert(html, string.format('<p>Size: %s</p>', size))
    end
    
    if weight and weight ~= "" then
        table.insert(html, string.format('<p>It weighs %s oz.</p>', weight))
    end
    
    if special and special ~= "" then
        table.insert(html, formatLine(special, "color-columbia"))
    end
    
    if rarity ~= "common" then
        table.insert(html, formatLine(rarityText, "color-" .. rarityColor))
    end
    
    if action and action ~= "" then
        table.insert(html, formatLine("[" .. action .. "]", "color-silver"))
    end
    
    if itemType ~= "" then
        table.insert(html, formatLine("[" .. itemType .. "]", "color-silver"))
    end
    
    if category and category ~= "" then
        table.insert(html, formatLine("[" .. category .. "]", "color-silver"))
    end
    
    table.insert(html, '</div>')
    
    return frame:preprocess(table.concat(html, '\n'))
end

return p