local getArgs = require('Module:Arguments').getArgs  local p = {}  Config data ---------- local namedColours = mw.loadData( 'মডিউল:বাক্স-উপর/রং' ) 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 `    ` 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, ' />$', '>') 	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 <= 1 then 		R1 = C 		G1 = X 		B1 = 0 	elseif H_prime <= 2 then 		R1 = X 		G1 = C 		B1 = 0 	elseif H_prime <= 3 then 		R1 = 0 		G1 = C 		B1 = X 	elseif H_prime <= 4 then 		R1 = 0 		G1 = X 		B1 = C 	elseif H_prime <= 5 then 		R1 = X 		G1 = 0 		B1 = C 	elseif H_prime <= 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 >= 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 < colourAsNumber ) and ( colourAsNumber < 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 == '' 	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 .. '] ' 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 "· ' .. linktext .. ' ' 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 < 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 < min_contrast_ratio_normal_text )  	if insufficientTitleContrast and insufficientBodyContrast then 		return 'Category:Box-header with insufficient title contrastCategory: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 = '' 	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') .. ' ') 		end 		table.insert(output, '</div>') 	end 	table.insert(output, boxHeaderTitle(args)) 	table.insert(output, '</div>') 	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, ".", 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 '', 		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 	 return p