Generates a table of Palette variables along with contrast information. See MediaWiki:Gadget-Palette/table.


local export = {}

-- Given a CSS hex value, return a table of the RGB components.
-- The RGB is normalized so that white is (1.0, 1.0, 1.0).
local function get_rgb_from_hex(hex_colour)
	hex_colour = hex_colour:gsub("#", "")

	if #hex_colour == 3 then
		return {
			tonumber(hex_colour:sub(1, 1), 16) * 17 / 255,
			tonumber(hex_colour:sub(2, 2), 16) * 17 / 255,
			tonumber(hex_colour:sub(3, 3), 16) * 17 / 255
		}
	elseif #hex_colour == 6 then
		return {
			tonumber(hex_colour:sub(1, 2), 16) / 255,
			tonumber(hex_colour:sub(3, 4), 16) / 255,
			tonumber(hex_colour:sub(5, 6), 16) / 255
		}
	end
end

-- Calculate the WCAG2.1 relative luminance value given a RGB tuple.
-- https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html#dfn-relative-luminance
local function get_WCAG21_luminance(rgb)
	local luminance_parts = {}

	for _, rgb_part in ipairs(rgb) do
		if rgb_part > 0.04045 then
			table.insert(luminance_parts, ((rgb_part + 0.055) / 1.055) ^ 2.4)
		else
			table.insert(luminance_parts, rgb_part / 12.92)
		end
	end

	local luminance = 0.2126 * luminance_parts[1] + 0.7152 * luminance_parts[2] + 0.0722 * luminance_parts[3]
	return luminance
end

-- Calculate the WCAG2.1 contrast ratio given two RGB tuples.
-- https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html#dfn-contrast-ratio
local function get_WCAG21_contrast(rgb1, rgb2)
	local luminance1 = get_WCAG21_luminance(rgb1)
	local luminance2 = get_WCAG21_luminance(rgb2)

	-- The lighter colour is used in the numerator.
	local contrast_ratio
	if luminance1 >= luminance2 then
		contrast_ratio = (luminance1 + 0.05) / (luminance2 + 0.05)
	else
		contrast_ratio = (luminance2 + 0.05) / (luminance1 + 0.05)
	end

	return contrast_ratio
end

-- Calculate the APCA contrast ratio given a text RGB table and a background RGB table.
-- https://github.com/Myndex/SAPC-APCA/blob/master/documentation/APCA-W3-LaTeX.md
-- The variables are named to match the LaTeX document.
local function get_APCA_contrast(rgb_txt, rgb_bg)
	local Ys_txt = rgb_txt[1] ^ 2.4 * 0.2126729 + rgb_txt[2] ^ 2.4 * 0.7151522 + rgb_txt[3] ^ 2.4 * 0.0721750
	local Ys_bg = rgb_bg[1] ^ 2.4 * 0.2126729 + rgb_bg[2] ^ 2.4 * 0.7151522 + rgb_bg[3] ^ 2.4 * 0.0721750

	local function f_sc(Y_c)
		if Y_c <= 0 then
			return 0
		elseif Y_c <= 0.022 then
			return Y_c + (0.022 - Y_c) ^ 1.414
		else
			return Y_c
		end
	end

	local Y_txt = f_sc(Ys_txt)
	local Y_bg = f_sc(Ys_bg)

	local S_apc
	if Y_bg > Y_txt then
		S_apc = (Y_bg ^ 0.56 - Y_txt ^ 0.57) * 1.14
	else
		S_apc = (Y_bg ^ 0.65 - Y_txt ^ 0.62) * 1.14
	end

	local L_c
	if math.abs(S_apc) < 0.1 then
		L_c = 0
	elseif S_apc > 0 then
		L_c = (S_apc - 0.027) * 100
	else
		L_c = (S_apc + 0.027) * 100
	end

	return L_c
end

-- Get the WCAG2.1 rating as HTML, given a contrast value.
local function get_WCAG21_rating(contrast_value)
	if contrast_value >= 7 then
		return "<span style=\"color:var(--wikt-palette-deepblue)\">(AAA)</span>"
	elseif contrast_value >= 4.5 then
		return "<span style=\"color:var(--wikt-palette-forestgreen)\">(AA)</span>"
	else
		return "<span style=\"color:var(--wikt-palette-red)\">(FAIL)</span>"
	end
end

-- Get the APCA rating as HTML, given a contrast value.
-- Using defaults text values: font size = 14px, font weight = 400
local function get_APCA_rating(contrast_value)
	local abs_contrast_value = math.abs(contrast_value)
	if abs_contrast_value >= 100 then
		return "<span style=\"color:var(--wikt-palette-deepblue)\">(R4)</span>"
	elseif abs_contrast_value >= 95 then
		return "<span style=\"color:var(--wikt-palette-forestgreen)\">(R3)</span>"
	elseif abs_contrast_value >= 90 then
		return "<span style=\"color:var(--wikt-palette-forestgreen)\">(R2)</span>"
	elseif abs_contrast_value >= 85 then
		return "<span style=\"color:var(--wikt-palette-forestgreen)\">(R1)</span>"
	else
		return "<span style=\"color:var(--wikt-palette-red)\">(R0)</span>"
	end
end

function export.show(frame)
	local pageContent = mw.title.new("MediaWiki:Gadget-Palette.css"):getContent()

	local lightMode = true;
	local lightModeData = {}
	local darkModeData = {}
	local seenVariables = {} -- In order to maintain the order.

	local output = {
[=[{| class="palette-table wikitable"
|+Table of palette variables in light and dark mode
|-
!scope="col"| Variable name
!scope="col"| Light mode
!scope="col"| Dark mode
!scope="col"| Use as a background colour?
!scope="col"| Use as a text colour?
|-"
]=]}
	for line in pageContent:gmatch("[^\r\n]*") do
		-- Hack to check whether we've reached the dark mode styles.
		if line == "@media screen {" then
			lightMode = false;
		end

		local definedVariable = line:match("--wikt%-palette%-[a-z]+")
		if definedVariable then
			if lightMode then
				table.insert(seenVariables, definedVariable)
				lightModeData[definedVariable] = line:match("#[a-zA-Z0-9]+")
			else
				darkModeData[definedVariable] = line:match("#[a-zA-Z0-9]+")
			end
		end
	end

	for _, var in ipairs(seenVariables) do
		table.insert(output, "|<code>" .. var .. "</code>")
		table.insert(output, "||class=\"wikt-auto-contrast-exempt\" style=\"background:" .. lightModeData[var] .. "\"|<span class=\"palette-highlight\">" .. lightModeData[var] .. "</span>")
		table.insert(output, "||class=\"wikt-auto-contrast-exempt\" style=\"background:" .. darkModeData[var] .. "\"|<span class=\"palette-highlight\">" .. darkModeData[var] .. "</span>")

		-- Contrast on body text in light mode.
		local WCAG_21_contrast_text_lightmode = get_WCAG21_contrast(get_rgb_from_hex("#202122"), get_rgb_from_hex(lightModeData[var]))
		local APCA_contrast_text_lightmode = get_APCA_contrast(get_rgb_from_hex("#202122"), get_rgb_from_hex(lightModeData[var]))

		table.insert(output, "||'''Light mode:''' WCAG " .. string.format("%.2f", WCAG_21_contrast_text_lightmode) .. " " .. get_WCAG21_rating(WCAG_21_contrast_text_lightmode))
		table.insert(output, ", APCA " .. string.format("%.2f", APCA_contrast_text_lightmode) .. " " .. get_APCA_rating(APCA_contrast_text_lightmode))

		-- Contrast on body text in dark mode.
		local WCAG_21_contrast_text_darkmode = get_WCAG21_contrast(get_rgb_from_hex("#eaecf0"), get_rgb_from_hex(darkModeData[var]))
		local APCA_contrast_text_darkmode = get_APCA_contrast(get_rgb_from_hex("#eaecf0"), get_rgb_from_hex(darkModeData[var]))

		table.insert(output, "<br> '''Dark mode:''' WCAG " .. string.format("%.2f", WCAG_21_contrast_text_darkmode) .. " " .. get_WCAG21_rating(WCAG_21_contrast_text_darkmode))
		table.insert(output, ", APCA " .. string.format("%.2f", APCA_contrast_text_darkmode) .. " " .. get_APCA_rating(APCA_contrast_text_darkmode))

		-- Contrast on default background in light mode.
		local WCAG_21_contrast_bg_lightmode = get_WCAG21_contrast(get_rgb_from_hex(lightModeData[var]), get_rgb_from_hex("#ffffff"))
		local APCA_contrast_bg_lightmode = get_APCA_contrast(get_rgb_from_hex(lightModeData[var]), get_rgb_from_hex("#ffffff"))

		table.insert(output, "||'''Light mode:''' WCAG " .. string.format("%.2f", WCAG_21_contrast_bg_lightmode) .. " " .. get_WCAG21_rating(WCAG_21_contrast_bg_lightmode))
		table.insert(output, ", APCA " .. string.format("%.2f", APCA_contrast_bg_lightmode) .. " " .. get_APCA_rating(APCA_contrast_bg_lightmode))

		-- Contrast on default background in dark mode.
		local WCAG_21_contrast_bg_darkmode = get_WCAG21_contrast(get_rgb_from_hex(darkModeData[var]), get_rgb_from_hex("#101418"))
		local APCA_contrast_bg_darkmode = get_APCA_contrast(get_rgb_from_hex(darkModeData[var]), get_rgb_from_hex("#101418"))

		table.insert(output, "<br> '''Dark mode:''' WCAG " .. string.format("%.2f", WCAG_21_contrast_bg_darkmode) .. " " .. get_WCAG21_rating(WCAG_21_contrast_bg_darkmode))
		table.insert(output, ", APCA " .. string.format("%.2f", APCA_contrast_bg_darkmode) .. " " .. get_APCA_rating(APCA_contrast_bg_darkmode))

		table.insert(output, "\n|-\n")
	end

	table.insert(output, "|-\n")
	table.insert(output, "|colspan=\"5\" style=\"padding:1em;background:var(--wikt-palette-lavender,#f8f8ff)\"|According to [[mw:Recommendations for night mode compatibility on Wikimedia wikis]], editors should ensure that colours meet the WCAC 2.1 AA contrast standards (APCA is significantly stricter but corresponds more closely to human visual perception — see [[phab:T308772]]). Contrast ratios are calculated assuming standard text formatting (14px, unbolded) and standard colours on the rest of the page. To check contrast values between any two colours, see https://contrast.tools/.\n")
	table.insert(output, "\n|}")
	table.insert(output, frame:extensionTag("templatestyles", "", {src="Module:palette/styles.css"}))

	return table.concat(output)
end

return export