Module:Box-header
local getArgs = require('Module:Arguments').getArgs
 
 local p = {}
 ---------- Config data ----------
 local namedColours = mw.loadData( 'Module:Box-header/colours' )
 local modes = {
 	lightest = { sat=0.10, val=1.00 },
 	light    = { sat=0.15, val=0.95 },
 	normal   = { sat=0.40, val=0.85 },
 	dark     = { sat=0.90, val=0.70 },
 	darkest  = { sat=1.00, val=0.45 },
 	content  = { sat=0.04, val=1.00 },
 	grey     = { sat=0.00 }
 }
 local min_contrast_ratio_normal_text = 7  -- i.e 7:1
 local min_contrast_ratio_large_text  = 4.5  -- i.e. 4.5:1
 
 -- Template parameter aliases
 --   Specify each as either a single value, or a table of values
 --   Aliases are checked left-to-right, i.e. `['one'] = { 'two', 'three' }` is equivalent to using `{{{one| {{{two| {{{three|}}} }}} }}}` in a template
 local parameterAliases = {
 	['1'] = 1,
 	['2'] = 2,
 	['colour'] = 'color'
 }
 
 ---------- Dependecies ----------
 local colourContrastModule = require('Module:Color contrast')
 local hex = require( 'luabit.hex' )
 
 ---------- Utility functions ----------
 local function getParam(args, parameter)
 	if args[parameter] then
 		return args[parameter]
 	end
 	local aliases = parameterAliases[parameter]
 	if not aliases then
 		return nil
 	end
 	if type(aliases) ~= 'table' then
 		return args[aliases]
 	end
 	for _, alias in ipairs(aliases) do
 		if args[alias] then
 			return args[alias]
 		end
 	end
 	return nil
 end
 
 local function setCleanArgs(argsTable)
 	local cleanArgs = {}
 	for key, val in pairs(argsTable) do
 		if type(val) == 'string' then
 			val = val:match('^%s*(.-)%s*$')
 			if val ~= '' then
 				cleanArgs[key] = val
 			end
 		else
 			cleanArgs[key] = val
 		end
 	end
 	return cleanArgs
 end
 
 -- Merge two tables into a new table. If the are any duplicate keys, the values from the second overwrite the values from the first.
 local function mergeTables(first, second)
 	local merged = {}
 	for key, val in pairs(first) do
 		merged[key] = val
 	end
 	for key, val in pairs(second) do
 		merged[key] = val
 	end
 	return merged
 end
 
 local function toOpenTagString(selfClosedHtmlObject)
 	local closedTagString = tostring(selfClosedHtmlObject)
 	local openTagString = mw.ustring.gsub(closedTagString, ' /&gt;$', '&gt;')
 	return openTagString
 end
 
 local function normaliseHexTriplet(hexString)
 	if not hexString then return nil end
 	local hexComponent = mw.ustring.match(hexString, '^#(%x%x%x)$') or mw.ustring.match(hexString, '^#(%x%x%x%x%x%x)$')
 	if hexComponent and #hexComponent == 6 then
 		return mw.ustring.upper(hexString)
 	end
 	if hexComponent and #hexComponent == 3 then
 		local r = mw.ustring.rep(mw.ustring.sub(hexComponent, 1, 1), 2)
 		local g = mw.ustring.rep(mw.ustring.sub(hexComponent, 2, 2), 2)
 		local b = mw.ustring.rep(mw.ustring.sub(hexComponent, 3, 3), 2)
 		return '#' .. mw.ustring.upper(r .. g .. b)
 	end
 	return nil
 end
 
 ---------- Conversions ----------
 local function decimalToPaddedHex(number)
 	local prefixedHex = hex.to_hex(tonumber(number)) -- prefixed with '0x'
 	local padding =  #prefixedHex == 3 and '0' or '' 
 	return mw.ustring.gsub(prefixedHex, '0x', padding)
 end
 local function hexToDecimal(hexNumber)
 	return tonumber(hexNumber, 16)
 end
 local function RGBtoHexTriplet(R, G, B)
 	return '#' .. decimalToPaddedHex(R) .. decimalToPaddedHex(G) .. decimalToPaddedHex(B)
 end
 local function hexTripletToRGB(hexTriplet)
 	local R_hex, G_hex, B_hex = string.match(hexTriplet, '(%x%x)(%x%x)(%x%x)')
 	return hexToDecimal(R_hex), hexToDecimal(G_hex), hexToDecimal(B_hex)
 end
 local function HSVtoRGB(H, S, V) -- per [[HSL and HSV#Converting_to_RGB]]
 	local C = V * S
 	local H_prime = H / 60
 	local X = C * ( 1 - math.abs(math.fmod(H_prime, 2) - 1) )
 	local R1, G1, B1
 	if H_prime &lt;= 1 then
 		R1 = C
 		G1 = X
 		B1 = 0
 	elseif H_prime &lt;= 2 then
 		R1 = X
 		G1 = C
 		B1 = 0
 	elseif H_prime &lt;= 3 then
 		R1 = 0
 		G1 = C
 		B1 = X
 	elseif H_prime &lt;= 4 then
 		R1 = 0
 		G1 = X
 		B1 = C
 	elseif H_prime &lt;= 5 then
 		R1 = X
 		G1 = 0
 		B1 = C
 	elseif H_prime &lt;= 6 then
 		R1 = C
 		G1 = 0
 		B1 = X
 	end	
 	local m = V - C
 	local R = R1 + m
 	local G = G1 + m
 	local B = B1 + m
 
 	local R_255 = math.floor(R*255)
 	local G_255 = math.floor(G*255)
 	local B_255 = math.floor(B*255)
 	return R_255, G_255, B_255
 end
 local function RGBtoHue(R_255, G_255, B_255) -- per [[HSL and HSV#Hue and chroma]]
 	local R = R_255/255
 	local G = G_255/255
 	local B = B_255/255
 
 	local M = math.max(R, G, B)
 	local m = math.min(R, G, B)
 	local C = M - m
 	local H_prime
 	if C == 0 then
 		return null
 	elseif M == R then
 		H_prime = math.fmod(((G - B)/C + 6), 6) -- adding six before taking mod ensures positive value
 	elseif M == G then
 		H_prime = (B - R)/C + 2
 	elseif M == B then
 		H_prime = (R - G)/C + 4
 	end
 	local H = 60 * H_prime
 	return H
 end
 local function nameToHexTriplet(name)
 	if not name then return nil end
 	local codename = mw.ustring.gsub(mw.ustring.lower(name), ' ', '')
 	return namedColours[codename]
 end
 
 ---------- Choose colours ----------
 local function calculateColours(H, S, V, minContrast)
 	local bgColour = RGBtoHexTriplet(HSVtoRGB(H, S, V))
 	local textColour = colourContrastModule._greatercontrast({bgColour})
 	local contrast = colourContrastModule._ratio({ bgColour, textColour })
 	if contrast &gt;= minContrast then
 		return bgColour, textColour
 	elseif textColour == '#FFFFFF' then
 		-- make the background darker and slightly increase the saturation
 		return calculateColours(H, math.min(1, S+0.005), math.max(0, V-0.03), minContrast)
 	else
 		-- make the background lighter and slightly decrease the saturation
 		return calculateColours(H, math.max(0, S-0.005), math.min(1, V+0.03), minContrast)
 	end
 end
 
 local function makeColours(hue, modeName)
 	local mode = modes[modeName]
 	local isGrey = not(hue)
 	if isGrey then hue = 0 end
 
 	local borderSat = isGrey and modes.grey.sat or 0.15
 	local border = RGBtoHexTriplet(HSVtoRGB(hue, borderSat, 0.75))
 
 	local titleSat = isGrey and modes.grey.sat or mode.sat
 	local titleBackground, titleForeground = calculateColours(hue, titleSat, mode.val, min_contrast_ratio_large_text)
 
 	local contentSat = isGrey and modes.grey.sat or modes.content.sat
 	local contentBackground, contentForeground = calculateColours(hue, contentSat, modes.content.val, min_contrast_ratio_normal_text)
 
 	return border, titleForeground, titleBackground, contentForeground, contentBackground
 end
 
 local function findHue(colour)
 	local colourAsNumber = tonumber(colour)
 	if colourAsNumber and ( -1 &lt; colourAsNumber ) and ( colourAsNumber &lt; 360) then
 		return colourAsNumber
 	end
 
 	local colourAsHexTriplet = normaliseHexTriplet(colour) or nameToHexTriplet(colour)
 	if colourAsHexTriplet then
 		return RGBtoHue(hexTripletToRGB(colourAsHexTriplet))
 	end
 
 	return null
 end
 
 local function normaliseMode(mode)
 	if not mode or not modes[mw.ustring.lower(mode)] or mw.ustring.lower(mode) == 'grey' then
 		return 'normal'
 	end
 	return mw.ustring.lower(mode)
 end
 ---------- Build output ----------
 local function boxHeaderOuter(args)
 	local baseStyle = {
 		clear = 'both',
 		['box-sizing'] = 'border-box',
 		border = ( getParam(args, 'border-type') or 'solid' ) .. ' ' .. ( getParam(args, 'titleborder') or getParam(args, 'border') or '#ababab' ),
 		background = getParam(args, 'titlebackground') or '#bcbcbc',
 		color = getParam(args, 'titleforeground') or '#000',
 		padding = getParam(args, 'padding') or '.1em',
 		['text-align'] = getParam(args, 'title-align') or 'center',
 		['font-family'] = getParam(args, 'font-family') or 'sans-serif',
 		['font-size'] = getParam(args, 'titlefont-size') or '100%',
 		['margin-bottom'] = '0px',
 	}
 
 
 	local tag = mw.html.create('div', {selfClosing = true})
 		:addClass('box-header-title-container')
 		:addClass('flex-columns-noflex')
 		:css(baseStyle)
 		:css('border-width', ( getParam(args, 'border-top') or getParam(args, 'border-width') or '1' ) .. 'px ' .. ( getParam(args, 'border-width') or '1' ) .. 'px 0')
 		:css('padding-top', getParam(args, 'padding-top') or '.1em')
 		:css('padding-left', getParam(args, 'padding-left') or '.1em')
 		:css('padding-right', getParam(args, 'padding-right') or '.1em')
 		:css('padding-bottom', getParam(args, 'padding-bottom') or '.1em')
 		:css('moz-border-radius', getParam(args, 'title-border-radius') or '0')
 		:css('webkit-border-radius', getParam(args, 'title-border-radius') or '0')
 		:css('border-radius', getParam(args, 'title-border-radius') or '0')
 	return toOpenTagString(tag)
 end
 
 local function boxHeaderTopLinks(args)
 	local style = {
 		float = 'right',
 		['margin-bottom'] = '.1em',
 		['font-size'] = getParam(args, 'font-size') or '80%',
 		color = getParam(args, 'titleforeground') or '#000'
 	}
 	local tag = mw.html.create('div', {selfClosing = true})
 		:addClass('plainlinks noprint' )
 		:css(style)
 	return toOpenTagString(tag)
 end
 
 local function boxHeaderEditLink(args)
 	local page = getParam(args, 'editpage')
 	if not page or page == '{{{2}}}'
 	then
 		return ''
 	end
 	local style = {
 		color = getParam(args, 'titleforeground') or '#000'
 	}
 	local tag = mw.html.create('span')
 		:css(style)
 		:wikitext('தொகு')
 	local linktext = tostring(tag)
 	local linktarget = tostring(mw.uri.fullUrl(page, {action='edit', section=getParam(args, 'section')}))
 	return '[' .. linktarget  .. ' ' .. linktext .. ']&amp;nbsp;'
 end
 
 local function boxHeaderViewLink(args)
 	local style = {
 		color = getParam(args, 'titleforeground') or '#000'
 	}
 	local tag = mw.html.create('span')
 		:css(style)
 		:wikitext('பார்')
 	local linktext = tostring(tag)
 	local linktarget = ':' .. getParam(args, 'viewpage')
 	return &quot;&lt;b&gt;·&lt;/b&gt;&amp;nbsp;[[&quot; .. linktarget  .. '|' .. linktext .. ']]&amp;nbsp;'
 end
 
 local function boxHeaderTitle(args)
 	local baseStyle = {
 		['font-family'] = getParam(args, 'title-font-family') or 'sans-serif',
 		['font-size'] = getParam(args, 'title-font-size') or '100%',
 		['font-weight'] = getParam(args, 'title-font-weight') or 'bold',
 		border = 'none',
 		margin = '0',
 		padding = '0',
 		color = getParam(args, 'titleforeground') or '#000';
 	}
 	local tagName = getParam(args, 'SPAN') and 'span' or 'h2'
 	local tag = mw.html.create(tagName)
 		:css(baseStyle)
 		:css('padding-bottom', '.1em')
 		:wikitext(getParam(args, 'title'))
 	if getParam(args, 'extra') then
 		local rules = mw.text.split(getParam(args, 'extra'), ';', true)
 		for _, rule in pairs(rules) do
 			local parts = mw.text.split(rule, ':', true)
 			local prop = parts[1]
 			local val = parts[2]
 			if prop and val then
 				tag:css(prop, val)
 			end
 		end
 	end
 	return tostring(tag)
 end
 
 local function boxBody(args)
 	local baseStyle = {
 		['box-sizing'] = 'border-box',
 		border = ( getParam(args, 'border-width') or '1' ) .. 'px solid ' .. ( getParam(args, 'border') or '#ababab'),
 		['vertical-align'] = 'top';
 		background = getParam(args, 'background') or '#fefeef',
 		opacity = getParam(args, 'background-opacity') or '1',
 		color = getParam(args, 'foreground') or '#000',
 		['text-align'] = getParam(args, 'text-align') or 'left',
 		margin = '0 0 10px',
 		padding = getParam(args, 'padding') or '1em',
 	}
 	local tag = mw.html.create('div', {selfClosing = true})
 		:css(baseStyle)
 		:css('border-top-width', ( getParam(args, 'border-top') or '1' ) .. 'px')
 		:css('padding-top', getParam(args, 'padding-top') or '.3em')
 		:css('border-radius', getParam(args, 'border-radius') or '0')
 	return toOpenTagString(tag)
 end
 
 local function contrastCategories(args)
 	local cats = ''
 
 	local titleText = nameToHexTriplet(getParam(args, 'titleforeground')) or normaliseHexTriplet(getParam(args, 'titleforeground')) or '#000000'
 	local titleBackground = nameToHexTriplet(getParam(args, 'titlebackground')) or normaliseHexTriplet(getParam(args, 'titlebackground')) or '#bcbcbc'
 	local titleContrast = colourContrastModule._ratio({titleBackground, titleText})
 	local insufficientTitleContrast = type(titleContrast) == 'number' and ( titleContrast &lt; min_contrast_ratio_large_text )
 
 	local bodyText = nameToHexTriplet(getParam(args, 'foreground')) or normaliseHexTriplet(getParam(args, 'foreground')) or '#000000'
 	local bodyBackground = nameToHexTriplet(getParam(args, 'background')) or normaliseHexTriplet(getParam(args, 'background')) or '#fefeef'
 	local bodyContrast =  colourContrastModule._ratio({bodyBackground, bodyText})
 	local insufficientBodyContrast = type(bodyContrast) == 'number' and ( bodyContrast &lt; min_contrast_ratio_normal_text )
 
 	if insufficientTitleContrast and insufficientBodyContrast then
 		return '[[Category:Box-header with insufficient title contrast]][[Category:Box-header with insufficient body contrast]]'
 	elseif insufficientTitleContrast then
 		return '[[Category:Box-header with insufficient title contrast]]'
 	elseif insufficientBodyContrast then
 		return '[[Category:Box-header with insufficient body contrast]]'
 	else
 		return ''
 	end
 end
 
 ---------- Main functions / entry points ----------
 
 -- Entry point for templates (manually-specified colours)
 function p.boxHeader(frame)
 	local args = getArgs(frame)
 	local page = args.editpage
 	if not args.editpage or args.editpage == '' then
 		page = mw.title.getCurrentTitle().prefixedText
 	end
 	local output = p._boxHeader(args, page)
 	if mw.ustring.find(output, '{') then
 		return frame:preprocess(output)
 	end
 	return output
 end
 
 -- Entry point for modules (manually-specified colours)
 function p._boxHeader(_args, page)
 	local args = setCleanArgs(_args)
 	if page and not args.editpage then
 		args.editpage = page
 	end
 	if not args.title then
 		args.title = '{{{title}}}'
 	end
 	local output = {}
 	table.insert(output, boxHeaderOuter(args))
 	if not getParam(args, 'EDITLINK') then
 		table.insert(output, boxHeaderTopLinks(args))
 		if not getParam(args, 'noedit') then
 			table.insert(output, boxHeaderEditLink(args))
 		end
 		if getParam(args, 'viewpage') then
 			table.insert(output, boxHeaderViewLink(args))
 		end
 		if getParam(args, 'top') then
 			table.insert(output, getParam(args, 'top') .. '&amp;nbsp;')
 		end
 		table.insert(output, '&lt;/div&gt;')
 	end
 	table.insert(output, boxHeaderTitle(args))
 	table.insert(output, '&lt;/div&gt;')
 	table.insert(output, boxBody(args))
 	if not getParam(args, 'TOC') then
 		table.insert(output, '__NOTOC__')
 	end
 	if not getParam(args, 'EDIT') then
 		table.insert(output, '__NOEDITSECTION__')
 	end
 	table.insert(output, contrastCategories(args))
 
 	return table.concat(output)
 end
 
 -- Entry point for templates (automatically calculated colours)
 function p.autoColour(frame)
 	local args = getArgs(frame)
 	local colourParam = getParam(args, 'colour')
 	local generatedColour = nil
 	if not colourParam or colourParam == '' then
 		-- convert the root page name into a number and use that
 		local root = mw.title.getCurrentTitle().rootPageTitle.prefixedText
 		local rootStart = mw.ustring.sub(root, 1, 12)
 		local digitsFromRootStart = mw.ustring.gsub(rootStart, &quot;.&quot;, function(s) return math.fmod(string.byte(s, 2) or string.byte(s, 1), 10) end)
 		local numberFromRoot = tonumber(digitsFromRootStart, 10)
 		generatedColour = math.fmod(numberFromRoot, 360)
 	end
 	local output = p._autoColour(args, generatedColour)
 	if mw.ustring.find(output, '{') then
 		return frame:preprocess(output)
 	end
 	return output
 end
 
 -- Entry point for modules (automatically calculated colours)
 function p._autoColour(_args, generatedColour)
 	local args = setCleanArgs(_args)
 	local hue = generatedColour or findHue(getParam(args, 'colour'))
 	local mode = normaliseMode(getParam(args, 'mode'))
 	local border, titleForeground, titleBackground, contentForeground, contentBackground = makeColours(hue, mode)
 	local boxTemplateArgs = mergeTables(args, {
 		title = getParam(args, '1') or '{{{1}}}',
 		editpage = getParam(args, '2') or '',
 		noedit = getParam(args, '2') and '' or 'yes',
 		border = border,
 		titleforeground = titleForeground,
 		titlebackground = titleBackground,
 		foreground = contentForeground,
 		background = contentBackground
 	})
 	return p._boxHeader(boxTemplateArgs)
 end