Module:Sandbox/LVL: Difference between revisions

From The Deadlock Wiki
Jump to navigation Jump to search
LVL (talk | contribs)
No edit summary
LVL (talk | contribs)
No edit summary
Line 1: Line 1:
-- Single-file Tabs module for deadlock.wiki
-- @Liquipedia
-- All dependencies inlined to avoid Lua.import
-- page=Module:TabsCombined
-- Combined version of Module:Tabs with all dependencies
local Tabs = {}
 
--[[
=================================================================
Start of dependency: Module:Table
=================================================================
]]
local Table = {}


-- Inlined Module:Array (partial stub - expand if needed)
function Table.size(tbl)
local Array = {}
local i = 0
Array.forEach = function(elements, funct)
for _ in pairs(tbl) do
for index, element in ipairs(elements) do
i = i + 1
funct(element, index)
end
end
return i
end
end
Array.any = function(tbl, predicate)
 
for _, v in ipairs(tbl) do
function Table.includes(tbl, value, isPattern)
if predicate(v) then return true end
for _, entry in pairs(tbl) do
if isPattern and string.find(entry, value)
or not isPattern and entry == value then
return true
end
end
end
return false
return false
end
end
Array.all = function(tbl, predicate)
 
for _, v in ipairs(tbl) do
function Table.getKeyOfValue(tbl, value)
if not predicate(v) then return false end
for key, entry in pairs(tbl) do
if entry == value then
return key
end
end
return nil
end
 
function Table.filter(tbl, predicate, argument)
local filteredTbl = {}
local foundMatches = 1
 
for _, entry in pairs(tbl) do
if predicate(entry, argument) then
filteredTbl[foundMatches] = entry
foundMatches = foundMatches + 1
end
end
 
return filteredTbl
end
 
function Table.filterByKey(tbl, predicate)
local filteredTbl = {}
 
for key, entry in pairs(tbl) do
if predicate(key, entry) then
filteredTbl[key] = entry
end
end
 
return filteredTbl
end
 
function Table.isEmpty(tbl)
if tbl == nil then
return true
end
for _, _ in pairs(tbl) do
return false
end
end
return true
return true
end
end


-- Inlined Module:Class
function Table.isNotEmpty(tbl)
local Class = {}
return not Table.isEmpty(tbl)
function Class.export(tbl, opts)
end
 
function Table.copy(tbl)
local result = {}
local result = {}
for _, name in ipairs(opts.exports) do
for key, entry in pairs(tbl) do
result[name] = tbl[name]
result[key] = entry
end
end
return result
return result
end
end


-- Inlined Module:Logic
function Table.deepCopy(tbl_, options)
options = options or {}
assert(type(tbl_) == 'table', 'Table.deepCopy: Input must be a table')
 
local function deepCopy(tbl)
local result = {}
for key, value in pairs(tbl) do
result[key] = type(value) == 'table' and deepCopy(value) or value
end
if options.copyMetatable then
local metatable = getmetatable(tbl)
if type(metatable) == 'table' then
setmetatable(result, deepCopy(metatable))
end
end
return result
end
 
return deepCopy(tbl_)
end
 
function Table.deepEquals(xTable, yTable)
assert(type(xTable) == 'table', 'Table.deepEquals: First argument must be a table')
assert(type(yTable) == 'table', 'Table.deepEquals: Second argument must be a table')
 
for key, value in pairs(xTable) do
if not Logic.deepEquals(value, yTable[key]) then
return false
end
end
 
for key, _ in pairs(yTable) do
if xTable[key] == nil then
return false
end
end
 
return true
end
 
function Table.mergeInto(target, ...)
local objs = Table.pack(...)
for i = 1, objs.n do
if type(objs[i]) == 'table' then
for key, value in pairs(objs[i]) do
target[key] = value
end
end
end
return target
end
 
function Table.merge(...)
return Table.mergeInto({}, ...)
end
 
function Table.deepMergeInto(target, ...)
local tbls = Table.pack(...)
for i = 1, tbls.n do
if type(tbls[i]) == 'table' then
for key, value in pairs(tbls[i]) do
if type(target[key]) == 'table' and type(value) == 'table' then
Table.deepMergeInto(target[key], value)
else
target[key] = value
end
end
end
end
return target
end
 
function Table.deepMerge(...)
return Table.deepMergeInto({}, ...)
end
 
function Table.map(xTable, f)
local yTable = {}
for xKey, xValue in pairs(xTable) do
local yKey, yValue = f(xKey, xValue)
yTable[yKey] = yValue
end
return yTable
end
 
function Table.mapArgumentsByPrefix(args, prefixes, f, noInterleave)
local function indexFromKey(key)
local prefix, index = key:match('^([%a_]+)(%d+)$')
if Table.includes(prefixes, prefix) then
return tonumber(index), prefix
else
return nil
end
end
 
return Table.mapArguments(args, indexFromKey, f, noInterleave)
end
 
function Table.mapArguments(args, indexFromKey, f, noInterleave)
local entriesByIndex = {}
 
-- Non-numeric args
for key, _ in pairs(args) do
local function post(index, ...)
if index and not entriesByIndex[index] then
entriesByIndex[index] = f(key, index, ...)
end
end
if type(key) == 'string' then
post(indexFromKey(key))
end
end
 
if noInterleave then
return entriesByIndex
end
 
-- Numeric index entries fills in gaps of prefixN= entries if not disabled
local entryIndex = 1
for argIndex = 1, math.huge do
if not args[argIndex] then
break
end
while entriesByIndex[entryIndex] do
entryIndex = entryIndex + 1
end
entriesByIndex[entryIndex] = f(argIndex, entryIndex)
end
 
return entriesByIndex
end
 
function Table.mapValues(xTable, f)
local yTable = {}
for xKey, xValue in pairs(xTable) do
yTable[xKey] = f(xValue)
end
return yTable
end
 
function Table.all(tbl, predicate)
for key, value in pairs(tbl) do
if not predicate(key, value) then
return false
end
end
return true
end
 
function Table.any(tbl, predicate)
for key, value in pairs(tbl) do
if predicate(key, value) then
return true
end
end
return false
end
 
function Table.groupBy(tbl, f)
local groups = {}
for key, value in pairs(tbl) do
local groupKey = f(key, value)
if not groups[groupKey] then
groups[groupKey] = {}
end
groups[groupKey][key] = value
end
return groups
end
 
function Table.extract(tbl, key)
local value = tbl[key]
tbl[key] = nil
return value
end
 
function Table.getByPathOrNil(tbl, path)
for _, fieldName in ipairs(path) do
if type(tbl) ~= 'table' then
return nil
end
tbl = tbl[fieldName]
end
return tbl
end
 
function Table.setByPath(tbl, path, value)
for i = 1, #path - 1 do
if tbl[path[i]] == nil then
tbl[path[i]] = {}
end
tbl = tbl[path[i]]
end
tbl[path[#path]] = value
end
 
function Table.uniqueKey(tbl)
local key0 = nil
for key, _ in pairs(tbl) do
if key0 ~= nil then return nil end
key0 = key
end
return key0
end
 
function Table.entries(tbl)
local entries = {}
for key, value in pairs(tbl) do
table.insert(entries, {key, value})
end
return entries
end
 
function Table.pack(...)
return {n = select('#', ...), ...}
end
 
Table.iter = {}
 
function Table.iter.spairs(tbl, order)
local keys = {}
for k in pairs(tbl) do keys[#keys+1] = k end
 
if order then
table.sort(keys, function(a,b) return order(tbl, a, b) end)
else
table.sort(keys)
end
 
local i = 0
return function()
i = i + 1
if keys[i] then
return keys[i], tbl[keys[i]]
end
end
end
 
function Table.iter.pairsByPrefix(tbl, prefixes, options)
options = options or {}
if type(prefixes) == 'string' then
prefixes = {prefixes}
end
 
local getByPrefixes = function(index)
for _, prefix in ipairs(prefixes) do
local key = prefix .. index
if tbl[key] then
return key, tbl[key]
end
end
end
 
local i = 1
return function()
local key, value = getByPrefixes(i)
if options.requireIndex == false and i == 1 and not value then
key, value = getByPrefixes('')
end
i = i + 1
if value then
return key, value, (i - 1)
else
return nil
end
end
end
 
--[[
=================================================================
Start of dependency: Module:Logic
=================================================================
]]
local Logic = {}
local Logic = {}
function Logic.emptyOr(val1, val2, default)
if not Logic.isEmpty(val1) then
return val1
elseif not Logic.isEmpty(val2) then
return val2
else
return default
end
end
function Logic.nilOr(...)
local args = Table.pack(...)
for i = 1, args.n do
local arg = args[i]
local val
if type(arg) == 'function' then
val = arg()
else
val = arg
end
if val ~= nil then
return val
end
end
return nil
end
function Logic.isEmpty(val)
if type(val) == 'table' then
return Table.isEmpty(val)
else
return val == '' or val == nil
end
end
function Logic.isNotEmpty(val)
if type(val) == 'table' then
return Table.isNotEmpty(val)
else
return val ~= nil and val ~= ''
end
end
function Logic.nilIfEmpty(val)
return Logic.isNotEmpty(val) and val or nil
end
function Logic.isDeepEmpty(val)
return Logic.isEmpty(val) or type(val) == 'table' and
Table.all(val, function(key, item) return Logic.isDeepEmpty(item) end)
end
function Logic.isNotDeepEmpty(val)
return not Logic.isDeepEmpty(val)
end
function Logic.readBool(val)
function Logic.readBool(val)
return val == true or val == 'true' or val == '1'
return val == 'true' or val == 't' or val == 'yes' or val == 'y' or val == true or val == '1' or val == 1
end
end
function Logic.readBoolOrNil(val)
function Logic.readBoolOrNil(val)
if val == nil then return nil end
if Logic.readBool(val) then
return Logic.readBool(val)
return true
elseif val == 'false' or val == 'f' or val == 'no' or val == 'n' or val == false or val == '0' or val == 0 then
return false
else
return nil
end
end
 
function Logic.nilThrows(val)
if val == nil then
error('Unexpected nil', 2)
end
return val
end
 
function Logic.tryCatch(try, catch)
local ran, result = pcall(try)
if not ran then
catch(result)
else
return result
end
end
 
function Logic.deepEquals(x, y)
if x == y then
return true
elseif type(x) == 'table' and type(y) == 'table' then
return Table.deepEquals(x, y)
else
return false
end
end
 
--[[
=================================================================
Start of dependency: Module:Array
=================================================================
]]
local Array = {}
 
function Array.randomize(tbl)
math.randomseed(os.time())
for i = #tbl, 2, -1 do
local j = math.random(i)
tbl[i], tbl[j] = tbl[j], tbl[i]
end
return tbl
end
 
function Array.isArray(tbl)
return type(tbl) == 'table' and Table.size(tbl) == #tbl
end
 
function Array.copy(tbl)
local copy = {}
for _, element in ipairs(tbl) do
table.insert(copy, element)
end
return copy
end
 
function Array.sub(tbl, startIndex, endIndex)
if startIndex < 0 then startIndex = #tbl + 1 + startIndex end
if not endIndex then endIndex = #tbl end
if endIndex < 0 then endIndex = #tbl + 1 + endIndex end
 
local subArray = {}
for index = startIndex, endIndex do
table.insert(subArray, tbl[index])
end
return subArray
end
 
function Array.map(elements, funct)
local mappedArray = {}
for index, element in ipairs(elements) do
local mappedElement = funct(element, index)
table.insert(mappedArray, mappedElement)
end
return mappedArray
end
end
function Logic.isEmpty(val)
 
return val == nil or val == ''
function Array.filter(tbl, predicate)
local filteredArray = {}
for index, element in ipairs(tbl) do
if predicate(element, index) then
table.insert(filteredArray, element)
end
end
return filteredArray
end
end
function Logic.isNotEmpty(val)
 
return not Logic.isEmpty(val)
function Array.flatten(tbl)
local flattenedArray = {}
for _, x in ipairs(tbl) do
if type(x) == 'table' then
for _, y in ipairs(x) do
table.insert(flattenedArray, y)
end
else
table.insert(flattenedArray, x)
end
end
return flattenedArray
end
end
function Logic.nilOr(val, fallback)
 
if val == nil then return fallback else return val end
function Array.flatMap(elements, funct)
return Array.flatten(Array.map(elements, funct))
end
end


-- Inlined Module:Operator
function Array.all(tbl, predicate)
local Operator = {}
for _, element in ipairs(tbl) do
function Operator.property(prop)
if not predicate(element) then
return function(tbl)
return false
return tbl and tbl[prop]
end
end
end
return true
end
end


-- Inlined Module:Page
function Array.any(tbl, predicate)
local Page = {}
for _, element in ipairs(tbl) do
function Page.makeInternalLink(_, text, link)
if predicate(element) then
return string.format('[[%s|%s]]', link, text)
return true
end
end
return false
end
end


-- Inlined Module:Table
function Array.find(tbl, predicate)
local Table = {}
for index, element in ipairs(tbl) do
function Table.extract(tbl, key)
if predicate(element, index) then
local val = tbl[key]
return element
if val ~= '' then return val end
end
end
return nil
return nil
end
end


-- Begin Tabs Module
function Array.groupBy(tbl, funct)
local Tabs = {}
local groupsByKey = {}
local groups = {}
for _, xValue in ipairs(tbl) do
local yValue = funct(xValue)
if yValue then
local group = groupsByKey[yValue]
if not group then
group = {}
groupsByKey[yValue] = group
table.insert(groups, group)
end
table.insert(group, xValue)
end
end
return groups, groupsByKey
end
 
function Array.groupAdjacentBy(array, f, equals)
equals = equals or Logic.deepEquals
local groups = {}
local currentKey
for index, elem in ipairs(array) do
local key = f(elem)
if index == 1 or not equals(key, currentKey) then
currentKey = key
table.insert(groups, {})
end
table.insert(groups[#groups], elem)
end
return groups
end
 
function Array.lexicalCompare(tblX, tblY)
for index = 1, math.min(#tblX, #tblY) do
if tblX[index] < tblY[index] then
return true
elseif tblX[index] > tblY[index] then
return false
end
end
return #tblX < #tblY
end
 
function Array.lexicalCompareIfTable(y1, y2)
if type(y1) == 'table' and type(y2) == 'table' then
return Array.lexicalCompare(y1, y2)
else
return y1 < y2
end
end
 
function Array.sortBy(tbl, funct, compare)
local copy = Table.copy(tbl)
Array.sortInPlaceBy(copy, funct, compare)
return copy
end
 
function Array.sortInPlaceBy(tbl, funct, compare)
compare = compare or Array.lexicalCompareIfTable
table.sort(tbl, function(x1, x2) return compare(funct(x1), funct(x2)) end)
end
 
function Array.reverse(tbl)
local reversedArray = {}
for index = #tbl, 1, -1 do
table.insert(reversedArray, tbl[index])
end
return reversedArray
end
 
function Array.append(tbl, ...)
return Array.appendWith(Array.copy(tbl), ...)
end
 
function Array.appendWith(tbl, ...)
local elements = Table.pack(...)
for index = 1, elements.n do
if elements[index] ~= nil then
table.insert(tbl, elements[index])
end
end
return tbl
end
 
function Array.extend(tbl, ...)
return Array.extendWith({}, tbl, ...)
end
 
function Array.extendWith(tbl, ...)
local arrays = Table.pack(...)
for index = 1, arrays.n do
if type(arrays[index]) == 'table' then
for _, element in ipairs(arrays[index]) do
table.insert(tbl, element)
end
elseif arrays[index] ~= nil then
table.insert(tbl, arrays[index])
end
end
return tbl
end
 
function Array.mapIndexes(funct)
local arr = {}
for index = 1, math.huge do
local y = funct(index)
if y then
table.insert(arr, y)
else
break
end
end
return arr
end
 
function Array.range(from, to)
local elements = {}
for element = from, to do
table.insert(elements, element)
end
return elements
end


function Tabs.static(args)
function Array.extractKeys(tbl, iterator, ...)
args = args or {}
iterator = iterator or pairs
local tabArgs = Tabs._readArguments(args, {allowThis2 = true})
local array = {}
local tabCount = #tabArgs
for key, _ in iterator(tbl, ...) do
if tabCount == 0 then return end
table.insert(array, key)
end
return array
end


Tabs._setThis(tabArgs)
function Array.extractValues(tbl, iterator, ...)
local tabs = mw.html.create('ul')
iterator = iterator or pairs
:attr('class', 'nav nav-tabs navigation-not-searchable tabs tabs' .. tabCount)
local array = {}
:attr('data-nosnippet')
for _, item in iterator(tbl, ...) do
table.insert(array, item)
end
return array
end


local subTabs = mw.html.create()
function Array.forEach(elements, funct)
for index, element in ipairs(elements) do
funct(element, index)
end
end


Array.forEach(tabArgs, function(tab)
function Array.reduce(array, operator, initialValue)
local name = tab.name or Tabs._getDisplayNameFromLink(tab.link)
local aggregate
local text = tab.link and Page.makeInternalLink({}, name, tab.link) or tab.name
if initialValue ~= nil then
tabs:tag('li'):addClass(tab.this and 'active' or nil):wikitext(text)
aggregate = initialValue
subTabs:node(tab.this and tab.tabs or nil)
else
end)
aggregate = array[1]
end


return mw.html.create()
for index = initialValue ~= nil and 1 or 2, #array do
:tag('div')
aggregate = operator(aggregate, array[index])
:addClass('tabs-static')
end
:attr('data-nosnippet', '')
return aggregate
:node(tabs)
:done()
:node(subTabs)
end
end


function Tabs.dynamic(args)
function Array.maxBy(array, funct, compare)
args = args or {}
compare = compare or Array.lexicalCompareIfTable
local tabArgs = Tabs._readArguments(args, {removeEmptyTabs = Logic.readBool(args.removeEmptyTabs)})
local max, maxScore
local tabCount = #tabArgs
for _, item in ipairs(array) do
if tabCount == 0 then return end
local score = funct(item)
if max == nil or compare(maxScore, score) then
max = item
maxScore = score
end
end
return max
end


local hasContent = Array.all(tabArgs, function(tab) return Logic.isNotEmpty(tab.content) end)
function Array.max(array, compare)
local allEmpty = Array.all(tabArgs, function(tab) return Logic.isEmpty(tab.content) end)
return Array.maxBy(array, function(x) return x end, compare)
assert(hasContent or allEmpty, 'Some of the tabs have contents while others do not')
end


local isSingular = tabCount == 1 and hasContent
function Array.minBy(array, funct, compare)
if isSingular and not Logic.readBool(args.showSingularAsTab) then
compare = compare or Array.lexicalCompareIfTable
return Tabs._single(tabArgs[1], not Logic.readBool(args.suppressHeader))
local min, minScore
for _, item in ipairs(array) do
local score = funct(item)
if min == nil or compare(score, minScore) then
min = item
minScore = score
end
end
end
return min
end
function Array.min(array, compare)
return Array.minBy(array, function(x) return x end, compare)
end


local tabs = mw.html.create('ul'):addClass('nav nav-tabs tabs tabs' .. tabCount)
function Array.indexOf(array, pred)
if not Array.any(tabArgs, Operator.property('this')) then
for ix, elem in ipairs(array) do
tabArgs[1].this = true
if pred(elem, ix) then
return ix
end
end
end
return 0
end


local build = function(obj, elementType, content, class, isActive)
function Array.unique(elements)
local element = mw.html.create(elementType)
local elementCache = {}
:addClass(class)
local uniqueElements = {}
:addClass(isActive and 'active' or nil)
for _, element in ipairs(elements) do
:newline()
if elementCache[element] == nil then
:node(content)
table.insert(uniqueElements, element)
obj:newline():node(element)
elementCache[element] = true
end
end
end
return uniqueElements
end
function Array.parseCommaSeparatedString(inputString, sep)
if Logic.isEmpty(inputString) then return {} end
return Array.map(mw.text.split(inputString, sep or ','), mw.text.trim)
end


Array.forEach(tabArgs, function(tabData, tabIndex)
function Array.interleave(elements, x)
build(tabs, 'li', tabData.name, 'tab' .. tabIndex, tabData.this)
local size = #elements
return Array.flatMap(elements, function(element, index)
if index == size then
return {element}
end
return {element, x}
end)
end)
end


if not Logic.nilOr(Logic.readBoolOrNil(args['hide-showall']), isSingular) then
--[[
tabs:tag('li'):addClass('show-all'):wikitext('Show All')
=================================================================
Start of dependency: Module:Operator
=================================================================
]]
local Operator = {}
 
function Operator.add(a, b) return a + b end
function Operator.sub(a, b) return a - b end
function Operator.mul(a, b) return a * b end
function Operator.div(a, b) return a / b end
function Operator.pow(a, b) return a ^ b end
function Operator.eq(a, b) return a == b end
function Operator.neq(a, b) return a ~= b end
function Operator.lt(a, b) return a < b end
function Operator.le(a, b) return a <= b end
function Operator.gt(a, b) return a > b end
function Operator.ge(a, b) return a >= b end
 
function Operator.property(item)
assert(type(item) == 'string' or type(item) == 'number', 'Invalid or missing input to `Operator.property`')
local pathSegments = mw.text.split(item, '.', true)
return function(tbl)
local selected = tbl
for segmentIndex, pathSegment in ipairs(pathSegments) do
if type(selected) ~= 'table' and segmentIndex == 1 then
error('Nil supplied to `Operator.property(' .. item .. ')`')
elseif type(selected) ~= 'table' then
local pathUntilHere = Array.sub(pathSegments, 1, segmentIndex - 1)
error('Could not index "tbl.' .. table.concat(pathUntilHere, '.') .. '"')
end
selected = selected[pathSegment] or selected[tonumber(pathSegment)]
end
return selected
end
end
 
function Operator.method(funcName, ...)
local args = {...}
return function(obj)
return obj[funcName](obj, unpack(args))
end
end
end
--[[
=================================================================
Start of dependency: Module:Page
=================================================================
]]
local Page = {}


tabs:newline()
function Page.exists(link)
local existingPage = mw.title.new(link)
if existingPage == nil then
return false
end
return existingPage.exists
end


local contents = Tabs._buildContentDiv(hasContent, Logic.readBool(args['hybrid-tabs']), Logic.readBool(args['no-padding']))
function Page.makeInternalLink(options, display, customLink)
if type(options) == 'string' then
customLink = display
display = options
end
if Logic.isEmpty(display) then
return nil
elseif Logic.isEmpty(customLink) then
customLink = display
end


if not hasContent then
if (options or {}).onlyIfExists == true and (not Page.exists(customLink)) then
return '<div class="tabs-dynamic navigation-not-searchable" data-nosnippet>\n' .. tostring(tabs) .. contents
return nil
end
end


Array.forEach(tabArgs, function(tabData, tabIndex)
return '[[' .. customLink .. '|' .. display .. ']]'
build(contents, 'div', tabData.content, 'content' .. tabIndex, tabData.this)
end
end)
 
function Page.makeExternalLink(display, link)
if Logic.isEmpty(display) or Logic.isEmpty(link) then
return nil
end
return '[' .. link .. ' ' .. display .. ']'
end


return mw.html.create('div')
function Page.pageifyLink(link)
:addClass('tabs-dynamic navigation-not-searchable')
if Logic.isEmpty(link) then
:attr('data-nosnippet')
return nil
:node(tabs)
end
:newline()
return (mw.ext.TeamLiquidIntegration.resolve_redirect(link):gsub(' ', '_'))
:node(contents)
end
end


--[[
=================================================================
Start of original Module:Tabs code
=================================================================
]]
function Tabs._readArguments(args, options)
function Tabs._readArguments(args, options)
local tabArgs = {}
local tabArgs = {}
Line 186: Line 913:


assert(Logic.isNotEmpty(tabArgs), 'You are trying to add a "Tabs" template without arguments for names nor links')
assert(Logic.isNotEmpty(tabArgs), 'You are trying to add a "Tabs" template without arguments for names nor links')
return tabArgs
return tabArgs
end
end
Line 200: Line 928:
if not link then return end
if not link then return end
link = link:gsub('_', ' ')
link = link:gsub('_', ' ')
local linkLength = #link
local linkLength = string.len(link)
local charAfter = fullPageName:sub(linkLength + 1, linkLength + 1)
local charAfter = string.sub(fullPageName, linkLength + 1, linkLength + 1)
local pagePartial = fullPageName:sub(1, linkLength)
local pagePartial = string.sub(fullPageName, 1, linkLength)
if pagePartial == link and (charAfter == '/' or charAfter == '') and linkLength > maxLinkLength then
if pagePartial == link and (charAfter == '/' or charAfter == '') and linkLength > maxLinkLength then
maxLinkLength = linkLength
maxLinkLength = linkLength
Line 209: Line 937:
end)
end)


if this then tabArgs[this].this = true end
if not this then return end
tabArgs[this].this = true
end
end


function Tabs._buildContentDiv(hasContent, hybridTabs, noPadding)
function Tabs._buildContentDiv(hasContent, hybridTabs, noPadding)
if hasContent then
if hasContent then
local contentDiv = mw.html.create('div'):addClass('tabs-content')
local contentDiv = mw.html.create('div')
:addClass('tabs-content')
if hybridTabs then
if hybridTabs then
contentDiv:css('border-style', 'none !important'):css('padding', '0 !important')
contentDiv
:css('border-style', 'none !important')
:css('padding', '0 !important')
elseif noPadding then
elseif noPadding then
contentDiv:css('padding', '0 !important')
contentDiv
:css('padding', '0 !important')
end
end
return contentDiv
return contentDiv
Line 224: Line 957:


local style = ''
local style = ''
if hybridTabs then style = 'border-style:none !important; padding:0 !important;'
if hybridTabs then
elseif noPadding then style = 'padding:0 !important;' end
style = 'border-style:none !important; padding:0 !important;'
return '\n<div class="tabs-content" style="' .. style .. '">' end
elseif noPadding then
style = 'padding:0 !important;'
end
return '\n<div class="tabs-content" style="' .. style .. '">'
end


function Tabs._single(tab, showHeader)
function Tabs._single(tab, showHeader)
local header
local header
if showHeader then
if showHeader then
header = mw.html.create():tag('h6'):wikitext(tab.name):done():newline()
header = mw.html.create()
:tag('h6'):wikitext(tab.name):done()
:newline()
end
end
return mw.html.create():node(header):node(tab.content)
return mw.html.create()
:node(header)
:node(tab.content)
end
end


function Tabs._getDisplayNameFromLink(link)
function Tabs._getDisplayNameFromLink(link)
local parts = mw.text.split(link, '/', true)
local linkParts = mw.text.split(link, '/', true)
return parts[#parts]
return linkParts[#linkParts]
end
 
function Tabs.static(args)
args = args or {}
 
local tabArgs = Tabs._readArguments(args, {allowThis2 = true})
local tabCount = #tabArgs
if tabCount == 0 then return end
 
Tabs._setThis(tabArgs)
 
local tabs = mw.html.create('ul')
:attr('class', 'nav nav-tabs navigation-not-searchable tabs tabs' .. tabCount)
:attr('data-nosnippet')
 
local subTabs = mw.html.create()
 
Array.forEach(tabArgs, function(tab)
local name = tab.name or Tabs._getDisplayNameFromLink(tab.link)
local text = tab.link and Page.makeInternalLink({}, name, tab.link) or tab.name
tabs:tag('li'):addClass(tab.this and 'active' or nil):wikitext(text)
subTabs:node(tab.this and tab.tabs or nil)
end)
 
return mw.html.create()
:tag('div')
:addClass('tabs-static')
:attr('data-nosnippet', '')
:node(tabs)
:done()
:node(subTabs)
end
 
function Tabs.dynamic(args)
args = args or {}
 
local tabArgs = Tabs._readArguments(args, {removeEmptyTabs = Logic.readBool(args.removeEmptyTabs)})
local tabCount = #tabArgs
if tabCount == 0 then return end
 
local hasContent = Array.all(tabArgs, function(tab)
return Logic.isNotEmpty(tab.content) end)
local allEmpty = Array.all(tabArgs, function(tab)
return Logic.isEmpty(tab.content) end)
assert(hasContent or allEmpty, 'Some of the tabs have contents while others do not')
 
local isSingular = tabCount == 1 and hasContent
if isSingular and not Logic.readBool(args.showSingularAsTab) then
return Tabs._single(tabArgs[1], not Logic.readBool(args.suppressHeader))
end
 
local tabs = mw.html.create('ul')
:addClass('nav nav-tabs tabs tabs' .. tabCount)
 
if not Array.any(tabArgs, Operator.property('this')) then
tabArgs[1].this = true
end
 
local build = function(obj, elementType, content, class, isActive)
local element = mw.html.create(elementType)
:addClass(class)
:addClass(isActive and 'active' or nil)
:newline()
:node(content)
 
obj:newline():node(element)
end
 
Array.forEach(tabArgs, function(tabData, tabIndex)
build(tabs, 'li', tabData.name, 'tab' .. tabIndex, tabData.this)
end)
 
if not Logic.nilOr(Logic.readBoolOrNil(args['hide-showall']), isSingular) then
tabs:tag('li')
:addClass('show-all')
:wikitext('Show All')
end
 
tabs:newline()
 
local contents = Tabs._buildContentDiv(
hasContent,
Logic.readBool(args['hybrid-tabs']),
Logic.readBool(args['no-padding'])
)
 
if not hasContent then
return '<div class="tabs-dynamic navigation-not-searchable" data-nosnippet>\n'
.. tostring(tabs) .. contents
end
 
Array.forEach(tabArgs, function(tabData, tabIndex)
build(contents, 'div', tabData.content, 'content' .. tabIndex, tabData.this)
end)
 
return mw.html.create('div')
:addClass('tabs-dynamic navigation-not-searchable')
:attr('data-nosnippet')
:node(tabs)
:newline()
:node(contents)
end
end


return {
return Tabs
  static = Tabs.static,
  dynamic = Tabs.dynamic,
}

Revision as of 19:25, 7 July 2025

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

-- @Liquipedia
-- page=Module:TabsCombined
-- Combined version of Module:Tabs with all dependencies
local Tabs = {}

--[[
=================================================================
Start of dependency: Module:Table
=================================================================
]]
local Table = {}

function Table.size(tbl)
	local i = 0
	for _ in pairs(tbl) do
		i = i + 1
	end
	return i
end

function Table.includes(tbl, value, isPattern)
	for _, entry in pairs(tbl) do
		if isPattern and string.find(entry, value)
			or not isPattern and entry == value then
				return true
		end
	end
	return false
end

function Table.getKeyOfValue(tbl, value)
	for key, entry in pairs(tbl) do
		if entry == value then
			return key
		end
	end
	return nil
end

function Table.filter(tbl, predicate, argument)
	local filteredTbl = {}
	local foundMatches = 1

	for _, entry in pairs(tbl) do
		if predicate(entry, argument) then
			filteredTbl[foundMatches] = entry
			foundMatches = foundMatches + 1
		end
	end

	return filteredTbl
end

function Table.filterByKey(tbl, predicate)
	local filteredTbl = {}

	for key, entry in pairs(tbl) do
		if predicate(key, entry) then
			filteredTbl[key] = entry
		end
	end

	return filteredTbl
end

function Table.isEmpty(tbl)
	if tbl == nil then
		return true
	end
	for _, _ in pairs(tbl) do
		return false
	end
	return true
end

function Table.isNotEmpty(tbl)
	return not Table.isEmpty(tbl)
end

function Table.copy(tbl)
	local result = {}
	for key, entry in pairs(tbl) do
		result[key] = entry
	end
	return result
end

function Table.deepCopy(tbl_, options)
	options = options or {}
	assert(type(tbl_) == 'table', 'Table.deepCopy: Input must be a table')

	local function deepCopy(tbl)
		local result = {}
		for key, value in pairs(tbl) do
			result[key] = type(value) == 'table' and deepCopy(value) or value
		end
		if options.copyMetatable then
			local metatable = getmetatable(tbl)
			if type(metatable) == 'table' then
				setmetatable(result, deepCopy(metatable))
			end
		end
		return result
	end

	return deepCopy(tbl_)
end

function Table.deepEquals(xTable, yTable)
	assert(type(xTable) == 'table', 'Table.deepEquals: First argument must be a table')
	assert(type(yTable) == 'table', 'Table.deepEquals: Second argument must be a table')

	for key, value in pairs(xTable) do
		if not Logic.deepEquals(value, yTable[key]) then
			return false
		end
	end

	for key, _ in pairs(yTable) do
		if xTable[key] == nil then
			return false
		end
	end

	return true
end

function Table.mergeInto(target, ...)
	local objs = Table.pack(...)
	for i = 1, objs.n do
		if type(objs[i]) == 'table' then
			for key, value in pairs(objs[i]) do
				target[key] = value
			end
		end
	end
	return target
end

function Table.merge(...)
	return Table.mergeInto({}, ...)
end

function Table.deepMergeInto(target, ...)
	local tbls = Table.pack(...)
	for i = 1, tbls.n do
		if type(tbls[i]) == 'table' then
			for key, value in pairs(tbls[i]) do
				if type(target[key]) == 'table' and type(value) == 'table' then
					Table.deepMergeInto(target[key], value)
				else
					target[key] = value
				end
			end
		end
	end
	return target
end

function Table.deepMerge(...)
	return Table.deepMergeInto({}, ...)
end

function Table.map(xTable, f)
	local yTable = {}
	for xKey, xValue in pairs(xTable) do
		local yKey, yValue = f(xKey, xValue)
		yTable[yKey] = yValue
	end
	return yTable
end

function Table.mapArgumentsByPrefix(args, prefixes, f, noInterleave)
	local function indexFromKey(key)
		local prefix, index = key:match('^([%a_]+)(%d+)$')
		if Table.includes(prefixes, prefix) then
			return tonumber(index), prefix
		else
			return nil
		end
	end

	return Table.mapArguments(args, indexFromKey, f, noInterleave)
end

function Table.mapArguments(args, indexFromKey, f, noInterleave)
	local entriesByIndex = {}

	-- Non-numeric args
	for key, _ in pairs(args) do
		local function post(index, ...)
			if index and not entriesByIndex[index] then
				entriesByIndex[index] = f(key, index, ...)
			end
		end
		if type(key) == 'string' then
			post(indexFromKey(key))
		end
	end

	if noInterleave then
		return entriesByIndex
	end

	-- Numeric index entries fills in gaps of prefixN= entries if not disabled
	local entryIndex = 1
	for argIndex = 1, math.huge do
		if not args[argIndex] then
			break
		end
		while entriesByIndex[entryIndex] do
			entryIndex = entryIndex + 1
		end
		entriesByIndex[entryIndex] = f(argIndex, entryIndex)
	end

	return entriesByIndex
end

function Table.mapValues(xTable, f)
	local yTable = {}
	for xKey, xValue in pairs(xTable) do
		yTable[xKey] = f(xValue)
	end
	return yTable
end

function Table.all(tbl, predicate)
	for key, value in pairs(tbl) do
		if not predicate(key, value) then
			return false
		end
	end
	return true
end

function Table.any(tbl, predicate)
	for key, value in pairs(tbl) do
		if predicate(key, value) then
			return true
		end
	end
	return false
end

function Table.groupBy(tbl, f)
	local groups = {}
	for key, value in pairs(tbl) do
		local groupKey = f(key, value)
		if not groups[groupKey] then
			groups[groupKey] = {}
		end
		groups[groupKey][key] = value
	end
	return groups
end

function Table.extract(tbl, key)
	local value = tbl[key]
	tbl[key] = nil
	return value
end

function Table.getByPathOrNil(tbl, path)
	for _, fieldName in ipairs(path) do
		if type(tbl) ~= 'table' then
			return nil
		end
		tbl = tbl[fieldName]
	end
	return tbl
end

function Table.setByPath(tbl, path, value)
	for i = 1, #path - 1 do
		if tbl[path[i]] == nil then
			tbl[path[i]] = {}
		end
		tbl = tbl[path[i]]
	end
	tbl[path[#path]] = value
end

function Table.uniqueKey(tbl)
	local key0 = nil
	for key, _ in pairs(tbl) do
		if key0 ~= nil then return nil end
		key0 = key
	end
	return key0
end

function Table.entries(tbl)
	local entries = {}
	for key, value in pairs(tbl) do
		table.insert(entries, {key, value})
	end
	return entries
end

function Table.pack(...)
	return {n = select('#', ...), ...}
end

Table.iter = {}

function Table.iter.spairs(tbl, order)
	local keys = {}
	for k in pairs(tbl) do keys[#keys+1] = k end

	if order then
		table.sort(keys, function(a,b) return order(tbl, a, b) end)
	else
		table.sort(keys)
	end

	local i = 0
	return function()
		i = i + 1
		if keys[i] then
			return keys[i], tbl[keys[i]]
		end
	end
end

function Table.iter.pairsByPrefix(tbl, prefixes, options)
	options = options or {}
	if type(prefixes) == 'string' then
		prefixes = {prefixes}
	end

	local getByPrefixes = function(index)
		for _, prefix in ipairs(prefixes) do
			local key = prefix .. index
			if tbl[key] then
				return key, tbl[key]
			end
		end
	end

	local i = 1
	return function()
		local key, value = getByPrefixes(i)
		if options.requireIndex == false and i == 1 and not value then
			key, value = getByPrefixes('')
		end
		i = i + 1
		if value then
			return key, value, (i - 1)
		else
			return nil
		end
	end
end

--[[
=================================================================
Start of dependency: Module:Logic
=================================================================
]]
local Logic = {}

function Logic.emptyOr(val1, val2, default)
	if not Logic.isEmpty(val1) then
		return val1
	elseif not Logic.isEmpty(val2) then
		return val2
	else
		return default
	end
end

function Logic.nilOr(...)
	local args = Table.pack(...)
	for i = 1, args.n do
		local arg = args[i]
		local val
		if type(arg) == 'function' then
			val = arg()
		else
			val = arg
		end
		if val ~= nil then
			return val
		end
	end
	return nil
end

function Logic.isEmpty(val)
	if type(val) == 'table' then
		return Table.isEmpty(val)
	else
		return val == '' or val == nil
	end
end

function Logic.isNotEmpty(val)
	if type(val) == 'table' then
		return Table.isNotEmpty(val)
	else
		return val ~= nil and val ~= ''
	end
end

function Logic.nilIfEmpty(val)
	return Logic.isNotEmpty(val) and val or nil
end

function Logic.isDeepEmpty(val)
	return Logic.isEmpty(val) or type(val) == 'table' and
		Table.all(val, function(key, item) return Logic.isDeepEmpty(item) end)
end

function Logic.isNotDeepEmpty(val)
	return not Logic.isDeepEmpty(val)
end

function Logic.readBool(val)
	return val == 'true' or val == 't' or val == 'yes' or val == 'y' or val == true or val == '1' or val == 1
end

function Logic.readBoolOrNil(val)
	if Logic.readBool(val) then
		return true
	elseif val == 'false' or val == 'f' or val == 'no' or val == 'n' or val == false or val == '0' or val == 0 then
		return false
	else
		return nil
	end
end

function Logic.nilThrows(val)
	if val == nil then
		error('Unexpected nil', 2)
	end
	return val
end

function Logic.tryCatch(try, catch)
	local ran, result = pcall(try)
	if not ran then
		catch(result)
	else
		return result
	end
end

function Logic.deepEquals(x, y)
	if x == y then
		return true
	elseif type(x) == 'table' and type(y) == 'table' then
		return Table.deepEquals(x, y)
	else
		return false
	end
end

--[[
=================================================================
Start of dependency: Module:Array
=================================================================
]]
local Array = {}

function Array.randomize(tbl)
	math.randomseed(os.time())
	for i = #tbl, 2, -1 do
		local j = math.random(i)
		tbl[i], tbl[j] = tbl[j], tbl[i]
	end
	return tbl
end

function Array.isArray(tbl)
	return type(tbl) == 'table' and Table.size(tbl) == #tbl
end

function Array.copy(tbl)
	local copy = {}
	for _, element in ipairs(tbl) do
		table.insert(copy, element)
	end
	return copy
end

function Array.sub(tbl, startIndex, endIndex)
	if startIndex < 0 then startIndex = #tbl + 1 + startIndex end
	if not endIndex then endIndex = #tbl end
	if endIndex < 0 then endIndex = #tbl + 1 + endIndex end

	local subArray = {}
	for index = startIndex, endIndex do
		table.insert(subArray, tbl[index])
	end
	return subArray
end

function Array.map(elements, funct)
	local mappedArray = {}
	for index, element in ipairs(elements) do
		local mappedElement = funct(element, index)
		table.insert(mappedArray, mappedElement)
	end
	return mappedArray
end

function Array.filter(tbl, predicate)
	local filteredArray = {}
	for index, element in ipairs(tbl) do
		if predicate(element, index) then
			table.insert(filteredArray, element)
		end
	end
	return filteredArray
end

function Array.flatten(tbl)
	local flattenedArray = {}
	for _, x in ipairs(tbl) do
		if type(x) == 'table' then
			for _, y in ipairs(x) do
				table.insert(flattenedArray, y)
			end
		else
			table.insert(flattenedArray, x)
		end
	end
	return flattenedArray
end

function Array.flatMap(elements, funct)
	return Array.flatten(Array.map(elements, funct))
end

function Array.all(tbl, predicate)
	for _, element in ipairs(tbl) do
		if not predicate(element) then
			return false
		end
	end
	return true
end

function Array.any(tbl, predicate)
	for _, element in ipairs(tbl) do
		if predicate(element) then
			return true
		end
	end
	return false
end

function Array.find(tbl, predicate)
	for index, element in ipairs(tbl) do
		if predicate(element, index) then
			return element
		end
	end
	return nil
end

function Array.groupBy(tbl, funct)
	local groupsByKey = {}
	local groups = {}
	for _, xValue in ipairs(tbl) do
		local yValue = funct(xValue)
		if yValue then
			local group = groupsByKey[yValue]
			if not group then
				group = {}
				groupsByKey[yValue] = group
				table.insert(groups, group)
			end
			table.insert(group, xValue)
		end
	end
	return groups, groupsByKey
end

function Array.groupAdjacentBy(array, f, equals)
	equals = equals or Logic.deepEquals
	local groups = {}
	local currentKey
	for index, elem in ipairs(array) do
		local key = f(elem)
		if index == 1 or not equals(key, currentKey) then
			currentKey = key
			table.insert(groups, {})
		end
		table.insert(groups[#groups], elem)
	end
	return groups
end

function Array.lexicalCompare(tblX, tblY)
	for index = 1, math.min(#tblX, #tblY) do
		if tblX[index] < tblY[index] then
			return true
		elseif tblX[index] > tblY[index] then
			return false
		end
	end
	return #tblX < #tblY
end

function Array.lexicalCompareIfTable(y1, y2)
	if type(y1) == 'table' and type(y2) == 'table' then
		return Array.lexicalCompare(y1, y2)
	else
		return y1 < y2
	end
end

function Array.sortBy(tbl, funct, compare)
	local copy = Table.copy(tbl)
	Array.sortInPlaceBy(copy, funct, compare)
	return copy
end

function Array.sortInPlaceBy(tbl, funct, compare)
	compare = compare or Array.lexicalCompareIfTable
	table.sort(tbl, function(x1, x2) return compare(funct(x1), funct(x2)) end)
end

function Array.reverse(tbl)
	local reversedArray = {}
	for index = #tbl, 1, -1 do
		table.insert(reversedArray, tbl[index])
	end
	return reversedArray
end

function Array.append(tbl, ...)
	return Array.appendWith(Array.copy(tbl), ...)
end

function Array.appendWith(tbl, ...)
	local elements = Table.pack(...)
	for index = 1, elements.n do
		if elements[index] ~= nil then
			table.insert(tbl, elements[index])
		end
	end
	return tbl
end

function Array.extend(tbl, ...)
	return Array.extendWith({}, tbl, ...)
end

function Array.extendWith(tbl, ...)
	local arrays = Table.pack(...)
	for index = 1, arrays.n do
		if type(arrays[index]) == 'table' then
			for _, element in ipairs(arrays[index]) do
				table.insert(tbl, element)
			end
		elseif arrays[index] ~= nil then
			table.insert(tbl, arrays[index])
		end
	end
	return tbl
end

function Array.mapIndexes(funct)
	local arr = {}
	for index = 1, math.huge do
		local y = funct(index)
		if y then
			table.insert(arr, y)
		else
			break
		end
	end
	return arr
end

function Array.range(from, to)
	local elements = {}
	for element = from, to do
		table.insert(elements, element)
	end
	return elements
end

function Array.extractKeys(tbl, iterator, ...)
	iterator = iterator or pairs
	local array = {}
	for key, _ in iterator(tbl, ...) do
		table.insert(array, key)
	end
	return array
end

function Array.extractValues(tbl, iterator, ...)
	iterator = iterator or pairs
	local array = {}
	for _, item in iterator(tbl, ...) do
		table.insert(array, item)
	end
	return array
end

function Array.forEach(elements, funct)
	for index, element in ipairs(elements) do
		funct(element, index)
	end
end

function Array.reduce(array, operator, initialValue)
	local aggregate
	if initialValue ~= nil then
		aggregate = initialValue
	else
		aggregate = array[1]
	end

	for index = initialValue ~= nil and 1 or 2, #array do
		aggregate = operator(aggregate, array[index])
	end
	return aggregate
end

function Array.maxBy(array, funct, compare)
	compare = compare or Array.lexicalCompareIfTable
	local max, maxScore
	for _, item in ipairs(array) do
		local score = funct(item)
		if max == nil or compare(maxScore, score) then
			max = item
			maxScore = score
		end
	end
	return max
end

function Array.max(array, compare)
	return Array.maxBy(array, function(x) return x end, compare)
end

function Array.minBy(array, funct, compare)
	compare = compare or Array.lexicalCompareIfTable
	local min, minScore
	for _, item in ipairs(array) do
		local score = funct(item)
		if min == nil or compare(score, minScore) then
			min = item
			minScore = score
		end
	end
	return min
end

function Array.min(array, compare)
	return Array.minBy(array, function(x) return x end, compare)
end

function Array.indexOf(array, pred)
	for ix, elem in ipairs(array) do
		if pred(elem, ix) then
			return ix
		end
	end
	return 0
end

function Array.unique(elements)
	local elementCache = {}
	local uniqueElements = {}
	for _, element in ipairs(elements) do
		if elementCache[element] == nil then
			table.insert(uniqueElements, element)
			elementCache[element] = true
		end
	end
	return uniqueElements
end

function Array.parseCommaSeparatedString(inputString, sep)
	if Logic.isEmpty(inputString) then return {} end
	return Array.map(mw.text.split(inputString, sep or ','), mw.text.trim)
end

function Array.interleave(elements, x)
	local size = #elements
	return Array.flatMap(elements, function(element, index)
		if index == size then
			return {element}
		end
		return {element, x}
	end)
end

--[[
=================================================================
Start of dependency: Module:Operator
=================================================================
]]
local Operator = {}

function Operator.add(a, b) return a + b end
function Operator.sub(a, b) return a - b end
function Operator.mul(a, b) return a * b end
function Operator.div(a, b) return a / b end
function Operator.pow(a, b) return a ^ b end
function Operator.eq(a, b) return a == b end
function Operator.neq(a, b) return a ~= b end
function Operator.lt(a, b) return a < b end
function Operator.le(a, b) return a <= b end
function Operator.gt(a, b) return a > b end
function Operator.ge(a, b) return a >= b end

function Operator.property(item)
	assert(type(item) == 'string' or type(item) == 'number', 'Invalid or missing input to `Operator.property`')
	local pathSegments = mw.text.split(item, '.', true)
	return function(tbl)
		local selected = tbl
		for segmentIndex, pathSegment in ipairs(pathSegments) do
			if type(selected) ~= 'table' and segmentIndex == 1 then
				error('Nil supplied to `Operator.property(' .. item .. ')`')
			elseif type(selected) ~= 'table' then
				local pathUntilHere = Array.sub(pathSegments, 1, segmentIndex - 1)
				error('Could not index "tbl.' .. table.concat(pathUntilHere, '.') .. '"')
			end
			selected = selected[pathSegment] or selected[tonumber(pathSegment)]
		end
		return selected
	end
end

function Operator.method(funcName, ...)
	local args = {...}
	return function(obj)
		return obj[funcName](obj, unpack(args))
	end
end

--[[
=================================================================
Start of dependency: Module:Page
=================================================================
]]
local Page = {}

function Page.exists(link)
	local existingPage = mw.title.new(link)
	if existingPage == nil then
		return false
	end
	return existingPage.exists
end

function Page.makeInternalLink(options, display, customLink)
	if type(options) == 'string' then
		customLink = display
		display = options
	end
	if Logic.isEmpty(display) then
		return nil
	elseif Logic.isEmpty(customLink) then
		customLink = display
	end

	if (options or {}).onlyIfExists == true and (not Page.exists(customLink)) then
		return nil
	end

	return '[[' .. customLink .. '|' .. display .. ']]'
end

function Page.makeExternalLink(display, link)
	if Logic.isEmpty(display) or Logic.isEmpty(link) then
		return nil
	end
	return '[' .. link .. ' ' .. display .. ']'
end

function Page.pageifyLink(link)
	if Logic.isEmpty(link) then
		return nil
	end
	return (mw.ext.TeamLiquidIntegration.resolve_redirect(link):gsub(' ', '_'))
end

--[[
=================================================================
Start of original Module:Tabs code
=================================================================
]]
function Tabs._readArguments(args, options)
	local tabArgs = {}
	local tabIndex = 1
	local this = tonumber(args.This)
	local this2 = tonumber(args.This2)

	while args['name' .. tabIndex] or args['link' .. tabIndex] do
		if args['content' .. tabIndex] or not options.removeEmptyTabs then
			table.insert(tabArgs, {
				name = Table.extract(args, 'name' .. tabIndex),
				link = Table.extract(args, 'link' .. tabIndex),
				content = Table.extract(args, 'content' .. tabIndex),
				tabs = Table.extract(args, 'tabs' .. tabIndex),
				this = this == tabIndex or (options.allowThis2 and this2 == tabIndex),
			})
		end
		tabIndex = tabIndex + 1
	end

	if Logic.readBool(args.returnIfEmpty) then
		return tabArgs
	end

	assert(Logic.isNotEmpty(tabArgs), 'You are trying to add a "Tabs" template without arguments for names nor links')

	return tabArgs
end

function Tabs._setThis(tabArgs)
	if Array.any(tabArgs, Operator.property('this')) then return end

	local fullPageName = mw.title.getCurrentTitle().prefixedText
	local this
	local maxLinkLength = -1

	Array.forEach(tabArgs, function (tab, tabIndex)
		local link = tab.link
		if not link then return end
		link = link:gsub('_', ' ')
		local linkLength = string.len(link)
		local charAfter = string.sub(fullPageName, linkLength + 1, linkLength + 1)
		local pagePartial = string.sub(fullPageName, 1, linkLength)
		if pagePartial == link and (charAfter == '/' or charAfter == '') and linkLength > maxLinkLength then
			maxLinkLength = linkLength
			this = tabIndex
		end
	end)

	if not this then return end
	tabArgs[this].this = true
end

function Tabs._buildContentDiv(hasContent, hybridTabs, noPadding)
	if hasContent then
		local contentDiv = mw.html.create('div')
			:addClass('tabs-content')
		if hybridTabs then
			contentDiv
				:css('border-style', 'none !important')
				:css('padding', '0 !important')
		elseif noPadding then
			contentDiv
				:css('padding', '0 !important')
		end
		return contentDiv
	end

	local style = ''
	if hybridTabs then
		style = 'border-style:none !important; padding:0 !important;'
	elseif noPadding then
		style = 'padding:0 !important;'
	end
	return '\n<div class="tabs-content" style="' .. style .. '">'
end

function Tabs._single(tab, showHeader)
	local header
	if showHeader then
		header = mw.html.create()
			:tag('h6'):wikitext(tab.name):done()
			:newline()
	end
	return mw.html.create()
		:node(header)
		:node(tab.content)
end

function Tabs._getDisplayNameFromLink(link)
	local linkParts = mw.text.split(link, '/', true)
	return linkParts[#linkParts]
end

function Tabs.static(args)
	args = args or {}

	local tabArgs = Tabs._readArguments(args, {allowThis2 = true})
	local tabCount = #tabArgs
	if tabCount == 0 then return end

	Tabs._setThis(tabArgs)

	local tabs = mw.html.create('ul')
		:attr('class', 'nav nav-tabs navigation-not-searchable tabs tabs' .. tabCount)
		:attr('data-nosnippet')

	local subTabs = mw.html.create()

	Array.forEach(tabArgs, function(tab)
		local name = tab.name or Tabs._getDisplayNameFromLink(tab.link)
		local text = tab.link and Page.makeInternalLink({}, name, tab.link) or tab.name
		tabs:tag('li'):addClass(tab.this and 'active' or nil):wikitext(text)
		subTabs:node(tab.this and tab.tabs or nil)
	end)

	return mw.html.create()
		:tag('div')
			:addClass('tabs-static')
			:attr('data-nosnippet', '')
			:node(tabs)
			:done()
		:node(subTabs)
end

function Tabs.dynamic(args)
	args = args or {}

	local tabArgs = Tabs._readArguments(args, {removeEmptyTabs = Logic.readBool(args.removeEmptyTabs)})
	local tabCount = #tabArgs
	if tabCount == 0 then return end

	local hasContent = Array.all(tabArgs, function(tab)
		return Logic.isNotEmpty(tab.content) end)
	local allEmpty = Array.all(tabArgs, function(tab)
		return Logic.isEmpty(tab.content) end)
	assert(hasContent or allEmpty, 'Some of the tabs have contents while others do not')

	local isSingular = tabCount == 1 and hasContent
	if isSingular and not Logic.readBool(args.showSingularAsTab) then
		return Tabs._single(tabArgs[1], not Logic.readBool(args.suppressHeader))
	end

	local tabs = mw.html.create('ul')
		:addClass('nav nav-tabs tabs tabs' .. tabCount)

	if not Array.any(tabArgs, Operator.property('this')) then
		tabArgs[1].this = true
	end

	local build = function(obj, elementType, content, class, isActive)
		local element = mw.html.create(elementType)
			:addClass(class)
			:addClass(isActive and 'active' or nil)
			:newline()
			:node(content)

		obj:newline():node(element)
	end

	Array.forEach(tabArgs, function(tabData, tabIndex)
		build(tabs, 'li', tabData.name, 'tab' .. tabIndex, tabData.this)
	end)

	if not Logic.nilOr(Logic.readBoolOrNil(args['hide-showall']), isSingular) then
		tabs:tag('li')
			:addClass('show-all')
			:wikitext('Show All')
	end

	tabs:newline()

	local contents = Tabs._buildContentDiv(
		hasContent,
		Logic.readBool(args['hybrid-tabs']),
		Logic.readBool(args['no-padding'])
	)

	if not hasContent then
		return '<div class="tabs-dynamic navigation-not-searchable" data-nosnippet>\n'
			.. tostring(tabs) .. contents
	end

	Array.forEach(tabArgs, function(tabData, tabIndex)
		build(contents, 'div', tabData.content, 'content' .. tabIndex, tabData.this)
	end)

	return mw.html.create('div')
		:addClass('tabs-dynamic navigation-not-searchable')
		:attr('data-nosnippet')
		:node(tabs)
		:newline()
		:node(contents)
end

return Tabs