Copy from TaskPaper for pasting to Bike

Somebody asked me this week about an old script for TaskPaper 3 – Copy as Bike Outline

That earlier script still works, but here, FWIW, is a variant which:

  • Maps Taskpaper projects to Bike headings
  • Maps tasks and notes directly from TaskPaper to Bike
  • Reads Taskpaper @tag(value) to Bike tag=value
  • Collects any bracketless @tag patterns to the space-delimited value of a single data-classes attribute.

For example, this pattern in TaskPaper 3

To Organize Items:
    - To indent items press the Tab key. @red @priority(2)
    - To un-indent items press Shift-Tab.
    - To mark a task done click leading dash.

Might look, in Bike 2.0, like:

And if you were to Save As Markdown text, those lines would become:

- # To Organize Items
    - [ ] To indent items press the Tab key. {.red priority=2}
    - [ ] To un-indent items press Shift-Tab.
    - [ ] To mark a task done click leading dash.

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

    ObjC.import("AppKit");

    // `Copy from TaskPaper 3 as Bike`
    // ( Creats a com.hogbaysoftware.bike.xml pasteboard ).

    // Copies selected lines of front TaskPaper 3 document
    // (or all visible rows in the TP3 outline
    // if the selection is collapsed).

    // Rob Trew @2023, @2026
    // Ver 0.02

    // TP to Bike row type mappings:
    //     project -> heading
    //     task -> task
    //     note -> note

    // TP to Bike tag mappings
    //     @tag(value) -> data-tag=value
    //     @tag -> .class
    //     Where .classes in Bike row are added to the space
    //     -delimited value of a single `data-classes` attribute.


    // ------------- JXA EVALUATION CONTEXT --------------

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

        return either(
            alert("Copy from Taskpaper 3 as Bike outline")
        )(
            bikeLines => (
                setClipOfTextType(
                    "com.hogbaysoftware.bike.xml"
                )(
                    `<?xml version="1.0" encoding="UTF-8"?>
                        <html>
                            <head><meta charset="utf-8" /></head>
                            <body><ul>${bikeLines}</ul></body>
                        </html>`
                ),
                "TaskPaper copied as Bike outline."
            )
        )(
            fmapLR(
                doc => doc.evaluate({ script })
            )(
                document.exists()
                    ? Right(document)
                    : Left("No document open in TaskPaper.")
            )
        );
    };


    // ---------- TASKPAPER EVALUATION CONTEXT -----------

    const script = (editor => {
        const tp3Main = () => {
            const selection = editor.selection;

            return (
                selection.isCollapsed
                    ? Item.getCommonAncestors(
                        editor.displayedItems
                    )
                    : selection.selectedItemsCommonAncestors
            )
                .flatMap(foldTree(bikeLine))
                .join("\n");
        };

        // ------------------ BIKE XML -------------------

        // bikeLine :: TP Node -> [String] -> [String]
        const bikeLine = tpNode =>
            xs => {
                const
                    txt = tpNode.bodyContentString,
                    attribs = bikeAttributes(tpNode);

                return [
                    // <li> opens,
                    0 < attribs.length
                        ? [`<li ${attribs}>`]
                        : ["<li>"],
                    // <p>
                    [
                        0 < txt.length
                            ? `<p>${txt}</p>`
                            : "<p/>"
                    ],
                    // then <ul> if there are children,
                    0 < xs.length
                        ? [`<ul>${xs.flat().join("\n")}</ul>`]
                        : [],
                    // and </li> closes.
                    ["</li>"]
                ]
                    .flat();
            };


        // bikeAttributes :: TaskPaper Node -> String
        const bikeAttributes = tpNode => {
            const classesAndKVs = classes => k => {
                const v = (dict[k] || "").trim();

                return k.startsWith("data-")
                    ? k === "data-type"
                        ? [
                            classes, [
                                v === "project"
                                    ? 'data-type="heading"'
                                    : `data-type="${v}"`
                            ]
                        ]
                        : 0 < v.length
                            ? [classes, `${k}="${v}"`]
                            : [classes.concat(k.slice(5)), []]
                    : [classes, []];

                return [[], [""]];
            };

            const
                dict = tpNode.attributes,
                [classes, kvs] = mapAccumL(classesAndKVs)([])(
                    Object.keys(dict)
                );

            return [
                0 < kvs.length
                    ? [kvs.flat().join(" ")]
                    : [],
                0 < classes.length
                    ? [`data-classes="${classes.join(' ')}"`]
                    : []
            ]
                .flat()
                .join(" ");
        };


        // ------- GENERICS FOR TASKAPAPER CONTEXT -------

        // 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(
                root(tree)
            )(
                nest(tree).map(go)
            );

            return go;
        };


        // mapAccumL :: (acc -> x -> (acc, y)) -> acc -> [x] -> (acc, [y])
        const mapAccumL = f =>
            // A tuple of an accumulation and a list
            // obtained by a combined map and fold,
            // with accumulation from left to right.
            acc => xs => {
                const
                    n = xs.length,
                    ys = new Array(n);

                let mutableAcc = acc;

                for (let i = 0; i < n; i++) {
                    const [nextAcc, y] = f(mutableAcc)(xs[i], i);

                    mutableAcc = nextAcc;
                    ys[i] = y;
                }

                return [mutableAcc, ys];
            };


        // nest :: Tree a -> [a]
        const nest = tree =>
            tree.children;

        // root :: Tree a -> a
        const root = tree =>
            // The value attached to a tree node.
            tree;


        return tp3Main();
    })
        .toString();


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


    // setClipOfTextType :: String -> String -> IO String
    const setClipOfTextType = utiOrBundleID =>
        txt => {
            const pb = $.NSPasteboard.generalPasteboard;

            return (
                pb.clearContents,
                pb.setStringForType(
                    $(txt),
                    utiOrBundleID
                ),
                txt
            );
        };

    // ------------ GENERICS FOR JXA CONTEXT -------------

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


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


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

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

The script copies the current TaskPaper selection (or all visible rows if the selection cursor is collapsed), creating a com.hogbaysoftware.bike.xml pasteboard item, ready for pasting into Bike.

(key=value or .class attributes in Bike rows can be made visible with custom style-sheets or extensions)

2 Likes