Neopets: Stocks enhancer

Show only the stocks you want, and adds columns for monthly highs and lows

// ==UserScript==
// @name         Neopets: Stocks enhancer
// @author       Tombaugh Regio
// @version      1.4
// @description  Show only the stocks you want, and adds columns for monthly highs and lows
// @namespace    https://greasyfork.org/users/780470
// @match        http://www.neopets.com/stockmarket.phtml?type=list&search=%&bargain=true
// @match        http://www.neopets.com/stockmarket.phtml?type=portfolio
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_openInTab
// @grant        GM_xmlhttpRequest
// ==/UserScript==

// ======================================================================

const PRICE = {
    //Lowest price that can be purchased. Default lowest is 15
    lowest: 15,

    //Highest price to consider
    highest: 15
}

// ======================================================================

//Get stock market data from Neostocks
GM_xmlhttpRequest({
    method: 'GET',
    url: 'https://neostocks.info/?period=1m',
    headers: {
        'User-agent': 'Mozilla/4.0 (compatible) Greasemonkey',
        'Accept': 'application/atom+xml,application/xml,text/xml',
    },
    onload: function(responseDetails) {
        return new Promise((resolve, reject) => {
            resolve(GM_setValue("responseText", responseDetails.responseText))
        })
    }
})

const stockData = GM_getValue("responseText").match(/{"summary_data":{.+"1m":\[{(.+)}\],"all":.+}/)[1]
const stockTickerInfos = stockData.split("},{")
const monthlyStocks = []

for(const datapoints of stockTickerInfos) {
    const datapoint = datapoints.split(",")
    const ticker = datapoint[0].match(/.+:"(.+)"/)[1]
    const high = datapoint[4].match(/\d+/)[0]
    const low = datapoint[6].match(/\d+/)[0]

    monthlyStocks.push({ticker: ticker, high: high, low: low})
}

let stocksTable, stockContainer
const URL = document.URL

if(URL.includes("portfolio")) {

    //Get the table with all the stock information
    stocksTable = document.evaluate("//a[contains(., 'here')]",
                                    document, null, XPathResult.ANY_TYPE, null ).iterateNext()
        .parentNode.querySelector("table")
    stocksTable.style.marginTop = "2em"
    stockContainer = stocksTable.querySelectorAll("tr")

    //Separate stock headers from rows, and push headers into an array
    let portfolioHeaders, portfolioSubheaders, portfolioRows
    [portfolioHeaders, portfolioSubheaders, ...portfolioRows] = stockContainer

    let topHeaderTitles = [], subheaderTitles = []

    const setCustomHeader = (i, number, header, HTML) => {if(i == number) header.innerHTML = HTML}
    function setNewHeaders(headers, titles, ARRAY){
        for (const [i, header] of headers.childNodes.entries()) {
            for(const OBJECT of ARRAY) {
                setCustomHeader(i, OBJECT.number, header, OBJECT.HTML)
            }
            if (header.nodeName != "#text") {titles.push(header.textContent)}
        }
    }

    setNewHeaders(portfolioHeaders, topHeaderTitles, [{number: 3, HTML: `<b>Month</b>`}])
    setNewHeaders(portfolioSubheaders, subheaderTitles, [{number: 5, HTML: `<b>High</b>`},{number: 9, HTML: `<b>Low</b>`}])


    for (const row of portfolioRows) {
        const cells = row.querySelectorAll("td")
        for (const [i, cell] of cells.entries()) {
            if (cells.length == 9){

                //Find the columns with highs and lows, and set their contents to data from Neostocks
                if (i == subheaderTitles.indexOf("Ticker")) {
                    const cellTicker = cell.querySelector("a").textContent
                    for (const stock of monthlyStocks) {
                        if (cellTicker == stock.ticker) {
                            const cellHigh = cells[subheaderTitles.indexOf("High")]
                            const cellLow = cells[subheaderTitles.indexOf("Low")]
                            cellHigh.innerHTML = `<a href="https://neostocks.info/tickers/${
                                                    stock.ticker}?period=1m" ><b>${
                                                    stock.high}</b></a>`

                            cellLow.innerHTML = `<a href="https://neostocks.info/tickers/${
                                                    stock.ticker}?period=1m" ><b>${
                                                    stock.low}</b></a>`

                            cellHigh.classList.add("monthly-high")
                            cellLow.classList.add("monthly-low")
                        }
                    }
                }

                //Style the current price if it's above the monthly high or below the monthly low
                if (i == subheaderTitles.indexOf("Current Price")) {
                    const cellHigh = cells[subheaderTitles.indexOf("High")]
                    const cellLow = cells[subheaderTitles.indexOf("Low")]

                    if (parseInt(cell.textContent) > parseInt(cellHigh.textContent)) {
                        cell.innerHTML = `<font color="green"><b>${cell.textContent}</b></font>`
                    }

                    if (parseInt(cell.textContent) < parseInt(cellLow.textContent)) {
                        cell.innerHTML = `<font color="red"><b>${cell.textContent}</b></font>`
                    }
                }

                //Hide stock logos
                if (i == subheaderTitles.indexOf("Icon")) {
                    const images = cell.querySelectorAll("img")
                    for(const image of images) {
                        if (image.title.length > 0) {
                            image.style.display = "none"
                        }
                    }
                }
            }
        }
    }

    //Swap rows
    for (const [i, row] of stockContainer.entries()) {
        if (i != 0 && row.children[3] != undefined) {row.children[3].after(row.children[2])}
    }
}

if(URL.includes("bargain")) {

    //Get the table with all the stock information
    stocksTable = document.evaluate("//b[contains(., 'Bargain Stocks')]",
                                    document, null, XPathResult.ANY_TYPE, null ).iterateNext()
        .nextSibling.nextSibling.nextSibling.nextSibling

    stocksTable.style.marginTop = "2em"
    stockContainer = stocksTable.querySelectorAll("tr")

    //Add columns for monthly lows and highs
    stockContainer.forEach(row => {
        row.appendChild(row.lastChild.cloneNode(true))
        row.appendChild(row.lastChild.cloneNode(true))
    })

    //Separate stock headers from rows, and push headers into an array
    let stockHeader, stockRows
    [stockHeader, ...stockRows] = stockContainer

    let headers = []
    stockHeader.childNodes.forEach((header, i) => {
        if (i == 7) {header.innerHTML = `<font color = "white"><b>High</b></font>`}
        if(i == 8) {header.innerHTML = `<font color = "white"><b>Low</b></font>`}
        headers.push(header.innerText)
    })

    //Hide logo and change columns
    for (const row of stockContainer) {
        for (const [i, column] of row.childNodes.entries()) {
            const logoColumn = headers.findIndex(e => e == "Logo")
            const changeColumn = headers.findIndex(e => e == "Change")

            if (i == logoColumn || i == changeColumn) {column.style.display = "none"}
        }
    }

    //For each row, add monthly highs and lows, and hide them if they're outside the specified price range
    for(const row of stockRows) {
        let monthlyHighHTML = `<a href="https://neostocks.info/bargain?period=1m"
                                  title="Click to check if stocks have updated.">n/a</a>`
        let monthlyLowHTML = monthlyHighHTML

        for (const [i, cell] of row.childNodes.entries()) {
            const cellText = cell.innerText
            const tickerColumn = headers.findIndex(e => e == "Ticker")
            const priceColumn = headers.findIndex(e => e == "Curr")
            const highColumn = headers.findIndex(e => e == "High")
            const lowColumn = headers.findIndex(e => e == "Low")

            //Add monthly highs and lows
            for (const stock of monthlyStocks) {
                if (i == tickerColumn && cellText == stock.ticker) {
                    monthlyHighHTML = `<a href="https://neostocks.info/tickers/${
                    stock.ticker}?period=1m" ><b>${
                    stock.high}</b></a>`
                    monthlyLowHTML = `<a href="https://neostocks.info/tickers/${
                    stock.ticker}?period=1m" ><b>${
                    stock.low}</b></a>`
                }
            }

            if (i == highColumn) {cell.innerHTML = monthlyHighHTML}
            if (i == lowColumn) {cell.innerHTML = monthlyLowHTML}


            //Style the price column like the hidden change column
            if (i == priceColumn) {
                const fontColor = cell.nextSibling.childNodes[0].color
                const tooltipText = cell.nextSibling.textContent
                cell.innerHTML = `<a href="#" title="${
                tooltipText}"><font color="${
                fontColor}"><b>${cellText}</b></font></a>`
            }

            //If price is outside prescribed range, hide the row
            if (i == priceColumn && (cellText < PRICE.lowest || cellText > PRICE.highest)) {
                row.style.display = "none"
            }
        }
    }
}