imported>RheingoldRiver
Adding default set of pages
 
mNo edit summary
 
(10 intermediate revisions by 3 users not shown)
Line 1: Line 1:
local counter
-- version 0.1.7
 
--------------------------------------
-- 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.
-- 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
-- set to `nil` (not in quotes) to remove such rows altogether in the tabs where they're missing
local TABBED_NONEXIST = ''
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 h = {}
local p = {}
local p = {}
local hooks = {}


function p.arraymap(frame)
function p.arraymap(frame)
Line 23: Line 37:


function p.main(frame)
function p.main(frame)
h.registerHooks()
h.increment()
h.increment()
local args = h.overwrite()
local args = h.overwrite()
local sep = args.sep or ','
local sep = args.sep or ','
h.castArgs(args, sep)
h.castArgs(args, sep)
    h.setMainImage(args.images[1])
if h.castBool(args.setmainimage or 'yes') then
return h.makeInfobox(args, sep)
    h.setMainImage(args.images[1])
end
    -- suggest to use HIDDENCAT here; will be used for maintenance & gadget imports
return h.makeInfobox(args, sep), '[[Category:Pages with DRUID infoboxes]]'
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
end


function h.increment()
function h.increment()
counter = mw.getCurrentFrame():callParserFunction('#var', {'DRUID_INFOBOX_ID', 0}) + 1
-- optional use of VariablesLua for better compatibility
mw.getCurrentFrame():callParserFunction('#vardefine', {'DRUID_INFOBOX_ID', counter})
local VariablesLua = mw.ext.VariablesLua
if VariablesLua == nil then
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
else
h.counter = VariablesLua.var('DRUID_INFOBOX_ID', 0) + 1
VariablesLua.vardefine('DRUID_INFOBOX_ID', h.counter)
end
end
end


function h.castArgs(args, sep)
function h.castArgs(args, sep)
h.runHook('onCastArgsStart', args, sep, args.kind)
args.tabs = h.split(args.tabs or args.image_labels, sep)
args.tabs = h.split(args.tabs or args.image_labels, sep)
args.images = h.getImages(args, sep)
args.images = h.getImages(args, sep)
args.sections = h.split(args.sections, sep)
args.sections = h.split(args.sections, sep)
for _, section in ipairs(args.sections) do
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] = 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
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)
end
end


Line 63: Line 126:


function h.setMainImage(file)
function h.setMainImage(file)
if h.counter > 1 then return end
     if not file then return end
     if not file then return end
mw.getCurrentFrame():callParserFunction{
    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',
name = '#setmainimage',
args = { file:gsub('File:', '') },
args = { fileText },
}
} end)
end
end


function h.makeInfobox(args, sep)
function h.makeInfobox(args, sep)
local out = mw.html.create('table')
local out = mw.html.create(h.getTag('container'))
:addClass('druid-infobox')
:addClass('druid-infobox')
:addClass('druid-container')
:addClass('druid-container')
:attr('id', 'druid-container-' .. counter)
: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
if args.kind then out:addClass('druid-container-' .. h.escape(args.kind)) end
if args.title then
h.printTitle(out, args)
out:tag('tr')
:tag('th')
:addClass('druid-title')
:attr('colspan', 2)
:wikitext(args.title)
end
h.printImages(out, args.images, args)
h.printImages(out, args.images, args)
for _, section in ipairs(args.sections) do
for _, section in ipairs(args.sections) do
Line 90: Line 155:
out:node(makeSection(section, args[section], args, tonumber(cols)))
out:node(makeSection(section, args[section], args, tonumber(cols)))
end
end
return out
h.runHook('onMakeOutputEnd', out, args)
-- category for gadget loading
return out, '[[Category:Pages with DRUID infoboxes]]'
end
end


function h.printImages(out, images, args)
function h.printTitle(out, args)
if #images == 0 and #args.tabs == 0 then return end
local tabs = args.tabs
-- burden is on the user to format this as an image. this should be done in the infobox template,
if not tabs or #tabs == 0 then
-- with something like |image={{#if:{{{image|}}}|[[File:{{{image|}}}{{!}}300px{{!}}link=]]}}
h.printSimpleTitle(out, args)
local td = out:tag('tr')
return
:tag('td')
end
:attr('colspan', 2)
if not h.hasComplexData('title', tabs, args) then
h.printTabs(td, args.tabs, images, args)
h.printSimpleTitle(out, args)
if #images == 0 then return end
if #images == 1 then
td:addClass('druid-main-image')
:wikitext(images[1])
return
return
end
end
td:addClass('druid-main-images')
local node = h.printTitleWrapper(out)
local imagesContainer = td:tag('div')
h.printTabbedDataItem(node, 'title', tabs, args)
:addClass('druid-main-images-files')
end
for i, item in ipairs(images) do
 
local container = imagesContainer:tag('div')
function h.printSimpleTitle(out, args)
:addClass('druid-main-images-file')
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.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')
:addClass('druid-toggleable')
:attr('data-druid', counter .. '-' .. i)
:attr('data-druid', h.counter .. '-' .. i)
:wikitext(item)
:attr('data-druid-tab-key', label)
local labelText = args[item .. '_label'] or item or ('[[Category:Infoboxes missing image labels]]Image ' .. i)
if h.getTabbedContent(args, label, item) then
if args[labelText .. '_caption'] then
hasData = true
container:tag('div')
div:wikitext('\n\n' .. h.getTabbedContent(args, label, item))
:addClass('druid-main-images-caption')
div:addClass('druid-toggleable-data-nonempty')
:wikitext(args[labelText .. '_caption'])
else
div:addClass('druid-toggleable-data-empty')
end
end
if i == 1 then
container:addClass('focused')
if i == 1 then div:addClass('focused') end
end
return hasData
end
 
function h.getWornImage(imageStr)
    local fileName = imageStr:match("File:([^|%]]+)")
    if not fileName then return nil end
 
    local name, ext = fileName:match("^(.-)%.([^%.]+)$")
    if not name then return nil end
   
    local wornFileName = name .. "Worn." .. ext
    local wornTitle = mw.title.new('File:' .. wornFileName)
 
    if wornTitle and wornTitle.exists then
        return imageStr:gsub(fileName, wornFileName)
    end
    return nil
end
 
function h.printImages(out, images, args)
    if #images == 0 and #args.tabs == 0 then return end
   
    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
   
    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)
 
        local wornImage = h.getWornImage(image)
        if wornImage then
            container:addClass('has-worn-variant')
            container:tag('div'):addClass('image-base'):wikitext(image)
            container:tag('div'):addClass('image-worn'):wikitext(wornImage)
            container:tag('div'):addClass('worn-toggle-button'):wikitext('Show Worn')
        else
            container:wikitext(image)
        end
 
        if i == 1 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] = '[[Category:Infoboxes missing image labels]]Image ' .. i
end
end
i = i + 1
end
end
return texts
end
end


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


function h.makeGridSection(section, sectionFields, args, numCols)
function h.makeGridSection(section, sectionFields, args, numCols)
local shouldPrint = false
local numItems = h.countItems(sectionFields, section, args)
local node = mw.html.create()
if numItems == 0 then return end
local node = mw.html.create(h.getTag('section'))
:addClass('druid-section-container')
h.printSectionHeader(node, section, args)
h.printSectionHeader(node, section, args)
local tr = node:tag('tr')
h.printSectionTabs(node, section, args)
local tr = node:tag(h.getTag('row'))
:attr('data-druid-section-row', h.escape(section))
:attr('data-druid-section-row', h.escape(section))
if args[section .. '_collapsed'] then
if args[section .. '_collapsed'] then
tr:addClass('druid-collapsed')
tr:addClass('druid-collapsed')
end
end
local grid = tr:tag('td')
local grid = tr:tag(h.getTag('cell'))
:attr('colspan', 2)
:attr('colspan', 2)
:addClass('druid-grid-section')
:addClass('druid-grid-section')
:addClass('druid-grid-section-' .. h.escape(section))
:addClass('druid-grid-section-' .. h.escape(section))
:addClass(args[section .. '_class']) -- warning: class can be nil, don't concat anything
:tag('div')
:tag('div')
:addClass('druid-grid')
:addClass('druid-grid')
local row = 1
local row, col, i = 1, 1, 1
local col = 1
local sizeOfLastRow = numItems % numCols
local itemContainer
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
for _, item in ipairs(sectionFields) do
if args[item] then
local node = mw.html.create('div')
shouldPrint = true
local shouldPrint = h.printData(node, item, section, args)
itemContainer = grid:tag('div')
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')
:addClass('druid-grid-item-' .. h.escape(item))
:addClass('druid-grid-item-' .. h.escape(item))
:css('grid-column', col)
: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)
:css('grid-row', row)
if not args[item .. '_nolabel'] then
if not h.castBool(args[item .. '_nolabel']) then
h.printLabel(itemContainer:tag('div'), item, args)
h.printLabel(itemContainer:tag('div'), item, args)
end
end
h.printData(itemContainer:tag('div'), item, args)
itemContainer:node(node)
if col == numCols then
if col == numCols then
row = row + 1
row = row + 1
Line 197: Line 362:
end
end
end
end
grid:css('grid-template-columns', ('repeat(%s, 1fr)'):format(row > 1 and numCols or col - 1))
if not shouldPrint then return nil end
itemContainer:css('grid-column', ('%s / -1'):format(col - 1))
return node
return node
end
end


function h.makeSection(section, sectionFields, args)
function h.makeSection(section, sectionFields, args)
if section == '' then return end -- bruteforce fix for trailing commas
local shouldPrint = false
local shouldPrint = false
local node = mw.html.create()
local container = mw.html.create(h.getTag('section'))
h.printSectionHeader(node, section, args)
: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
for _, item in ipairs(sectionFields) do
if h.shouldPrint(item, args) then
local node = mw.html.create(h.getTag('cell'))
local shouldPrintItem = h.printData(node, item, section, args)
if shouldPrintItem then
shouldPrint = true
shouldPrint = true
local tr = node:tag('tr')
local tr = container:tag(h.getTag('row'))
:addClass('druid-row')
:addClass('druid-row')
:addClass('druid-row-' .. h.escape(item))
: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))
:attr('data-druid-section-row', h.escape(section))
if args[section .. '_collapsed'] then
if args[section .. '_collapsed'] then
tr:addClass('druid-collapsed')
tr:addClass('druid-collapsed')
end
end
if args[item .. '_wide'] or args[item .. '_nolabel'] then
if h.castBool(args[item .. '_wide']) or h.castBool(args[item .. '_nolabel']) then
local td = h.printData(tr:tag('td'), item, args)
node
td
:attr('colspan', 2)
:attr('colspan', 2)
:addClass('druid-data-wide')
:addClass('druid-data-wide')
else
else
h.printLabel(tr:tag('th'), item, args)
h.printLabel(tr:tag(h.getTag('label')), item, args)
h.printData(tr:tag('td'), item, args)
end
end
tr:node(node)
end
end
end
end
if not shouldPrint then return nil end
if not shouldPrint then return nil end
return node
return container
end
end


function h.shouldPrint(item, args)
function h.countItems(sectionFields, section, args)
if args[item] then return true end
local numItems = 0
for _, key in ipairs(args.tabs) do
for _, v in ipairs(sectionFields) do
if args[key .. '_' .. item] then
-- we aren't actually printing here, but we're finding out if we should print anything
return true
-- 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
end
end
return false
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
end


Line 249: Line 433:
end
end


function h.printData(node, item, args)
function h.printData(node, item, section, args)
if not args.tabs or #args.tabs == 0 then
local hasData = false
h.printSimpleData(node, item, args)
local sectionTabs = args[section .. '_tabs']
return node
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
end
if not h.hasComplexData(item, args) then
 
h.printSimpleData(node, item, args)
if h.hasComplexData(item, tabs, args) or args[item] then
return node
hasData = h.printTabbedDataItem(node, item, tabs, args)
end
end
for i, label in ipairs(args.tabs) do
 
local div = node:tag('div')
if hasData then
:addClass('druid-toggleable-data')
node:addClass('druid-data')
:addClass('druid-toggleable')
:attr('data-druid', counter .. '-' .. i)
if h.getTabbedContent(args, label, item) then
div:wikitext('\n\n' .. h.getTabbedContent(args, label, item))
else
div:addClass('druid-toggleable-data-empty')
end
if i == 1 then div:addClass('focused') end
end
end
return node
return hasData
end
end


Line 278: Line 460:


function h.printSimpleData(node, item, args)
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')
node:addClass('druid-data')
:addClass('druid-data-' .. h.escape(item))
:addClass('druid-data-' .. h.escape(item))
:addClass('druid-data-nonempty')
:addClass('druid-toggleable-data')
:addClass('focused')
:wikitext('\n\n' .. args[item])
:wikitext('\n\n' .. args[item])
return true
end
end


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


function h.printSectionHeader(node, section, args)
function h.printSectionHeader(node, section, args)
if args[section .. '_nolabel'] then return end
if h.castBool(args[section .. '_nolabel']) then return end
local tr = node:tag('tr')
local tr = node:tag(h.getTag('row'))
:attr('data-druid-section', h.escape(section))
:attr('data-druid-section', h.escape(section))
local th = tr:tag('th')
local th = tr:tag(h.getTag('sectionTitle'))
:attr('colspan', 2)
:attr('colspan', 2)
:addClass('druid-section')
:addClass('druid-section')
:addClass('druid-section-' .. h.escape(section))
:addClass('druid-section-' .. h.escape(section))
if args[section .. '_collapsible'] then
if args[section .. '_collapsible'] or args[section .. '_collapsed'] then
tr:addClass('druid-collapsible')
tr:addClass('druid-collapsible')
if args[section .. '_collapsed'] then
if args[section .. '_collapsed'] then
Line 322: Line 513:
:addClass('druid-toggleable-heading')
:addClass('druid-toggleable-heading')
:addClass('druid-toggleable')
:addClass('druid-toggleable')
:attr('data-druid', counter .. '-' .. i)
:attr('data-druid', h.counter .. '-' .. i)
:wikitext(args[section .. '_label'] or section)
:wikitext(args[section .. '_label'] or section)
-- we are going to print the section content even in empty nodes
-- we are going to print the section content even in empty nodes
Line 334: Line 525:
end
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))
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()
function h.overwrite()
Line 420: Line 631:
:gsub("!", '')
:gsub("!", '')
return s
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
end


return p
return p

Latest revision as of 13:27, 1 January 2026

Edit the documentation or categories for this module.

This module enables the creation of DRUID infoboxes.


-- version 0.1.7

--------------------------------------
-- 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
    -- suggest to use HIDDENCAT here; will be used for maintenance & gadget imports
	return h.makeInfobox(args, sep), '[[Category:Pages with DRUID infoboxes]]'
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
		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
	else
		h.counter = VariablesLua.var('DRUID_INFOBOX_ID', 0) + 1
		VariablesLua.vardefine('DRUID_INFOBOX_ID', h.counter)
	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)
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.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.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)
	-- category for gadget loading
	return out, '[[Category:Pages with DRUID infoboxes]]'
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.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 i == 1 then div:addClass('focused') end
	end
	return hasData
end

function h.getWornImage(imageStr)
    local fileName = imageStr:match("File:([^|%]]+)")
    if not fileName then return nil end

    local name, ext = fileName:match("^(.-)%.([^%.]+)$")
    if not name then return nil end
    
    local wornFileName = name .. "Worn." .. ext
    local wornTitle = mw.title.new('File:' .. wornFileName)

    if wornTitle and wornTitle.exists then
        return imageStr:gsub(fileName, wornFileName)
    end
    return nil
end

function h.printImages(out, images, args)
    if #images == 0 and #args.tabs == 0 then return end
    
    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
    
    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)

        local wornImage = h.getWornImage(image)
        if wornImage then
            container:addClass('has-worn-variant')
            container:tag('div'):addClass('image-base'):wikitext(image)
            container:tag('div'):addClass('image-worn'):wikitext(wornImage)
            container:tag('div'):addClass('worn-toggle-button'):wikitext('Show Worn')
        else
            container:wikitext(image)
        end

        if i == 1 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] = '[[Category:Infoboxes missing image labels]]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 i == 1 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)
	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 h.hasComplexData(item, tabs, args) or args[item] then
		hasData = h.printTabbedDataItem(node, item, tabs, args)
	end

	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')
		:addClass('druid-toggleable-data')
		:addClass('focused')
		: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 i == 1 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))
	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

function h.escape(s)
	s = s:gsub(' ', '')
		:gsub('"', '')
		:gsub("'", '')
		:gsub("%?", '')
		:gsub("%%", '')
		:gsub("%[", '')
		:gsub("%]", '')
		:gsub("{", '')
		:gsub("}", '')
		:gsub("!", '')
	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