Stack Pop

Adds a the first StackOverflow answer to your search query

ของเมื่อวันที่ 05-01-2023 ดู เวอร์ชันล่าสุด

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey to install this script.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Stack Pop
// @namespace    https://codeberg.org/happybits/stack-pop
// @version      1.1.1
// @license      MIT 
// @description  Adds a the first StackOverflow answer to your search query
// @author       happybits 
// @match        https://www.google.com/search*
// @icon
// @grant        GM_xmlhttpRequest
// ==/UserScript==

// This is a userscript StackPop that is supposted to run by Tampermonkey when the user enters a google search

try {

    go()

    async function go() {

        // Wait for the google search to appear in the DOM

        const rso = await waitFor("#rso")

        // Inject StackPop before the google search result

        const stackDiv = document.createElement("div")
        stackDiv.id = "stack-pop"
        rso.before(stackDiv)

        // Find the first search result that leads to Stack Overflow

        const googleResults = Array.from(rso.children)

        const firstStackOverflowHit = googleResults

            .filter(result => result.querySelector("a")?.href)
            .map(result => result.querySelector("a").href)
            .find(link => link.startsWith("https://stackoverflow.com"))

        // If any, get the answer inject it to the search page

        if (firstStackOverflowHit) {

            // HTTP call using Tampermonkey's built in function

            GM_xmlhttpRequest({
                method: "GET",
                url: firstStackOverflowHit,
                onload: function (response) {

                    // Build a DOM from the text-response and parse the question and the first answer

                    const stackOverFlowPage = new DOMParser().parseFromString(response.responseText, 'text/html');
                    const question = stackOverFlowPage.querySelector("h1").textContent
                    log(question)
                    const firstAnswerContent = stackOverFlowPage.querySelector(".answer .s-prose").innerHTML

                    // Styling for the StackOverflow answer 

                    const stackPopStyling = `
                    #stack-pop {
                        width: 700px; 
                    }
                    
                    #stack-pop li {
                        margin-left: 1.5em;
                    }
                    
                    #stack-pop pre {
                        background-color: #eee;
                        padding: 1em;
                        position:relative;
                    }
 
                    #stack-pop .copy-icon{
                        position:absolute;
                        right:4px;
                        top:4px;
                        height:33px;
                        width:33px;
                        opacity:0.5;
                        cursor:pointer;
                    }

                    #stack-pop .copy-icon:hover{
                        opacity:1;
                    }

                    #stack-pop code {
                        background-color: #eee;
                    }
                    
                    #stack-pop img {
                        max-width: 100%;
                    }
                    `

                    // Create the StackPop widget
                    // Yes I know, it's inline styling etc, but it's by design, I think it's easy to read and compact!
                    // Or am I wrong ;) ?

                    stackDiv.innerHTML = `
                    
                    <style>${stackPopStyling}</style>

                    <div style="border:solid 2px #f48225; margin: 1em 0; overflow: auto;">

                        <a href="${firstStackOverflowHit}" style="text-decoration:none">
                            <div style="cursor:pointer; background-color:#f48225; color:white; padding:0.5em; font-size: 1.5em">
                                ${encodeHTMLEntities(question)}
                            </div>
                        </a>

                        <div style="padding:0 1.5em 1.5em 1.5em;">
                            ${firstAnswerContent}
                        </div>

                    </div>
                    `

                    // Add the possiblitiy to copy code to the clipboard

                    stackDiv.querySelectorAll("pre").forEach(preElement => {

                        const elementHasCode = preElement.firstChild.nodeName.toLowerCase() === "code"

                        if (elementHasCode) {
                            const codeToCopy = preElement.querySelector("code").innerText
                            preElement.append(copyIcon(codeToCopy))
                        }
                    });

                }
            });
        }


    }

    // This is a generic method that can be used to select element that may take some time to appear in the DOM
    // The second parameter "scope" is optional, of you want to limit the query

    function waitFor(selector, scope) {

        const pause = 10
        let maxTime = 10000

        return new Promise(resolve => {

            function inner() {
                if (maxTime <= 0) {
                    throw "Timeout for select " + selector
                }
                const element = (scope ?? document).querySelector(selector)

                if (element) {
                    resolve(element)
                    return
                }
                maxTime -= pause
                setTimeout(inner)
            }

            inner()
        })
    }

    // A simple log function which shows with a TAMPER-prefix

    function log(...message) {
        console.log('%c TAMPER ', 'color: white; background-color: #61dbfb', ...message);
    }

    // Create a copy icon (used for copying code to the clipboard)

    function copyIcon(textToCopyIfClicked) {

        const img = document.createElement("img")
        img.className = "copy-icon"
        img.title = "Copy to clipboard"
        img.src = "data:image/png;base64, UklGRvACAABXRUJQVlA4WAoAAAAYAAAANgAAOAAAQUxQSHcAAAABcFvbbtv8PdShEksNghW0QhpITnNIlfZxQAuQzgHofSJiAvCUhsXrQHiz2mvxq/vqBR+L7yM/aa14txZAkuJfErCWiCuaHCI3Y4k5zkHm/yXGICNpCCUcQhyAJAEkAWBzZ4xHFmfCeF7v1ZHua7xJ3bT4nDrCUwBWUDggcgEAAPAKAJ0BKjcAOQA+bTSURyQjIiEkGA2wgA2JaQDREEGjmANij+OeoAXWR7EsWPRzuwcNBgq5Du9vVd2+k2kGrGmAE1sdF3OpU0UbjJe/ni0mmBjpwOA3uZJ6569JAWP1AAD+9jeN+v4qM9kSG8dSMALfMGwGD3FF9bbU+gpg4WjBy3tfK842lgA0VyUMfZz3ElSaGGuqLqd12/tyU3fr9anAddYtAn7Xq0b+enT1NycxT2/vZZhOO115TWb5or5bBv4paaHUOH9/Dfp+YtanA+tAAX/tuyu604rmWOEPcP8R9t3tjaIIR7lPJZ3PPJRH/nkg4qbqeRmNratNGmpid+8s5svRhbwBZLf6tfLdlpNcUi0mx5pqaLkFtP6i72HM4z95hucWvXu4cLVz713xnMz6Bjph+AbuLAYAVq4aY/4n5Jz4sEaioamwCrwdqcgG7je6Yy7bftlFoh7itBFC/4434b5/dISkOBAdk5rlzMNJQABFWElG2AAAAElJKgAIAAAABgASAQMAAQAAAAEAAAAaAQUAAQAAAFYAAAAbAQUAAQAAAF4AAAAoAQMAAQAAAAMAAAAxAQIAEQAAAGYAAABphwQAAQAAAHgAAAAAAAAAo5MAAOgDAACjkwAA6AMAAHBhaW50Lm5ldCA0LjMuMTIAAAUAAJAHAAQAAAAwMjMwAaADAAEAAAABAAAAAqAEAAEAAAA3AAAAA6AEAAEAAAA5AAAABaAEAAEAAAC6AAAAAAAAAAIAAQACAAQAAABSOTgAAgAHAAQAAAAwMTAwAAAAAA=="
        img.onclick = () => copyTextToClipboard(textToCopyIfClicked)
        return img
    }

    // Copy text to clipboard (surprise)

    function copyTextToClipboard(text) {

        navigator.clipboard.writeText(text).then(function () {
            log('Copied to clipboard');
        }, function (err) {
            throw err
        });
    }

    // Encode and decode string to and from HTML

    function encodeHTMLEntities(rawStr) {
        return rawStr.replace(/[\u00A0-\u9999<>\&]/g, ((i) => `&#${i.charCodeAt(0)};`));
    }

    function decodeHTMLEntities(rawStr) {
        return rawStr.replace(/&#(\d+);/g, ((_, dec) => `${String.fromCharCode(dec)}`));
    }

}

// Unexpected errors is shown in red

catch (exception) {
    console.log('%c TAMPER ', 'color: white; background-color: red', exception);
}