Jump to content

Module:Box-header: Difference between revisions

From Wikipedia, the free encyclopedia
Content deleted Content added
remove moz and webkit as standard now well supported
move defaults to templatestyles. who let someone define borders to be numerical only ;_;
 
Line 64: Line 64:
end
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.
-- 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 function mergeTables(first, second)
local merged = {}
local merged = {}
Line 113: Line 114:
return hexToDecimal(R_hex), hexToDecimal(G_hex), hexToDecimal(B_hex)
return hexToDecimal(R_hex), hexToDecimal(G_hex), hexToDecimal(B_hex)
end
end
local function HSVtoRGB(H, S, V) -- per [[HSL and HSV#Converting_to_RGB]]
-- per [[HSL and HSV#Converting_to_RGB]]
local function HSVtoRGB(H, S, V)
local C = V * S
local C = V * S
local H_prime = H / 60
local H_prime = H / 60
Line 153: Line 155:
return R_255, G_255, B_255
return R_255, G_255, B_255
end
end

local function RGBtoHue(R_255, G_255, B_255) -- per [[HSL and HSV#Hue and chroma]]
-- per [[HSL and HSV#Hue and chroma]]
local function RGBtoHue(R_255, G_255, B_255)
local R = R_255/255
local R = R_255/255
local G = G_255/255
local G = G_255/255
Line 235: Line 239:
---------- Build output ----------
---------- Build output ----------
local function boxHeaderOuter(args)
local function boxHeaderOuter(args)
local border_top_width = getParam(args, 'border-top') or getParam(args, 'border-width')
local baseStyle = {
if border_top_width then border_top_width = border_top_width .. 'px' end
clear = 'both',
['box-sizing'] = 'border-box',
local border_left_width = getParam(args, 'border-width')
border = ( getParam(args, 'border-type') or 'solid' ) .. ' ' .. ( getParam(args, 'titleborder') or getParam(args, 'border') or '#ababab' ),
if border_left_width then border_left_width = border_left_width .. 'px' end
background = getParam(args, 'titlebackground') or '#bcbcbc',
color = getParam(args, 'titleforeground') or '#000',
padding = getParam(args, 'padding') or '.1em',
local border_right_width = getParam(args, 'border-width')
if border_right_width then border_right_width = border_right_width .. 'px' end
['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})
local tag = mw.html.create('div', {selfClosing = true})
:addClass('box-header-title-container')
:addClass('box-header-title-container flex-columns-noflex')
:css({
:addClass('flex-columns-noflex')
['border-style'] = getParam(args, 'border-type'),
: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')
['border-color'] = getParam(args, 'titleborder') or getParam(args, 'border'),
:css('padding-top', getParam(args, 'padding-top') or '.1em')
background = getParam(args, 'titlebackground'),
:css('padding-left', getParam(args, 'padding-left') or '.1em')
color = getParam(args, 'titleforeground'),
:css('padding-right', getParam(args, 'padding-right') or '.1em')
padding = getParam(args, 'padding'),
:css('padding-bottom', getParam(args, 'padding-bottom') or '.1em')
['text-align'] = getParam(args, 'title-align'),
:css('moz-border-radius', getParam(args, 'title-border-radius') or '0')
['font-family'] = getParam(args, 'font-family'),
:css('webkit-border-radius', getParam(args, 'title-border-radius') or '0')
['font-size'] = getParam(args, 'titlefont-size'),
})
:css('border-radius', getParam(args, 'title-border-radius') or '0')
:css('border-top-width', border_top_width)
:css('border-left-width', border_left_width)
:css('border-right-width', border_right_width)
:css('padding-top', getParam(args, 'padding-top'))
:css('padding-left', getParam(args, 'padding-left'))
:css('padding-right', getParam(args, 'padding-right'))
:css('padding-bottom', getParam(args, 'padding-bottom'))
:css('border-radius', getParam(args, 'title-border-radius'))
return toOpenTagString(tag)
return toOpenTagString(tag)
end
end


local function boxHeaderTopLinks(args)
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})
local tag = mw.html.create('div', {selfClosing = true})
:addClass('plainlinks noprint' )
:addClass('box-header-top plainlinks noprint')
:css(style)
:css({
['font-size'] = getParam(args, 'font-size'),
color = getParam(args, 'titleforeground')
})
return toOpenTagString(tag)
return toOpenTagString(tag)
end
end
Line 283: Line 287:
return ''
return ''
end
end
local style = {
color = getParam(args, 'titleforeground') or '#000'
}
local tag = mw.html.create('span')
local tag = mw.html.create('span')
:addClass('box-header-viewedit')
:css(style)
:css({ color = getParam(args, 'titleforeground') })
:wikitext('edit')
:wikitext('edit')
local linktext = tostring(tag)
local linktext = tostring(tag)
Line 295: Line 298:


local function boxHeaderViewLink(args)
local function boxHeaderViewLink(args)
local style = {
color = getParam(args, 'titleforeground') or '#000'
}
local tag = mw.html.create('span')
local tag = mw.html.create('span')
:addClass('box-header-viewedit')
:css(style)
:css({ color = getParam(args, 'titleforeground') })
:wikitext('view')
:wikitext('view')
local linktext = tostring(tag)
local linktext = tostring(tag)
Line 307: Line 308:


local function boxHeaderTitle(args)
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 tagName = getParam(args, 'SPAN') and 'span' or 'h2'
local tag = mw.html.create(tagName)
local tag = mw.html.create(tagName)
:addClass('box-header-title')
:css(baseStyle)
:css('padding-bottom', '.1em')
:css({
['font-family'] = getParam(args, 'title-font-family'),
['font-size'] = getParam(args, 'title-font-size'),
['font-weight'] = getParam(args, 'title-font-weight'),
color = getParam(args, 'titleforeground')
})
:wikitext(getParam(args, 'title'))
:wikitext(getParam(args, 'title'))
if getParam(args, 'extra') then
if getParam(args, 'extra') then
Line 336: Line 333:


local function boxBody(args)
local function boxBody(args)
local border_width = getParam(args, 'border-width')
local baseStyle = {
if border_width then border_width = border_width .. 'px' end
['box-sizing'] = 'border-box',
border = ( getParam(args, 'border-width') or '1' ) .. 'px solid ' .. ( getParam(args, 'border') or '#ababab'),
local border_top_width = getParam(args, 'border-top')
['vertical-align'] = 'top';
if border_top_width then border_top_width = border_top_width .. 'px' end
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})
local tag = mw.html.create('div', {selfClosing = true})
:addClass('box-header-body')
:css(baseStyle)
:css({
:css('border-top-width', ( getParam(args, 'border-top') or '1' ) .. 'px')
:css('padding-top', getParam(args, 'padding-top') or '.3em')
['border-color'] = getParam(args, 'border'),
:css('border-radius', getParam(args, 'border-radius') or '0')
['border-width'] = border_width,
background = getParam(args, 'background'),
opacity = getParam(args, 'background-opacity'),
color = getParam(args, 'foreground'),
['text-align'] = getParam(args, 'text-align'),
padding = getParam(args, 'padding'),
})
:css('border-top-width', border_top_width)
:css('padding-top', getParam(args, 'padding-top'))
:css('border-radius', getParam(args, 'border-radius'))
return toOpenTagString(tag)
return toOpenTagString(tag)
end
end
Line 405: Line 406:
end
end
local output = {}
local output = {}
table.insert(output, mw.getCurrentFrame():extensionTag{
name = 'templatestyles', args = { src = 'Module:Box-header/styles.css' }
})
table.insert(output, boxHeaderOuter(args))
table.insert(output, boxHeaderOuter(args))
if not getParam(args, 'EDITLINK') then
if not getParam(args, 'EDITLINK') then

Latest revision as of 05:08, 2 June 2025

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, ' />$', '>')
	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
-- per [[HSL and HSV#Converting_to_RGB]]
local function HSVtoRGB(H, S, V) 
	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

-- per [[HSL and HSV#Hue and chroma]]
local function RGBtoHue(R_255, G_255, B_255)
	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 border_top_width = getParam(args, 'border-top') or getParam(args, 'border-width')
	if border_top_width then border_top_width = border_top_width .. 'px' end
	
	local border_left_width = getParam(args, 'border-width')
	if border_left_width then border_left_width = border_left_width .. 'px' end
	
	local border_right_width = getParam(args, 'border-width')
	if border_right_width then border_right_width = border_right_width .. 'px' end
	
	local tag = mw.html.create('div', {selfClosing = true})
		:addClass('box-header-title-container flex-columns-noflex')
		:css({
			['border-style'] = getParam(args, 'border-type'),
			['border-color'] = getParam(args, 'titleborder') or getParam(args, 'border'),
			background = getParam(args, 'titlebackground'),
			color = getParam(args, 'titleforeground'),
			padding = getParam(args, 'padding'),
			['text-align'] = getParam(args, 'title-align'),
			['font-family'] = getParam(args, 'font-family'),
			['font-size'] = getParam(args, 'titlefont-size'),
		})
		:css('border-top-width', border_top_width)
		:css('border-left-width', border_left_width)
		:css('border-right-width', border_right_width)
		:css('padding-top', getParam(args, 'padding-top'))
		:css('padding-left', getParam(args, 'padding-left'))
		:css('padding-right', getParam(args, 'padding-right'))
		:css('padding-bottom', getParam(args, 'padding-bottom'))
		:css('border-radius', getParam(args, 'title-border-radius'))
	return toOpenTagString(tag)
end

local function boxHeaderTopLinks(args)
	local tag = mw.html.create('div', {selfClosing = true})
		:addClass('box-header-top plainlinks noprint')
		:css({
			['font-size'] = getParam(args, 'font-size'),
			color = getParam(args, 'titleforeground')
		})
	return toOpenTagString(tag)
end

local function boxHeaderEditLink(args)
	local page = getParam(args, 'editpage')
	if not page or page == '{{{2}}}'
	then
		return ''
	end
	
	local tag = mw.html.create('span')
		:addClass('box-header-viewedit')
		:css({ color = getParam(args, 'titleforeground') })
		:wikitext('edit')
	local linktext = tostring(tag)
	local linktarget = tostring(mw.uri.fullUrl(page, {action='edit', section=getParam(args, 'section')}))
	return '[' .. linktarget  .. ' ' .. linktext .. ']&nbsp;'
end

local function boxHeaderViewLink(args)
	local tag = mw.html.create('span')
		:addClass('box-header-viewedit')
		:css({ color = getParam(args, 'titleforeground') })
		:wikitext('view')
	local linktext = tostring(tag)
	local linktarget = ':' .. getParam(args, 'viewpage')
	return "<b>·</b>&nbsp;[[" .. linktarget  .. '|' .. linktext .. ']]&nbsp;'
end

local function boxHeaderTitle(args)
	local tagName = getParam(args, 'SPAN') and 'span' or 'h2'
	local tag = mw.html.create(tagName)
		:addClass('box-header-title')
		:css({
			['font-family'] = getParam(args, 'title-font-family'),
			['font-size'] = getParam(args, 'title-font-size'),
			['font-weight'] = getParam(args, 'title-font-weight'),
			color = getParam(args, 'titleforeground')
		})
		: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 border_width = getParam(args, 'border-width')
	if border_width then border_width = border_width .. 'px' end
	
	local border_top_width = getParam(args, 'border-top')
	if border_top_width then border_top_width = border_top_width .. 'px' end
	
	local tag = mw.html.create('div', {selfClosing = true})
		:addClass('box-header-body')
		:css({
			['border-color'] = getParam(args, 'border'),
			['border-width'] = border_width,
			background = getParam(args, 'background'),
			opacity = getParam(args, 'background-opacity'),
			color = getParam(args, 'foreground'),
			['text-align'] = getParam(args, 'text-align'),
			padding = getParam(args, 'padding'),
		})
		:css('border-top-width', border_top_width)
		:css('padding-top', getParam(args, 'padding-top'))
		:css('border-radius', getParam(args, 'border-radius'))
	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 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, mw.getCurrentFrame():extensionTag{
		name = 'templatestyles', args = { src = 'Module:Box-header/styles.css' }
	})
	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') .. '&nbsp;')
		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 '{{{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
	
return p