Script :: Open all links in selected lines

While the GUI helpfully lets us look before we leap, (inspect a URL visually before we follow a link) there may be moments at the start of work when we want to batch-open all the links in all selected rows, without needing to visually inspect them first.

Here’s a Keyboard Maestro version, bound by default to ⌘J

Bike Outliner – Open all links in selected lines - Macro Library - Keyboard Maestro Discourse

and here’s the script itself, for testing in Script Editor.app (with language selector at top left set to JavaScript) or for using with other launchers, like FastScripts or Alfred.

See: Using Scripts - Bike

Expand disclosure triangle to view JS Source
(() => {
    "use strict";

    ObjC.import("AppKit");

    // Directly open (leap without look)
    // any links in the selected Bike rows.

    // main : IO ()
    const main = () => {
        const doc = Application("Bike").documents.at(0);

        return doc.exists() ? (() => {
            const
                message = alert(
                    "Open links in selected rows"
                );

            return either(message)(
                xs => 0 < xs.length ? (() => {
                    const
                        links = nub(xs.map(
                            x => x.attributes.href
                        ));

                    return Object.assign(
                        Application.currentApplication(), {
                            includeStandardAdditions: true
                        }
                    )
                    .doShellScript(
                        links.map(x => `open "${x}"`)
                        .join("\n")
                    ),
                    links.join("\n");
                })() : message(
                    "No links found in selected rows."
                )
            )(
                fmapLR(
                    filterTree(
                        x => Boolean(x.attributes.href)
                    )
                )(
                    dictFromHTML(
                        doc.export({
                            from: doc.rows.where({
                                selected: true
                            }),
                            as: "bike format",
                            all: false
                        })
                    )
                )
            );
        })() : "No documents open in Bike";
    };


    // ----------------------- XML -----------------------

    // dictFromHTML :: String -> Either String Tree Dict
    const dictFromHTML = html => {
        const
            error = $(),
            node = $.NSXMLDocument.alloc
            .initWithXMLStringOptionsError(
                html, 0, error
            );

        return Boolean(error.code) ? (
            Left("Not parseable as XML: " + (
                `${html}`
            ))
        ) : Right(xmlNodeDict(node));
    };

    // xmlNodeDict :: NSXMLNode -> Node Dict
    const xmlNodeDict = xmlNode => {
        const
            unWrap = ObjC.unwrap,
            blnChiln = 0 < parseInt(
                xmlNode.childCount, 10
            );

        return Node({
            name: unWrap(xmlNode.name),
            content: blnChiln ? (
                undefined
            ) : (unWrap(xmlNode.stringValue) || " "),
            attributes: (() => {
                const attrs = unWrap(xmlNode.attributes);

                return Array.isArray(attrs) ? (
                    attrs.reduce(
                        (a, x) => Object.assign(a, {
                            [unWrap(x.name)]: unWrap(
                                x.stringValue
                            )
                        }),
                        {}
                    )
                ) : {};
            })()
        })(
            blnChiln ? (
                unWrap(xmlNode.children)
                .reduce(
                    (a, x) => a.concat(xmlNodeDict(x)),
                    []
                )
            ) : []
        );
    };

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

    // alert :: String => String -> IO String
    const alert = title =>
        s => {
            const sa = Object.assign(
                Application("System Events"), {
                    includeStandardAdditions: true
                });

            return (
                sa.activate(),
                sa.displayDialog(s, {
                    withTitle: title,
                    buttons: ["OK"],
                    defaultButton: "OK"
                }),
                s
            );
        };

    // --------------------- GENERIC ---------------------

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


    // Node :: a -> [Tree a] -> Tree a
    const Node = v =>
    // Constructor for a Tree node which connects a
    // value of some kind to a list of zero or
    // more child trees.
        xs => ({
            type: "Node",
            root: v,
            nest: xs || []
        });


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


    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        // Application of the function fl to the
        // contents of any Left value in e, or
        // the application of fr to its Right value.
        fr => e => e.Left ? (
            fl(e.Left)
        ) : fr(e.Right);


    // filterTree (a -> Bool) -> Tree a -> [a]
    const filterTree = p =>
    // List of all values in the tree
    // which match the predicate p.
        foldTree(x => xs =>
            (
                p(x) ? [
                    [x], ...xs
                ] : xs
            ).flat(1)
        );


    // fmapLR (<$>) :: (b -> c) -> Either a b -> Either a c
    const fmapLR = f =>
    // Either f mapped into the contents of any Right
    // value in e, or e unchanged if is a Left value.
        e => "Left" in e ? (
            e
        ) : Right(f(e.Right));


    // foldTree :: (a -> [b] -> b) -> Tree a -> b
    const foldTree = f => {
        // The catamorphism on trees. A summary
        // value obtained by a depth-first fold.
        const go = tree => f(
            tree.root
        )(
            tree.nest.map(go)
        );

        return go;
    };

    // nub :: Eq a => [a] -> [a]
    const nub = xs =>
        [...new Set(xs)];


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

3 Likes