BIKE Outliner – Toggle a Markdown checkbox in selected lines

Toggle a Markdown checkbox in selected rows of Jesse Grosjean's Bike Outliner.

An earlier script toggled single Unicode prefix characters.

This version is based on a general script for toggling prefixes of any length.

In this case it cycles between two Markdown checkbox states and one no-prefix state:

[ ] -> [x] -> '' -> [ ] ...

BIKE – Toggle a Markdown checkbox in selected lines.kmmacros (6.3 KB)


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

    // Cycle a set of prefixes (single or multi character)
    // in selected (non-empty) lines of Bike 1.3

    // Rob Trew @2022
    // Ver 0.01

    // --------------------- OPTION ----------------------
    // Which prefixes to cycle ?
    // (An empty string is interpreted as a
    //  no-prefix stage in the cycle).
    const prefixCycle = ["[ ]", "[x]", ""];

    // ----------------- PREFIX TOGGLED ------------------
    // main :: IO ()
    const main = () => {
        const
            bike = Application("Bike"),
            doc = bike.documents.at(0);

        return doc.exists() ? (() => {
            const
                selectedNonEmptyRows = doc.rows.where({
                    selected: true,
                    _not: [{
                        name: ""
                    }]
                });

            return Boolean(selectedNonEmptyRows.length) ? (
                bikeRowsPrefixCycled([...prefixCycle])(
                    selectedNonEmptyRows
                )
            ) : "No non-empty rows selected in Bike.";
        })() : "No documents open in Bike.";
    };

    // ------------------ PREFIX CYCLED ------------------

    // bikeRowsPrefixCycled :: [String] ->
    // IO Bike Rows -> IO String
    const bikeRowsPrefixCycled = prefixes =>
        // Any selected non-empty rows in Bike cycled
        // to the the next prefix in prefixes
        selectedNonEmptyRows => {
            const
                n = selectedNonEmptyRows.length,
                plural = 1 < n ? "s" : "",
                [f, change] = (() => {
                    const
                        iPrefix = prefixes
                        .findIndex(
                            k => selectedNonEmptyRows
                            .at(0).name()
                            .startsWith(k)
                        ),
                        nextPrefix = prefixes[
                            -1 !== iPrefix ? (
                                (1 + iPrefix) % prefixes
                                .length
                            ) : 0
                        ];

                    return [
                        updatedLine(prefixes)(nextPrefix),
                        "" !== nextPrefix ? (
                            `Set ${nextPrefix}`
                        ) : "Cleared"
                    ];
                })();

            return (
                zipWith(row => s => row.name = s)(
                    selectedNonEmptyRows()
                )(
                    selectedNonEmptyRows.name().map(f)
                ),
                [
                    `${change} prefix`,
                    `in ${n} selected line${plural}.`
                ]
                .join("\n")
            );
        };


    // updatedLine :: [String] -> String -> String -> String
    const updatedLine = prefixes =>
        // A line in which the prefix has been
        // replaced by the next option in a cycle.
        // An empty string in the prefixes list is
        // interpreted as no-prefix stage in the cycle.
        pfx => s => {
            const pre = Boolean(pfx) ? `${pfx} ` : "";

            return Boolean(s) ? (() => {
                const
                    iExisting = prefixes.findIndex(
                        k => Boolean(k) && s.startsWith(k)
                    );

                return -1 !== iExisting ? (
                    pre + s.slice(
                        1 + prefixes[iExisting].length
                    )
                ) : (pre + s.trim());
            })() : pre;
        };


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

    // zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
    const zipWith = f =>
        // A list constructed by zipping with a
        // custom function, rather than with the
        // default tuple constructor.
        xs => ys => xs.map(
            (x, i) => f(x)(ys[i])
        ).slice(
            0, Math.min(xs.length, ys.length)
        );

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

Other Keyboard Maestro macros for BIKE Outliner

2 Likes