Clickable palette of backlinks to Bike files which contain links to the active document

Thinking about this expression of interest in backlinks in a recent TaskPaper thread:

and not being entirely sure what backlinks were, I have sketched here a first draft of a Keyboard Maestro macro for Bike, which shows a clickable palette of links back to all .bike files which:

  1. contain links (file:// or bike://) to the active Bike document, and
  2. are in the same folder as the active Bike document.

Not sure how others use these things (or if these are actually what is meant by backlinks) but in my case, for example:

  • I have a folder containing an automatically-created working Bike file for every day’s working notes (notes2023-01-27.bike today, for example),
  • and in the same folder I have some project files, on which I may work for some number of days.

If for example, I am looking at a project file, and some of my day files contain links to it, then the clickable palette displayed by this macro:

  1. Aims to show clickable links for all the Bike files which contain links to this open project file, (reminding me of which days I worked on it) and
  2. will let me jump to any one of them, to read any dated meta notes on the project.

Very rough and experimental. Under the hood just uses grep, at this stage.

If you do try it, please let me know:

  • what is missing,
  • what doesn’t work in the way that you would expect,
  • and whether this is actually what is meant by backlinks at all : -)

BIKE – Back-links to files which link to this one.kmmacros.zip (17.4 KB)

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

    ObjC.import("AppKit");

    // Assumes that Keyboard Maestro is installed.
    // (https://www.keyboardmaestro.com/main/)

    // ------------------- ROUGH DRAFT -------------------

    // A clickable palette of *backlinks* to all Bike files
    // which contain links to the active Bike document.

    // RobTrew @2023
    // Ver 0.01

    // main :: IO ()
    const main = () => {
        // -------- BACK-LINK PALETTE DIMENSIONS. --------
        const
            // As integers,
            // or as Keyboard Maestro expressions.
            left = "SCREEN(Main, Left, 20%)",
            top = "SCREEN(Main, Top, 20%)",
            width = 500,
            height = 560;

        // -------------- PALETTE DISPLAYED --------------
        const
            bike = Application("Bike"),
            doc = bike.documents.at(0),
            file = doc.file();

        return either(
            alert("Links back to active Bike file")
        )(
            html => (
                keyboardMaestroPrompt(
                    linkPaletteHTML(
                        `${left},${top},${width},${height}`
                    )(file)(html)
                ),
                `Showed links to: ${file}`
            )
        )(
            doc.exists() ? (
                null !== file ? (
                    backLinksHtmlLR(doc)(file)
                ) : Left(
                    "Active document not yet saved."
                )
            ) : Left("No documents open in Bike")
        );
    };


    // linkPaletteHTML :: String -> FilePath ->
    // HTML String -> HTML String
    const linkPaletteHTML = rectString =>
        // HTML for a list of clickable backlinks.
        fp => html => [
            "<head><meta charset=\"utf-8\"/></head>",
            `<body data-kmwindow="${rectString}">`,
            "<style>",
            "body {font-family:-apple-system; color:#A0A0A0;}",
            "</style>",
            `<p><em>Links to ${fp}</em><p>`,
            html,
            "<p><br><em>Esc exits</em></p>",
            "</body>"
        ]
        .join("\n");


    // backLinksHtmlLR :: Bike Document ->
    // File -> Either String String
    const backLinksHtmlLR = doc =>
        // Either a message or an HTML string consisting
        // of <p> elements wrapping labelled backlinks.
        file => {
            const
                docID = doc.id(),
                fpDoc = `${file}`,
                fpFolder = takeDirectory(fpDoc),
                sa = Object.assign(
                    Application.currentApplication(),
                    {includeStandardAdditions: true}
                ),
                rgxLabel = /">([^<]*)</u,
                fp = encodeURI(fpDoc),
                grp = `egrep -B 1 -e '(${docID}|${fp})'`;

            const
                backLinks = groupOnKey(x => "--" === x)(
                    lines((() => {
                        try {
                            return sa.doShellScript(
                                `${grp} ${fpFolder}/*.bike`
                            );
                        } catch (e) {
                            return "";
                        }
                    })())
                )
                .flatMap(matchingPara(sa)(rgxLabel));

            return 0 < backLinks.length
                ? Right(backLinks.join("\n"))
                : Left(
                    [
                        "No links back to this file found:",
                        `\n\n\t${fpDoc}`
                    ]
                    .join("")
                );

        };

    // matchingPara :: StandardAdditions -> Regex ->
    // (String, String) -> [String]
    const matchingPara = sa =>
        // Either an empty list or a list containing, if
        // found, a row string label for a backlink.
        rgxLabel => kv => !kv[0] ? (() => {
            const
                ab = kv[1],
                txt = ab[1],
                [fp, li] = ab[0].split("-      ");

            return doesFileExist(fp) ? (() => {
                const fName = takeFileName(fp);

                return para(fName)(
                    aLink(
                        bkLink(
                            sa.doShellScript(docGrep(fp))
                            .split("\"")[1]
                        )(
                            Boolean(li)
                                ? li.split("\"")[1]
                                : ""
                        )
                    )(fName)
                )(
                    Boolean(txt)
                        ? (() => {
                            const m = rgxLabel.exec(txt);

                            return m ? m[1] : "";
                        })()
                        : ""
                );
            })() : [];
        })() : [];


    // keyboardMaestroPrompt :: HTML String -> IO ()
    const keyboardMaestroPrompt = html => {
        // Displays a clickable palette of backlinks.
        const
            pl = "-//Apple//DTD PLIST 1.0//EN",
            dtds = "www.apple.com/DTDs/",
            dtd = `http://${dtds}PropertyList-1.0.dtd`,
            code = "encoding=\"UTF-8\"";

        Application("Keyboard Maestro Engine")
        .doScript(`<?xml version="1.0" ${code}?>
            <!DOCTYPE plist PUBLIC "${pl}" "${dtd}">
            <plist version="1.0"><array><dict>
                <key>ActionUID</key>
                <integer>12935899</integer>
                <key>Floating</key>
                <true/>
                <key>MacroActionType</key>
                <string>CustomPrompt</string>
                <key>Resizable</key>
                <true/>
                <key>Text</key>
                <string>${htmlEncoded(html)}</string>
                <key>TimeOutAbortsMacro</key>
                <true/>
                <key>UseText</key>
                <true/>
            </dict></array></plist>`);
    };


    // docGrep :: FilePath -> String
    const docGrep = fp =>
        `grep -e '<ul id=' ${fp}`;


    // aLink, bkLink, para :: String ->
    // String -> String
    const aLink = url =>
        k => `<a href="${url}">${k}</a>`;


    const bkLink = docID =>
        rowID => Boolean(rowID)
            ? `bike://${docID}#${rowID}`
            : `bike://${docID}`;


    const para = fName =>
        link => label =>
            `<p>${link} → <em>${label || fName}</em></p>`;


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

    // doesFileExist :: FilePath -> IO Bool
    const doesFileExist = fp => {
        const ref = Ref();

        return $.NSFileManager.defaultManager
        .fileExistsAtPathIsDirectory(
            $(fp)
            .stringByStandardizingPath, ref
        ) && !ref[0];
    };

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

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


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


    // groupOnKey :: Eq k => (a -> k) -> [a] -> [(k, [a])]
    const groupOnKey = f => {
    // A list of (k, [a]) tuples, in which each [a]
    // contains only elements for which f returns the
    // same value, and in which k is that value.
    // The concatenation of the [a] in each tuple === xs.
        const go = xs =>
            0 < xs.length ? (() => {
                const
                    x = xs[0],
                    fx = f(x),
                    [yes, no] = span(y => fx === f(y))(
                        xs.slice(1)
                    );

                return [[fx, [x, ...yes]]].concat(go(no));
            })() : [];

        return go;
    };


    // 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 => "Left" in e ? (
            fl(e.Left)
        ) : fr(e.Right);


    // htmlEncoded :: String -> String
    const htmlEncoded = s => {
    // A string in which all punctuation is
    // encoded as numeric entities.
        const rgx = /[\w\s]/u;

        return [...(s || "")].map(
            c => rgx.test(c) ? (
                c
            ) : `&#${c.codePointAt(0)};`
        )
        .join("");
    };


    // lines :: String -> [String]
    const lines = s =>
    // A list of strings derived from a single string
    // which is delimited by \n or by \r\n or \r.
        Boolean(s.length) ? (
            s.split(/\r\n|\n|\r/u)
        ) : [];


    // span :: (a -> Bool) -> [a] -> ([a], [a])
    const span = p =>
    // Longest prefix of xs consisting of elements which
    // all satisfy p, tupled with the remainder of xs.
        xs => {
            const i = xs.findIndex(x => !p(x));

            return -1 !== i
                ? [xs.slice(0, i), xs.slice(i)]
                : [xs, []];
        };


    // takeFileName :: FilePath -> FilePath
    const takeFileName = fp =>
    // The file name component of a filepath.
        Boolean(fp) ? (
            "/" !== fp[fp.length - 1] ? (
                fp.split("/").slice(-1)[0]
            ) : ""
        ) : "";


    // takeDirectory :: FilePath -> FilePath
    const takeDirectory = fp =>
    // The directory component of a filepath.
        "" !== fp
            ? (
                xs => 0 < xs.length
                    ? xs.join("/")
                    : "."
            )(fp.split("/").slice(0, -1))
            : ".";


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

2 Likes