Jump to content

Module:User contrib/sandbox

From Wikisource
require('strict')
--[=[
Implementation logic for [[Template:User contrib]]
]=]

local p = {} --p stands for package

local getArgs = require('Module:Arguments').getArgs
local yesno = require('Module:Yesno')
local greatercontrast = require('Module:Color contrast')['_greatercontrast']
local userbox = require('Module:Userbox').userbox

--[=[
Color scheme based on n
]=]

--[=[
Ensure a number is within a range
]=]
local function number_in_range(args)
	if tonumber(args[1]) then
		return math.min(math.max(tonumber(args[1]), args[2] or 0), args[3] or 1)
	else
		return nil
	end
end

--[=[
Convert hex, HSL, or RBG color to RGB(A) table
]=]
local function color_to_rgb_table(c)
	if c == nil or c == "" then
		return nil
	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()
	
	local ctable
	
	-- convert from rgb
	if mw.ustring.match(c, '^rgb%([%s]*[0-9][0-9]*[%s]*,[%s]*[0-9][0-9]*[%s]*,[%s]*[0-9][0-9]*[%s]*%)$') then
		local R, G, B = mw.ustring.match(c, '^rgb%([%s]*([0-9][0-9]*)[%s]*,[%s]*([0-9][0-9]*)[%s]*,[%s]*([0-9][0-9]*)[%s]*%)$')
		ctable = {tonumber(R), tonumber(G), tonumber(B)}
	end
	
	-- convert from rgba
	if mw.ustring.match(c, '^rgb%([%s]*[0-9][0-9]*[%s]*,[%s]*[0-9][0-9]*[%s]*,[%s]*[0-9][0-9]*[%s]*,[%s]*[0-9][0-9]*[%s]*%)$') then
		local R, G, B, A = mw.ustring.match(c, '^rgb%([%s]*([0-9][0-9]*)[%s]*,[%s]*([0-9][0-9]*)[%s]*,[%s]*([0-9][0-9]*)[%s]*,[%s]*([0-9][0-9]*)[%s]*%)$')
		ctable = {tonumber(R), tonumber(G), tonumber(B), tonumber(A)}
	end
	
	-- convert from rgb percent
	if mw.ustring.match(c, '^rgb%([%s]*[0-9][0-9%.]*%%[%s]*,[%s]*[0-9][0-9%.]*%%[%s]*,[%s]*[0-9][0-9%.]*%%[%s]*%)$') then
		local R, G, B = mw.ustring.match(c, '^rgb%([%s]*([0-9][0-9%.]*)%%[%s]*,[%s]*([0-9][0-9%.]*)%%[%s]*,[%s]*([0-9][0-9%.]*)%%[%s]*%)$')
		ctable = {255*tonumber(R)/100, 255*tonumber(G)/100, 255*tonumber(B)/100}
	end
	
	-- convert from rgba percent
	if mw.ustring.match(c, '^rgb%([%s]*[0-9][0-9%.]*%%[%s]*,[%s]*[0-9][0-9%.]*%%[%s]*,[%s]*[0-9][0-9%.]*%%[%s]*,[%s]*[0-9][0-9%.]*%%[%s]*%)$') then
		local R, G, B, A = mw.ustring.match(c, '^rgb%([%s]*([0-9][0-9%.]*)%%[%s]*,[%s]*([0-9][0-9%.]*)%%[%s]*,[%s]*([0-9][0-9%.]*)%%[%s]*,[%s]*([0-9][0-9%.]*)%%[%s]*%)$')
		ctable = {255*tonumber(R)/100, 255*tonumber(G)/100, 255*tonumber(B)/100, 255*tonumber(A)/100}
	end
	
	-- convert from hsl
	if mw.ustring.match(c, '^hsl%([%s]*[0-9][0-9%.]*[%s]*,[%s]*[0-9][0-9%.]*%%[%s]*,[%s]*[0-9][0-9%.]*%%[%s]*%)$') then
		local H, S, L = mw.ustring.match(c, '^hsl%([%s]*([0-9][0-9%.]*)[%s]*,[%s]*([0-9][0-9%.]*)%%[%s]*,[%s]*([0-9][0-9%.]*)%%[%s]*%)$')
		H, S, L = math.fmod(number_in_range({H, 0, 360}), 360), number_in_range({S}), number_in_range({L})
		
		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 H < 60 then
			R = R + C
			G = G + X
		elseif H < 120 then
			R = R + X
			G = G + C
		elseif H < 180 then
			G = G + C
			B = B + X
		elseif H < 240 then
			G = G + X
			B = B + C
		elseif H < 300 then
			R = R + X
			B = B + C
		elseif H < 360 then
			R = R + C
			B = B + X
		end
		
		ctable = {255 * R, 255 * G, 255 * B}
	end
	
	-- convert from hex
	
	-- remove leading # (if there is one) and whitespace
	c = mw.ustring.match(c, '^[%s#]*([a-f0-9]*)[%s]*$')
	-- split into rgb(a)
	local cs = mw.text.split(c or '', '')
	if #cs == 6 then
		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])
		ctable = {R, G, B}
	elseif #cs == 3  then
		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])
		ctable = {R, G, B}
	elseif #cs == 8 then
		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])
		local A = 16*tonumber('0x' .. cs[7]) + tonumber('0x' .. cs[8])
		ctable = {R, G, B, A}
	elseif #cs == 4 then
		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])
		local A = 16*tonumber('0x' .. cs[4]) + tonumber('0x' .. cs[4])
		ctable = {R, G, B, A}
	else
		ctable = nil
	end
	
	return ctable
end

local function blend_two_colors(args)
	local color1 = color_to_rgb_table(args[1] or "#ffffff")
	local color2 = color_to_rgb_table(args[2] or "#ffffff")
	local ratio = number_in_range({args[3]}) or 0.5
	
	local blend = {}
	for i = 1, math.min(#color1, #color2) do
		blend[i] = color1[i] * ratio + color2[i] * (1 - ratio)
	end
	return "rgb(" .. table.concat(blend, ", ") .. ")"
end

local function bg_colors(n)
	local max_n = 100000
	local colors = {
		"#000000", "#003208", "#006411", "#198616", "#39a11a", "#60b71e", "#8dc722", "#b7d828", "#dbea31", "#fffb3b",
		"#e9e559", "#afdb63", "#7dd06e", "#54c279", "#3ab082", "#269988", "#197f8c", "#175a85", "#193079", "#1f0266",
		"#000000", "#39154d", "#742975", "#b43c51", "#dd562f", "#fa730d", "#fd9719", "#ffb834", "#ffd76b", "#ffedb1",
		"#e9d982", "#f0d17c", "#efbd7d", "#e8a184", "#dd8590", "#cf719d", "#bc6cac", "#a679be", "#8c94d2", "#71b1e5",
		"#9ccff1", "#cbe5d4", "#e8f2b2", "#f3ef9e", "#f4e097", "#f1cb98", "#eab7a1", "#dfa8be", "#d097ee", "#c278f0"
	}
	
	local color_index = math.floor((n % (max_n/2)) * #colors / (max_n/2)) + 1
	local base_color
	if n < max_n then
		base_color = colors[color_index]
	else
		base_color = "#ffcc33"
	end
	local blended_color = blend_two_colors({base_color, "#ffffff", 0.5})
	
	if n < max_n/2 then
		return {
			['id_bg'] = blended_color,
			['info_bg'] = base_color
		}
	else
		return {
			['id_bg'] = base_color,
			['info_bg'] = blended_color
		}
	end
end

--[=[
Light mode or dark mode based on background color
]=]
local function light_dark_class(bg)
	local font = greatercontrast({bg})
	if font == "#FFFFFF" then
		return "darkmode"
	elseif font == "#000000" then
		return "lightmode"
	else
		return ""
	end
end

--[=[
Info message
]=]--
local function info_message(args)
	local n = args.n
	local bot = yesno(args.bot) or false
	local is_log = yesno(args.is_log) or false
	local link = args.link
	local username = mw.uri.encode(args.username or mw.title.getCurrentTitle().baseText, "WIKI")
	local actionlink = args.actionlink
	local display_n = args.display_n
	local lang = args.lang
	local deleted = args.deleted
	local articles = args.articles
	local distinct = args.distinct or args.unique
	local images = args.images
	local cur_images = args.cur_images or args['cur-images']
	local insane = yesno(args.insane) or false
	local total = yesno(args.total) or false
	
	local action1 = "user has made"
	if bot then
		action1 = "bot has logged"
	elseif is_log then
		action1 = "user has logged"
	end
	
	local actionlink = link or "https://xtools.wmflabs.org/ec/en.wikisource.org/" .. username
	
	local action2 = "contributions to"
	if n == 1 and is_log then
		action2 = "action on"
	elseif is_log == "yes" then
		action2 = "actions on"
	elseif n == 1 then
		action2 = "contribution to"
	end
	
	local actionlink_text = "[" .. actionlink .. " at least '''" .. display_n .. "''' " .. action2 .. "]"
	
	local project_name = "Wikisource"
	if lang then
		project_name = "the " .. lang .. " Wikisource"
	end
	
	local message = "This " .. action1 .. " " .. actionlink_text .. " " .. project_name
	
	if deleted then
		message = message .. ", at least '''" .. deleted .. "''' of which were to pages that are now deleted"
	end
	
	if articles then
		if deleted then
			message = message .. " and"
		else
			message = message .. ","
		end
		message = message .. " at least '''" .. articles .. "''' of which were to articles"
	end
	
	if distinct then
		if deleted or articles then
			message = message .. ","
		end
		message = message .. " on at least '''" .. distinct .. "''' pages"
	end
	
	if images then
		message = message .. ", including at least '''" .. images .. "''' images"
		if cur_images then
			message = message .. ", at least '''" .. cur_images .. "''' of which are still current"
		end
	elseif cur_images then
		message = message .. ", including at least '''" .. cur_images .. "''' images which are still current"
	end
	
	if insane then
		if images then
			message = message .. ","
		end
		message = message .. " and, as a result, may be slightly insane"
	end
	
	message = message .. "."
	
	if total then
		message = message .. " [https://en.wikisource.org/w/api.php?action=query&list=users&usprop=editcount&ususers=" .. username .. " '''(total)''']"
	end
	
	return message
end

--[=[
Make userbox
]=]
function p._user_contrib(args)
	args.n = args.n or args[1]
	local n = math.max(tonumber(args.n) or 0, 0) -- everyone has made at least zero edits
	if not yesno(args.format_n or args['format'] or true) then
		args.display_n = args.n or ""
	else
		args.display_n = n
	end
	args.n = n
	
	local id_bg = args.id_bg or args['id-bg'] or bg_colors(n).id_bg
	local info_bg = args.info_bg or args['info-bg'] or bg_colors(n).info_bg
	
	local id_s
	if mw.ustring.len(args.display_n) > 4 then
		id_s = 9
	else
		id_s = 10
	end
	
	local assignments = {
		['border-c'] = args.border,
		['id'] = args.display_n .. "+",
		['id-c'] = id_bg,
		['id-fc'] = args.id_font or args['id-font'] or greatercontrast({[1] = id_bg}),
		['id-s'] = id_s,
		['id-op'] = "white-space:nowrap;",
		['id-class'] = "user-contrib-id plainlinks neverexpand " .. light_dark_class(id_bg),
		['info'] = info_message(args),
		['info-c'] = info_bg,
		['info-fc'] = args.info_font or args['info-font'] or greatercontrast({[1] = info_bg}),
		['info-s'] = 8,
		['info-class'] = "user-contrib-info plainlinks neverexpand " .. light_dark_class(info_bg),
		['float'] = args.float
	}
	return userbox(assignments)
end

function p.user_contrib(frame)
	return p._user_contrib(getArgs(frame))
end

return p