Stack Pop

Adds the first StackOverflow and/or Reddit answer to your search result

当前为 2023-03-05 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Stack Pop
// @namespace    https://codeberg.org/happybits/stack-pop
// @license      MIT
// @version      1.2
// @description  Adds the first StackOverflow and/or Reddit answer to your search result
// @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 stackPopDiv = document.createElement("div")
        stackPopDiv.id = "stack-pop"
        rso.before(stackPopDiv)

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

        insertIfFound(
            {
                urlStartWith: "https://stackoverflow.com",
                questionSelector: page => page.querySelector("h1").textContent,
                answerSelector: page => page.querySelector(".answer .s-prose").innerHTML,
                rso: rso,
                stackPopDiv: stackPopDiv
            }
        )

        // Find the first search result that leads to Reddit

        insertIfFound(
            {
                urlStartWith: "https://www.reddit.com",
                questionSelector: page => page.querySelector("h1").textContent,
                answerSelector: page => page.querySelector(".Comment").querySelector(".RichTextJSON-root").innerHTML,
                rso: rso,
                stackPopDiv: stackPopDiv
            }
        )
    }

    // Insert StackPop if part of the search result (ex StackOverflow or Reddit)

    function insertIfFound({ urlStartWith, questionSelector, answerSelector, rso, stackPopDiv}) {

        const firstHit = firstLinkThatStartsWith(urlStartWith, rso)

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

        if (firstHit) {

            // HTTP call using Tampermonkey's built in function

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

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

                    const page = new DOMParser().parseFromString(response.responseText, 'text/html');
                    const question = questionSelector(page) //  page.querySelector(questionSelector).textContent
                    const firstAnswerContent = answerSelector(page) // page.querySelector(answerSelector).innerHTML

                    // Create the StackPop widget

                    const widget = document.createElement("div")

                    const backgroundColor = "#f4ad25"
                    widget.innerHTML = `

                    <style>${stackPopStyling}</style>

                    <div style="border:solid 2px ${backgroundColor}; margin: 1em 0; overflow: auto;">

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

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

                    </div>
                    `

                    // Add the possiblitiy to copy code to the clipboard

                    addCopyCodeButtons(widget)

                    // Add widget below

                    stackPopDiv.append(widget)

                }
            });


        }


    }

    // Add the possiblitiy to copy code to the clipboard

    function addCopyCodeButtons(element) {
        element.querySelectorAll("pre").forEach(preElement => {

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

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


    // Get the first link in the Google search result area that starts with a specific string

    function firstLinkThatStartsWith(urlPart, rso) {

        const googleResults = Array.from(rso.children)

        return googleResults

            .filter(result => result.querySelector("a")?.href)
            .map(result => result.querySelector("a").href)
            .find(link => link.startsWith(urlPart))
    }


    // CSS Styling for StackPop box

    const stackPopStyling = `
        #stack-pop {
            width: 700px;
        }

        #stack-pop li {
            margin-left: 1.5em;
            list-style: normal;
        }

        #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%;
        }
        `

    // 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);
}