Module:Colors

From Coral Island Wiki
Jump to navigation Jump to search

Documentation for this module may be created at Module:Colors/doc

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

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