Jump to content

Module talk:Color contrast

From bestpressey

Template:WikiProject banner shell Template:Copied

Making the lum function accessible from modules

I've made the lum function accessible from modules by changing the code from:

function p.lum(frame)
	return color2lum(frame.args[1] or frame:getParent().args[1])
end

to:

function p.lum(frame)
	local args = frame.args[1] or frame:getParent().args[1]
	return p._lum(args)
end

function p._lum(args)
	return color2lum(args)
end

The changes are on the sandbox and will implement them if there are no comments. --Gonnym (talk) 11:03, 6 January 2019 (UTC)

Template:Re That looks like a very sensible and useful improvement. My only reservation is that passing the parameters as a table gives the person wanting to import the routine no idea of what values/types to supply to the routine without having to read through all of the code to determine them. In general I'd usually recommend either documenting a brief list of required values for the args table or passing the parameters as a list of obviously-named variables rather than a table. But that's just a minor point and shouldn't stop you from updating the main module. --RexxS (talk) 13:03, 6 January 2019 (UTC)
Template:Ping I don't mind doing either if that helps. I just followed the current style used for the other three public functions. Do you want me to change _lum(args) to _lum(color)? --Gonnym (talk) 13:27, 6 January 2019 (UTC)
Template:Re Sorry, my confusion: now I've looked harder at it, you're actually passing a string representing the colour, not a table (which is what args would most commonly indicate). I would suggest:<syntaxhighlight lang=Lua>

-- use Script error: The function "somecolor" does not exist. directly or Script error: You must specify a function to call. from a wrapper template: function p.lum(frame) local color = frame.args[1] or frame:getParent().args[1] return p._lum(color) end

-- This exports the function for use in other modules. -- The colour is passed as a string: function p._lum(color) return color2lum(color) end</syntaxhighlight> I know we can put fuller information in the documentation, but I always suggest leaving small annotations in the code to help any re-users.

You could also write p._lum = color2lum instead of the second function definition, but setting it out explicitly, as you have done, helps whoever is importing the module to see what parameters to supply. Cheers --RexxS (talk) 15:41, 6 January 2019 (UTC)
Sure, looks good. I'll update the sandbox version. --Gonnym (talk) 15:43, 6 January 2019 (UTC)

Possible typos in code?

Observing the code, I might have spotted a couple of typos:

In p._greatercontrast function, lines 160 and 161

I think <syntaxhighlight lang='lua'>if mw.ustring.match(v3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') or mw.ustring.match(v3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') then</syntaxhighlight> should read <syntaxhighlight lang='lua'>if mw.ustring.match(c3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') or mw.ustring.match(c3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') then</syntaxhighlight> instead. Here, v3 is expected to be a number, thus there is no point in applying it a regular expression. Besides, given the four lines above, it seems logical to use c3 variable here instead of v3.

In p._styleratio function, line 209

I think <syntaxhighlight lang="lua">if( lum ~= ) then bg, lum_fg = v, lum end</syntaxhighlight> should read <syntaxhighlight lang="lua">if( lum ~= ) then fg, lum_fg = v, lum end</syntaxhighlight> instead, since at this point we have isolated a forecolor specification from input CSS.

My apologies if I would have misinterpreted your code.

Kindly, Olivier Ph. (talk) 12:11, 13 October 2020 (UTC)

Categorisation broken?

Through Documentation subpage, Category:Modules handling colors is added as expected. However, the page does not appear in Category:Modules handling colors (1). -DePiep (talk) 15:12, 7 January 2021 (UTC)

Template:Re categorisation behaves as something of a black art. The page appears in Category:Modules handling colors now, but obviously didn't earlier today. Wikipedia:FAQ/Categorization #Why might a category list not be up to date? discusses some of the issues that may cause the lag. I usually try a null edit to "touch" the pages concerned and often find the page then appears in the category, but it's not always 100% successful. --RexxS (talk) 16:07, 7 January 2021 (UTC)
I had just edited out the {{Sandbox other}} [1]. So we have a cause (bug). Did not investigate problem further.-DePiep (talk) 16:11, 7 January 2021 (UTC)
Yes I saw that. But because the category was being added via {{Sandbox other}} and now is added directly via Module:Color contrast/doc, that becomes effectively a different transclusion and the usual caveats about delays because of cached transclusions apply. --RexxS (talk) 16:56, 7 January 2021 (UTC)
{{Sandbox other}} should be checked for Module ns then? But here it seems to work. (To be clear: before my edit the category was empty, and no delay was working, no recent edits). I'll leave it for now. -DePiep (talk) 17:18, 7 January 2021 (UTC)

Template-protected edit request on 18 March 2025

Template:Edit template-protected Line 135: Template:Td Line 137: Template:Td Change the specification of the default colors from hex triplets <syntaxhighlight lang="lua" inline>'#FFFFFF'</syntaxhighlight> and <syntaxhighlight lang="lua" inline>'#000000'</syntaxhighlight> to named HTML colors <syntaxhighlight lang="lua" inline>'white'</syntaxhighlight> and <syntaxhighlight lang="lua" inline>'black'</syntaxhighlight> respectively. This allows the default case to take advantage of the pre-computed table of relative luminances of named HTML colors at Module:Color contrast/colors checked by the color2lum() function instead of calculating the relative luminances of white and black every time (as the module as currently written does), as well as, at least for the default colors, avoiding a parser bug that causes output that begins with # to result in a numbered list. Kinsio (talk ★ contribs ★ rights) 18:55, 19 March 2025 (UTC)

 Done * Pppery * it has begun... 20:30, 30 March 2025 (UTC)

Template-protected edit request on 20 July 2026

Template:Edit template-protected Please update the module to support additional CSS3/CSS4 color formats ("rgba", "hsla", 4-digit and 8-digit hex values) and incorporate several code fixes, bringing it up to speed with the improvements from "ptwiki".

Key changes

  1. Updated regex patterns in "color2lum" to support "rgba()", "hsla()", space-separated values, and "#RGBA" (4-digit) / "#RRGGBBAA" (8-digit) hex formats.
  2. Fixed a variable check bug in "_greatercontrast" where "v3" was tested instead of "c3".
  3. Stripped newlines and normalized whitespace in the CSS output for "_greatercontrast" when the "css" flag is active.
  4. Cleaned up variable scoping in "_styleratio" to prevent shadowing loop index "k" and removed unused variables ("bg", "fg").

Replace the current code of the module with:

Template:Collapse-top <syntaxhighlight lang="lua"> -- -- This module implements -- Template:Color contrast ratio -- -- Template:ColorToLum -- Template:RGBColorToLum -- local p = {} local HTMLcolor = mw.loadData( 'Module:Color contrast/colors' )

local function sRGB (v)

   if (v <= 0.03928) then
       v = v / 12.92
   else
       v = math.pow((v+0.055)/1.055, 2.4)
   end
   return v

end

local function rgbdec2lum(R, G, B)

   if ( 0 <= R and R < 256 and 0 <= G and G < 256 and 0 <= B and B < 256 ) then
       return 0.2126 * sRGB(R/255) + 0.7152 * sRGB(G/255) + 0.0722 * sRGB(B/255)
   else
       return 
   end

end

local function hsl2lum(h, s, l)

   if ( 0 <= h and h < 360 and 0 <= s and s <= 1 and 0 <= l and l <= 1 ) then
       local c = (1 - math.abs(2*l - 1))*s
       local x = c*(1 - math.abs( math.fmod(h/60, 2) - 1) )
       local m = l - c/2
       local r, g, b = m, m, m
       if( 0 <= h and h < 60 ) then
           r = r + c
           g = g + x
       elseif( 60 <= h and h < 120 ) then
           r = r + x
           g = g + c
       elseif( 120 <= h and h < 180 ) then
           g = g + c
           b = b + x
       elseif( 180 <= h and h < 240 ) then
           g = g + x
           b = b + c
       elseif( 240 <= h and h < 300 ) then
           r = r + x
           b = b + c
       elseif( 300 <= h and h < 360 ) then
           r = r + c
           b = b + x
       end
       return rgbdec2lum(255*r, 255*g, 255*b)
   else
       return 
   end

end

local function color2lum(c)

   if (c == nil) then
       return 
   end
   -- html '#' entity
   c = c:gsub("#", "#")
   -- whitespace
   c = c:match( '^%s*(.-)[%s;]*$' )
   -- unstrip nowiki strip markers
   c = mw.text.unstripNoWiki(c)
   -- lowercase
   c = c:lower()
   -- first try to look it up
   local L = HTMLcolor[c]
   if (L ~= nil) then
       return L
   end
   -- convert from hsl / hsla (accepts comma or space delimiters)
   if mw.ustring.match(c,'^hsla?%([%s]*[0-9][0-9%.]*[%s]*[,%s][%s]*[0-9][0-9%.]*%%[%s]*[,%s][%s]*[0-9][0-9%.]*%%.*%)$') then
       local h, s, l = mw.ustring.match(c,'^hsla?%([%s]*([0-9][0-9%.]*)[%s]*[,%s][%s]*([0-9][0-9%.]*)%%[%s]*[,%s][%s]*([0-9][0-9%.]*)%%.*%)$')
       return hsl2lum(tonumber(h), tonumber(s)/100, tonumber(l)/100)
   end
   -- convert from rgb / rgba (numeric values 0 to 255)
   if mw.ustring.match(c,'^rgba?%([%s]*[0-9][0-9]*[%s]*[,%s][%s]*[0-9][0-9]*[%s]*[,%s][%s]*[0-9][0-9]*.*%)$') then
       local R, G, B = mw.ustring.match(c,'^rgba?%([%s]*([0-9][0-9]*)[%s]*[,%s][%s]*([0-9][0-9]*)[%s]*[,%s][%s]*([0-9][0-9]*).*%)$')
       return rgbdec2lum(tonumber(R), tonumber(G), tonumber(B))
   end
   -- convert from rgb / rgba percent
   if mw.ustring.match(c,'^rgba?%([%s]*[0-9][0-9%.]*%%[%s]*[,%s][%s]*[0-9][0-9%.]*%%[%s]*[,%s][%s]*[0-9][0-9%.]*%%.*%)$') then
       local R, G, B = mw.ustring.match(c,'^rgba?%([%s]*([0-9][0-9%.]*)%%[%s]*[,%s][%s]*([0-9][0-9%.]*)%%[%s]*[,%s][%s]*([0-9][0-9%.]*)%%.*%)$')
       return rgbdec2lum(255*tonumber(R)/100, 255*tonumber(G)/100, 255*tonumber(B)/100)
   end
   -- remove leading # (if there is one) and whitespace
   c = mw.ustring.match(c, '^[%s#]*([a-f0-9]*)[%s]*$')
   -- split into rgb
   local cs = mw.text.split(c or , )
   if( #cs == 6 or #cs == 8 ) then -- Accepts 6 digits (#RRGGBB) or 8 digits (#RRGGBBAA)
       local R = 16*tonumber('0x' .. cs[1]) + tonumber('0x' .. cs[2])
       local G = 16*tonumber('0x' .. cs[3]) + tonumber('0x' .. cs[4])
       local B = 16*tonumber('0x' .. cs[5]) + tonumber('0x' .. cs[6])
       return rgbdec2lum(R, G, B)
   elseif ( #cs == 3 or #cs == 4 ) then -- Accepts 3 digits (#RGB) or 4 digits (#RGBA)
       local R = 16*tonumber('0x' .. cs[1]) + tonumber('0x' .. cs[1])
       local G = 16*tonumber('0x' .. cs[2]) + tonumber('0x' .. cs[2])
       local B = 16*tonumber('0x' .. cs[3]) + tonumber('0x' .. cs[3])
       return rgbdec2lum(R, G, B)
   end
   -- failure, return blank
   return 

end

-- This exports the function for use in other modules. -- The colour is passed as a string. function p._lum(color)

   return color2lum(color)

end

function p._greatercontrast(args)

   local bias = tonumber(args['bias'] or '0') or 0
   local css = (args['css'] and args['css'] ~= ) and true or false
   local v1 = color2lum(args[1] or )
   local c2 = args[2] or 'white'
   local v2 = color2lum(c2)
   local c3 = args[3] or 'black'
   local v3 = color2lum(c3)
   local ratio1 = -1;
   local ratio2 = -1;
   if (type(v1) == 'number' and type(v2) == 'number') then
       ratio1 = (v2 + 0.05)/(v1 + 0.05)
       ratio1 = (ratio1 < 1) and 1/ratio1 or ratio1
   end
   if (type(v1) == 'number' and type(v3) == 'number') then
       ratio2 = (v3 + 0.05)/(v1 + 0.05)
       ratio2 = (ratio2 < 1) and 1/ratio2 or ratio2
   end
   if css then
       local c1 = args[1] or 
       if mw.ustring.match(c1, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') or
           mw.ustring.match(c1, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') then
               c1 = '#' .. c1
       end
       if mw.ustring.match(c2, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') or
           mw.ustring.match(c2, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') then
               c2 = '#' .. c2
       end
       if mw.ustring.match(c3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') or
           mw.ustring.match(c3, '^[A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9][A-Fa-f0-9]$') then
               c3 = '#' .. c3
       end
       local css_output = 'background-color:' .. c1 .. '; color:' .. ((ratio1 > 0) and (ratio2 > 0) and ((ratio1 + bias > ratio2) and c2 or c3) or ) .. ';'
       
       -- Remove newlines (\n, \r) and extra spaces before returning CSS
       return css_output:gsub('[\r\n]', ):gsub('%s+', ' ')
   end
   return (ratio1 > 0) and (ratio2 > 0) and ((ratio1 + bias > ratio2) and c2 or c3) or 

end

function p._ratio(args)

   local v1 = color2lum(args[1])
   local v2 = color2lum(args[2])
   if (type(v1) == 'number' and type(v2) == 'number') then
       -- v1 should be the brighter of the two.
       if v2 > v1 then
           v1, v2 = v2, v1
       end
       return (v1 + 0.05)/(v2 + 0.05)
   else
       return args['error'] or '?'
   end

end

function p._styleratio(args)

   local style = (args[1] or ):lower()
   local lum_bg, lum_fg = 1, 0
   if args[2] then
       local lum = color2lum(args[2])
       if lum ~=  then lum_bg = lum end
   end
   if args[3] then
       local lum = color2lum(args[3])
       if lum ~=  then lum_fg = lum end
   end
   local slist = mw.text.split(mw.ustring.gsub(mw.ustring.gsub(style or , '&#[Xx]23;', '#'), '#', '#'), ';')
   for k = 1,#slist do
       local s = slist[k]
       local prop, val = s:match( '^[%s]*([^:]-):([^:]-)[%s;]*$' )
       prop = prop or 
       val = val or 
       if (prop:match('^[%s]*(background)[%s]*$') or prop:match('^[%s]*(background%-color)[%s]*$')) then
           local lum = color2lum(val)
           if( lum ~=  ) then lum_bg = lum end
       elseif (prop:match('^[%s]*(color)[%s]*$')) then
           local lum = color2lum(val)
           if( lum ~=  ) then lum_fg = lum end
       end
   end
   if lum_bg > lum_fg then
       return (lum_bg + 0.05)/(lum_fg + 0.05)
   else
       return (lum_fg + 0.05)/(lum_bg + 0.05)
   end

end

--[[ Use Script error: The function "somecolor" does not exist. directly or Script error: You must specify a function to call. from a wrapper template.

Parameters:

   -- |1=    — required; A color to check.

--]] function p.lum(frame)

   local color = frame.args[1] or frame:getParent().args[1]
   return p._lum(color)

end

function p.ratio(frame)

   local args = frame.args[1] and frame.args or frame:getParent().args
   return p._ratio(args)

end

function p.styleratio(frame)

   local args = frame.args[1] and frame.args or frame:getParent().args
   return p._styleratio(args)

end

function p.greatercontrast(frame)

   local args = frame.args[1] and frame.args or frame:getParent().args
   return p._greatercontrast(args)

end

return p </syntaxhighlight> Template:Collapse-bottom --GKNishimoto (talk) 05:16, 20 July 2026 (UTC)

I've gone ahead and done some cleanup on the code and everything generally looks good, but I do have a question. Since this adds support for specifying colours with an alpha, is it correct behaviour to just ignore that alpha property entirely? It feels like that should matter for the purpose of calculating the ratio, though I'm not familiar with the exact scope of this module. Aidan9382 (talk) 21:33, 26 July 2026 (UTC)
Hi @Aidan9382,
Thanks for reviewing and refining the code in the sandbox! The regex fix in '_greatercontrast' to support 4-digit and 8-digit hex values when using the 'css' flag is a great spot.
To answer your question directly: Yes, ignoring the alpha component is the intended and correct behavior for this module.
Under the WCAG 2.1 accessibility guidelines, computing the exact contrast ratio of a semi-transparent color requires blending it against whatever background is rendered behind it. Since this Lua module evaluates color values statically without access to the surrounding page layout or DOM context, it cannot reliably determine the underlying background layer.
Stripping or ignoring the alpha channel allows the module to gracefully fall back to evaluating the base RGB/HSL contrast without throwing errors or returning blank values when editors pass modern CSS formats (such as '#RRGGBBAA' or 'rgba()').
The code currently in the sandbox looks great and is ready to be applied. Thanks again for the help! -- GKNishimoto (talk) 21:42, 26 July 2026 (UTC)
P.S. Thanks again! Once this goes live here, I'll be porting this updated version directly to ptwiki as well to keep both modules completely aligned.-- GKNishimoto (talk) 21:51, 26 July 2026 (UTC)
In that case this seems fine,  Done. Aidan9382 (talk) 12:42, 27 July 2026 (UTC)
This change broke the layout at Template:Basketballbox/testcases for transclusions using <syntaxhighlight lang="wikitext" inline style="display: none;" />|bg=. Please test the sandbox module with that template. – Jonesey95 (talk) 16:11, 27 July 2026 (UTC)
More specifically, it caused output in the form of <syntaxhighlight lang="text" class="" style="" inline="1">background-color:#eeeeee; color:#202122;1</syntaxhighlight>, breaking following css rules. LittlePuppers (talk) 16:16, 27 July 2026 (UTC)
Well, it seems I didn't aniticipate that scribunto would just implicitly concatenate multiple returns instead of taking just the first return, which gsub will cause. Thanks for that. I've gone ahead and fixed that in the sandbox. Aidan9382 (talk) 16:23, 27 July 2026 (UTC)
Updated module sandbox to fix the CSS output formatting issue. Removed the :gsub() whitespace/newline compression that was causing multiple return values and appending an unintended '1' to the style string, fully resolving the layout breaks on templates using |bg= (such as Basketballbox) while retaining all new CSS3/CSS4 color format features.
While Aidan9382's quick fix in the sandbox solved the immediate multiple-return bug by keeping a single :gsub('[\r\n]', ''), completely removing the extra string manipulation—as we did in our version—is cleaner and fully avoids any potential edge cases with complex template markup or string concatenation in Scribunto. It keeps the CSS output pure, exact, and 100% robust. --GKNishimoto (talk) 17:46, 27 July 2026 (UTC)