Jump to content

User:Isaac (WMF)/link gender.js

From Wikipedia, the free encyclopedia
Note: After saving, you have to bypass your browser's cache to see the changes. Google Chrome, Firefox, Microsoft Edge and Safari: Hold down the ⇧ Shift key and click the Reload toolbar button. For details and instructions about other browsers, see Wikipedia:Bypass your cache.
// <pre> Avoid triggering: https://en.wikipedia.org/wiki/Special:WantedTemplates
// Slightly tweaked version of original script: https://en.wikipedia.org/wiki/User:TayIorRobinson/wikigender.js
// Developed as part of Wikimania 2021 Hackathon: https://phabricator.wikimedia.org/T288666

/**
 * @typedef {HTMLDivElement} GenderBar
 * @typedef {'male' | 'female' | 'nb' | 'none'} Gender
 * @typedef {{gender: string, title: string}} GenderedLink
 * @typedef {{gender: string, num_links: number, pct_links: number}} GenderSummary
 * @typedef {{article: string, details: GenderedLink[], num_outlinks: number, summary: GenderSummary[]}} GenderData
 */
// </pre>

(async() => {
    const GENDERDATA_OUTLINKS_API = "https://article-link-gender-data.toolforge.org/api/v1/outlinks-details";
    const GENDERDATA_INLINKS_API = "https://article-link-gender-data.toolforge.org/api/v1/inlinks-details";

    /**
     * Requests the gender data for the current page.
     * @returns {Promise<GenderData>} The analysed gender data for the page.
     */
    async function fetchData() {
        let lang = location.host.split(".")[0];
        let article;
        let response;
        if (mw.config.get('wgCanonicalSpecialPageName') == 'Whatlinkshere') {
	        article = location.pathname.split("/")[3];
        	response = await fetch(GENDERDATA_INLINKS_API + "?lang=" + lang + "&title=" + article);
        } else {
        	article = location.pathname.split("/")[2];
        	response = await fetch(GENDERDATA_OUTLINKS_API + "?lang=" + lang + "&title=" + article);
        }
        let data = await response.json();

        return data;
    }

    /**
     * Creates an instance of a gender bar on the current page.
     * @returns {GenderBar}
     */
    function createGenderBar() {
        let element = document.createElement("div");
        element.id = "genderbar";
        element.style.maxHeight = "5px";
        element.style.width = "70%";
        element.style.marginTop = "-0.5em";
        element.style.overflow = "hidden";
        element.onmouseenter = () => element.style.maxHeight = "1000px";
        element.onmouseleave = () => element.style.maxHeight = "5px";

        document.querySelector("#mw-content-text").insertAdjacentElement("afterbegin", element);
        return element;
    }

    /**
     * Takes a gender and gives back a hex colour code
     * @param {string} name
     * @returns {string}
     */
    function getGenderColour(name) {
        switch (name) {
        case "N/A":
            return "#EEEEEE";
        case "male":
            return "#517FC1";
        case "transgender male":
        case "transmasculine":
            return "#3d5f8f";
        case "female":
            return "#F19359";
        case "transgender female":
        case "transfeminine":
            return "#cf7d4a";
        default:
            return "#FAD965";
        }
    }
    /* Order the genders so it looks nicer on the graph */
    const ORDER = [
        "female", "transgender female", "transfeminine",
        "male", "transgender male", "transmasculine",
        "N/A"
    ];

    /**
     * Make rows for each gender category for the table
     * @param {GenderSummary[]} literals
     * @returns {string}
     */
    function makeRows(list) {
        return `<td>${list.map((o) => `<p style="margin: 0;padding-left:0.5em;border-left: 4px solid ${getGenderColour(o.gender)}"> ${o.gender}</p>`).join("")}</td>
        <td style="text-align:right;">${list.map((o) => o.num_links.toLocaleString() + " link" + (o.num_links != 1 ? "s" : "")).join("<br>")}</td>
        <td style="text-align:right;">${list.map((o) => Math.round(o.pct_links * 100) + "%").join("<br>")}</td>`;
    }

    /**
     * Fills a genderbar with a gender summary.
     * @param {GenderBar} genderbar The gender bar to fill.
     * @param {GenderSummary[]} summaries The summary of the genders.
     */
    function fillGenderBar(genderbar, summaries) {
        genderbar.innerHTML = ""; // Clear the gender bar of any previous elements.

        let x = 0;
        let gradientParts = [];
        for (let summary of summaries.sort((a, b) => ORDER.indexOf(a.gender) - ORDER.indexOf(b.gender))) {
            gradientParts.push(getGenderColour(summary.gender) + " " + x + "%");
            x += summary.pct_links * 100;
            gradientParts.push(getGenderColour(summary.gender) + " " + x + "%");
        }
        genderbar.style.backgroundImage = "linear-gradient(to right, " + gradientParts.join(", ") + ")";

        // Fill table
        let maleList = [];
        let femaleList = [];
        let nbList = [];
        for (let summary of summaries) {
            switch (summary.gender) {
            case "N/A":
                break;
            case "male":
            case "transgender male":
            case "transmasculine":
                maleList.push(summary);
                break;
            case "female":
            case "transgender female":
            case "transfeminine":
                femaleList.push(summary);
                break;
            default:
                nbList.push(summary);
                break;
            }
        }
        let table = document.createElement("table");
        table.style.width = "100%";
        table.style.background = "#eee";
        table.style.paddingTop = "5px";
        table.style.backgroundClip = "content-box";
        table.style.borderSpacing = "0.5em";
        // I will admit the following code is **not** pretty.
        // I am less than proud.
        table.innerHTML = `
        <tr>
            ${true ? `<th>Non-Binary Genders</th>
            <td style="text-align:right;">${nbList.reduce((previous, value) => previous + value.num_links, 0).toLocaleString()} links</td>
            <td style="text-align:right;">${Math.round(nbList.reduce((previous, value) => previous + value.pct_links, 0) * 100)}%</td>` : ""}
            ${true ? `<th>Female</th>
            <td style="text-align:right;">${femaleList.reduce((previous, value) => previous + value.num_links, 0).toLocaleString()} links</td>
            <td style="text-align:right;">${Math.round(femaleList.reduce((previous, value) => previous + value.pct_links, 0) * 100)}%</td>` : ""}
            ${true ? `<th>Male</th>
            <td style="text-align:right;">${maleList.reduce((previous, value) => previous + value.num_links, 0).toLocaleString()} links</td>
            <td style="text-align:right;">${Math.round(maleList.reduce((previous, value) => previous + value.pct_links, 0) * 100)}%</td>` : ""}
        </tr>
        <tr style="vertical-align: top">
            ${true ? makeRows(nbList) : ""}
            ${true ? makeRows(femaleList) : ""}
            ${true ? makeRows(maleList) : ""}
        </tr>
        `;
        genderbar.appendChild(table);
    }

    /**
     * Applies colour to links on the page
     * @param {GenderedLink[]} links The links to colour.
     */
    function applyLinkColours(links) {
        let pageLinks;
        if (mw.config.get('wgCanonicalSpecialPageName') == 'Whatlinkshere') {
	        pageLinks = document.querySelector("#mw-whatlinkshere-list").querySelectorAll("a[href^=\"/wiki/\"]");
        } else {
        	pageLinks = document.querySelector("#bodyContent").querySelectorAll("a[href^=\"/wiki/\"]");
        }
        // Create an object of all the links on the page.
        let linksOnPage = {};
        for (let link of pageLinks) {
            let linkText = decodeURIComponent(link.href.split("/")[4].toLowerCase().replace(/_/g, " "));
            // Build array of all instances of a link appearing on page
            if (!(linkText in linksOnPage)) {
                linksOnPage[linkText] = [];
            }
            linksOnPage[linkText].push(link);
        }
        // Colour the links.
        for (let link of links) {
            let linkOnPage = linksOnPage[link.title];
            if (!linkOnPage) continue;
            for (let l of linkOnPage) {
                l.style.backgroundColor = getGenderColour(link.gender) + "88";
            }
        }
    }

	if (location.host.split(".")[1] == "wikipedia") {
	    let mwIndicator = document.createElement("div");
	    mwIndicator.className = "mw-indicator";
	    mwIndicator.id = "mw-indicator-gender";
	    document.querySelector(".mw-indicators").appendChild(mwIndicator);
	
	    let link = document.createElement("a");
	    link.href = "javascript:void(0)";
	    link.onclick = async() => {
	        link.remove();
	        let data = await fetchData();
	        fillGenderBar(createGenderBar(), data.summary);
	        applyLinkColours(data.details);
	    };
	    link.title = "Analyse the gender of the links on the page";
	    mwIndicator.appendChild(link);
	
	    let img = document.createElement("img");
	    img.alt = "Gender equality icon";
	    img.src = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d7/Gender_equality.png/20px-Gender_equality.png";
	    img.srcset = "https://upload.wikimedia.org/wikipedia/commons/thumb/d/d7/Gender_equality.png/30px-Gender_equality.png 1.5x, https://upload.wikimedia.org/wikipedia/commons/thumb/d/d7/Gender_equality.png/40px-Gender_equality.png 2x";
	    img.decoding = "async";
	    img.height = 20;
	    img.width = 20;
	    link.appendChild(img);
	}
})();