First N search results (Chrome | Safari) as MD and/or opened tabs

As a follow-up or generalisation of an earlier (Google and Safari-specific) macro, here is a draft macro which aims for a bit more flexibility by working with either:

  1. Chrome or Safari, and
  2. DuckDuckGo or Google search.

You can specify whether you want it to:

  • open a new tab for each search result,
  • copy all search results as Markdown links
  • limit the maximum number of results that are opened or copied.

Ver 0.2 (Re-enabled blnOpenTabs option – disabled during testing)

First N DuckDuck or Google search results (Safari or Chrome).kmmacros (30.0 KB)

Javascript for Automation source

(() => {
    'use strict';

    // Ver 0.2
    // removed `|| true` after blnOpenTabs

    const main = () => {
        const
            kme = Application('Keyboard Maestro Engine'),
            blnOpenTabs = Boolean(eval(
                kme
                .getvariable('openResultTabs')
            )),
            intMaxLinks = parseInt(
                kme.getvariable('maxResultTabs'),
                10
            ) || 10,
            xpathDuck = "//*[@class='result__a']",
            xpathGoogle = "//*[@class='r']/a";
        return bindLR(
            frontAppNameLR(),
            strApp => elem(
                strApp, ['Safari', 'Google Chrome']
            ) ? (() => {
                const
                    mbLinks = foldl(
                        (a, xpath) => a.Nothing ? (
                            xPathHarvestMay(
                                strApp,
                                blnOpenTabs,
                                xpath,
                                intMaxLinks
                            )
                        ) : a, Nothing(), [xpathDuck, xpathGoogle]
                    );
                return mbLinks.Nothing ? (
                    Left(
                        'No DuckDuck or Google result links on this page.'
                    )
                ) : Right(mbLinks.Just);
            })() : Left('Neither Safari nor Google Chrome at front')
        );
    };

    // CHROME AND SAFARI  ---------------------------------------
    const xPathHarvestMay = (appName, blnOpenTabs, strXPath, intMax) => {
        const
            browser = Application(appName),
            ws = browser.windows;
        return bindMay(
            0 < ws.length ? (
                Just(ws.at(0))
            ) : Nothing(),
            w => {
                const
                    xs = take(
                        intMax,
                        pageXPathHarvest(
                            browser, w, strXPath
                        )
                    );
                return 0 < xs.length ? (
                    // Optional browser effect
                    blnOpenTabs && tabsOpened(browser, w, xs),
                    // Keyboard Maestro value
                    Just(
                        xs
                        .reduce(
                            (a, link) =>
                            `${a}[${link[0]}](${link[1]})\n`,
                            ''
                        )
                    )
                ) : Nothing();
            }
        );
    };

    // Harvest elements from Safari or Chrome by XPath pattern
    const pageXPathHarvest = (browser, oWin, strXPath) => {
        const strApp = browser.name();
        return 'Safari' === strApp ? (
            browser.doJavaScript(
                `(${xpathHarvest})("${strXPath}")`, { in: oWin.currentTab
                }
            )
        ) : strApp.startsWith('Google') ? (
            browser.execute(oWin.activeTab(), {
                javascript: `(${xpathHarvest})("${strXPath}")`
            })
        ) : [];
    };

    // tabsOpened :: Application -> Window -> (String, String) -> IO()
    const tabsOpened = (browser, oWin, links) => {
        const winTabs = oWin.tabs;
        links.forEach(
            link => winTabs.push(
                Object.assign(
                    browser.Tab(), {
                        url: link[1]
                    }
                )
            )
        );
    };

    // Harvesting function to run in the browser context
    const xpathHarvest = strPath => {
        const
            r = document.evaluate(
                strPath, document, null, XPathResult.ANY_TYPE, null
            ),
            xs = [];
        var oNode;
        while (oNode = r.iterateNext()) {
            xs.push([oNode.text, oNode.href]);
        }
        return xs;
    };

    // GENERIC FUNCTIONS ----------------------------------

    // https://github.com/RobTrew/prelude-jxa

    // Left :: a -> Either a b
    const Left = x => ({
        type: 'Either',
        Left: x
    });

    // Right :: b -> Either a b
    const Right = x => ({
        type: 'Either',
        Right: x
    });

    // Just :: a -> Just a
    const Just = x => ({
        type: 'Maybe',
        Nothing: false,
        Just: x
    });

    // Nothing :: () -> Nothing
    const Nothing = () => ({
        type: 'Maybe',
        Nothing: true,
    });

    // bindLR (>>=) :: Either a -> (a -> Either b) -> Either b
    const bindLR = (m, mf) =>
        undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // bindMay (>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
    const bindMay = (mb, mf) =>
        mb.Nothing ? mb : mf(mb.Just);

    // elem :: Eq a => a -> [a] -> Bool
    const elem = (x, xs) => xs.includes(x);

    // foldl :: (a -> b -> a) -> a -> [b] -> a
    const foldl = (f, a, xs) => xs.reduce(f, a);

    // showLog :: a -> IO ()
    const showLog = (...args) =>
        console.log(
            args
            .map(JSON.stringify)
            .join(' -> ')
        );

    // take :: Int -> [a] -> [a]
    const take = (n, xs) => xs.slice(0, n);

    // JXA ------------------------------------------------

    // frontAppNameLR :: () -> Either String String
    const frontAppNameLR = () => {
        const
            xs = Application('System Events')
            .applicationProcesses.where({
                frontmost: true
            });
        return 0 < xs.length ? (
            Right(xs[0].name())
        ) : Left('No frontmost application found');
    };

    // MAIN ---
    return main();
})();

Thanks for sharing. Looks very useful.

There seems to be a glitch. Even though I set "openResultTabs" to "false", it opened the links in tabs.

With just a bit of debug, I found:

/* blnOpenTabs:true */
/* openResultTabs:false */

Well caught – || true was left in after blnOpenTabs during testing.

Now removed above.

1 Like