Jump to content

Module:Box-header

Permanently protected module
From Wikipedia, the free encyclopedia
This is an old revision of this page, as edited by Evad37 (talk | contribs) at 08:50, 14 July 2018 (boxHeader function). The present address (URL) is a permanent link to this revision, which may differ significantly from the current revision.

local p = {}
---------- Config data ----------
local namedColours = mw.loadData( 'Module:Sandbox/Evad37/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 = 0.7
local min_contrast_ratio_large_text  = 0.45

-- Template parameter names
--   Specify each as either a single string, or a table of strings (aliases)
--   Aliases are checked left-to-right, i.e. `{ "one", "two" }` is equivalent to using `{{{one| {{{two|}}} }}}` in a template
local parameterNames = {
	['1'] = {'1', 1},
	['2'] = {'2', 2},
	['colour'] = {'colour', 'color'},
	['mode'] = 'mode',
	['watch'] = 'watch'
}

---------- Dependecies ----------
local colourContrastModule = require('Module:Color contrast')
local hex = require( 'luabit.hex' )

---------- Utility functions ----------
function getParameterValue(args, parameter)
	suffix = suffix or ''
	if type( parameterNames[parameter] ) ~= 'table' then
		return args[parameterNames[parameter]]
	end
	for _i, parameterAlias in ipairs(parameterNames[parameter]) do
		if args[parameterAlias] then
			return args[parameterAlias]
		end
	end
	return nil
end

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

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)
	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 style = {
		clear = 'both',
		['box-sizing'] = 'border-box',
		border = ( args['border-type'] or 'solid' ) .. ' ' .. ( args.titleborder or args.border or '#ababab' ),
		['border-width'] = ( args['border-top'] or args['border-width'] or '1' ) .. 'px ' .. ( args['border-width'] or '1' ) .. 'px 0',
		background = args.titlebackground or '#bcbcbc',
		color = args.titleforeground or '#000',
		padding = args.padding or '.1em',
		['padding-top'] = args['padding-top'] or '.1em',
		['padding-left'] = args['padding-left'] or '.1em',
		['padding-right'] = args['padding-right'] or '.1em',
		['padding-bottom'] = args['padding-bottom'] or '.1em',
		['text-align'] = args['title-align'] or 'center',
		['font-family'] = args['font-family'] or 'sans-serif',
		['font-size'] = args['titlefont-size'] or '100%',
		['margin-bottom'] = '0px',
		['-moz-border-radius'] = args['title-border-radius'] or '0',
		['-webkit-border-radius'] = args['title-border-radius'] or '0',
		['border-radius'] = args['title-border-radius'] or '0'
	}
	local tag = mw.html.create('div', {selfClosing = true})
		:addClass('box-header-title-container' )
		:css(style)
	return toOpenTagString(tag)
end

local function boxHeaderTopLinks(args)
	local style = {
		float = 'right',
		['margin-bottom'] = '.1em',
		['font-size'] = args['font-size'] or '80%',
		color = 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 style = {
		color = args.titleforeground or '#000'
	}
	local tag = mw.html.create('span')
		:css(style)
		:wikitext('edit')
	local linktext = tostring(tag)
	local linktarget = mw.uri.fullUrl(args.editpage, {action='edit', section=args.section})
	return '[' .. linktarget  .. '|' .. linktext .. ']&nbsp;'
end

local function boxHeaderViewLink(args)
	local style = {
		color = args.titleforeground or '#000'
	}
	local tag = mw.html.create('span')
		:css(style)
		:wikitext('view')
	local linktext = tostring(tag)
	local linktarget = ':' .. args.viewpage
	return "'''·'''&nbsp;[[" .. linktarget  .. '|' .. linktext .. ']&nbsp;'
end

local function boxHeaderTitle(args)
	local style = {
		['font-family'] = args['title-font-family'] or 'sans-serif',
		['font-size'] = args['title-font-size'] or '100%',
		['font-weight'] = args['title-font-weight'] or 'bold',
		border = 'none',
		margin = '0',
		padding = '0',
		['padding-bottom']= '.1em',
		color = args['titleforeground'] or '#000';
	}
	if args.extra then
		local rules = mw.text.split(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
				style[prop] = val
			end
		end
	end
	local tagName = args.SPAN and 'span' or 'h2'
	local tag = mw.html.create(tagName)
		:css(style)
		:wikitext(args.title)
	return tostring(tag)
end

local function boxBody(args)
	local style = {
		['box-sizing'] = 'border-box',
		border = ( args['border-width'] or '1' ) .. 'px solid ' .. ( args.border or '#ababab'),
		['border-top-width'] = ( args['border-top'] or '1') .. 'px',
		['vertical-align'] = 'top';
		background = args.background or '#fefeef',
		opacity = args[background-opacity] or '1',
		color = args.foreground or '#000',
		['text-align'] = args['text-align'] or 'left',
		margin = '0 0 10px',
		padding = args.padding or '1em',
		['padding-top'] = args['padding-top'] or '.3em',
		['-moz-border-radius'] = args['border-radius'] or '0',
		['-webkit-border-radius'] = args['border-radius'] or '0',
		['border-radius'] = args['border-radius'] or '0'
	}
	local tag = mw.html.create('div', {selfClosing = true})
		:css(style)
	return toOpenTagString(tag)
end


function p.boxHeader(frame)
	local parent = frame.getParent(frame)
	local output = p._boxHeader(parent.args)
	if mw.ustring.find(output, '{') then
		return frame:preprocess(output)
	end
	return output
end

function p._boxheader(_args)
	local args = setCleanArgs(_args)
	local output = {}
	table.insert(output, boxHeaderOuter(args))
	if not args.EDITLINK then
		table.insert(output, boxHeaderTopLinks(args))
		if not args.noedit then
			table.insert(output, boxHeaderEditLink(args))
		end
		if args.viewpage then
			table.insert(output, boxHeaderViewLink(args))
		end
	end
	table.insert(output, boxHeaderTitle(args))
	table.insert(output, '</div>')
	table.insert(output, boxBody(args))
	return table.concat(output)
end


---------- Main ----------

-- Entry point for templates
function p.main(frame)
	local parent = frame.getParent(frame)
	local boxTemplateName, boxTemplateArgs = p._main(parent.args)
	return frame:expandTemplate{ title = boxTemplateName, args = boxTemplateArgs }
end

-- Entry point for modules
function p._main(_args)
	local args = setCleanArgs(_args)

	local hue = findHue(getParameterValue(args, 'colour'))
	local mode = normaliseMode(getParameterValue(args, 'mode'))

	local border, titleForeground, titleBackground, contentForeground, contentBackground = makeColours(hue, mode)
	
	local boxTemplateName = getParameterValue(args, 'watch') and 'Box-header' .. getParameterValue(args, 'watch') or 'Box-header'
	local boxTemplateArgs = {
		title = getParameterValue(args, '1') or '',
		editpage = getParameterValue(args, '2') or '',
		noedit =  getParameterValue(args, '2') and '' or 'yes',
		border = border,
		titleforeground = titleForeground,
		titlebackground = titleBackground,
		foreground = contentForeground,
		background = contentBackground
	}
	return boxTemplateName, boxTemplateArgs
end
	
return p