Interim script: Inspect URL behind blue text

A temporary script while (Preview 69) has blued text with hyperlinks but not yet a “look before you leap” tooltip to visually check/inspect the URL with.

(Displays a dialog showing the link(s) behind blue text in the current line, and offers to open one or all of them, or Cancel)

BIKE - Inspect link url(s) behind blue text in current row before opening.kmmacros.zip (5.5 KB)

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

    // BIKE (Preview 69) draft script.
    // "Look before leap" for any links in current row.

    // Display a dialog showing Title and URL for
    // each link (if any) in the current Bike row,
    // offering a chance to open one or all, or cancel.

    // Rob Trew @2022
    // Ver 0.06

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

        return doc.exists() ? (
            either(
                alert("Links in current row")
            )(xs => {
                const n = xs.length;

                return 0 < n ? (
                    1 < n ? (
                        showLinks(xs)
                    ) : showSingleLink(xs[0])
                ) : "No links found in current row.";
            })(
                titleLinkPairsFromBikeRowHtmlLR(
                    doc.selectionRow.htmlContent()
                )
            )
        ) : "No documents open in Bike";
    };

    // -------------- LINKS IN CURRENT ROW ---------------

    // showLinks :: [(String, URL String)] -> IO ()
    const showLinks = xs =>
        0 < xs.length ? (() => {
            const
                se = Object.assign(
                    Application("System Events"),
                    {includeStandardAdditions: true}
                ),
                links = xs.map(snd),
                choices = (
                    se.activate(),
                    se.chooseFromList(links, {
                        withTitle: "Links in row",
                        withPrompt: xs.map(
                            (x, i) => `${1 + i}. ${x[0]}`
                        ).join("\n"),
                        defaultItems: [links[0]],
                        okButtonName: "Open",
                        cancelButtonName: "Cancel",
                        multipleSelectionsAllowed: true,
                        emptySelectionAllowed: true
                    })
                );

            return Boolean(choices) ? (
                Object.assign(
                    Application.currentApplication(),
                    {includeStandardAdditions: true}
                ).doShellScript(
                    choices.map(k => `open ${k}`)
                    .join("\n")
                )
            ) : "No link chosen.";
        })() : "'showLinks' undefined for empty list.";


    // showSingleLink :: (String, URL String) -> IO ()
    const showSingleLink = ([title, url]) => {
        // Display, and choice of following or ignoring,
        // a single (Title, URL) pair.
        const
            se = Object.assign(
                Application("System Events"),
                {includeStandardAdditions: true}
            ),
            legend = `Name: ${title}\n\nLink: ${url}`;

        try {
            return (
                se.activate(),
                se.displayDialog(legend, {
                    buttons: ["Cancel", "Open"],
                    defaultButton: "Open",
                    withTitle: (
                        "First link in selected Bike row"
                    ),
                    givingUpAfter: 30
                }),
                Object.assign(
                    Application.currentApplication(),
                    {includeStandardAdditions: true}
                ).doShellScript(`open ${url}`),
                `Opening: ${url}`
            );
        } catch (e) {
            return e.message;
        }
    };


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

    // titleLinkPairsFromBikeRowHtmlLR ::
    // HTML String -> Either String [(String, String)]
    const titleLinkPairsFromBikeRowHtmlLR = rowHTML =>
    // A sequence of plain text (Title, URL) pairs
    // extracted from any inline formatting in
    // the HTML of a Bike rows.
        fmapLR(
            compose(
                map(x => [
                    contentFromNest(x),
                    x[0].root.attributes.href
                ]),
                groupBy(on(eq)(
                    v => v.root.attributes.href
                )),
                treeMatches(dct => "a" === dct.name)
            )
        )(
            dictFromHTML(rowHTML)
        );


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


    // contentFromNest :: [Node Dict] -> String
    const contentFromNest = forest =>
    // Concatenation of any descendent content attributes.
        forest.map(
            t => foldTree(x => vs => {
                const maybeText = x.content;

                return Boolean(maybeText) ? (
                    [maybeText].concat(vs.flat())
                ) : vs.flat();
            })(t).join("")
        ).join("");


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


    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        // A pair of values, possibly of
        // different types.
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2,
            *[Symbol.iterator]() {
                for (const k in this) {
                    if (!isNaN(k)) {
                        yield this[k];
                    }
                }
            }
        });


    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
    // A function defined by the right-to-left
    // composition of all the functions in fs.
        fs.reduce(
            (f, g) => x => f(g(x)),
            x => 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);


    // eq (==) :: Eq a => a -> a -> Bool
    const eq = a =>
    // True when a and b are equivalent in the terms
    // defined below for their shared data type.
        b => a === b;


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


    // groupBy :: (a -> a -> Bool) -> [a] -> [[a]]
    const groupBy = eqOp =>
    // A list of lists, each containing only elements
    // equal under the given equality operator,
    // such that the concatenation of these lists is xs.
        xs => Boolean(xs.length) ? (() => {
            const [h, ...t] = xs;
            const [groups, g] = t.reduce(
                ([gs, a], x) => eqOp(x)(a[0]) ? (
                    Tuple(gs)([...a, x])
                ) : Tuple([...gs, a])([x]),
                Tuple([])([h])
            );

            return [...groups, g];
        })() : [];


    // map :: (a -> b) -> [a] -> [b]
    const map = f =>
    // The list obtained by applying f
    // to each element of xs.
    // (The image of xs under f).
        xs => [...xs].map(f);


    // on :: (b -> b -> c) -> (a -> b) -> a -> a -> c
    const on = f =>
    // e.g. groupBy(on(eq)(length))
        g => a => b => f(g(a))(g(b));


    // snd :: (a, b) -> b
    const snd = tpl =>
    // Second member of a pair.
        tpl[1];


    // treeMatches :: (a -> Bool) -> Tree a -> [Tree a]
    const treeMatches = p => {
    // A list of all nodes in the tree which match
    // a predicate p.
    // For the first match only, see findTree.
        const go = tree =>
            p(tree.root) ? (
                [tree]
            ) : tree.nest.flatMap(go);

        return go;
    };


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

To test in Script Editor, set language selector at top left to JavaScript.

See: Using Scripts - Bike

1 Like