User:Surjection/auto-contrast-fixer.js

Hello, you have come here looking for the meaning of the word User:Surjection/auto-contrast-fixer.js. In DICTIOUS you will not only get to know all the dictionary meanings for the word User:Surjection/auto-contrast-fixer.js, but we will also tell you about its etymology, its characteristics and you will know how to say User:Surjection/auto-contrast-fixer.js in singular and plural. Everything you need to know about the word User:Surjection/auto-contrast-fixer.js you have here. The definition of the word User:Surjection/auto-contrast-fixer.js will help you to be more precise and correct when speaking or writing your texts. Knowing the definition ofUser:Surjection/auto-contrast-fixer.js, as well as those of other words, enriches your vocabulary and provides you with more and better linguistic resources.
// <nowiki>
/* jshint maxerr:1048576, strict:true, undef:true, latedef:true, es5:true */
/* global $ */

/**
 * Automatically adjusts contrast of templates in night mode.
 * 
 * Author(s): Surjection
 * Last updated: 2024-09-16
 */

/* Color conversion code. */
/**************************/

function parseCssValue(cssAlpha, divisor) {
    /**
     * Parses a numeric value in CSS syntax, either given directly or
     * as a percentage.
     *
     * If a divisor is given, it is used to scale the value, unless it is
     * a percentage.
     */
    if (cssAlpha.endsWith("%")) {
        return parseCssValue(cssAlpha.slice(0, -1).trim(), 100.0);
    }
    return Number(cssAlpha) / (divisor ?? 1.0);
}

// RegEx used by parseCssColor
const RE_CSS_HEX_COLOR_3 = /^#()()()$/;
const RE_CSS_HEX_COLOR_6 =
    /^#({2})({2})({2})$/;
const RE_CSS_RGB_COLOR =
    /^rgb\(\s*(+\s*%?)(?:\s+|\s*,\s*)(+\s*%?)(?:\s+|\s*,\s*)(+\s*%?)\s*\)$/;
const RE_CSS_RGBA_COLOR =
    /^rgba?\(\s*(+\s*%?)\s*,\s*(+\s*%?)\s*,\s*(+\s*%?)\s*,\s*(+\s*%?)\s*\)$/;
const RE_CSS_RGBA_SLASH_COLOR =
    /^rgba?\(\s*(+\s*%?)\s+(+\s*%?)\s+(+\s*%?)\s*\/\s*(+\s*%?)\s*\)$/;

function parseCssColor(cssColor) {
    /**
     * Parses a color in CSS syntax and returns it in RGBA format:
     *
     * 
     *      with each value within .
     *
     * Returns undefined if the color could not be parsed.
     */
    let m;
    if ((m = cssColor.match(RE_CSS_RGB_COLOR))) {
        return [
            parseCssValue(m, 255.0),
            parseCssValue(m, 255.0),
            parseCssValue(m, 255.0),
            1.0,
        ];
    }
    if ((m = cssColor.match(RE_CSS_RGBA_COLOR))) {
        return [
            parseCssValue(m, 255.0),
            parseCssValue(m, 255.0),
            parseCssValue(m, 255.0),
            parseCssValue(m),
        ];
    }
    if ((m = cssColor.match(RE_CSS_RGBA_SLASH_COLOR))) {
        return [
            parseCssValue(m, 255.0),
            parseCssValue(m, 255.0),
            parseCssValue(m, 255.0),
            parseCssValue(m),
        ];
    }
    if ((m = cssColor.match(RE_CSS_HEX_COLOR_6))) {
        return [
            parseInt(m, 16) / 255.0,
            parseInt(m, 16) / 255.0,
            parseInt(m, 16) / 255.0,
            1.0,
        ];
    }
    if ((m = cssColor.match(RE_CSS_HEX_COLOR_3))) {
        return [
            parseInt(m, 16) / 15.0,
            parseInt(m, 16) / 15.0,
            parseInt(m, 16) / 15.0,
            1.0,
        ];
    }
    if (cssColor === "transparent") return ;
    return undefined;
}

function makeCssColor(rgbaColor) {
    /**
     * Converts the given RGBA color into CSS format.
     */
    const  = rgbaColor;
    const normR = Math.round(r * 255.0);
    const normG = Math.round(g * 255.0);
    const normB = Math.round(b * 255.0);
    if (a >= 1.0) {
        return `rgb(${normR}, ${normG}, ${normB})`;
    } else {
        return `rgba(${normR}, ${normG}, ${normB}, ${a})`;
    }
}

function rgbToHsl(r, g, b) {
    /**
     * Converts colors from RGB to HSL (hue, saturation, lightness).
     *
     * The RGB inputs should all lie within .
     *
     * With valid inputs, the hue value will lie within ,
     * while the saturation and lightness values will lie within .
     */
    const min = Math.min(r, g, b);
    const max = Math.max(r, g, b);
    const delta = max - min;
    const l = (max + min) / 2;

    if (delta === 0) {
        return ;
    }

    const s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min);

    let h;
    if (max === r) h = (g - b) / delta + (g < b ? 6 : 0);
    else if (max === g) h = (b - r) / delta + 2;
    else if (max === b) h = (r - g) / delta + 4;
    return ;
}

function hslToRgb(h, s, l) {
    /**
     * Converts colors from HSL (hue, saturation, lightness) to RGB.
     *
     * The hue value should lie within , and the saturation and
     * lightness values within .
     *
     * With valid inputs, the RGB outputs will all lie within .
     */
    if (s === 0) {
        return ;
    }

    const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
    const p = 2 * l - q;
    h /= 6;
    if (h < 0) h = 1 - h;
    h %= 1;

    function hueToRgb(p, q, t) {
        if (t < 0) t += 1;
        else if (t > 1) t -= 1;

        if (t < 1 / 6) return p + (q - p) * 6 * t;
        if (t < 1 / 2) return q;
        if (t < 2 / 3) return p + (q - p) * 6 * (2 / 3 - t);
        return p;
    }

    const r = hueToRgb(p, q, h + 1 / 3);
    const g = hueToRgb(p, q, h);
    const b = hueToRgb(p, q, h - 1 / 3);
    return ;
}

/* Color processing code. */
/**************************/

function colorLuminance(rgbaColor) {
    /**
     * Returns the luminance of the RGB color (alpha is ignored) using the
     * sRGB/BT.709 luminance formula.
     */
    const  = rgbaColor;
    return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

function colorContrast(fgRgbaColor, bgRgbaColor) {
    /**
     * Returns the WCAG contrast ratio (>= 1) for the given two colors.
     */
    const q =
        (colorLuminance(fgRgbaColor) + 0.05) /
        (colorLuminance(bgRgbaColor) + 0.05);
    return q < 1 ? 1 / q : q;
}

function darkenBackgroundColor(rgbaColor) {
    /**
     * Darkens the given RGBA color for it to make it a suitable
     * background color in night mode.
     */
    const  = rgbaColor;
    const  = rgbToHsl(r, g, b);

    let lAdj = 1 - colorLuminance(rgbaColor);
    lAdj = Math.pow(lAdj, 0.75);
    lAdj = 0.05 + 0.95 * lAdj;

    const lScale = Math.min(Math.max(lAdj, 0.01) / Math.max(l, 0.01), 1);
    const  = hslToRgb(h, (s + s * lScale) / 2, lAdj);
    return ;
}

function darkenBorderColor(rgbaColor) {
    /**
     * Darkens the given RGBA color for it to make it a suitable
     * border color in night mode.
     */
    const  = rgbaColor;
    const  = rgbToHsl(r, g, b);
    const  = hslToRgb(h, s * 0.5, l * 0.5);
    return ;
}

/* Auto-fixer parameters. */
/**************************/

/** Any background-foreground combo with the contrast ratio exceeding
 * this value are ignored by the fixer. */
const PARAMETER_MAXIMUM_CONTRAST_TO_FIX = 3;
/** The background color must have at least this much alpha,
 * or the element is ignored by the fixer. */
const PARAMETER_MINIMUM_BG_ALPHA = 0.5;
/** The foreground color must have at least this much luminance,
 * or the element is ignored by the fixer. */
const PARAMETER_MINIMUM_FG_LUMA = 0.5;
/** The border color must have at least this much luminance,
 * or it is ignored by the fixer. */
const PARAMETER_MINIMUM_BORDER_LUMA = 0.7;

/* Auto-fixer framework. */
/*************************/

function isSkinNightMode() {
    /**
     * Returns true if the user has requested night mode.
     */
    return (
        window.matchMedia &&
        window.matchMedia("screen").matches &&
        (document.documentElement.classList.contains(
            "skin-theme-clientpref-night",
        ) ||
            (document.documentElement.classList.contains(
                "skin-theme-clientpref-os",
            ) &&
                window.matchMedia("(prefers-color-scheme: dark)").matches))
    );
}

function doAutoContrastFix() {
    /**
     * Applies the auto-contrast fix to all elements in the
     * wikipage body.
     */
    // see if the user has requested night mode.
    if (!isSkinNightMode()) return;

    const view = document.defaultView;
    const ELEMENTS_PER_FRAME = 50;

    function isCandidate(el) {
        const style = view.getComputedStyle(el);
        return style.background !== "none" || style.borderStyle !== "none";
    }

    // TODO optimize; can we skip searching candidates or split it up
    // into multiple frames to avoid blocking the page for some time?
    const candidates = Array.prototype.filter.call(
        document.querySelectorAll("#mw-content-text *"),
        isCandidate,
    );

    let autoContrastElementCount = 0;

    function autoContrastReport(el) {
        ++autoContrastElementCount;
        el.classList.add("wikt-auto-contrast-fixed");
    }

    function doAutoContrastFixOne(el) {
        /**
         * Applies the auto-contrast fix to the specified element.
         */
        const computedStyle = view.getComputedStyle(el);

        // get background and foreground colors.
        const cssBackgroundColor = computedStyle.backgroundColor;
        const parsedBackgroundColor = parseCssColor(cssBackgroundColor);
        if (parsedBackgroundColor == null) return;

        const cssForegroundColor = computedStyle.color;
        const parsedForegroundColor = parseCssColor(cssForegroundColor);
        if (parsedForegroundColor == null) return;

        let fixedElement = false;

        // check that:
        // * the background color has enough alpha
        // * the text color is actually light
        // * the contrast is bad enough to consider fixing
        if (
            parsedBackgroundColor >= PARAMETER_MINIMUM_BG_ALPHA &&
            colorLuminance(parsedForegroundColor) >=
                PARAMETER_MINIMUM_FG_LUMA &&
            colorContrast(parsedForegroundColor, parsedBackgroundColor) <=
                PARAMETER_MAXIMUM_CONTRAST_TO_FIX
        ) {
            // generate a background color and apply it, but only if it is darker.
            const newBackgroundColor = darkenBackgroundColor(
                parsedBackgroundColor,
            );
            if (
                colorLuminance(newBackgroundColor) <
                colorLuminance(parsedBackgroundColor)
            ) {
                fixedElement = true;
                el.style.backgroundColor = makeCssColor(newBackgroundColor);
            }
        }

        if (computedStyle.borderStyle !== "none") {
            // odds are the border color needs the same treatment.
            const cssBorderColor = view.getComputedStyle(el).borderColor;
            const parsedBorderColor = parseCssColor(cssBorderColor);

            if (
                parsedBorderColor != null &&
                colorLuminance(parsedBorderColor) >=
                    PARAMETER_MINIMUM_BORDER_LUMA
            ) {
                // generate a border color and apply it, but only if it is darker.
                const newBorderColor = darkenBorderColor(parsedBorderColor);
                if (
                    colorLuminance(newBorderColor) <
                    colorLuminance(parsedBorderColor)
                ) {
                    fixedElement = true;
                    el.style.borderColor = makeCssColor(newBorderColor);
                }
            }
        }

        if (fixedElement) {
            // report that we found an element to fix.
            autoContrastReport(el);
        }
    }

    function autoContrastFixDone() {
        /**
         * Executed when the auto-contrast fix has been applied
         * to all elements.
         */
        if (autoContrastElementCount > 0) {
            console.log(
                `Applied auto-contrast fix to ${autoContrastElementCount} elements`,
            );
        }
    }

    function doAutoContrastFixBatch(base) {
        /**
         * Applies the auto-contrast fix to the next batch
         * of elements.
         */
        for (let offset = 0; offset < ELEMENTS_PER_FRAME; ++offset) {
            const index = base + offset;
            if (index >= candidates.length) {
                autoContrastFixDone();
                return;
            }
            doAutoContrastFixOne(candidates);
        }

        window.requestAnimationFrame(() =>
            doAutoContrastFixBatch(base + ELEMENTS_PER_FRAME),
        );
    }

    doAutoContrastFixBatch(0);
}

jQuery(document).ready(function () {
    doAutoContrastFix();
});

// </nowiki>