Bike to and from TP

A first draft of a basic Copy As Bike Outline for TaskPaper 3

  • Drops task bullets and project colons
  • Copies TaskPaper tags as literal text

Other refinements could be added, if they seemed necessary (perhaps tags as Bike attributes rather than literal text, and/or special handling of any links in the TaskPaper.

But for the moment, probably sensible to test a basic version and see if we can get that working OK for you:

TaskPaper 3 – Copy As Bike Outline.kmmacros.zip (2.8 KB)

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

    ObjC.import("AppKit");

    // First draft of a basic
    // `Copy from TaskPaper 3 as Bike`
    // com.hogbaysoftware.bike.xml
    // clipboard.

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

    // Rob Trew @2023
    // Ver 0.01

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

    // eslint-disable-next-line max-lines-per-function
    const tp3Context = editor => {
        // eslint-disable-next-line max-lines-per-function
        const tp3Main = () => {
            const selection = editor.selection;

            return (
                selection.isCollapsed ? (
                    // eslint-disable-next-line no-undef
                    Item.getCommonAncestors(
                        editor.displayedItems
                    )
                ) : selection.selectedItemsCommonAncestors
            )
            .flatMap(foldTreeTP(bikeLine))
            .join("\n");
        };


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

        // bikeLine :: TP Node -> [String] -> String
        const bikeLine = tpNode => xs => {
            // Nest of <li><p> ... </li>
            const
                txt = tpNode.bodyContentString,
                tags = tagText(tpNode),
                txtAndTags = tags ? (
                    `${txt} ${tags}`
                ) : `${txt}`;

            return [
                ["<li>"],
                [
                    Boolean(txt) ? (
                        `<p>${txtAndTags}</p>`
                    ) : "<p/>"
                ],
                0 < xs.length ? [(
                    `<ul>${xs.flat().join("\n")}</ul>`
                )] : [],
                ["</li>"]
            ]
            .flat();
        };

        // tagText :: TP Node -> String
        const tagText = tpNode => {
            // Any trailling tags, or an empty string.
            const dict = tpNode.attributes;

            return Object.keys(dict).flatMap(
                k => k.startsWith("data-") && (
                    k !== "data-type"
                ) ? (() => {
                        const
                            short = k.slice(5),
                            v = dict[k];

                        return v ? (
                            `@${short}(${v})`
                        ) : `@${short}`;
                    })() : []
            )
            .join(" ");
        };

        // ----------- GENERICS FOR TASKPAPER ------------

        // foldTreeTP :: (TPNode -> [a] -> a) -> TPNode -> a
        const foldTreeTP = f => {
            // The catamorphism on TP3 trees. A summary
            // value obtained by a depth-first fold.
            const go = node => f(node)(
                node.children.map(go)
            );

            return go;
        };

        return tp3Main();
    };

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

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

        return doc.exists() ? (() => {
            const
                bikeLines = doc.evaluate({
                    script: `${tp3Context}`
                });

            return (
                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."
            );
        })() : "No document open in TaskPaper.";
    };

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

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

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

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

And, FWIW, here is an example of a variant which applies Bike rich text formats to different parts of the copied TaskPaper outline:

  • Tags formatted as code
  • Project names formatted bold
  • Notes formatted italic

TaskPaper 3 – Copy As Bike Outline (Rich Text).kmmacros.zip (3.0 KB)

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

    ObjC.import("AppKit");

    // First draft of a basic
    // `Copy from TaskPaper 3 as Bike`
    // com.hogbaysoftware.bike.xml
    // clipboard.

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

    // Rob Trew @2023
    // Ver 0.02

    // TaskPaper tag text formatted as <code>
    // TaskPaper project names formatted as <strong>
    // TaskPaper notes formatted as <em>

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

    // eslint-disable-next-line max-lines-per-function
    const tp3Context = editor => {
    // eslint-disable-next-line max-lines-per-function
        const tp3Main = () => {
            const selection = editor.selection;

            return (
                selection.isCollapsed ? (
                // eslint-disable-next-line no-undef
                    Item.getCommonAncestors(
                        editor.displayedItems
                    )
                ) : selection.selectedItemsCommonAncestors
            )
            .flatMap(foldTreeTP(bikeLine))
            .join("\n");
        };


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

        // bikeLine :: TP Node -> [String] -> String
        const bikeLine = tpNode => xs => {
        // Nest of <li><p> ... </li>
            const
                txt = tpNode.bodyContentString,
                dict = tpNode.attributes,
                nodeType = dict["data-type"],
                richText = nodeType === "project" ? (
                    `<strong>${txt}</strong>`
                ) : nodeType === "note" ? (
                    `<em>${txt}</em>`
                ) : txt,
                tags = tagText(dict),
                txtAndTags = tags ? (
                    `${richText} <code>${tags}</code>`
                ) : `${richText}`;

            return [
                ["<li>"],
                [
                    Boolean(txt) ? (
                        `<p>${txtAndTags}</p>`
                    ) : "<p/>"
                ],
                0 < xs.length ? [
                    `<ul>${xs.flat().join("\n")}</ul>`
                ] : [],
                ["</li>"]
            ]
            .flat();
        };

        // tagText :: Attribute Dict -> String
        const tagText = dict =>
        // Any trailling tags, or an empty string.
            Object.keys(dict).flatMap(
                k => k.startsWith("data-") && (
                    k !== "data-type"
                ) ? (() => {
                        const
                            short = k.slice(5),
                            v = dict[k];

                        return v ? (
                            `@${short}(${v})`
                        ) : `@${short}`;
                    })() : []
            )
            .join(" ");


        // ----------- GENERICS FOR TASKPAPER ------------

        // foldTreeTP :: (TPNode -> [a] -> a) -> TPNode -> a
        const foldTreeTP = f => {
        // The catamorphism on TP3 trees. A summary
        // value obtained by a depth-first fold.
            const go = node => f(node)(
                node.children.map(go)
            );

            return go;
        };

        return tp3Main();
    };

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

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

        return doc.exists() ? (() => {
            const
                bikeLines = doc.evaluate({
                    script: `${tp3Context}`
                });

            return (
                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."
            );
        })() : "No document open in TaskPaper.";
    };

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

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

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

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

As above, to test in Script Editor, set the language selector at top left to JavaScript.

See: Using Scripts - Bike

1 Like

perfect!
thanks a lot…
I answer late because I am at CET (now 10 am)

1 Like

Might it be useful, do you think, to adjust the Copy As TaskPaper (for Bike) so that treats any fully italicised paragraphs which it finds in Bike as Notes for the purposes of TaskPaper ?

(i.e. simply doesn’t prepend them with a - bullet)


I’m also wondering if any other adjustments would reduce any round-tripping friction between that pair of Copy As scripts.

We should, for example, probably make sure that any row in Bike which already ends in : doesn’t get an extra one in TaskPaper.

(Let me know if anything else turns up, or comes to mind)

Should fully bolded Bike rows be copied as Project rows for TaskPaper, for example ?

1 Like

I agree for " any fully italicised paragraphs " and also for

but perhaps is my usage…
one has to pay attention in teh round-trips waiting for tags (and notes… :slight_smile: ) come to Bike

A second draft of a Copy As TaskPaper for Bike:

BIKE - Copy as TaskPaper.kmmacros.zip (15.2 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 TaskPaper text

    // RobTrew @2022
    // Ver 0.02

    // Rows are treated as Projects (in TaskPaper terms)
    // if they are either at top level, or end in a colon,
    // or are fully bolded from start to end.

    // Rows are treated as TaskPaper notes
    // (i.e. no leading bullet is added)
    // if they are fully formatted as Italic from start to end.

    // Other rows are interpreted as TaskPaper tasks,
    // and copied with a prepended bullet ("- ")

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

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

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

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

        return tp3OutlineFromForest(
            forestFromIndentedLines(
                zip3(
                    rows.level()
                )(
                    rows.name()
                )(
                    rows()
                )
            )
        );
    };

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


    // tp3OutlineFromForest ::
    // Forest {level :: Int, text :: String} -> String
    const tp3OutlineFromForest = trees => {
        const go = tabs => baseLevel => tree => {
            const
                node = tree.root,
                txt = node.text,
                // attribs = node.attribs,
                // kvs = attribs.map(
                //     attr => [attr.name(), attr.value()]
                // ),
                hasText = 0 < txt.length,
                textContent = hasText ? (
                    node.textContent
                ) : null,
                [bold, italic] = (null !== textContent) ? [
                    textContent.bold(), textContent.italic()
                ] : [false, false],
                finalColon = hasText && txt.endsWith(":"),
                isProject = (baseLevel === node.level) || bold || (
                    finalColon
                ),
                isNote = italic;

            return [
                hasText ? (
                    isProject ? (
                        `${tabs}${finalColon ? txt : txt}:`
                    ) : isNote ? (
                        `${tabs}${txt}`
                    ) : `${tabs}- ${txt}`
                ) : `${tabs}`,
                ...tree.nest.flatMap(
                    go(`\t${tabs}`)(baseLevel)
                )
            ];
        };

        return 0 < trees.length ? (
            trees.flatMap(
                go("")(trees[0].root.level)
            )
            .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, row] = xs[0],
                    [tree, rest] = span(x => depth < x[0])(
                        xs.slice(1)
                    );

                return [
                    Node({
                        text: body,
                        level: depth,
                        attribs: row.attributes(),
                        textContent: row.textContent
                    })(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)([]);
        };


    // zip3 :: [a] -> [b] -> [c] -> [(a, b, c)]
    const zip3 = xs =>
        ys => zs => xs.slice(
            0,
            Math.min(...[xs, ys, zs].map(x => x.length))
        )
        .map((x, i) => [x, ys[i], zs[i]]);


    // showLog :: a -> IO ()
    const showLog = (...args) =>
    // eslint-disable-next-line no-console
        console.log(
            args
            .map(JSON.stringify)
            .join(" -> ")
        );

    return main();
})();

It now interprets any fully italicised rows as Notes for TaskPaper purposes, and prepends no bullet to them.

Rows matching any one of the following conditions:

  • Top level (no indent),
  • or fully bolded (from start to end)
  • or terminating with a :

are treated as Projects in TaskPaper terms, and copied with a trailing colon and no leading bullet.

All other Bike lines are interpreted as Tasks for use in TaskPaper, and prepended with a bullet.


Let me know if anything else can usefully be adjusted or fixed.

thanks
a bit hesitating on this

but I must admit has a logic…
in my workflow anyway -if this doesn’t get too complicated- I prefer not to have a fully bolded sentence as a project, only the two other matching conditions…

1 Like

Those conditions are not all required of course – any one of them is sufficient on its own, in this draft.

(Just top-level position, or alternatively just a trailing colon, is enough)


But not ideal, of course, to have types depend on styles – the other way around seems simpler : -)

(if @jessegrosjean decides, at any point, to develop stylesheets for Bike, then we will be able to copy TaskPaper type attributes directly (Project | Task | Note) and use attribute-based styles to differentiate them visually in any way that a user happens to prefer)

2 Likes

ok
keep waiting for Jesse
Thanks a lot Rob!