Module:Box-header: Difference between revisions

From Saintapedia
Jump to navigation Jump to search
>Tomoneill
(change to VE edit)
 
(Created page with "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....")
Line 13: Line 13:
local min_contrast_ratio_normal_text = 7  -- i.e 7:1
local min_contrast_ratio_normal_text = 7  -- i.e 7:1
local min_contrast_ratio_large_text  = 4.5  -- i.e. 4.5:1
local min_contrast_ratio_large_text  = 4.5  -- i.e. 4.5:1
-- Template parameter aliases
-- Template parameter aliases
--  Specify each as either a single value, or a table of values
--  Specify each as either a single value, or a table of values
Line 23: Line 22:
}
}


---------- Dependecies ----------
---------- Dependencies ----------
local colourContrastModule = require('Module:Color contrast')
local colourContrastModule = require('Module:Color contrast')
local hex = require( 'luabit.hex' )
local hex = require( 'luabit.hex' )
Line 33: Line 32:
end
end
local aliases = parameterAliases[parameter]
local aliases = parameterAliases[parameter]
if not aliases then
if not aliases then
return nil
return nil
end
end
if type(aliases) ~= 'table' then
if type(aliases) ~= 'table' then
return args[aliases]
return args[aliases]
end
end
for _, alias in ipairs(aliases) do
for _, alias in ipairs(aliases) do
if args[alias] then
if args[alias] then
return args[alias]
return args[alias]
end
end
end
end
return nil
return nil
end
end


local function setCleanArgs(argsTable)
local function setCleanArgs(argsTable)
local cleanArgs = {}
local cleanArgs = {}
for key, val in pairs(argsTable) do
for key, val in pairs(argsTable) do
if type(val) == 'string' then
if type(val) == 'string' then
val = val:match('^%s*(.-)%s*$')
val = val:match('^%s*(.-)%s*$')
if val ~= '' then
if val ~= '' then
cleanArgs[key] = val
cleanArgs[key] = val
end
end
else
else
cleanArgs[key] = val
cleanArgs[key] = val
end
end
end
end
return cleanArgs
return cleanArgs
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 = {}
for key, val in pairs(first) do
for key, val in pairs(first) do
merged[key] = val
merged[key] = val
end
end
for key, val in pairs(second) do
for key, val in pairs(second) do
merged[key] = val
merged[key] = val
end
end
return merged
return merged
end
end


local function toOpenTagString(selfClosedHtmlObject)
local function toOpenTagString(selfClosedHtmlObject)
local closedTagString = tostring(selfClosedHtmlObject)
local closedTagString = tostring(selfClosedHtmlObject)
local openTagString = mw.ustring.gsub(closedTagString, ' />$', '>')
local openTagString = mw.ustring.gsub(closedTagString, ' />$', '>')
return openTagString
return openTagString
end
end


local function normaliseHexTriplet(hexString)
local function normaliseHexTriplet(hexString)
if not hexString then return nil end
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)$')
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
if hexComponent and #hexComponent == 6 then
return mw.ustring.upper(hexString)
return mw.ustring.upper(hexString)
end
end
if hexComponent and #hexComponent == 3 then
if hexComponent and #hexComponent == 3 then
local r = mw.ustring.rep(mw.ustring.sub(hexComponent, 1, 1), 2)
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 g = mw.ustring.rep(mw.ustring.sub(hexComponent, 2, 2), 2)
local b = mw.ustring.rep(mw.ustring.sub(hexComponent, 3, 3), 2)
local b = mw.ustring.rep(mw.ustring.sub(hexComponent, 3, 3), 2)
return '#' .. mw.ustring.upper(r .. g .. b)
return '#' .. mw.ustring.upper(r .. g .. b)
end
end
return nil
return nil
end
end


---------- Conversions ----------
---------- Conversions ----------
local function decimalToPaddedHex(number)
local function decimalToPaddedHex(number)
local prefixedHex = hex.to_hex(tonumber(number)) -- prefixed with '0x'
local prefixedHex = hex.to_hex(tonumber(number)) -- prefixed with '0x'
local padding =  #prefixedHex == 3 and '0' or ''  
local padding =  #prefixedHex == 3 and '0' or ''  
return mw.ustring.gsub(prefixedHex, '0x', padding)
return mw.ustring.gsub(prefixedHex, '0x', padding)
end
end
local function hexToDecimal(hexNumber)
local function hexToDecimal(hexNumber)
return tonumber(hexNumber, 16)
return tonumber(hexNumber, 16)
end
end
local function RGBtoHexTriplet(R, G, B)
local function RGBtoHexTriplet(R, G, B)
return '#' .. decimalToPaddedHex(R) .. decimalToPaddedHex(G) .. decimalToPaddedHex(B)
return '#' .. decimalToPaddedHex(R) .. decimalToPaddedHex(G) .. decimalToPaddedHex(B)
end
end
local function hexTripletToRGB(hexTriplet)
local function hexTripletToRGB(hexTriplet)
local R_hex, G_hex, B_hex = string.match(hexTriplet, '(%x%x)(%x%x)(%x%x)')
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)
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]]
local function HSVtoRGB(H, S, V) -- per [[HSL and HSV#Converting_to_RGB]]
local C = V * S
local C = V * S
local H_prime = H / 60
local H_prime = H / 60
local X = C * ( 1 - math.abs(math.fmod(H_prime, 2) - 1) )
local X = C * ( 1 - math.abs(math.fmod(H_prime, 2) - 1) )
local R1, G1, B1
local R1, G1, B1
if H_prime <= 1 then
if H_prime <= 1 then
R1 = C
R1 = C
G1 = X
G1 = X
B1 = 0
B1 = 0
elseif H_prime <= 2 then
elseif H_prime <= 2 then
R1 = X
R1 = X
G1 = C
G1 = C
B1 = 0
B1 = 0
elseif H_prime <= 3 then
elseif H_prime <= 3 then
R1 = 0
R1 = 0
G1 = C
G1 = C
B1 = X
B1 = X
elseif H_prime <= 4 then
elseif H_prime <= 4 then
R1 = 0
R1 = 0
G1 = X
G1 = X
B1 = C
B1 = C
elseif H_prime <= 5 then
elseif H_prime <= 5 then
R1 = X
R1 = X
G1 = 0
G1 = 0
B1 = C
B1 = C
elseif H_prime <= 6 then
elseif H_prime <= 6 then
R1 = C
R1 = C
G1 = 0
G1 = 0
B1 = X
B1 = X
end
end
local m = V - C
local m = V - C
local R = R1 + m
local R = R1 + m
local G = G1 + m
local G = G1 + m
local B = B1 + m
local B = B1 + m


local R_255 = math.floor(R*255)
local R_255 = math.floor(R*255)
local G_255 = math.floor(G*255)
local G_255 = math.floor(G*255)
local B_255 = math.floor(B*255)
local B_255 = math.floor(B*255)
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]]
local function RGBtoHue(R_255, G_255, B_255) -- per [[HSL and HSV#Hue and chroma]]
local R = R_255/255
local R = R_255/255
local G = G_255/255
local G = G_255/255
local B = B_255/255
local B = B_255/255


local M = math.max(R, G, B)
local M = math.max(R, G, B)
local m = math.min(R, G, B)
local m = math.min(R, G, B)
local C = M - m
local C = M - m
local H_prime
local H_prime
if C == 0 then
if C == 0 then
return null
return null
elseif M == R then
elseif M == R then
H_prime = math.fmod(((G - B)/C + 6), 6) -- adding six before taking mod ensures positive value
H_prime = math.fmod(((G - B)/C + 6), 6) -- adding six before taking mod ensures positive value
elseif M == G then
elseif M == G then
H_prime = (B - R)/C + 2
H_prime = (B - R)/C + 2
elseif M == B then
elseif M == B then
H_prime = (R - G)/C + 4
H_prime = (R - G)/C + 4
end
end
local H = 60 * H_prime
local H = 60 * H_prime
return H
return H
end
end
local function nameToHexTriplet(name)
local function nameToHexTriplet(name)
if not name then return nil end
if not name then return nil end
local codename = mw.ustring.gsub(mw.ustring.lower(name), ' ', '')
local codename = mw.ustring.gsub(mw.ustring.lower(name), ' ', '')
return namedColours[codename]
return namedColours[codename]
end
end


---------- Choose colours ----------
---------- Choose colours ----------
local function calculateColours(H, S, V, minContrast)
local function calculateColours(H, S, V, minContrast)
local bgColour = RGBtoHexTriplet(HSVtoRGB(H, S, V))
local bgColour = RGBtoHexTriplet(HSVtoRGB(H, S, V))
local textColour = colourContrastModule._greatercontrast({bgColour})
local textColour = colourContrastModule._greatercontrast({bgColour})
local contrast = colourContrastModule._ratio({ bgColour, textColour })
local contrast = colourContrastModule._ratio({ bgColour, textColour })
if contrast >= minContrast then
if contrast >= minContrast then
return bgColour, textColour
return bgColour, textColour
elseif textColour == '#FFFFFF' then
elseif textColour == '#FFFFFF' then
-- make the background darker and slightly increase the saturation
-- 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)
return calculateColours(H, math.min(1, S+0.005), math.max(0, V-0.03), minContrast)
else
else
-- make the background lighter and slightly decrease the saturation
-- 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)
return calculateColours(H, math.max(0, S-0.005), math.min(1, V+0.03), minContrast)
end
end
end
end


local function makeColours(hue, modeName)
local function makeColours(hue, modeName)
local mode = modes[modeName]
local mode = modes[modeName]
local isGrey = not(hue)
local isGrey = not(hue)
if isGrey then hue = 0 end
if isGrey then hue = 0 end


local borderSat = isGrey and modes.grey.sat or 0.15
local borderSat = isGrey and modes.grey.sat or 0.15
local border = RGBtoHexTriplet(HSVtoRGB(hue, borderSat, 0.75))
local border = RGBtoHexTriplet(HSVtoRGB(hue, borderSat, 0.75))


local titleSat = isGrey and modes.grey.sat or mode.sat
local titleSat = isGrey and modes.grey.sat or mode.sat
local titleBackground, titleForeground = calculateColours(hue, titleSat, mode.val, min_contrast_ratio_large_text)
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 contentSat = isGrey and modes.grey.sat or modes.content.sat
local contentBackground, contentForeground = calculateColours(hue, contentSat, modes.content.val, min_contrast_ratio_normal_text)
local contentBackground, contentForeground = calculateColours(hue, contentSat, modes.content.val, min_contrast_ratio_normal_text)


return border, titleForeground, titleBackground, contentForeground, contentBackground
return border, titleForeground, titleBackground, contentForeground, contentBackground
end
end


local function findHue(colour)
local function findHue(colour)
local colourAsNumber = tonumber(colour)
local colourAsNumber = tonumber(colour)
if colourAsNumber and ( -1 < colourAsNumber ) and ( colourAsNumber < 360) then
if colourAsNumber and ( -1 < colourAsNumber ) and ( colourAsNumber < 360) then
return colourAsNumber
return colourAsNumber
end
end


local colourAsHexTriplet = normaliseHexTriplet(colour) or nameToHexTriplet(colour)
local colourAsHexTriplet = normaliseHexTriplet(colour) or nameToHexTriplet(colour)
if colourAsHexTriplet then
if colourAsHexTriplet then
return RGBtoHue(hexTripletToRGB(colourAsHexTriplet))
return RGBtoHue(hexTripletToRGB(colourAsHexTriplet))
end
end


return null
return null
end
end


local function normaliseMode(mode)
local function normaliseMode(mode)
if not mode or not modes[mw.ustring.lower(mode)] or mw.ustring.lower(mode) == 'grey' then
if not mode or not modes[mw.ustring.lower(mode)] or mw.ustring.lower(mode) == 'grey' then
return 'normal'
return 'normal'
end
end
return mw.ustring.lower(mode)
return mw.ustring.lower(mode)
end
end
---------- Build output ----------
---------- Build output ----------
local function boxHeaderOuter(args)
local function boxHeaderOuter(args)
local baseStyle = {
local baseStyle = {
clear = 'both',
clear = 'both',
['box-sizing'] = 'border-box',
['box-sizing'] = 'border-box',
border = ( getParam(args, 'border-type') or 'solid' ) .. ' ' .. ( getParam(args, 'titleborder') or getParam(args, 'border') or '#ababab' ),
border = ( getParam(args, 'border-type') or 'solid' ) .. ' ' .. ( getParam(args, 'titleborder') or getParam(args, 'border') or '#ababab' ),
background = getParam(args, 'titlebackground') or '#bcbcbc',
background = getParam(args, 'titlebackground') or '#bcbcbc',
color = getParam(args, 'titleforeground') or '#000',
color = getParam(args, 'titleforeground') or '#000',
padding = getParam(args, 'padding') or '.1em',
padding = getParam(args, 'padding') or '.1em',
['text-align'] = getParam(args, 'title-align') or 'center',
['text-align'] = getParam(args, 'title-align') or 'center',
['font-family'] = getParam(args, 'font-family') or 'sans-serif',
['font-family'] = getParam(args, 'font-family') or 'sans-serif',
['font-size'] = getParam(args, 'titlefont-size') or '100%',
['font-size'] = getParam(args, 'titlefont-size') or '100%',
['margin-bottom'] = '0px',
['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')
:addClass('flex-columns-noflex')
:addClass('flex-columns-noflex')
:css(baseStyle)
: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('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-top', getParam(args, 'padding-top') or '.1em')
:css('padding-left', getParam(args, 'padding-left') or '.1em')
:css('padding-left', getParam(args, 'padding-left') or '.1em')
:css('padding-right', getParam(args, 'padding-right') or '.1em')
:css('padding-right', getParam(args, 'padding-right') or '.1em')
:css('padding-bottom', getParam(args, 'padding-bottom') or '.1em')
:css('padding-bottom', getParam(args, 'padding-bottom') or '.1em')
:css('moz-border-radius', getParam(args, 'title-border-radius') or '0')
:css('moz-border-radius', getParam(args, 'title-border-radius') or '0')
:css('webkit-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')
:css('border-radius', getParam(args, 'title-border-radius') or '0')
return toOpenTagString(tag)
return toOpenTagString(tag)
end
end


local function boxHeaderTopLinks(args)
local function boxHeaderTopLinks(args)
local style = {
local style = {
float = 'right',
float = 'right',
['margin-bottom'] = '.1em',
['margin-bottom'] = '.1em',
['font-size'] = getParam(args, 'font-size') or '80%',
['font-size'] = getParam(args, 'font-size') or '80%',
color = getParam(args, 'titleforeground') or '#000'
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('plainlinks noprint' )
:css(style)
:css(style)
return toOpenTagString(tag)
return toOpenTagString(tag)
end
end


local function boxHeaderEditLink(args)
local function boxHeaderEditLink(args)
local style = {
local style = {
color = getParam(args, 'titleforeground') or '#000'
color = getParam(args, 'titleforeground') or '#000'
}
}
local tag = mw.html.create('span')
local tag = mw.html.create('span')
:css(style)
:css(style)
:wikitext('edit')
:wikitext('edit')
local linktext = tostring(tag)
local linktext = tostring(tag)
local linktarget = tostring(mw.uri.fullUrl(getParam(args, 'editpage'), {veaction='edit', section=getParam(args, 'section')}))
local linktarget = tostring(mw.uri.fullUrl(getParam(args, 'editpage'), {veaction='edit', section=getParam(args, 'section')}))
return '[' .. linktarget  .. ' ' .. linktext .. ']&nbsp;'
return '[' .. linktarget  .. ' ' .. linktext .. ']&nbsp;'
end
end


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


local function boxHeaderTitle(args)
local function boxHeaderTitle(args)
local baseStyle = {
local baseStyle = {
['font-family'] = getParam(args, 'title-font-family') or 'sans-serif',
['font-family'] = getParam(args, 'title-font-family') or 'sans-serif',
['font-size'] = getParam(args, 'title-font-size') or '100%',
['font-size'] = getParam(args, 'title-font-size') or '100%',
['font-weight'] = getParam(args, 'title-font-weight') or 'bold',
['font-weight'] = getParam(args, 'title-font-weight') or 'bold',
border = 'none',
border = 'none',
margin = '0',
margin = '0',
padding = '0',
padding = '0',
color = getParam(args, 'titleforeground') or '#000';
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)
:css(baseStyle)
:css(baseStyle)
:css('padding-bottom', '.1em')
:css('padding-bottom', '.1em')
:wikitext(getParam(args, 'title'))
:wikitext(getParam(args, 'title'))
if getParam(args, 'extra') then
if getParam(args, 'extra') then
local rules = mw.text.split(getParam(args, 'extra'), ';', true)
local rules = mw.text.split(getParam(args, 'extra'), ';', true)
for _, rule in pairs(rules) do
for _, rule in pairs(rules) do
local parts = mw.text.split(rule, ':', true)
local parts = mw.text.split(rule, ':', true)
local prop = parts[1]
local prop = parts[1]
local val = parts[2]
local val = parts[2]
if prop and val then
if prop and val then
tag:css(prop, val)
tag:css(prop, val)
end
end
end
end
end
end
return tostring(tag)
return tostring(tag)
end
end


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


local function contrastCategories(args)
local function contrastCategories(args)
local cats = ''
local cats = ''


local titleText = nameToHexTriplet(getParam(args, 'titleforeground')) or normaliseHexTriplet(getParam(args, 'titleforeground')) or '#000000'
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 titleBackground = nameToHexTriplet(getParam(args, 'titlebackground')) or normaliseHexTriplet(getParam(args, 'titlebackground')) or '#bcbcbc'
local titleContrast = colourContrastModule._ratio({titleBackground, titleText})
local titleContrast = colourContrastModule._ratio({titleBackground, titleText})
local insufficientTitleContrast = type(titleContrast) == 'number' and ( titleContrast < min_contrast_ratio_large_text )
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 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 bodyBackground = nameToHexTriplet(getParam(args, 'background')) or normaliseHexTriplet(getParam(args, 'background')) or '#fefeef'
local bodyContrast =  colourContrastModule._ratio({bodyBackground, bodyText})
local bodyContrast =  colourContrastModule._ratio({bodyBackground, bodyText})
local insufficientBodyContrast = type(bodyContrast) == 'number' and ( bodyContrast < min_contrast_ratio_normal_text )
local insufficientBodyContrast = type(bodyContrast) == 'number' and ( bodyContrast < min_contrast_ratio_normal_text )


if insufficientTitleContrast and insufficientBodyContrast then
if insufficientTitleContrast and insufficientBodyContrast then
return '[[Category:Box-header with insufficient title contrast]][[Category:Box-header with insufficient body contrast]]'
return '[[Category:Box-header with insufficient title contrast]][[Category:Box-header with insufficient body contrast]]'
elseif insufficientTitleContrast then
elseif insufficientTitleContrast then
return '[[Category:Box-header with insufficient title contrast]]'
return '[[Category:Box-header with insufficient title contrast]]'
elseif insufficientBodyContrast then
elseif insufficientBodyContrast then
return '[[Category:Box-header with insufficient body contrast]]'
return '[[Category:Box-header with insufficient body contrast]]'
else
else
return ''
return ''
end
end
end
end


Line 377: Line 660:


-- Entry point for templates (manually-specified colours)
-- Entry point for templates (manually-specified colours)
function p.boxHeader(frame)
function p.boxHeader(frame)
local parent = frame.getParent(frame)
local parent = frame.getParent(frame)
local parentArgs = parent.args
local parentArgs = parent.args
local page = parentArgs.editpage
local page = parentArgs.editpage
if not parentArgs.editpage or parentArgs.editpage == '' then
if not parentArgs.editpage or parentArgs.editpage == '' then
page = parent:preprocess('{{FULLPAGENAME}}')
page = parent:preprocess('{{FULLPAGENAME}}')
end
end
local output = p._boxHeader(parentArgs, page)
local output = p._boxHeader(parentArgs, page)
if mw.ustring.find(output, '{') then
if mw.ustring.find(output, '{') then
return frame:preprocess(output)
return frame:preprocess(output)
end
end
return output
return output
end
end


-- Entry point for modules (manually-specified colours)
-- Entry point for modules (manually-specified colours)
function p._boxHeader(_args, page)
function p._boxHeader(_args, page)
local args = setCleanArgs(_args)
local args = setCleanArgs(_args)
if page and not args.editpage then
if page and not args.editpage then
args.editpage = page
args.editpage = page
end
end
if not args.title then
if not args.title then
args.title = '{{{title}}}'
args.title = '{{{title}}}'
end
end
local output = {}
local output = {}
table.insert(output, boxHeaderOuter(args))
table.insert(output, boxHeaderOuter(args))
if not getParam(args, 'EDITLINK') then
if not getParam(args, 'EDITLINK') then
table.insert(output, boxHeaderTopLinks(args))
table.insert(output, boxHeaderTopLinks(args))
if not getParam(args, 'noedit') then
if not getParam(args, 'noedit') then
table.insert(output, boxHeaderEditLink(args))
table.insert(output, boxHeaderEditLink(args))
end
end
if getParam(args, 'viewpage') then
if getParam(args, 'viewpage') then
table.insert(output, boxHeaderViewLink(args))
table.insert(output, boxHeaderViewLink(args))
end
end
if getParam(args, 'top') then
if getParam(args, 'top') then
table.insert(output, getParam(args, 'top') .. '&nbsp;')
table.insert(output, getParam(args, 'top') .. '&nbsp;')
end
end
table.insert(output, '</div>')
table.insert(output, '</div>')
end
end
table.insert(output, boxHeaderTitle(args))
table.insert(output, boxHeaderTitle(args))
table.insert(output, '</div>')
table.insert(output, '</div>')
table.insert(output, boxBody(args))
table.insert(output, boxBody(args))
if not getParam(args, 'TOC') then
if not getParam(args, 'TOC') then
table.insert(output, '__NOTOC__')
table.insert(output, '__NOTOC__')
end
end
if not getParam(args, 'EDIT') then
if not getParam(args, 'EDIT') then
table.insert(output, '__NOEDITSECTION__')
table.insert(output, '__NOEDITSECTION__')
end
end
table.insert(output, contrastCategories(args))
table.insert(output, contrastCategories(args))


return table.concat(output)
return table.concat(output)
end
end


-- Entry point for templates (automatically calculated colours)
-- Entry point for templates (automatically calculated colours)
function p.autoColour(frame)
function p.autoColour(frame)
local parent = frame.getParent(frame)
local parent = frame.getParent(frame)
local parentArgs = parent.args
local parentArgs = parent.args
local colourParam = getParam(parentArgs, 'colour')
local colourParam = getParam(parentArgs, 'colour')
local generatedColour = nil
local generatedColour = nil
if not colourParam or colourParam == '' then
if not colourParam or colourParam == '' then
-- convert the root page name into a number and use that
-- convert the root page name into a number and use that
local root = parent:preprocess('{{ROOTPAGENAME}}')
local root = parent:preprocess('{{ROOTPAGENAME}}')
local rootStart = mw.ustring.sub(root, 1, 12)
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 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)
local numberFromRoot = tonumber(digitsFromRootStart, 10)
generatedColour = math.fmod(numberFromRoot, 360)
generatedColour = math.fmod(numberFromRoot, 360)
end
end
local output = p._autoColour(parent.args, generatedColour)
local output = p._autoColour(parent.args, generatedColour)
if mw.ustring.find(output, '{') then
if mw.ustring.find(output, '{') then
return frame:preprocess(output)
return frame:preprocess(output)
end
end
return output
return output
end
end


-- Entry point for modules (automatically calculated colours)
-- Entry point for modules (automatically calculated colours)
function p._autoColour(_args, generatedColour)
function p._autoColour(_args, generatedColour)
local args = setCleanArgs(_args)
local args = setCleanArgs(_args)
local hue = generatedColour or findHue(getParam(args, 'colour'))
local hue = generatedColour or findHue(getParam(args, 'colour'))
local mode = normaliseMode(getParam(args, 'mode'))
local mode = normaliseMode(getParam(args, 'mode'))
local border, titleForeground, titleBackground, contentForeground, contentBackground = makeColours(hue, mode)
local border, titleForeground, titleBackground, contentForeground, contentBackground = makeColours(hue, mode)
local boxTemplateArgs = mergeTables(args, {
local boxTemplateArgs = mergeTables(args, {
title = getParam(args, '1') or '{{{1}}}',
title = getParam(args, '1') or '{{{1}}}',
editpage = getParam(args, '2') or '',
editpage = getParam(args, '2') or '',
noedit = getParam(args, '2') and '' or 'yes',
noedit = getParam(args, '2') and '' or 'yes',
border = border,
border = border,
titleforeground = titleForeground,
titleforeground = titleForeground,
titlebackground = titleBackground,
titlebackground = titleBackground,
foreground = contentForeground,
foreground = contentForeground,
background = contentBackground
background = contentBackground
})
})
return p._boxHeader(boxTemplateArgs)
return p._boxHeader(boxTemplateArgs)
end
end
return p
return p

Revision as of 14:29, 5 September 2024

This module creates the header section for boxed content. It implements {{box-header}}. It is intended to mainly be used in portals, but can also be used elsewhere.

Usage

{{#invoke:Box-header|boxHeader}}

For use in templates; calls _boxHeader with the parameters passed to the template as arguments.

{{#invoke:Box-header|_boxHeader|args}}

For use in modules; constructs the box header (and the start of the box body). The args are the parameters accepted by Template:Box-header. (The output may need to be expanded, depending on the values in the args.)

{{#invoke:Box-header|autoColour}}

For use in templates; calls _autoColour with the parameters passed to the template as arguments.

{{#invoke:Box-header|_autoColour|args}}

For use in modules; calculates appropriate colours for the box header, and then constructs it using _boxHeader. The args are the parameters accepted by Template:Box-header colour – the same as for Template:Box-header, apart from those specifying colours and the title. (The output may need to be expanded, depending on the values in the args.)

See also


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'
}

---------- Dependencies ----------
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 style = {

		color = getParam(args, 'titleforeground') or '#000'

	}

	local tag = mw.html.create('span')

		:css(style)

		:wikitext('edit')

	local linktext = tostring(tag)

	local linktarget = tostring(mw.uri.fullUrl(getParam(args, 'editpage'), {veaction='edit', section=getParam(args, 'section')}))

	return '[' .. linktarget  .. ' ' .. linktext .. ']&nbsp;'

end

local function boxHeaderViewLink(args)

	local style = {

		color = getParam(args, 'titleforeground') or '#000'

	}

	local tag = mw.html.create('span')

		:css(style)

		:wikitext('view')

	local linktext = tostring(tag)

	local linktarget = ':' .. getParam(args, 'viewpage')

	return "<b>·</b>&nbsp;[[" .. linktarget  .. '|' .. linktext .. ']]&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('-moz-border-radius', getParam(args, 'border-radius') or '0')

		:css('-webkit-border-radius', getParam(args, 'border-radius') or '0')

		: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 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 parent = frame.getParent(frame)

	local parentArgs = parent.args

	local page = parentArgs.editpage

	if not parentArgs.editpage or parentArgs.editpage == '' then

		page = parent:preprocess('{{FULLPAGENAME}}')

	end

	local output = p._boxHeader(parentArgs, 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') .. '&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 parent = frame.getParent(frame)

	local parentArgs = parent.args

	local colourParam = getParam(parentArgs, 'colour')

	local generatedColour = nil

	if not colourParam or colourParam == '' then

		-- convert the root page name into a number and use that

		local root = parent:preprocess('{{ROOTPAGENAME}}')

		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(parent.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