Jump to content

Module:Sandbox/Stevium: Difference between revisions

From The Petit Planet Wiki
Created Module:Sandbox/Stevium
 
Testing subtitle
 
Line 1: Line 1:
-- Imported from: https://defaultloadout.wiki.gg/wiki/Module:Navbox
-- Imported from: https://defaultloadout.wiki.gg/wiki/Module:Infobox
-- version 1.2.5.1
-- version 0.1.11


-- config table for RANGER.
--------------------------------------
-- If you want to change the default config, DO NOT change it here,
-- User settings, you can modify these
-- please do it via the `onLoadConfig` hook in [[Module:Navbox/Hooks]].
--------------------------------------
local config = {
default_navbox_class = "navigation-not-searchable noexcerpt",  -- Base value of the `class` parameter.
default_title_class = nil,    -- Base value of the `title_class` parameter.
default_above_class = nil,    -- Base value of the `above_class` parameter.
default_below_class = nil,    -- Base value of the `below_class` parameter.
default_section_class =nil,  -- Base value of the `section_class` parameter.
default_header_class = nil,  -- Base value of the `header_class` parameter.
default_group_class = nil,    -- Base value of the `group_class` parameter.
default_list_class = 'hlist', -- Base value of the `list_class` parameter.
default_header_state = nil, -- Base value of the `state` parameter.


editlink_hover_message_key = 'Navbox-edit-hover', -- The system message name for hover text of the edit icon.
-- if you want to not always use divs in your wiki (as opposed to tables), you can change this default
-- just remember to change it back each time you update from the main "branch" on the support wiki!
-- you can also control it per infobox with `|useDivs=yes` or `|useDivs=no`
local USE_DIVS = true -- `false` or `true`


auto_flatten_top_level = true, -- If true, when a section has only one list with no content and no corresponding group but has sublists, these sublists will be moved to top level.
-- default value to show if a param is missing in some but not all tabs.
-- This helps make the hierarchy of sections and content clearer.
-- set to `nil` (not in quotes) to remove such rows altogether in the tabs where they're missing
-- An example:
local TABBED_NONEXIST = nil -- `''` or `nil` or `'N/A'` etc. Don't put nil in quotes.
-- {{navbox
-- ...
--  |header1 = Items
--  | group1.1 = Weapons
--  |  list1.1 = Swords · Guns · Wands
--  | group1.2 = Armors
--  |  list1.2 = Head pieces · Capes
--  |header2 = NPCs
--  | group2.1 = Town NPCs
--  |  list2.1 = Guide · Witch
-- ...
-- }}
-- will be equal to:
-- {{navbox
-- ...
--  |header1 = Items
--  | group2 = Weapons
--  |  list2 = Swords · Guns · Wands
--  | group3 = Armors
--  |  list3 = Head pieces · Capes
--  |header5 = NPCs
--  | group6 = Town NPCs
--  |  list6 = Guide · Witch
-- ...
-- }}
custom_render_handle = nil, -- usually for debugging purposes only. if set, it should be a function accept 2 parameters: `dataTree` and `args`, and return a string as module output.
}


---------------------------------------------------------------------
---------------------------------------------------------------------------
-- Do not modify anything below this line unless you know what you're doing
---------------------------------------------------------------------------


-- Argument alias.
local h = {}
local CANONICAL_NAMES = {
local p = {}
['titlestyle'] = 'title_style',
local hooks = {}
['listclass'] = 'list_class',
['groupstyle'] = 'group_style',
['collapsible'] = 'state',
['editlink'] = 'meta',
['editlinks'] = 'meta',
['editicon'] = 'meta',
['edit_link'] = 'meta',
['edit_links'] = 'meta',
['edit_icon'] = 'meta',
['navbar'] = 'meta',
['name'] = 'template',
['evenodd'] = 'striped',
['class'] = 'navbox_class',
['css'] = 'navbox_style',
['style'] = 'navbox_style',
['group'] = '1:group',
['list'] = '1:list',
}


local DEFAULT_ARGS = {
function p.arraymap(frame)
['meta'] = true,
-- a lua implementation of Page Forms' arraymap
}
local args = h.overwrite()
local items = h.split(args[1], args[2] or ',')
for i, item in ipairs(items) do
items[i] = args[4]:gsub(args[3], item)
end
return table.concat(items, args[5] or ',')
end


local STATES = {
function p.preprocess(frame)
['no'] = '',
return frame:preprocess(frame.args[1] or frame:getParent().args[1])
['off'] = '',
end
['plain'] = '',
['collapsed'] = 'mw-collapsible mw-collapsed',
['expanded'] = 'mw-collapsible',
}


local BOOL_FALSE = {
function p.main(frame)
['no'] = true,
h.registerHooks()
['off'] = true,
h.increment()
['false'] = true,
local args = h.overwrite()
}
local sep = args.sep or ','
h.castArgs(args, sep)
if h.castBool(args.setmainimage or 'yes') then
h.setMainImage(args.images[1])
end
return h.makeInfobox(args, sep)
end


local STRIPED = {
function h.registerHooks()
['odd'] = 'striped-odd',
if not mw.title.new('Module:Infobox/Hooks').exists then return end
['swap'] = 'striped-odd',
hooks = require('Module:Infobox/Hooks')
['y'] = 'striped-even',
end
['yes'] = 'striped-even',
['on'] = 'striped-even',
['even'] = 'striped-even',
['striped'] = 'striped-even',
}


local NAVBOX_CHILD_INDICATOR = '!!C$H$I$L$D!!'
function h.runHook(key, ...)
local NAVBOX_CHILD_INDICATOR_LENGTH = string.len( NAVBOX_CHILD_INDICATOR )
if hooks[key] then
 
hooks[key](...)
local CLASS_PREFIX = 'ranger-'
end
 
---------------------------------------------------------------------
 
local p = {}
local h = {} -- non-public
local hooks = mw.title.new('Module:Navbox/Hooks').exists and require('Module:Navbox/Hooks') or {}
 
---------------------------------------------------------------------
 
-- For templates: {{#invoke:navbox|main|...}}
function p.main(frame)
local args = p.mergeArgs(frame)
return p.build(args, true)
end
end


-- For modules: return require('module:navbox').build(args)
function h.increment()
-- By default this method will skip the arguments sanitizing phase
-- optional use of VariablesLua for better compatibility
-- (and onSanitizeArgsStart/onSanitizeArgsEnd hooks).
local VariablesLua = mw.ext.VariablesLua
-- Set `doParseArgs` to true to do arguments sanitizing.
if VariablesLua ~= nil then
-- If `customConfig` table is provided, it will be merged into default config table (after onLoadConfig()).
h.counter = VariablesLua.var('DRUID_INFOBOX_ID', 0) + 1
-- If `customHooks` table is provided, all default hook handles will be overrided, unprovided hooks will be empty.
VariablesLua.vardefine('DRUID_INFOBOX_ID', h.counter)
function p.build(args, doParseArgs, customConfig, customHooks)
return
if customHooks then
hooks = customHooks
end
end
local res
if doParseArgs then  
-- try to fall back to normal Variables
args = h.parseArgs(args)
res, h.counter = pcall(
function()
return mw.getCurrentFrame():callParserFunction('#var', {'DRUID_INFOBOX_ID', 0}) + 1
end
)
if res then
mw.getCurrentFrame():callParserFunction('#vardefine', {'DRUID_INFOBOX_ID', h.counter})
else
-- else use a random number so at least there's some unique id
h.counter = math.random(100000000000000000) -- random integer
end
end
end


h.runHook('onLoadConfig', config, args)
function h.castArgs(args, sep)
 
h.runHook('onCastArgsStart', args, sep, args.kind)
if customConfig then
args.tabs = h.split(args.tabs or args.image_labels, sep)
for k,v in pairs(customConfig) do
args.images = h.getImages(args, sep)
config[k] = v
args.sections = h.split(args.sections, sep)
for _, section in ipairs(args.sections) do
if h.castBool(args[section .. '_isdata']) then
args[section .. 'Data'] = args[section]
args[section] = section .. 'Data'
args[section .. 'Data_nolabel'] = 'true' -- will be cast later
end
end
end
args[section] = h.split(args[section], sep)
args[section .. '_tabs'] = h.split(args[section .. '_tabs'], sep)
--merge default args
if #args.tabs > 0 and #args[section .. '_tabs'] > 0 then
for k,v in pairs(DEFAULT_ARGS) do
error(('You cannot specify |tabs= and |%s= at the same time, please pick one'):format(section .. '_tabs'))
if args[k] == nil then
args[k] = DEFAULT_ARGS[k]
end
end
end
end
 
if args.useDivs then
h.runHook('onBuildTreeStart', args)
USE_DIVS = h.castBool(args.useDivs)
local dataTree = h.buildDataTree(args)
h.runHook('onBuildTreeEnd', dataTree, args)
if type(config.custom_render_handle) == 'function' then
return config.custom_render_handle(dataTree, args)
else
return h.render(dataTree)
end
end
-- this would be in the outer scope, but we're hiding it
h.entityType = USE_DIVS and 'div' or 'table' -- key of h.htmlEntities
h.runHook('onCastArgsEnd', args, sep, args.kind)
h.focusTab(args)
end
end


-- merge args from frame and frame:getParent()
function h.getImages(args, sep)
-- It may be used when creating custom wrapping navbox module.
if args.image and not args.images then
--
args.images = args.image
-- For example, Module:PillNavbox
end
--
if args.images then
-- local RANGER = require('Module:Navbox')
return h.split(args.images, sep)
-- local p = {}
-- function p.main(frame)
--    return RANGER.build(RANGER.mergeArgs(frame), true, {
--        default_navbox_class = 'pill', -- use "pill" style by default.
--    })
-- end
-- return p
--
function p.mergeArgs(frame)
local inputArgs = {}
for k, v in pairs(frame.args) do
v = mw.text.trim(tostring(v))
if v ~= '' then
inputArgs[k] = v
end
end
end
if not args.tabs then return {} end
for k, v in pairs(frame:getParent().args) do
local ret = {}
v = mw.text.trim(v)
for _, key in ipairs(args.tabs) do
if v ~= '' then
if args[key .. '_image'] then
inputArgs[k] = v
ret[#ret+1] = args[key .. '_image']
end
end
end
end
return ret
return inputArgs
end
end


------------------------------------------------------------------------
function h.setMainImage(file)
if h.counter > 1 then return end
if not file then return end
local fileText = file:gsub('.-:', '')
fileText = fileText:gsub('^([^|%]]+).*', '%1')
-- setmainimage is guaranteed to exist on wiki.gg but may not exist on other wikis
-- it's not a crucial piece of functionality so we'll fail silently if it doesn't exist
pcall(function() mw.getCurrentFrame():callParserFunction{
name = '#setmainimage',
args = { fileText },
} end)
end


-- equivalent to mw.text.split(str, div, true) for non-empty separator, but can be over 60x faster, since the latter is Unicode-aware.
function h.focusTab(args)
-- Original version credit: http://richard.warburton.it.
local hasFocusedTab = false
function h.explode(div, str)
if not args.tabs[1] then return end
if (div=='') then return nil end
for i, tab in ipairs(args.tabs) do
local pos,arr = 0,{}
if h.castBool(args[tab .. '_focused']) then
-- for each divider found
hasFocusedTab = true
for st,sp in function() return string.find(str,div,pos,true) end do
args[tab .. '_focused'] = true -- cast to bool
arr[#arr+1] = string.sub(str,pos,st-1) -- Attach chars left of current divider
else
pos = sp+1 -- Jump past current divider
args[tab .. '_focused'] = false
end
end
end
arr[#arr+1] = string.sub(str,pos) -- Attach chars right of last divider
if hasFocusedTab then return end
return arr
args[args.tabs[1] .. '_focused'] = true
end
end


function h.parseArgs(inputArgs)
function h.makeInfobox(args, sep)
h.runHook('onSanitizeArgsStart', inputArgs)
local out = mw.html.create(h.getTag('container'))
:addClass('druid-infobox')
local args = {}
:addClass('druid-container')
:addClass('noexcerpt')
for k, v in pairs(inputArgs) do
:addClass(args.class) -- warning: class can be nil, don't concat anything
if type(k) == 'string' then
:attr('id', args.id or ('druid-container-' .. h.counter))
-- all named args have already been trimmed
h.runHook('onMakeOutputStart', out, args)
local key = h.normalizeKey(k)
if args.kind then out:addClass('druid-container-' .. h.escape(args.kind)) end
args[key] = h.normalizeValue(key, v)
h.printTitle(out, args)
else
h.printSubtitle(out, args)
args[k] = mw.text.trim(v) -- keep number-index arguments (for {{navbox|child|...}})
h.printImages(out, args.images, args)
end
for _, section in ipairs(args.sections) do
-- cannot begin tagging here because we don't know if any applicable args are present
local cols = args[section .. '_columns']
local makeSection = cols and h.makeGridSection or h.makeSection
out:node(makeSection(section, args[section], args, tonumber(cols)))
end
end
h.runHook('onMakeOutputEnd', out, args)
h.runHook('onSanitizeArgsEnd', args, inputArgs)
return out
return args
end
end


-- Normalize the name string of arguments.
function h.printTitle(out, args)
-- the normalized form is (index:)?name, in which:
local tabs = args.tabs
-- index is number index such as 1, 1.3, 1.2.45,
if not tabs or #tabs == 0 then
-- name is in lowercase underscore-case, such as group, group_style
h.printSimpleTitle(out, args)
-- e.g: header_state, 1.3:list_style
return
-- the input argument name can be:
end
-- * camel-case: listStyle, ListStyle
if not h.hasComplexData('title', tabs, args) then
-- * space separated: list style
h.printSimpleTitle(out, args)
-- * prefix+index+postfix?, and can be in camel-case or space/hyphen separated or mixed: list 1 style, list1, list1Style, list1_style
return
-- * index.name: 1.3.list
end
-- * index_name: 1.3_list (Space separated are treated as underscore separated, therefore 1.3 list are vaild too)
local node = h.printTitleWrapper(out)
function h.normalizeKey(s)
h.printTabbedDataItem(node, 'title', tabs, args)
-- camel-case to lowercase underscore-case
end
s = s:gsub('%l%f[%u]', '%0_') -- listStyle to list_style
s = (s:gsub(' ', '_')):lower() -- space to underscore
s = s:gsub('%l%f[%d]', '%0_') -- group1* to group_1*
s = s:gsub('%d%f[%l]', '%0_') -- *1style to *1_style
-- number format x_y_z to x.y.z
s = s:gsub('(%d)_%f[%d]', '%1%.')
-- move index to the beginning:
-- group_1.2_style to 1.2:group_style
-- group_1 to 1:group
s = s:gsub('^([%l_]+)_([%d%.]+)', '%2:%1')
-- support index.name and index_name:
-- 1.2.group / 1.2_group to 1.2:group
s = s:gsub('^([%d%.]+)[%._]%f[%l]', '%1:')
-- now the key should be in normalized form, if the origin key is vaild


-- standardize *_css to *_style
function h.printSimpleTitle(out, args)
s = s:gsub('_css$', '_style')
if args.title then
-- standardize *collapsible to *state
local node = h.printTitleWrapper(out)
s = s:gsub('collapsible$', 'state')
node:wikitext(args.title)
end
end


-- standardize all aliases to the canonical name
function h.printTitleWrapper(out)
return CANONICAL_NAMES[s] or s
return out:tag(h.getTag('titleOuter'))
:tag(h.getTag('titleInner'))
:addClass('druid-title')
:attr('colspan', 2)
end
end


function h.normalizeValue(k, v)
function h.printSubtitle(out, args)
k = tostring(k)
if args.subtitle and args.subtitle ~= '' then
if k:find('_style$') then
out:tag(h.getTag('titleOuter'))
v = (v .. ';'):gsub(';;', ';')
:tag(h.getTag('titleInner'))
return v
:addClass('druid-subtitle')
elseif k:find('state$') then
:attr('colspan', 2)
return STATES[v]
:wikitext(args.subtitle)
elseif k == 'striped' then
return STRIPED[v]
elseif k == 'meta' then
return not BOOL_FALSE[v]
elseif v:sub(1, 2) == '{|' or v:match('^[*:;#]') then
-- Applying nowrap to lines in a table does not make sense.
-- Add newlines to compensate for trim of x in |parm=x in a template.
return '\n' .. v ..'\n'
end
end
return v
end
end


-- parse arguments, convert them to structured data tree
function h.printTabbedDataItem(node, item, tabs, args)
function h.buildDataTree(args)
-- hasData isn't used in the title case but we will need to track this
-- parse args to a tree
-- when we're printing section data later on
local tree = h.buildTree(args)
-- so we'll just track it always
local hasData = false
for i, label in ipairs(tabs) do
local div = node:tag('div')
:addClass('druid-toggleable-data')
:addClass('druid-toggleable')
:attr('data-druid', h.counter .. '-' .. i)
:attr('data-druid-tab-key', label)
if h.getTabbedContent(args, label, item) then
hasData = true
div:wikitext('\n\n' .. h.getTabbedContent(args, label, item))
div:addClass('druid-toggleable-data-nonempty')
else
div:addClass('druid-toggleable-data-empty')
end
if args[label .. '_focused'] then div:addClass('focused') end
end
return hasData
end


-- build root navbox data
function h.printImages(out, images, args)
local data = h.buildNavboxData(tree.info)
if #images == 0 and #args.tabs == 0 then return end
-- burden is on the user to format this as an image. this should be done in the infobox template,
-- Recursively build section tree
-- with something like |image={{#if:{{{image|}}}|[[File:{{{image|}}}{{!}}300px{{!}}link=]]}}
if tree.children then
local td = out:tag(h.getTag('section'))
data.sections = h.buildSections(tree.children, {
:addClass('druid-section-container')
listClass = h.mergeAttrs(args.list_class, config.default_list_class),
:tag(h.getTag('cell'))
listStyle = args.list_style,
:attr('colspan', 2)
groupClass = h.mergeAttrs(args.group_class, config.default_group_class),
local tabs = args.tabs
groupStyle = args.group_style,
local tabTexts = h.getImageTabTexts(tabs, images, args)
sectionClass = h.mergeAttrs(args.section_class, config.default_section_class),
h.printTabs(td, tabs, tabTexts, false, args)
sectionStyle = args.section_style,
if #images == 0 then return end
headerClass = h.mergeAttrs(args.header_class, config.default_header_class),
if #images == 1 then
headerStyle = args.header_style,
td:addClass('druid-main-image')
headerState = args.header_state or config.default_header_state,
:wikitext(images[1])
})
if args.caption then
td:tag('div')
:addClass('druid-main-image-caption')
:wikitext(args.caption)
end
return
end
end
if args[1] == 'child' then
td:addClass('druid-main-images')
data.CHILD_MODE = true
local imagesContainer = td:tag('div')
:addClass('druid-main-images-files')
for i, image in ipairs(images) do
local container = imagesContainer:tag('div')
:addClass('druid-main-images-file')
:addClass('druid-toggleable')
:attr('data-druid', h.counter .. '-' .. i)
:wikitext(image)
:attr('data-druid-tab-key', tabs[i])
local labelDisplay
local labelKey = tabs[i]
local caption = args.caption
if labelKey then
labelDisplay = args[labelKey .. '_label'] or labelKey
caption = args[labelKey .. '_caption'] or caption
else
labelDisplay = 'Image ' .. i
end
caption = args[labelDisplay .. '_caption'] or caption
if caption then
container:tag('div')
:addClass('druid-main-images-caption')
:wikitext(caption)
end
if labelKey and args[labelKey .. '_focused'] then
container:addClass('focused')
end
end
end
end


return data
function h.getImageTabTexts(tabs, images, args)
if #tabs == 0 and #images <= 1 then return {} end
local texts = {}
local i = 1
while images[i] or tabs[i] do
if tabs[i] then
texts[i] = args[tabs[i] .. '_label'] or tabs[i]
else
texts[i] = 'Image ' .. i
end
i = i + 1
end
return texts
end
end


function h.buildSections(list, defaults)
function h.printTabs(td, tabs, texts, isSection, args)
local sections = {}
if #texts == 0 then return end
local section = nil
local container = td:tag('div')
for k, node in h.orderedPairs(list) do
:addClass('druid-main-images-labels')
local info = node.info or {}
:addClass('druid-tabs')
--start a new section if needed
if isSection then
if info.header or not section then
container:addClass('druid-section-tabs')
section = {
end
class = h.mergeAttrs(info.section_class, defaults.sectionClass),
for i, item in ipairs(tabs) do
style = h.mergeAttrs(info.section_style, defaults.sectionStyle),
local label = container:tag('div')
body = {}
:addClass('druid-main-images-label')
}
:addClass('druid-tab')
-- Section header if needed.
:addClass('druid-toggleable')
-- If the value of a `|header_n=` is two or more consecutive "-" characters (e.g. --, -----),
:attr('data-druid', h.counter .. '-' .. i)
-- it means start a new section without header, and the new section will be not collapsable.
:wikitext(texts[i])
if info.header and not string.match(info.header, '^%-%-+$') then
:attr('data-druid-tab-key', item)
section.header = {
if isSection then
content = info.header,
label:addClass('druid-section-tab')
class = h.mergeAttrs(info.header_class, defaults.headerClass),
else
style = h.mergeAttrs(info.header_style, defaults.headerStyle),
label:addClass('druid-title-tab')
}
section.state = info.state or defaults.headerState or 'mw-collapsible'
end
sections[#sections+1] = section
end
end
-- above/below for this section
if args[item .. '_focused'] then
if info.above then
label:addClass('focused')
section.above = {
content = info.above,
class= h.mergeAttrs(info.above_class, config.default_above_class),
style = info.above_style,
}
end
end
if info.below then
-- this can be null, don't concat anything here
section.below = {
label:addClass(args[item .. '_class'])
content = info.below,
end
class= h.mergeAttrs(info.below_class, config.default_below_class),
end
style = info.below_style,
 
}
function h.makeGridSection(section, sectionFields, args, numCols)
end
local numItems = h.countItems(sectionFields, section, args)
-- this group+list row
if numItems == 0 then return end
if info.group or info.list or node.children then
local node = mw.html.create(h.getTag('section'))
local row = {}
:addClass('druid-section-container')
section.body[#section.body+1] = row
h.printSectionHeader(node, section, args)
if info.group then
h.printSectionTabs(node, section, args)
row.group = {
local tr = node:tag(h.getTag('row'))
content = info.group,
:attr('data-druid-section-row', h.escape(section))
class = h.mergeAttrs(info.group_class, defaults.groupClass),
if args[section .. '_collapsed'] then
style = h.mergeAttrs(info.group_style, defaults.groupStyle),
tr:addClass('druid-collapsed')
}
end
local grid = tr:tag(h.getTag('cell'))
:attr('colspan', 2)
:addClass('druid-grid-section')
:addClass('druid-grid-section-' .. h.escape(section))
:addClass(args[section .. '_class']) -- warning: class can be nil, don't concat anything
:tag('div')
:addClass('druid-grid')
local row, col, i = 1, 1, 1
local sizeOfLastRow = numItems % numCols
local lcm = h.getNumGridCols(numItems, sizeOfLastRow, numCols)
grid:css('grid-template-columns', ('repeat(%s, 1fr)'):format(lcm))
local size = lcm / numCols
for _, item in ipairs(sectionFields) do
local node = mw.html.create('div')
local shouldPrint = h.printData(node, item, section, args)
if shouldPrint then
if i == numItems - sizeOfLastRow + 1 then
size = lcm / sizeOfLastRow
end
end
if info.list then
i = i + 1
if string.sub(info.list, 1, NAVBOX_CHILD_INDICATOR_LENGTH) == NAVBOX_CHILD_INDICATOR then
local gStart = (col - 1) * size + 1
-- it is from {{navbox|child| ... }}
local gEnd = (col) * size + 1
row.sections = mw.text.jsonDecode(string.sub(info.list, NAVBOX_CHILD_INDICATOR_LENGTH+1))
local itemContainer = grid:tag('div')
else
:addClass('druid-grid-item')
row.list = {
:addClass('druid-grid-item-' .. h.escape(item))
content = info.list,
:addClass(args[item .. '_class']) -- warning: class can be nil, don't concat anything
class = h.mergeAttrs(info.list_class, defaults.listClass),
:css('grid-column', ('%s / %s'):format(gStart, gEnd))
style = h.mergeAttrs(info.list_style, defaults.listStyle),
:css('grid-row', row)
}
if not h.castBool(args[item .. '_nolabel']) then
end
h.printLabel(itemContainer:tag('div'), item, args)
end
end
-- sub-nodes, will override {{navbox|child| ... }}
itemContainer:node(node)
if node.children then
if col == numCols then
row.sections = h.buildSections(node.children, defaults)
row = row + 1
col = 1
else
col = col + 1
end
end
end
end
end
end
-- flatten if needed:
return node
-- If a section has only one list with no content and no corresponding group but has sublists, these sublists will be moved to upper level.
end
if config.auto_flatten_top_level then
 
for _, sect in ipairs(sections) do
function h.makeSection(section, sectionFields, args)
if #sect.body == 1 then
if section == '' then return end -- bruteforce fix for trailing commas
local node = sect.body[1]
local shouldPrint = false
if not node.group and not node.list and node.sections and #node.sections == 1 and not node.sections[1].header then
local container = mw.html.create(h.getTag('section'))
sect.body = node.sections[1].body
:addClass('druid-section-container')
end
:addClass(args[section .. '_class']) -- warning: class can be nil, don't concat anything
h.printSectionHeader(container, section, args)
h.printSectionTabs(container, section, args)
for _, item in ipairs(sectionFields) do
local node = mw.html.create(h.getTag('cell'))
local shouldPrintItem = h.printData(node, item, section, args)
if shouldPrintItem then
shouldPrint = true
local tr = container:tag(h.getTag('row'))
:addClass('druid-row')
:addClass('druid-row-' .. h.escape(item))
:addClass(args[item .. '_class']) -- warning: class can be nil, don't concat anything
:attr('data-druid-section-row', h.escape(section))
if args[section .. '_collapsed'] then
tr:addClass('druid-collapsed')
end
if h.castBool(args[item .. '_wide']) or h.castBool(args[item .. '_nolabel']) then
node
:attr('colspan', 2)
:addClass('druid-data-wide')
else
h.printLabel(tr:tag(h.getTag('label')), item, args)
end
end
tr:node(node)
end
end
end
end
return sections
if not shouldPrint then return nil end
return container
end
end
 
function h.buildNavboxData(info)
function h.countItems(sectionFields, section, args)
local data = {
local numItems = 0
state = info.state or 'mw-collapsible', -- here we need a default value for empty input
for _, v in ipairs(sectionFields) do
striped = info.striped,
-- we aren't actually printing here, but we're finding out if we should print anything
class = h.mergeAttrs(info.navbox_class, config.default_navbox_class),
-- because we need the count of columns before we print anything in grid data
style = info.navbox_style,
if h.printData(mw.html.create(), v, section, args) then
}
numItems = numItems + 1
-- data for titlebar
if info.title or info.meta or data.state ~= '' then
data.title = {
content = info.title,
class = h.mergeAttrs(info.title_class, config.default_title_class),
style = info.title_style,
}
if info.meta then
data.metaLinks = {
link = info.meta_link, -- will be used as [[$link|$text]]
url = info.meta_url,  -- will be used as [$url $text], only if there is no data.metaLinks.link
text = info.meta_text, --hovertext
}
if not info.meta_link and not info.meta_url then
-- default link target
local title = mw.title.new(info.template or mw.getCurrentFrame():getParent():getTitle(), 'Template')
if not title then
error('Invalid title ' .. info.template)
end
data.metaLinks.link = title.fullText
end
if not info.meta_text then
local msg = mw.message.new(config.editlink_hover_message_key)
data.metaLinks.text = msg:exists() and msg:plain() or 'View or edit this template'
end
end
end
end
end
return numItems
-- above/below
end
if info.above then
 
data.above = {
function h.getNumGridCols(numItems, sizeOfLastRow, numCols)
content = info.above,
if not numCols then return numItems, 1 end
class= h.mergeAttrs(info.above_class, config.default_above_class),
if numItems < numCols then return numItems, 1 end
style = info.above_style,
if sizeOfLastRow == 0 then
}
return numCols, 1
end
end
 
local a, b = sizeOfLastRow, numCols
if info.below then
while b ~= 0 do
data.below = {
a, b = b, a % b
content = info.below,
class= h.mergeAttrs(info.below_class, config.default_below_class),
style = info.below_style,
}
end
end
local lcm = sizeOfLastRow * numCols / a
return data
return lcm
end
end


-- parse arguments, convert them into a tree based on their index
function h.printLabel(node, item, args)
-- each node on tree is { info = { #data for this node# }, children = {#children nodes#}  }
return node
function h.buildTree(args, defaults)
:addClass('druid-label')
local tree = { info = {} }
:addClass('druid-label-' .. h.escape(item))
local check = function(key, value)
:wikitext(args[item .. '_display'] or args[item .. '_label'] or item)
local index, name = string.match(key, '^([%d%.]+):(.+)$')
end


-- no number index found, for root node
function h.printData(node, item, section, args)
if not index then
-- prints data to the node
tree.info[key] = value
-- and also returns whether the item is nonempty or not
return
local hasData = false
end
local sectionTabs = args[section .. '_tabs']
 
local tabs = args.tabs
-- filter invalid number index
if sectionTabs and #sectionTabs > 0 then
if string.match(index, '^%.') or string.match(index, '%.$') or string.match(index, '%.%.') then
tabs = sectionTabs
return
end
end
if not tabs or #tabs == 0 then
return h.printSimpleData(node, item, args)
-- find the node that matches the index in the tree
end
local arr = h.explode('.', index)
if not h.hasComplexData(item, tabs, args) then
local node = tree
return h.printSimpleData(node, item, args)
for _, v in ipairs(arr) do
v = tonumber(v)
if not node.children then
node.children = {}
end
if not node.children[v] then  
node.children[v] = { info = {} }
end
node = node.children[v]
end
node.info[name] = value
end
end
for k,v in pairs(args) do
hasData = hasData or h.printTabbedDataItem(node, item, tabs, args)
check(k, v)
if hasData then
node:addClass('druid-data')
end
end
return tree
return hasData
end
 
function h.getTabbedContent(args, label, item)
return args[label .. '_' .. item] or args[item] or TABBED_NONEXIST
end
end


function h.render(data)
function h.printSimpleData(node, item, args)
-- handle {{navbox|child|...}} syntax
if args[item] and type(args[item]) ~= 'string' then
if data.CHILD_MODE then
error(("Invalid use of field %s as both a section and a data value"):format(item))
return NAVBOX_CHILD_INDICATOR..mw.text.jsonEncode(data.sections)
end
end
if not args[item] then return false end
node:addClass('druid-data')
:addClass('druid-data-' .. h.escape(item))
:addClass('druid-data-nonempty')
:wikitext('\n\n' .. args[item])
return true
end


-----  normal case -----
function h.hasComplexData(item, tabs, args)
for _, v in ipairs(tabs) do
local out = mw.html.create()
if args[v .. '_' .. item] then return true end
end
-- build navbox container
return false
local navbox = out:tag('div')
end
:attr('role', 'navigation'):attr('aria-label', 'Navbox')
:addClass(CLASS_PREFIX..'navbox')
:addClass(data.class)
:addClass(data.striped)
:addClass(data.state)
:cssText(data.style)


--title bar
function h.printSectionHeader(node, section, args)
if data.title then
if h.castBool(args[section .. '_nolabel']) then return end
local titlebar = navbox:tag('div'):addClass(CLASS_PREFIX..'title')
local tr = node:tag(h.getTag('row'))
titlebar:tag('div'):addClass('mw-collapsible-toggle-placeholder')
:attr('data-druid-section', h.escape(section))
if data.metaLinks then
local th = tr:tag(h.getTag('sectionTitle'))
titlebar:node(h.renderMetaLinks(data.metaLinks))
:attr('colspan', 2)
:addClass('druid-section')
:addClass('druid-section-' .. h.escape(section))
if args[section .. '_collapsible'] or args[section .. '_collapsed'] then
tr:addClass('druid-collapsible')
if args[section .. '_collapsed'] then
tr:addClass('druid-collapsible-collapsed')
end
end
if data.title then
end
titlebar:addClass(data.title.class):tag('div')
local emptySections = {}
:addClass(CLASS_PREFIX..'title-text')
for _, label in ipairs(args.tabs) do
:addClass(data.title.class)
local hasLabel = false
:cssText(data.title.style)
for _, item in ipairs(args[section] or {}) do
:wikitext(data.title.content)
if h.getTabbedContent(args, label, item) then
hasLabel = true
end
end
end
if not hasLabel then emptySections[label] = true end
end
end
 
if not next(emptySections) then
--above
th:wikitext(args[section .. '_label'] or section)
if data.above then
return
navbox:tag('div')
:addClass(CLASS_PREFIX..'above mw-collapsible-content')
:addClass(data.above.class)
:cssText(data.above.style)
:wikitext(data.above.content)
:attr('id', (not data.title) and mw.uri.anchorEncode(data.above.content) or nil) -- id for aria-labelledby attribute, if no title
end
end
for i, label in ipairs(args.tabs) do
-- sections
local div = th:tag('div')
if data.sections then
:addClass('druid-toggleable-heading')
h.renderSections(data.sections, navbox, 0, true)
:addClass('druid-toggleable')
else
:attr('data-druid', h.counter .. '-' .. i)
-- Insert a blank section for completely empty navbox to ensure it behaves correctly when collapsed.
:wikitext(args[section .. '_label'] or section)
if not data.above and not data.below then  
-- we are going to print the section content even in empty nodes
navbox:tag('div'):addClass(CLASS_PREFIX..'section mw-collapsible-content')
-- for compatibility with browsers without :has, where hiding empty rows won't happen
if emptySections[label] then
div:addClass('druid-toggleable-heading-empty')
end
if args[label .. '_focused'] then
div:addClass('focused')
end
end
end
end
end


--below
function h.printSectionTabs(node, section, args)
if data.below then
local tabs = args[section .. '_tabs']
navbox:tag('div')
if not tabs or #tabs == 0 then return end
:addClass(CLASS_PREFIX..'below mw-collapsible-content')
local tr = node:tag(h.getTag('sectionTabsOuter'))
:addClass(data.below.class)
:attr('data-druid-section', h.escape(section))
:cssText(data.below.style)
:attr('data-druid-section-tabs', h.escape(section))
:wikitext(data.below.content)
local th = tr:tag(h.getTag('sectionTabs'))
:attr('colspan', 2)
:addClass('druid-section-tabs')
:addClass('druid-section-tabs-' .. h.escape(section))
local texts = {}
for i, item in ipairs(tabs) do
texts[i] = args[item .. '_label'] or item
end
end
return tostring(out)
h.printTabs(th, tabs, texts, true, args)
end
end


function h.renderSections(data, container, level, even)
----------------------------
for i,sect in ipairs(data) do
-- general utility functions
--section box
----------------------------
local section = container:tag('div')
 
:addClass(CLASS_PREFIX..'section mw-collapsible-content')
function h.overwrite()
:addClass(sect.class)
-- this is a generic utility function that collects args from the invoke call & the parent template.
:addClass(sect.state)
-- normally, you merge args with parent template overwriting the invoke call, but
:cssText(sect.style)
-- since we'll be putting markup/formatting into our invoke call,
-- section header
-- we actually want to overwrite what the user sent.
if sect.header then
local f = mw.getCurrentFrame()
section:tag('div')
local origArgs = f.args
:addClass(CLASS_PREFIX..'header')
local parentArgs = f:getParent().args
:addClass(sect.header.class)
 
:cssText(sect.header.style)
local args = {}
:tag('div'):addClass('mw-collapsible-toggle-placeholder'):done()
:tag('div'):addClass(CLASS_PREFIX..'header-text'):wikitext(sect.header.content)
for k, v in pairs(parentArgs) do
v = mw.text.trim(v)
if v ~= '' then
args[k] = v
end
end
-- above:
end
if sect.above then
section:tag('div')
for k, v in pairs(origArgs) do
:addClass(CLASS_PREFIX..'above mw-collapsible-content')
v = mw.text.trim(tostring(v))
:addClass(sect.above.class)
if v ~= '' then
:cssText(sect.above.style)
args[k] = v
:wikitext(sect.above.content)
end
-- body: groups&lists
local box = section:tag('div'):addClass(CLASS_PREFIX..'section-body mw-collapsible-content')
even = h.renderBody(sect.body, box, level, (level==0) and true or even) -- reset even status each section
-- below:
if sect.below then
section:tag('div')
:addClass(CLASS_PREFIX..'below mw-collapsible-content')
:addClass(sect.below.class)
:cssText(sect.below.style)
:wikitext(sect.below.content)
end
end
end
end
return even
return args
end
end


-- generic utility functions
-- these would normally be provided by other modules, but to make installation easy
-- I'm including everything here


function h.renderMetaLinks(info)
function h.split(text, pattern, plain)
local box = mw.html.create('span'):addClass(CLASS_PREFIX..'meta')
if not text then
local meta = box:tag('span'):addClass('nv nv-view')
return {}
end
if info.link then
local ret = {}
meta:wikitext('[['..info.link..'|')
for m in h.gsplit(text, pattern, plain) do
:tag('span'):wikitext(info.text):attr('title', info.text):done()
ret[#ret+1] = m
:wikitext(']]')
elseif info.url then
meta:wikitext('['..info.url..' ')
:tag('span'):wikitext(info.text):attr('title', info.text):done()
:wikitext(']')
end
end
return ret
return box
end
end


function h.renderBody(info, box, level, even)
function h.gsplit( text, pattern, plain )
local count = 0
if not pattern then pattern = ',' end
for _,v in h.orderedPairs(info) do
if not plain then
if v.group or v.list or v.sections then
pattern = '%s*' .. pattern .. '%s*'
count = count + 1
end
-- row container
local s, l = 1, text:len()
local row = box:tag('div'):addClass(CLASS_PREFIX..'row')
return function ()
-- group cell
if s then
if v.group or (v.sections and level > 0 and not v.list) then
local e, n = text:find( pattern, s, plain )
local groupCell = row:tag('div')
local ret
:addClass(CLASS_PREFIX..'group level-'..level)
if not e then
:addClass((level > 0) and CLASS_PREFIX..'subgroup' or nil)
ret = text:sub( s )
local groupContentWrap = groupCell:tag('div'):addClass(CLASS_PREFIX..'wrap')
s = nil
if v.group then
elseif n < e then
groupCell:addClass(v.group.class):cssText(v.group.style)
-- Empty separator!
groupContentWrap:wikitext(v.group.content)
ret = text:sub( s, e )
if e < l then
s = e + 1
else
else
groupCell:addClass('empty')
s = nil
row:addClass('empty-group-list')
end
end
else
else
row:addClass('empty-group')
ret = e > s and text:sub( s, e - 1 ) or ''
end
s = n + 1
-- list cell
local listCell = row:tag('div'):addClass(CLASS_PREFIX..'listbox')
if not v.list and not v.sections then
listCell:addClass('empty')
row:addClass('empty-list')
end
if v.list or (v.group and not v.sections) then
--listCell:node(h.renderList(v['list'] or '', k, level, args))
even = not even -- flip even/odd status
local cell = listCell:tag('div')
:addClass(CLASS_PREFIX..'wrap')
:addClass(even and CLASS_PREFIX..'even' or CLASS_PREFIX..'odd')
if v.list then
cell:addClass(v.list.class):cssText(v.list.style)
:tag('div'):addClass(CLASS_PREFIX..'list'):wikitext(v.list.content)
end
end
if v.sections then
local sublistBox = listCell:tag('div'):addClass(CLASS_PREFIX..'sublist level-'..level)
even = h.renderSections(v.sections, sublistBox, level+1, even)
end
end
return ret
end
end
end
end, nil, nil
if count > 0 then
box:css('--count', count) -- for flex-grow
end
return even
end
end


-- pairs, but sort the keys alphabetically
-- char-set of characters to escape/replace
function h.orderedPairs(t, f)
local escapePattern = '[' -- required for pattern; do not remove.
local a = {}
.. ' '
for n in pairs(t) do table.insert(a, n) end
.. '"'
table.sort(a, f)
.. "'"
local i = 0      -- iterator variable
.. "%?"
local iter = function ()  -- iterator function
.. "%%"
i = i + 1
.. "%["
if a[i] == nil then return nil
.. "%]"
else return a[i], t[a[i]]
.. "{"
end
.. ']' -- required for pattern; do not remove.
end
 
return iter
function h.escape(s)
s = s:gsub(escapePattern, '')
return s
end
end


-- For cascading parameters, such as style or class, they are merged in exact order (from general to specific).
 
-- Any parameter starting with multiple hyphens(minus signs) will terminate the cascade.
-- normally I would make these constants at the top of the file
-- An example:
-- but I don't want to mistake them with user-set constants
-- For group_1.1, its style is affected by parameters |group_1.1_style=... , |subgroup_level_1_style=... , and |subgroup_style=... .
h.boolFalse = { ['false'] = true, ['no'] = true, [''] = true, ['0'] = true, ['nil'] = true }
-- If we have |group_1.1_style= color:red; |subgroup_level_1_style= font-weight: bold; and |subgroup_style= color: green; ,
 
-- the style of group_1.1 will be style="color:green; font-weight: bold; color: red;" ;
function h.castBool(x)
-- if we have |group_1.1_style= -- color:red; |subgroup_level_1_style= font-weight: bold; and |subgroup_style= color: green; ,
if not x then return false end
-- the style of group_1.1 will be style="color: red;" only, and the cascade is no longer performed for |subgroup_level_1_style and |subgroup_style.
return not h.boolFalse[tostring(x):lower()]
function h.mergeAttrs(...)
local trim = mw.text.trim
local s = ''
for i=1, select('#', ...) do
local v = trim(select(i, ...) or '')
local str = string.match(v, '^%-%-+(.*)$')
if str then
s = trim(str..' '..s)
break
else
s = trim(v..' '..s)
end
end
if s == '' then s = nil end
return s
end
end


function h.runHook(key, ...)
h.htmlEntities = {
if hooks[key] then
table = {
hooks[key](...)
container = 'table',
end
titleOuter = 'tr',
titleInner = 'th',
section = '',
sectionTitle = 'th',
sectionTabsOuter = 'tr',
sectionTabs = 'td',
row = 'tr',
label = 'th',
cell = 'td',
},
div = {
container = 'div',
titleOuter = 'div',
titleInner = 'div',
section = 'div',
sectionTitle = 'div',
sectionTabsOuter = 'div',
sectionTabs = 'div',
row = 'div',
label = 'div',
cell = 'div',
}
}
 
function h.getTag(key)
-- try not to totally fail here
return h.htmlEntities[h.entityType or 'div'][key]
end
end


-----------------------------------------------
return p
return p

Latest revision as of 11:02, 21 February 2026

Documentation for this module may be created at Module:Sandbox/Stevium/doc

-- Imported from: https://defaultloadout.wiki.gg/wiki/Module:Infobox
-- version 0.1.11

--------------------------------------
-- User settings, you can modify these
--------------------------------------

-- if you want to not always use divs in your wiki (as opposed to tables), you can change this default
-- just remember to change it back each time you update from the main "branch" on the support wiki!
-- you can also control it per infobox with `|useDivs=yes` or `|useDivs=no`
local USE_DIVS = true -- `false` or `true`

-- default value to show if a param is missing in some but not all tabs.
-- set to `nil` (not in quotes) to remove such rows altogether in the tabs where they're missing
local TABBED_NONEXIST = nil -- `''` or `nil` or `'N/A'` etc. Don't put nil in quotes.

---------------------------------------------------------------------------
-- Do not modify anything below this line unless you know what you're doing
---------------------------------------------------------------------------

local h = {}
local p = {}
local hooks = {}

function p.arraymap(frame)
	-- a lua implementation of Page Forms' arraymap
	local args = h.overwrite()
	local items = h.split(args[1], args[2] or ',')
	for i, item in ipairs(items) do
		items[i] = args[4]:gsub(args[3], item)
	end
	return table.concat(items, args[5] or ',')
end

function p.preprocess(frame)
	return frame:preprocess(frame.args[1] or frame:getParent().args[1])
end

function p.main(frame)
	h.registerHooks()
	h.increment()
	local args = h.overwrite()
	local sep = args.sep or ','
	h.castArgs(args, sep)
	if h.castBool(args.setmainimage or 'yes') then
		h.setMainImage(args.images[1])
	end
	return h.makeInfobox(args, sep)
end

function h.registerHooks()
	if not mw.title.new('Module:Infobox/Hooks').exists then return end
	hooks = require('Module:Infobox/Hooks')
end

function h.runHook(key, ...)
	if hooks[key] then
		hooks[key](...)
	end
end

function h.increment()
	-- optional use of VariablesLua for better compatibility
	local VariablesLua = mw.ext.VariablesLua
	if VariablesLua ~= nil then
		h.counter = VariablesLua.var('DRUID_INFOBOX_ID', 0) + 1
		VariablesLua.vardefine('DRUID_INFOBOX_ID', h.counter)
		return
	end
	local res
	-- try to fall back to normal Variables
	res, h.counter = pcall(
		function()
			return mw.getCurrentFrame():callParserFunction('#var', {'DRUID_INFOBOX_ID', 0}) + 1
		end
	)
	if res then
		mw.getCurrentFrame():callParserFunction('#vardefine', {'DRUID_INFOBOX_ID', h.counter})
	else
		-- else use a random number so at least there's some unique id
		h.counter = math.random(100000000000000000) -- random integer
	end
end

function h.castArgs(args, sep)
	h.runHook('onCastArgsStart', args, sep, args.kind)
	args.tabs = h.split(args.tabs or args.image_labels, sep)
	args.images = h.getImages(args, sep)
	args.sections = h.split(args.sections, sep)
	for _, section in ipairs(args.sections) do
		if h.castBool(args[section .. '_isdata']) then
			args[section .. 'Data'] = args[section]
			args[section] = section .. 'Data'
			args[section .. 'Data_nolabel'] = 'true' -- will be cast later
		end
		args[section] = h.split(args[section], sep)
		args[section .. '_tabs'] = h.split(args[section .. '_tabs'], sep)
		if #args.tabs > 0 and #args[section .. '_tabs'] > 0 then
			error(('You cannot specify |tabs= and |%s= at the same time, please pick one'):format(section .. '_tabs'))
		end
	end
	if args.useDivs then
		USE_DIVS = h.castBool(args.useDivs)
	end
	-- this would be in the outer scope, but we're hiding it
	h.entityType = USE_DIVS and 'div' or 'table' -- key of h.htmlEntities
	h.runHook('onCastArgsEnd', args, sep, args.kind)
	h.focusTab(args)
end

function h.getImages(args, sep)
	if args.image and not args.images then
		args.images = args.image
	end
	if args.images then
		return h.split(args.images, sep)
	end
	if not args.tabs then return {} end
	local ret = {}
	for _, key in ipairs(args.tabs) do
		if args[key .. '_image'] then
			ret[#ret+1] = args[key .. '_image']
		end
	end
	return ret
end

function h.setMainImage(file)
	if h.counter > 1 then return end
	if not file then return end
	local fileText = file:gsub('.-:', '')
	fileText = fileText:gsub('^([^|%]]+).*', '%1')
	-- setmainimage is guaranteed to exist on wiki.gg but may not exist on other wikis
	-- it's not a crucial piece of functionality so we'll fail silently if it doesn't exist
	pcall(function() mw.getCurrentFrame():callParserFunction{
		name = '#setmainimage',
		args = { fileText },
	} end)
end

function h.focusTab(args)
	local hasFocusedTab = false
	if not args.tabs[1] then return end
	for i, tab in ipairs(args.tabs) do
		if h.castBool(args[tab .. '_focused']) then
			hasFocusedTab = true
			args[tab .. '_focused'] = true -- cast to bool
		else
			args[tab .. '_focused'] = false
		end
	end
	if hasFocusedTab then return end
	args[args.tabs[1] .. '_focused'] = true
end

function h.makeInfobox(args, sep)
	local out = mw.html.create(h.getTag('container'))
		:addClass('druid-infobox')
		:addClass('druid-container')
		:addClass('noexcerpt')
		:addClass(args.class) -- warning: class can be nil, don't concat anything
		:attr('id', args.id or ('druid-container-' .. h.counter))
	h.runHook('onMakeOutputStart', out, args)
	if args.kind then out:addClass('druid-container-' .. h.escape(args.kind)) end
	h.printTitle(out, args)
	h.printSubtitle(out, args)
	h.printImages(out, args.images, args)
	for _, section in ipairs(args.sections) do
		-- cannot begin tagging here because we don't know if any applicable args are present
		local cols = args[section .. '_columns']
		local makeSection = cols and h.makeGridSection or h.makeSection
		out:node(makeSection(section, args[section], args, tonumber(cols)))
	end
	h.runHook('onMakeOutputEnd', out, args)
	return out
end

function h.printTitle(out, args)
	local tabs = args.tabs
	if not tabs or #tabs == 0 then
		h.printSimpleTitle(out, args)
		return
	end
	if not h.hasComplexData('title', tabs, args) then
		h.printSimpleTitle(out, args)
		return
	end
	local node = h.printTitleWrapper(out)
	h.printTabbedDataItem(node, 'title', tabs, args)
end

function h.printSimpleTitle(out, args)
	if args.title then
		local node = h.printTitleWrapper(out)
		node:wikitext(args.title)
	end
end

function h.printTitleWrapper(out)
	return out:tag(h.getTag('titleOuter'))
		:tag(h.getTag('titleInner'))
			:addClass('druid-title')
			:attr('colspan', 2)
end

function h.printSubtitle(out, args)
	if args.subtitle and args.subtitle ~= '' then
		out:tag(h.getTag('titleOuter'))
			:tag(h.getTag('titleInner'))
				:addClass('druid-subtitle')
				:attr('colspan', 2)
				:wikitext(args.subtitle)
	end
end

function h.printTabbedDataItem(node, item, tabs, args)
	-- hasData isn't used in the title case but we will need to track this
	-- when we're printing section data later on
	-- so we'll just track it always
	local hasData = false
	for i, label in ipairs(tabs) do
		local div = node:tag('div')
			:addClass('druid-toggleable-data')
			:addClass('druid-toggleable')
			:attr('data-druid', h.counter .. '-' .. i)
			:attr('data-druid-tab-key', label)
		if h.getTabbedContent(args, label, item) then
			hasData = true
			div:wikitext('\n\n' .. h.getTabbedContent(args, label, item))
			div:addClass('druid-toggleable-data-nonempty')
		else
			div:addClass('druid-toggleable-data-empty')
		end
		
		if args[label .. '_focused'] then div:addClass('focused') end
	end
	return hasData
end

function h.printImages(out, images, args)
	if #images == 0 and #args.tabs == 0 then return end
	-- burden is on the user to format this as an image. this should be done in the infobox template,
	-- with something like |image={{#if:{{{image|}}}|[[File:{{{image|}}}{{!}}300px{{!}}link=]]}}
	local td = out:tag(h.getTag('section'))
		:addClass('druid-section-container')
		:tag(h.getTag('cell'))
		:attr('colspan', 2)
	local tabs = args.tabs
	local tabTexts = h.getImageTabTexts(tabs, images, args)
	h.printTabs(td, tabs, tabTexts, false, args)
	if #images == 0 then return end
	if #images == 1 then
		td:addClass('druid-main-image')
			:wikitext(images[1])
		if args.caption then
			td:tag('div')
				:addClass('druid-main-image-caption')
				:wikitext(args.caption)
		end
		return
	end
	td:addClass('druid-main-images')
	local imagesContainer = td:tag('div')
		:addClass('druid-main-images-files')
	for i, image in ipairs(images) do
		local container = imagesContainer:tag('div')
			:addClass('druid-main-images-file')
			:addClass('druid-toggleable')
			:attr('data-druid', h.counter .. '-' .. i)
			:wikitext(image)
			:attr('data-druid-tab-key', tabs[i])
		local labelDisplay
		local labelKey = tabs[i]
		local caption = args.caption
		if labelKey then
			labelDisplay = args[labelKey .. '_label'] or labelKey
			caption = args[labelKey .. '_caption'] or caption
		else
			labelDisplay = 'Image ' .. i
		end
		caption = args[labelDisplay .. '_caption'] or caption
		if caption then
			container:tag('div')
				:addClass('druid-main-images-caption')
				:wikitext(caption)
		end
		if labelKey and args[labelKey .. '_focused'] then
			container:addClass('focused')
		end
	end
end

function h.getImageTabTexts(tabs, images, args)
	if #tabs == 0 and #images <= 1 then return {} end
	local texts = {}
	local i = 1
	while images[i] or tabs[i] do
		if tabs[i] then
			texts[i] = args[tabs[i] .. '_label'] or tabs[i]
		else
			texts[i] = 'Image ' .. i
		end
		i = i + 1
	end
	return texts
end

function h.printTabs(td, tabs, texts, isSection, args)
	if #texts == 0 then return end
	local container = td:tag('div')
		:addClass('druid-main-images-labels')
		:addClass('druid-tabs')
	if isSection then
		container:addClass('druid-section-tabs')
	end
	for i, item in ipairs(tabs) do
		local label = container:tag('div')
			:addClass('druid-main-images-label')
			:addClass('druid-tab')
			:addClass('druid-toggleable')
			:attr('data-druid', h.counter .. '-' .. i)
			:wikitext(texts[i])
			:attr('data-druid-tab-key', item)
		if isSection then
			label:addClass('druid-section-tab')
		else
			label:addClass('druid-title-tab')
		end
		if args[item .. '_focused'] then
			label:addClass('focused')
		end
		-- this can be null, don't concat anything here
		label:addClass(args[item .. '_class'])
	end
end

function h.makeGridSection(section, sectionFields, args, numCols)
	local numItems = h.countItems(sectionFields, section, args)
	if numItems == 0 then return end
	local node = mw.html.create(h.getTag('section'))
		:addClass('druid-section-container')
	h.printSectionHeader(node, section, args)
	h.printSectionTabs(node, section, args)
	local tr = node:tag(h.getTag('row'))
		:attr('data-druid-section-row', h.escape(section))
	if args[section .. '_collapsed'] then
		tr:addClass('druid-collapsed')
	end
	local grid = tr:tag(h.getTag('cell'))
		:attr('colspan', 2)
		:addClass('druid-grid-section')
		:addClass('druid-grid-section-' .. h.escape(section))
		:addClass(args[section .. '_class']) -- warning: class can be nil, don't concat anything
		:tag('div')
			:addClass('druid-grid')
	local row, col, i = 1, 1, 1
	local sizeOfLastRow = numItems % numCols
	local lcm = h.getNumGridCols(numItems, sizeOfLastRow, numCols)
	grid:css('grid-template-columns', ('repeat(%s, 1fr)'):format(lcm))
	local size = lcm / numCols
	for _, item in ipairs(sectionFields) do
		local node = mw.html.create('div')
		local shouldPrint = h.printData(node, item, section, args)
		if shouldPrint then
			if i == numItems - sizeOfLastRow + 1 then
				size = lcm / sizeOfLastRow
			end
			i = i + 1
			local gStart = (col - 1) * size + 1
			local gEnd = (col) * size + 1
			local itemContainer = grid:tag('div')
				:addClass('druid-grid-item')
				:addClass('druid-grid-item-' .. h.escape(item))
				:addClass(args[item .. '_class']) -- warning: class can be nil, don't concat anything
				:css('grid-column', ('%s / %s'):format(gStart, gEnd))
				:css('grid-row', row)
			if not h.castBool(args[item .. '_nolabel']) then
				h.printLabel(itemContainer:tag('div'), item, args)
			end
			itemContainer:node(node)
			if col == numCols then
				row = row + 1
				col = 1
			else
				col = col + 1
			end
		end
	end
	return node
end

function h.makeSection(section, sectionFields, args)
	if section == '' then return end -- bruteforce fix for trailing commas
	local shouldPrint = false
	local container = mw.html.create(h.getTag('section'))
		:addClass('druid-section-container')
		:addClass(args[section .. '_class']) -- warning: class can be nil, don't concat anything
	h.printSectionHeader(container, section, args)
	h.printSectionTabs(container, section, args)
	for _, item in ipairs(sectionFields) do
		local node = mw.html.create(h.getTag('cell'))
		local shouldPrintItem = h.printData(node, item, section, args)
		if shouldPrintItem then
			shouldPrint = true
			local tr = container:tag(h.getTag('row'))
				:addClass('druid-row')
				:addClass('druid-row-' .. h.escape(item))
				:addClass(args[item .. '_class']) -- warning: class can be nil, don't concat anything
				:attr('data-druid-section-row', h.escape(section))
			if args[section .. '_collapsed'] then
				tr:addClass('druid-collapsed')
			end
			if h.castBool(args[item .. '_wide']) or h.castBool(args[item .. '_nolabel']) then
				node
					:attr('colspan', 2)
					:addClass('druid-data-wide')
			else
				h.printLabel(tr:tag(h.getTag('label')), item, args)
			end
			tr:node(node)
		end
	end
	if not shouldPrint then return nil end
	return container
end

function h.countItems(sectionFields, section, args)
	local numItems = 0
	for _, v in ipairs(sectionFields) do
		-- we aren't actually printing here, but we're finding out if we should print anything
		-- because we need the count of columns before we print anything in grid data
		if h.printData(mw.html.create(), v, section, args) then
			numItems = numItems + 1
		end
	end
	return numItems
end

function h.getNumGridCols(numItems, sizeOfLastRow, numCols)
	if not numCols then return numItems, 1 end
	if numItems < numCols then return numItems, 1 end
	if sizeOfLastRow == 0 then
		return numCols, 1
	end
	local a, b = sizeOfLastRow, numCols
	while b ~= 0 do
		a, b = b, a % b
	end
	local lcm = sizeOfLastRow * numCols / a
	return lcm
end

function h.printLabel(node, item, args)
	return node
		:addClass('druid-label')
		:addClass('druid-label-' .. h.escape(item))
		:wikitext(args[item .. '_display'] or args[item .. '_label'] or item)
end

function h.printData(node, item, section, args)
	-- prints data to the node
	-- and also returns whether the item is nonempty or not
	local hasData = false
	local sectionTabs = args[section .. '_tabs']
	local tabs = args.tabs
	if sectionTabs and #sectionTabs > 0 then
		tabs = sectionTabs
	end
	if not tabs or #tabs == 0 then
		return h.printSimpleData(node, item, args)
	end
	if not h.hasComplexData(item, tabs, args) then
		return h.printSimpleData(node, item, args)
	end
	hasData = hasData or h.printTabbedDataItem(node, item, tabs, args)
	if hasData then
		node:addClass('druid-data')
	end
	return hasData
end

function h.getTabbedContent(args, label, item)
	return args[label .. '_' .. item] or args[item] or TABBED_NONEXIST
end

function h.printSimpleData(node, item, args)
	if args[item] and type(args[item]) ~= 'string' then
		error(("Invalid use of field %s as both a section and a data value"):format(item))
	end
	if not args[item] then return false end
	node:addClass('druid-data')
		:addClass('druid-data-' .. h.escape(item))
		:addClass('druid-data-nonempty')
		:wikitext('\n\n' .. args[item])
	return true
end

function h.hasComplexData(item, tabs, args)
	for _, v in ipairs(tabs) do
		if args[v .. '_' .. item] then return true end
	end
	return false
end

function h.printSectionHeader(node, section, args)
	if h.castBool(args[section .. '_nolabel']) then return end
	local tr = node:tag(h.getTag('row'))
		:attr('data-druid-section', h.escape(section))
	local th = tr:tag(h.getTag('sectionTitle'))
		:attr('colspan', 2)
		:addClass('druid-section')
		:addClass('druid-section-' .. h.escape(section))
	if args[section .. '_collapsible'] or args[section .. '_collapsed'] then
		tr:addClass('druid-collapsible')
		if args[section .. '_collapsed'] then
			tr:addClass('druid-collapsible-collapsed')
		end
	end
	local emptySections = {}
	for _, label in ipairs(args.tabs) do
		local hasLabel = false
		for _, item in ipairs(args[section] or {}) do
			if h.getTabbedContent(args, label, item) then
				hasLabel = true
			end
		end
		if not hasLabel then emptySections[label] = true end
	end
	if not next(emptySections) then
		th:wikitext(args[section .. '_label'] or section)
		return
	end
	for i, label in ipairs(args.tabs) do
		local div = th:tag('div')
			:addClass('druid-toggleable-heading')
			:addClass('druid-toggleable')
			:attr('data-druid', h.counter .. '-' .. i)
			:wikitext(args[section .. '_label'] or section)
		-- we are going to print the section content even in empty nodes
		-- for compatibility with browsers without :has, where hiding empty rows won't happen
		if emptySections[label] then
			div:addClass('druid-toggleable-heading-empty')
		end
		if args[label .. '_focused'] then
			div:addClass('focused')
		end
	end
end

function h.printSectionTabs(node, section, args)
	local tabs = args[section .. '_tabs']
	if not tabs or #tabs == 0 then return end
	local tr = node:tag(h.getTag('sectionTabsOuter'))
		:attr('data-druid-section', h.escape(section))
		:attr('data-druid-section-tabs', h.escape(section))
	local th = tr:tag(h.getTag('sectionTabs'))
		:attr('colspan', 2)
		:addClass('druid-section-tabs')
		:addClass('druid-section-tabs-' .. h.escape(section))
	local texts = {}
	for i, item in ipairs(tabs) do
		texts[i] = args[item .. '_label'] or item
	end
	h.printTabs(th, tabs, texts, true, args)
end

----------------------------
-- general utility functions
----------------------------

function h.overwrite()
	-- this is a generic utility function that collects args from the invoke call & the parent template.
	-- normally, you merge args with parent template overwriting the invoke call, but
	-- since we'll be putting markup/formatting into our invoke call,
	-- we actually want to overwrite what the user sent.
	local f = mw.getCurrentFrame()
	local origArgs = f.args
	local parentArgs = f:getParent().args

	local args = {}
	
	for k, v in pairs(parentArgs) do
		v = mw.text.trim(v)
		if v ~= '' then
			args[k] = v
		end
	end
	
	for k, v in pairs(origArgs) do
		v = mw.text.trim(tostring(v))
		if v ~= '' then
			args[k] = v
		end
	end
	
	return args
end

-- generic utility functions
-- these would normally be provided by other modules, but to make installation easy
-- I'm including everything here

function h.split(text, pattern, plain)
	if not text then
		return {}
	end
	local ret = {}
	for m in h.gsplit(text, pattern, plain) do
		ret[#ret+1] = m
	end
	return ret
end

function h.gsplit( text, pattern, plain )
	if not pattern then pattern = ',' end
	if not plain then
		pattern = '%s*' .. pattern .. '%s*'
	end
	local s, l = 1, text:len()
	return function ()
		if s then
			local e, n = text:find( pattern, s, plain )
			local ret
			if not e then
				ret = text:sub( s )
				s = nil
			elseif n < e then
				-- Empty separator!
				ret = text:sub( s, e )
				if e < l then
					s = e + 1
				else
					s = nil
				end
			else
				ret = e > s and text:sub( s, e - 1 ) or ''
				s = n + 1
			end
			return ret
		end
	end, nil, nil
end

-- char-set of characters to escape/replace
local escapePattern = '[' -- required for pattern; do not remove.
	.. ' '
	.. '"'
	.. "'"
	.. "%?"
	.. "%%"
	.. "%["
	.. "%]"
	.. "{"
	.. ']' -- required for pattern; do not remove.

function h.escape(s)
	s = s:gsub(escapePattern, '')
	return s
end


-- normally I would make these constants at the top of the file
-- but I don't want to mistake them with user-set constants
h.boolFalse = { ['false'] = true, ['no'] = true, [''] = true, ['0'] = true, ['nil'] = true }

function h.castBool(x)
	if not x then return false end
	return not h.boolFalse[tostring(x):lower()]
end

h.htmlEntities = {
	table = {
		container = 'table',
		titleOuter = 'tr',
		titleInner = 'th',
		section = '',
		sectionTitle = 'th',
		sectionTabsOuter = 'tr',
		sectionTabs = 'td',
		row = 'tr',
		label = 'th',
		cell = 'td',
	},
	div = {
		container = 'div',
		titleOuter = 'div',
		titleInner = 'div',
		section = 'div',
		sectionTitle = 'div',
		sectionTabsOuter = 'div',
		sectionTabs = 'div',
		row = 'div',
		label = 'div',
		cell = 'div',
	}
}

function h.getTag(key)
	-- try not to totally fail here
	return h.htmlEntities[h.entityType or 'div'][key]
end

return p