BIKE Outliner – Copy as Bulleted Plain Text

A macro for Jesse Grosjean's Bike Outliner.

Bike's default Edit > Copy places a clean unbulleted (tab-indented) outline in the clipboard (together with OPML and HTML outlines).

This macro provides a variant which copies either:

  1. the whole Bike document, or
  2. the selected rows (if the selection is extended)

as a bulleted plain text (tab-indented) outline.

BIKE - Copy as Bulleted Plain Text.kmmacros (6.3 KB)


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

    ObjC.import("AppKit");

    // Copy Bike document
    // (or Bike selected rows, if selected is extended)
    // as bulleted plain text.

    // RobTrew @2022
    // Ver 0.01

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

        return doc.exists() ? (
            copyText(
                bulletedTextFromBikeDoc(doc)
            )
        ) : "No document open in Bike";
    };

    // ---------------------- BIKE -----------------------

    // bulletedTextFromBikeDoc :: Bike Doc -> IO String
    const bulletedTextFromBikeDoc = doc => {
        const
            rows = Boolean(doc.selectedText()) ? (
                doc.rows.where({selected: true})
            ) : doc.rows;

        return bulletOutlineFromForest(
            forestFromIndentedLines(
                zip(
                    rows.level()
                )(
                    rows.name()
                )
            )
        );
    };

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

    // copyText :: String -> IO String
    const copyText = s => {
        const pb = $.NSPasteboard.generalPasteboard;

        return (
            pb.clearContents,
            pb.setStringForType(
                $(s),
                $.NSPasteboardTypeString
            ),
            s
        );
    };

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

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


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


    // bulletOutlineFromForest :: Forest String -> String
    const bulletOutlineFromForest = trees => {
        const go = tabs => tree => {
            const txt = tree.root.text;

            return [
                Boolean(txt) ? (
                    `${tabs}- ${txt}`
                ) : `${tabs}`,
                ...tree.nest.flatMap(go(`\t${tabs}`))
            ];
        };

        return trees.flatMap(go("")).join("\n");
    };


    // forestFromIndentedLines :: [(Int, String)] ->
    // [Tree {text:String, body:Int}]
    const forestFromIndentedLines = tuples => {
        const go = xs =>
            0 < xs.length ? (() => {
            // First line and its sub-tree,
                const [depth, body] = xs[0],
                    [tree, rest] = span(x => depth < x[0])(
                        xs.slice(1)
                    );

                return [
                    Node({
                        text: body,
                        level: depth
                    })(go(tree))
                ]
                // followed by the rest.
                    .concat(go(rest));
            })() : [];

        return go(tuples);
    };


    // 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 ? (
                Tuple(xs.slice(0, i))(
                    xs.slice(i)
                )
            ) : Tuple(xs)([]);
        };


    // zip :: [a] -> [b] -> [(a, b)]
    const zip = xs =>
    // The paired members of xs and ys, up to
    // the length of the shorter of the two lists.
        ys => Array.from({
            length: Math.min(xs.length, ys.length)
        }, (_, i) => [xs[i], ys[i]]);

    return main();
})();

Other Keyboard Maestro macros for BIKE Outliner

2 Likes