FANDOM


--- Colors library for embedded color processing in the FANDOM environment.
--  It ports and extends functionality in the Colors JS library written by
--  [[User:Pecoes|Pecoes]]. The module supports HSL, RGB and hexadecimal web
--  colors.
--  
--  The module offers numerous features:
--   * Color parameter support in Lua modules.
--   * Color parameter insertion in wiki templates.
--   * Color variable parsing for style templating.
--   * Color item creation and conversion utilities.
--   * A vast array of color processing methods.
--   * Alpha and boolean support for flexible color logic.
--  
--  **This module will not work as expected on UCP wikis.**
--  
--  @module             colors
--  @alias              p
--  @release            unmaintained
--  @author             [[User:Speedit|Speedit]]
--  @version            2.5.2
--  @require            Module:I18n
--  @require            Module:Yesno
--  @require            Module:Entrypoint
--  <nowiki>
 
-- Module package.
local p, utils, Color = {}, {}
 
-- Module utilites, configuration/cache variables.
local yesno = require('Dev:Yesno')
local entrypoint = require('Dev:Entrypoint')
local sassParams = mw.site.sassParams or {}
 
-- Web color RGB presets.
local presets = mw.loadData('Dev:Colors/presets')
 
-- Error message data.
local i18n = require('Dev:I18n').loadMessages('Colors')
 
-- Validation ranges for color types and number formats.
local ranges = {
    rgb         = {    0, 255 },
    hsl         = {    0,   1 },
    hue         = {    0, 360 },
    percentage  = { -100, 100 },
    prop        = {    0, 100 },
    degree      = { -360, 360 }
}
 
-- Internal color utilities.
 
--- Boundary validation for color types.
--  @function           utils.check
--  @param              {string} t Range type.
--  @param              {number} n Number to validate.
--  @error[65]          {string} 'invalid color value input: type($n) "$n"'
--  @error[67]          {string} 'color value $n out of $t bounds'
--  @return             {boolean} Validity of number.
--  @local
function utils.check(t, n)
    local min = ranges[t][1] -- Boundary variables
    local max = ranges[t][2]
 
    if type(n) ~= 'number' then
        error(i18n:msg('invalid-value', type(n), tostring(n)))
    elseif n < min or n > max then
        error(i18n:msg('out-of-bounds', n, t))
    end
end
 
--- Rounding utility for color tuples.
--  @function           utils.round
--  @param              {number} tup Color tuple.
--  @param[opt]         {number} dec Number of decimal places.
--  @return             {number} Rounded tuple value.
--  @local
function utils.round(tup, dec)
    local ord = 10^(dec or 0)
    return math.floor(tup * ord + 0.5) / ord
end
 
--- Cloning utility for color items.
--  @function           utils.clone
--  @param              {table} clr Color instance.
--  @param              {string} typ Color type of clone.
--  @return             {table} New (clone) color instance.
--  @local
function utils.clone(clr, typ)
    local c = Color:new( clr.tup, clr.typ, clr.alp ) -- new color
    utils.convert(c, typ) -- conversion
    return c -- output
end
 
--- Range limiter for color processing.
--  @function           utils.limit
--  @param              {number} val Numeric value to limit.
--  @param              {number} max Maximum value for limit boundary.
--  @return             {number} Limited value.
--  @local
function utils.limit(val, max)
    return math.max(0, math.min(val, max))
end
 
--- Circular spatial processing for ranges.
--  @function           utils.circle
--  @param              {number} val Numeric value to cycle.
--  @param              {number} max Maximum value for cycle boundary.
--  @return             {number} Cyclical positive value below max.
--  @local
function utils.circle(val, max)
    if val < 0 then        -- negative; below cycle minimum
        val = val + max
    elseif val > max then  -- exceeds cycle maximum
        val = val - max
    end
    return val -- output
end
 
--- Color space converter.
--  @function           utils.convert
--  @param              {table} clr Color instance.
--  @param              {string} typ Color type to output.
--  @return             {table} Converted color instance.
--  @local
function utils.convert(clr, typ)
    if clr.typ ~= typ then
        clr.typ   = typ
        if typ == 'rgb' then
            clr.tup = utils.hslToRgb(clr.tup)
        else
            clr.tup = utils.rgbToHsl(clr.tup)
        end
    end
 
    for i, t in ipairs(clr.tup) do
        if clr.typ == 'rgb' then
            clr.tup[i] = utils.round(clr.tup[i], 0)
        elseif clr.typ == 'hsl' then
            clr.tup[i] = i == 1
                and utils.round(clr.tup[i], 0)
                or  utils.round(clr.tup[i], 2)
        end
    end
end
 
--- RGB-HSL tuple converter.
--  @function           utils.rgbToHsl
--  @param              {table} rgb Tuple table of RGB values.
--  @return             {table} Tuple table of HSL values.
--  @see                http://www.easyrgb.com/en/math.php#m_rgb_hsl
--  @local
function utils.rgbToHsl(rgb)
    for i, t in ipairs(rgb) do
        rgb[i] = t/255
    end
    local r,g,b = rgb[1], rgb[2], rgb[3]
 
    local min = math.min(r, g, b)
    local max = math.max(r, g, b)
    local d = max - min
 
    local h, s, l = 0, 0, ((min + max) / 2)
 
    if d > 0 then
        s = l < 0.5 and d / (max + min) or d / (2 - max - min)
 
        h = max == r and (g - b) / d or
            max == g and 2 + (b - r)/d or
            max == b and 4 + (r - g)/d
        h = utils.circle(h/6, 1)
    end
 
    return { h * 360, s, l }
end
 
--- HSL component conversion subroutine to RGB.
--  @function           utils.hueToRgb
--  @param              {number} p Temporary variable 1.
--  @param              {number} q Temporary variable 2.
--  @param              {number} t Modifier for primary color.
--  @return             {number} HSL component.
--  @see                http://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/
--  @local
function utils.hueToRgb(p, q, t)
    if t < 0 then
        t = t + 1
    elseif t > 1 then
        t = t - 1
    end
 
    if t < 1/6 then
        return p + (q - p) * 6 * t
    elseif t < 1/2 then
        return q
    elseif t < 2/3 then
        return p + (q - p) * (2/3 - t) * 6
    else
        return p
    end
end
 
--- HSL-RGB tuple converter.
--  @function           utils.hslToRgb
--  @param              {table} hsl Tuple table of HSL values.
--  @return             {table} Tuple table of RGB values.
--  @local
function utils.hslToRgb(hsl)
    local h, s, l = hsl[1]/360, hsl[2], hsl[3]
    local r
    local g
    local b
    local p
    local q
 
    if s == 0 then
        r, g, b = l, l, l
 
    else
        q = l < 0.5 and l * (1 + s) or l + s - l * s
 
        p = 2 * l - q
 
        r = utils.hueToRgb(p, q, h + 1/3)
        g = utils.hueToRgb(p, q, h)
        b = utils.hueToRgb(p, q, h - 1/3)
    end
 
    return { r * 255, g * 255, b * 255 }
end
 
--- Percentage-number conversion utility.
--  @function       utils.np
--  @param          {string} str Number string.
--  @param          {number} mul Upper bound number multiplier.
--  @return         {number} Bounded number.
--  @local
function utils.np(str, mul)
    str = str:match('^%s*([^%%]+)%%%s*$')
    local pct = tonumber(str)
    return pct * mul
end
 
--- CSS color functional notation parser.
--  @function       utils.parseColorSpace
--  @param          {string} str Color string.
--  @param          {string} spc Color function name.
--  @param          {table} mul Tuple color multipliers.
--  @return         {table} Color space tuple.
--  @return         {table} Color alpha value.
--  @local
function utils.parseColorSpace(str, spc, mul)
    local PTN = '^' .. spc .. 'a?%(([%d%s%%.,]+)%)$'
    local tup = mw.text.split(str:match(PTN), '[%s,]+')
 
    tup[4] = tup[4] or '1'
    local alp = tup[4]:find('%%$')
        and utils.np(tup[4], 1/100)
        or  tonumber(tup[4])
    table.remove(tup, 4)
 
    for i, t in ipairs(tup) do
        tup[i] = t:find('%%$')
            and utils.np(t, mul[i])
            or  tonumber(t)
    end
 
    return tup, alp
end
 
--- Color item class, used for color processing.
--  The class provides color prop getter-setters, procedures for color computation,
--  compositing methods and serialisation into CSS color formats.
--  @type               Color
Color = {}
Color.__index = Color
Color.__tostring = function() return 'Color' end
 
--- Color tuple.
--  @table              Color.tup
Color.tup = {}
 
--- Color space type.
--  @member             {string} Color.typ
Color.typ = ''
 
--- Color alpha channel value.
--  @member             {number} Color.alp
Color.alp = 1
 
--- Color instance constructor.
--  @function           Color:new
--  @param              {string} typ Color space type (`'hsl'` or `'rgb'`).
--  @param              {table} tup Color tuple in HSL or RGB
--  @param              {number} alp Alpha value range (`0` - `1`).
--  @error[304]         {string} 'no color data provided'
--  @error[309]         {string} 'invalid color type "$1" specified'
--  @return             {Color} Color instance.
function Color:new(tup, typ, alp)
    local o = {}
    setmetatable(o, self)
 
    -- is color tuple valid?
    if type(tup) ~= 'table' or #tup ~= 3 then
        error(i18n:msg('no-data'))
    end
 
    -- is color type valid?
    if typ ~= 'rgb' and typ ~= 'hsl' then
        error(i18n:msg('invalid-type', typ))
    end
 
    -- are color tuple entries valid?
    for n = 1, 3 do
        utils.check( (n == 1 and typ == 'hsl') and 'hue' or typ, tup[n])
    end
    utils.check('hsl', alp)
 
    o.tup = tup
    o.typ = typ
    o.alp = alp
    return o
end
 
--- Color string default output.
--  @function           Color:string
--  @return             {string} Hexadecimal 6-digit or HSLA color string.
--  @usage              colors.parse('hsl(214, 15%, 30%)'):string() == '#404a57'
--  @usage              colors.parse('#404a5780'):string() == 'hsl(214, 15%, 30%, 0.5)'
function Color:string()
    return self.alp ~= 1 and self:hsl() or self:hex()
end
 
--- Color hexadecimal string output.
--  @function           Color:hex
--  @return             {string} Hexadecimal color string.
--  @usage              colors.parse('hsl(214, 15%, 30%)'):hex() == '#404a57'
function Color:hex()
    local this = utils.clone(self, 'rgb')
    local hex = '#'
 
    for i, t in ipairs(this.tup) do
        -- Hexadecimal conversion.
        hex = #string.format('%x', t) == 1 -- leftpad
            and hex .. '0' .. string.format('%x', t)
            or hex .. string.format('%x', t)
    end
 
    local alp = string.format('%x', this.alp * 255)
    if alp ~= 'ff' then
        hex = #alp == 1 and hex .. '0' .. alp or hex .. alp
    end
 
    return hex
end
 
--- RGBA functional color string output.
--  @function           Color:rgb
--  @return             {string} RGBA color string.
--  @see                https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#rgb()_and_rgba()
--  @usage              colors.parse('hsl(214, 15%, 30%)'):rgb() == 'rgb(64, 74, 87)'
function Color:rgb()
    local this = utils.clone(self, 'rgb')
 
    return this.alp ~= 1
        and 'rgba(' .. table.concat(this.tup, ', ') .. ', ' .. this.alp .. ')'
        or  'rgb(' .. table.concat(this.tup, ', ') .. ')'
end
 
--- HSL functional color string output.
--  @function           Color:hsl
--  @return             {string} HSLA color string.
--  @see                https://developer.mozilla.org/en-US/docs/Web/CSS/color_value#hsl()_and_hsla()
--  @usage              colors.parse('rgb(64, 74, 87)'):hsl() == 'hsl(214, 15%, 30%)'
function Color:hsl()
    local this = utils.clone(self, 'hsl')
 
    for i, t in ipairs(this.tup) do
        if i == 2 or i == 3 then
            this.tup[i] = tostring(t*100) .. '%'
        end
    end
 
    return this.alp ~= 1
        and 'hsla(' .. table.concat(this.tup, ', ') .. ', ' .. this.alp .. ')'
        or  'hsl(' .. table.concat(this.tup, ', ') .. ')' 
end
 
--- Red color property getter-setter.
--  @function           Color:red
--  @param[opt]         {number} val Red value to set (`1` - `255`).
--  @return             {Color} Color instance.
--  @usage              colors.parse('#000000'):red(255):string() == '#ff0000'
function Color:red(val)
    local this = utils.clone(self, 'rgb')
    if val then
        utils.check('rgb', val)
 
        this.tup[1] = val
        return this -- chainable
    else
        return this.tup[1]
    end
end
 
--- Green color property getter-setter.
--  @function           Color:green
--  @param[opt]         {number} val Green value to set (`1` - `255`).
--  @return             {Color} Color instance.
--  @usage              colors.parse('#ffffff'):green(1):string() == '#ff00ff'
function Color:green(val)
    local this = utils.clone(self, 'rgb')
    if val then
        utils.check('rgb', val)
 
        this.tup[2] = val
        return this -- chainable
    else
        return this.tup[2]
    end
end
 
--- Blue color property getter-setter.
--  @function           Color:blue
--  @param[opt]         {number} val Blue value to set (`1` - `255`).
--  @return             {Color} Color instance.
--  @usage              colors.parse('#00ff00'):blue(255):string() == '#00ffff'
function Color:blue(val)
    local this = utils.clone(self, 'rgb')
    if val then
        utils.check('rgb', val)
 
        this.tup[3] = val
        return this -- chainable
    else
        return this.tup[3]
    end
end
 
--- Hue color property getter-setter.
--  @function           Color:hue
--  @param[opt]         {number} val Hue value to set (`0` - `360`).
--  @return             {Color} Color instance.
--  @usage              colors.parse('#ff0000'):hue(180):string() == '#00ffff'
function Color:hue(val) 
    local this = utils.clone(self, 'hsl')
    if val then
        utils.check('hue', val)
 
        this.tup[1] = val
        return this -- chainable
    else
        return this.tup[1]
    end
end
 
--- Saturation color property getter-setter.
--  @function           Color:sat
--  @param[opt]         {number} val Saturation value to set (`0` - `100`).
--  @return             {Color} Color instance.
--  @usage              colors.parse('#ff0000'):sat(0):string() == '#808080'
function Color:sat(val)
    local this = utils.clone(self, 'hsl')
    if val then
        val = val / 100
        utils.check('hsl', val)
 
        this.tup[2] = val
        return this -- chainable
    else
        return this.tup[2]
    end
end
 
--- Lightness color property getter-setter.
--  @function           Color:lum
--  @param[opt]         {number} val Luminosity value to set (`0` - `100`).
--  @return             {Color} Color instance.
--  @usage              colors.parse('#ff0000'):lum(0):string() == '#000000'
function Color:lum(val)
    local this = utils.clone(self, 'hsl')
    if val then
        val = val / 100
        utils.check('hsl', val)
 
        this.tup[3] = val
        return this -- chainable
    else
        return this.tup[3]
    end
end
 
--- Alpha getter-setter for color compositing.
--  @function           Color:alpha
--  @param              {number} val Modifier 0 - 100.
--  @return             {Color} Color instance.
--  @usage              colors.parse('#ffffff'):alpha(0):string() == 'hsla(0, 0%, 0%, 0)'
function Color:alpha(val)
    if val then
        utils.check('prop', val)
        self.alp = val / 100
 
        return self
    else
        return self.alp
    end
end
 
--- Post-processing operator for color hue rotation.
--  @function           Color:rotate
--  @param              {number} mod Modifier (`-360` - `360`).
--  @return             {Color} Color instance.
--  @usage              colors.parse('#ff0000'):rotate(60):string() == '#ffff00'
function Color:rotate(mod) 
    utils.check('degree', mod)
    local this = utils.clone(self, 'hsl')
    this.tup[1] = utils.circle(this.tup[1] + mod, 360)
    return this
end
 
--- Post-processing operator for web color saturation.
--  @function           Color:saturate
--  @param              {number} mod Modifier (`-100` - `100`).
--  @return             {Color} Color instance.
--  @usage              colors.parse('#ff0000'):saturate(-25):string() == '#df2020'
function Color:saturate(mod)
    utils.check('percentage', mod)
    local this = utils.clone(self, 'hsl')
    this.tup[2] = utils.limit(this.tup[2] + (mod / 100), 1)
    return this
end
 
--- Post-processing operator for web color lightness.
--  @function           Color:lighten
--  @param              {number} mod Modifier (`-100` - `100`).
--  @return             {Color} Color instance.
--  @usage              colors.parse('#ff0000'):lighten(25):string() == '#ff8080'
function Color:lighten(mod)
    utils.check('percentage', mod)
    local this = utils.clone(self, 'hsl')
    this.tup[3] = utils.limit(this.tup[3] + (mod / 100), 1)
    return this
end
 
--- Opacification utility for color compositing.
--  @function           Color:opacify
--  @param              {number} mod Modifier (`-100` - `100`).
--  @return             {Color} Color instance.
--  @usage              colors.parse('#ffffff'):opacify(-25):string() == 'hsla(0, 0%, 100%, 0.75)'
function Color:opacify(mod)
    utils.check('percentage', mod)
    self.alp = utils.limit(self.alp + (mod / 100), 1)
    return self
end
 
--- Color additive mixing utility.
--  @function           Color:mix
--  @param              {string|table} other Module-compatible color string or instance.
--  @param[opt]         {number} weight Color weight of original (`0` - `100`). Default: `50`.
--  @return             {Color} Color instance.
--  @usage              colors.parse('#fff'):mix('#000', 80):hex() == '#cccccc'
function Color:mix(other, weight)
    if not p.instance(other) then
        other = p.parse(other)
        utils.convert(other, 'rgb')
    else
        other = utils.clone(other, 'rgb')
    end
 
    weight = weight or 50
    utils.check('prop', weight)
    weight = weight/100
    local this = utils.clone(self, 'rgb')
 
    for i, t in ipairs(this.tup) do
        this.tup[i] = t * weight + other.tup[i] * (1 - weight)
        this.tup[i] = utils.limit(this.tup[i], 255)
    end
    return this
end
 
--- Color inversion utility.
--  @function           Color:invert
--  @return             {Color} Color instance.
--  @usage              colors.parse('#ffffff'):invert():hex() == '#000000'
function Color:invert()
    local this = utils.clone(self, 'rgb')
 
    for i, t in ipairs(this.tup) do
        this.tup[i] = 255 - t
    end
    return this
end
 
--- Complementary color utility.
--  @function           Color:complement
--  @return             {Color} Color instance.
--  @usage              colors.parse('#ff8000'):complement():hex() == '#0080ff'
function Color:complement()
    return self:rotate(180)
end
 
--- Color brightness status testing.
--  @function           Color:bright
--  @param[opt]         {number} lim Luminosity threshold. Default: `50`.
--  @return             {boolean} Boolean for tone beyond threshold.
--  @usage              colors.parse('#ff8000'):bright() == true
--  @usage              colors.parse('#ff8000'):bright(60) == false
function Color:bright(lim)
    lim = lim and tonumber(lim)/100 or 0.5
    local this = utils.clone(self, 'hsl')
    return this.tup[3] >= lim
end
 
--- Color luminance status testing.
--  @function            Color:luminant
--  @param[opt]          {number} lim Luminance threshold. Default: `50`.
--  @return              {boolean} Boolean for luminance beyond threshold.
--  @see                 [[wikipedia:Relative luminance|Relative luminance (Wikipedia)]]
--  @usage               colors.parse('#ffff00'):luminant() == true
--  @usage               colors.parse('#ffff00'):luminant(95) == false
function Color:luminant(lim)
    lim = lim and tonumber(lim)/100 or 0.5
    utils.check('hsl', lim)
 
    local hsl = utils.clone(self, 'hsl')
    local sat = hsl.tup[2]
    local lum = hsl.tup[3]
    local rgb = utils.clone(self, 'rgb').tup
 
    for i, t in ipairs(rgb) do
        rgb[i] = t > 0.4045 and
            math.pow(((t + 0.05) / 1.055), 2.4) or
            t / 12.92
    end
 
    local rel =
        rgb[1] * 0.2126 +
        rgb[2] * 0.7152 +
        rgb[3] * 0.0722
 
    local quo = sat * (0.2038 * (rel - 0.5) / 0.5)
 
    return (lum >= (lim - quo))
end
 
--- Color saturation and visibility status testing.
--  @function           Color:chromatic
--  @return             {boolean} Boolean for color status.
--  @usage              colors.parse('#ffff00'):chromatic() == true
function Color:chromatic()
    local this = utils.clone(self, 'hsl')
    return this.tup[2] ~= 0 and -- sat   = not 0
           this.tup[3] ~= 0 and -- lum   = not 0
           this.alp ~= 0        -- alpha = not 0
end
 
-- Package methods and members.
 
--- Creation of RGB color instances.
--  @function           p.fromRgb
--  @param              {number} r Red value (`0` - `255`).
--  @param              {number} g Green value (`0` - `255`).
--  @param              {number} b Blue (`0` - `255`).
--  @param[opt]         {number} a Alpha value (`0` - `1`).
--  @return             {Color} Color instance.
--  @usage              colors.fromRgb(255, 255, 255, 0.2)
--  @see                Color:new
function p.fromRgb(r, g, b, a)
    return Color:new({ r, g, b }, 'rgb', a or 1);
end
 
--- Creation of HSL color instances.
--  @function           p.fromHsl
--  @param              {number} h Hue value (`0` - `360`)
--  @param              {number} s Saturation value (`0` - `1`). 
--  @param              {number} l Luminance value (`0` - `1`).
--  @param[opt]         {number} a Alpha channel (`0` - `1`).
--  @return             {Color} Color instance.
--  @usage              colors.fromHsl(0, 0, 1, 0.2)
--  @see                Color:new
function p.fromHsl(h, s, l, a)
    return Color:new({ h, s, l }, 'hsl', a or 1);
end
 
--- Parsing logic for color strings.
--  @function           p.parse
--  @param              {string} str Valid color string.
--  @error[756]         {string} 'cannot parse $str'
--  @return             {Color} Color instance.
--  @see                Color:new
--  @usage              colors.parse('#ffffff')
function p.parse(str)
    local typ
    local tup = {}
    local alp = 1
    str = mw.text.trim(str)
 
    local VAR_PTN = '^%$([%w-]+)$'
    if p.params and p.params[str:match(VAR_PTN) or ''] then
        str = p.params[str:match(VAR_PTN)]
    end
 
    -- Hexadecimal color patterns.
    local HEX_PTN_3 = '^%#(%x)(%x)(%x)$'
    local HEX_PTN_4 = '^%#(%x)(%x)(%x)(%x)$'
    local HEX_PTN_6 = '^%#(%x%x)(%x%x)(%x%x)$'
    local HEX_PTN_8 = '^%#(%x%x)(%x%x)(%x%x)(%x%x)$'
 
    -- Hexadecimal color parsing.
    if
        str:match('^%#[%x]+$')  and
        (#str == 4 or #str == 5 or -- #xxxx?
        #str == 7 or #str == 9)    -- #xxxxxxx?x?
    then
        if #str == 4 then
            tup[1], tup[2], tup[3] = str:match(HEX_PTN_3)
        elseif #str == 5 then
            tup[1], tup[2], tup[3], alp = str:match(HEX_PTN_4)
            alp = alp .. alp
        elseif #str == 7 then
            tup[1], tup[2], tup[3] = str:match(HEX_PTN_6)
            alp = 1
        elseif #str == 9 then
            tup[1], tup[2], tup[3], alp = str:match(HEX_PTN_8)
        end
 
        for i, t in ipairs(tup) do
            tup[i] = tonumber(#t == 2 and t or t .. t, 16)
        end
        if #str == 5 or #str == 9 then
            alp = tonumber(alp, 16)
        end
        typ = 'rgb'
 
    -- Color functional notation parsing.
    elseif str:find('rgba?%([%d%s,.%%]+%)') then
        tup, alp = utils.parseColorSpace(str, 'rgb', { 255, 255, 255 })
        typ = 'rgb'
 
    elseif str:find('hsla?%([%d%s,.%%]+%)') then
        tup, alp = utils.parseColorSpace(str, 'hsl', { 360, .01, .01 })
        typ = 'hsl'
 
    -- Named color parsing.
    elseif presets[str] then
        local p = presets[str]
        tup = { p[1], p[2], p[3] }
        typ = 'rgb'
 
    -- Transparent color parsing.
    elseif str == 'transparent' then
        tup = {    0,    0,    0 }
        typ = 'rgb'
        alp = 0
 
    else error(i18n:msg('unparse', (str or ''))) end
 
    return Color:new(tup, typ, alp)
end
 
--- Instance test function for colors.
--  @function           p.instance
--  @param              {Color|string} item Color item or string.
--  @return             {boolean} Whether the color item was instantiated.
--  @usage              colors.instance('#ffffff')
function p.instance(item)
    return tostring(item) == 'Color'
end
 
--- Color SASS parameter access utility for templating.
--  @function           p.wikia
--  @param              {table} frame Frame invocation object.
--  @error[778]         {string} 'invalid SASS parameter name supplied'
--  @return             {string} Color string aligning with parameter.
--  @usage              {{colors|wikia|key}}
function p.wikia(frame)
    if not frame or not frame.args[1] then
        error(i18n:msg('invalid-param'))
    end
 
    local key = mw.text.trim(frame.args[1])
    local val = p.params[key]
        and p.params[key]
        or  '<Dev:Colors: ' .. i18n:msg('invalid-param') .. '>'
 
    return mw.text.trim(val)
end
 
--- Color parameter parser for inline styling.
--  @function           p.css
--  @param              {table} frame Frame invocation object.
--  @param              {string} frame.args[1] Inline CSS stylesheet.
--  @error[799]         {string} 'no styling supplied'
--  @return             {string} CSS styling with $parameters from
--                      @{colors.params}.
--  @usage              {{colors|css|styling}}
function p.css(frame)
    if not frame.args[1] then
        error(i18n:msg('no-style'))
    end
 
    local styles = mw.text.trim(frame.args[1])
 
    local o = styles:gsub('%$([%w-]+)', p.params)
 
    return o
end
 
--- Color generator for high-contrast text.
--  @function           p.text
--  @param              {table} frame Frame invocation object.
--  @param              {string} frame.args[1] Color to process.
--  @param[opt]         {string} frame.args[2] Dark color to return.
--  @param[opt]         {string} frame.args[3] Light color to return.
--  @param[opt]         {string} frame.args.lum Whether luminance is used.
--  @error[822]         {string} 'no color supplied'
--  @return             {string} Color string `'#000000'`/$2 or
--                      `'#ffffff'`/$3.
--  @usage              {{colors|text|bg|dark color|light color}}
function p.text(frame)
    if not frame or not frame.args[1] then
        error(i18n:msg('no-color'))
    end
 
    local str = mw.text.trim(frame.args[1])
    local clr = {
        (mw.text.trim(frame.args[2] or '#000000')),
        (mw.text.trim(frame.args[3] or '#ffffff')),
    }
 
    local b = yesno(frame.args.lum, false)
        and p.parse(str):luminant()
        or  p.parse(str):bright()
 
    return b and clr[1] or clr[2]
end
 
--- CSS variables stylesheet generator.
--  @function           p.variables
--  @param              {table} frame Frame invocation object.
--  @param[opt]         {string} frame.args.s Optional number of spaces.
--  @return             {string} CSS variable declaration.
--  @usage              {{colors|variables|s {{=}} 4}}
function p.variables(frame)
    local s = frame.args.s
    local m = s ~= nil
    local n = tonumber(s)
 
    local sep = (m and n) and '\n' or ' '
    local ind = (m and n) and string.rep(' ', n) or ''
 
    local ret = {}
 
    local sortkeys = {}
    for k, p in pairs(p.params) do
        table.insert(sortkeys, k)
    end
    table.sort(sortkeys)
 
    table.insert(ret, ':root {')
    for i, k in ipairs(sortkeys) do
        local val = p.params[k]
        if
            tostring(val) ~= 'false' and
            tostring(val) ~= 'true'
        then
            if tonumber(val) ~= nil then
                val = tostring(val) .. 'px'
            elseif #mw.text.trim(val) == 0 then
                val = '""'
            end
 
            local prop = table.concat({
                ind,
                '--', k, ': ',
                val, ';'
            })
            table.insert(ret, prop)
        end
    end
    table.insert(ret, '}')
 
    return table.concat(ret, sep)
end
 
--- SASS color parameter table for Lua modules.
--  These can be accessed elsewhere in the module:
--   * @{colors.wikia} acts as a template getter.
--   * @{colors.css}, @{colors.text} & @{colors.parse} accept
--  `$parameter` syntax.
--  @table              p.params
--  @field              {string} background-dynamic Whether the background is split. Default: `'false'`.
--  @field              {string} background-image Background image URL. Default: `''`.
--  @field              {string} background-image-height Background image height. Default: `0`.
--  @field              {string} background-image-width Background image width. Default: `0`.
--  @field              {string} color-body Background color.
--  @field              {string} color-body-middle Background split color.
--  @field              {string} color-buttons Button color.
--  @field              {string} color-community-header Community header color.
--  @field              {string} color-header Legacy wiki header color.
--  @field              {string} color-links Wiki link color.
--  @field              {string} color-page Page color.
--  @field              {string} color-text Page text color.
--  @field              {string} color-contrast Page contrast color.
--  @field              {string} color-page-border In-page border color.
--  @field              {string} color-button-highlight Button highlight color.
--  @field              {string} color-button-text Button text color.
--  @field              {string} infobox-background Infobox background color.
--  @field              {string} infobox-section-header-background Infobox section header color.
--  @field              {string} color-community-header-text Infobox section header color.
--  @field              {string} dropdown-background-color Dropdown background color.
--  @field              {string} dropdown-menu-highlight Dropdown menu highlight color.
--  @field              {string} is-dark-wiki Whether the wiki has a dark theme (`'true'` or `'false'`).
--  @usage              colors.params['key']
p.params = {
    ['background-dynamic'] = sassParams['background-dynamic'] or 'false',
    ['background-image'] = sassParams['background-image'] or '',
    ['background-image-height'] = sassParams['background-image-height'] or '0',
    ['background-image-width'] = sassParams['background-image-width'] or '0',
    ['color-body'] = sassParams['color-body'] or '#f6f6f6',
    ['color-body-middle'] = sassParams['color-body-middle'] or '#f6f6f6',
    ['color-buttons'] = sassParams['color-buttons'] or '#a7d7f9',
    ['color-community-header'] = sassParams['color-community-header'] or '#f6f6f6',
    ['color-header'] = sassParams['color-header'] or '#f6f6f6',
    ['color-links'] = sassParams['color-links'] or '#0b0080',
    ['color-page'] = sassParams['color-page'] or '#ffffff'
}
 
-- Theme Designer color variables.
 
-- Brightness conditionals (post-processing).
local page_bright = p.parse('$color-page'):bright()
local page_bright_90 = p.parse('$color-page'):bright(90)
local header_bright = p.parse('$color-community-header'):bright()
local buttons_bright = p.parse('$color-buttons'):bright()
 
-- Derived opacity values.
local pi_bg_o = page_bright and 90 or 85
 
-- Derived colors and variables.
 
-- Main derived parameters.
p.params['color-text'] = page_bright and '#3a3a3a' or '#d5d4d4'
p.params['color-contrast'] = page_bright and '#000000' or '#ffffff'
p.params['color-page-border'] = page_bright
    and p.parse('$color-page'):mix('#000', 80):string()
    or  p.parse('$color-page'):mix('#fff', 80):string()
p.params['color-button-highlight'] = buttons_bright
    and p.parse('$color-buttons'):mix('#000', 80):string()
    or  p.parse('$color-buttons'):mix('#fff', 80):string()
p.params['color-button-text'] = buttons_bright and '#000000' or '#ffffff'
 
-- PortableInfobox color parameters.
local is_fandom = mw.site.server:match('%.fandom%.com$')
p.params['infobox-background'] = is_fandom
    and p.parse('$color-page'):mix('$color-links', pi_bg_o):string()
    or  '#f8f9fa'
p.params['infobox-section-header-background'] = is_fandom
    and p.parse('$color-page'):mix('$color-links', 75):string()
    or  '#eaecf0'
 
-- CommunityHeader color parameters.
p.params['color-community-header-text'] = header_bright
    and '#000000'
    or  '#ffffff'
p.params['dropdown-background-color'] = (function(clr)
    if page_bright_90 then
        return '#ffffff'
    elseif page_bright then
        return clr:mix('#fff', 90):string()
    else
        return clr:mix('#000', 90):string()
    end
end)(p.parse('$color-page'))
p.params['dropdown-menu-highlight'] = p.parse('$color-links'):alpha(10):rgb()
 
-- Custom SASS parameters.
p.params['is-dark-wiki'] = tostring(not page_bright)
 
--- Template entrypoint for [[Template:Colors]] access.
--  @function           p.main
--  @param              {table} f Frame object in module (child) context.
--  @return             {string} Module output in template (parent) context.
--  @usage              {{#invoke:colors|main}}
p.main = entrypoint(p)
 
return p
Community content is available under CC-BY-SA unless otherwise noted.

Fandom may earn an affiliate commission on sales made from links on this page.

Stream the best stories.

Fandom may earn an affiliate commission on sales made from links on this page.

Get Disney+