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

-- Single-file Tabs module for deadlock.wiki
-- All dependencies inlined to avoid Lua.import

-- Inlined Module:Array (partial stub - expand if needed)
local Array = {}
Array.forEach = function(elements, funct)
	for index, element in ipairs(elements) do
		funct(element, index)
	end
end
Array.any = function(tbl, predicate)
	for _, v in ipairs(tbl) do
		if predicate(v) then return true end
	end
	return false
end
Array.all = function(tbl, predicate)
	for _, v in ipairs(tbl) do
		if not predicate(v) then return false end
	end
	return true
end

-- Inlined Module:Class
local Class = {}
function Class.export(tbl, opts)
	local result = {}
	for _, name in ipairs(opts.exports) do
		result[name] = tbl[name]
	end
	return result
end

-- Inlined Module:Logic
local Logic = {}
function Logic.readBool(val)
	return val == true or val == 'true' or val == '1'
end
function Logic.readBoolOrNil(val)
	if val == nil then return nil end
	return Logic.readBool(val)
end
function Logic.isEmpty(val)
	return val == nil or val == ''
end
function Logic.isNotEmpty(val)
	return not Logic.isEmpty(val)
end
function Logic.nilOr(val, fallback)
	if val == nil then return fallback else return val end
end

-- Inlined Module:Operator
local Operator = {}
function Operator.property(prop)
	return function(tbl)
		return tbl and tbl[prop]
	end
end

-- Inlined Module:Page
local Page = {}
function Page.makeInternalLink(_, text, link)
	return string.format('[[%s|%s]]', link, text)
end

-- Inlined Module:Table
local Table = {}
function Table.extract(tbl, key)
	local val = tbl[key]
	if val ~= '' then return val end
	return nil
end

-- Begin Tabs Module
local Tabs = {}

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

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 = #link
		local charAfter = fullPageName:sub(linkLength + 1, linkLength + 1)
		local pagePartial = fullPageName:sub(1, linkLength)
		if pagePartial == link and (charAfter == '/' or charAfter == '') and linkLength > maxLinkLength then
			maxLinkLength = linkLength
			this = tabIndex
		end
	end)

	if this then tabArgs[this].this = true end
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 parts = mw.text.split(link, '/', true)
	return parts[#parts]
end

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