TaskPaper to Markdown Conversion

In the meanwhile, here is a very basic (few options) script which aims to copy the active TaskPaper 3 document to the clipboard as Markdown.

For the moment it assumes that you want your top-level headings to have a single MD # hash, and that all projects are hash headings.

(Could add an option to change that if anyone else does use this script)

(You can adjust whether or not to include the TaskPaper tags by editing the options dictionary at the top of the script)

const options = {
    includeTags: true,

    // If wholeDoc is *false*, only items  currently
    // displayed in the front document are copied as MD.
    wholeDoc: true
};

It’s a JavaScript for Automation script which you should be able to run either from Script Editor, with the language selector at top left set to JavaScript, or by assigning it to a keyboard shortcut with something like Keyboard Maestro or FastScripts. See Using Scripts in the TaskPaper User’s Guide.

(Be sure to copy every line of the code below. The last lines are:

    // MAIN ---
    return main();
})();
JavaScript source – click disclosure triangle
(() => {
    'use strict';

    ObjC.import('AppKit');

    // Copy TaskPaper 3 active document to clipboard
    // as basic Markdown.

    // Rob Trew 2020

    // Ver 0.04 wholeDoc=false -> folded lines hidden.
    // Ver 0.03
    // - Added a wholeDoc option. If this is set to false
    //   only focused projects will be copied as Markdown.
    // Ver 0.02
    // - Extended to cover top level lines which
    //   are not projects.
    // - Simplified TP Context code - projects list
    //   harvested with evaluateItemPath.

    const options = {
        includeTags: true,

        // If wholeDoc is false, only items  currently
        // displayed in the front document are copied as MD.
        wholeDoc: true
    };

    // ---------------------JXA CONTEXT---------------------
    const main = () => {
        const ds = Application('TaskPaper').documents;
        return either(
            alert('Problem')
        )(
            compose(
                alert('MD copied to clipboard'),
                copyText,
                mdFromTPForest
            )
        )(
            bindLR(
                0 < ds.length ? (
                    Right(ds.at(0))
                ) : Left('No TaskPaper documents open')
            )(
                d => d.evaluate({
                    script: tp3ProjectForest.toString(),
                    withOptions: options
                })
            )
        );
    };

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

    // tp3ProjectForest :: TP Editor -> [Tree Dict]
    const tp3ProjectForest = (editor, options) => {
        const showAll = options.wholeDoc;
        const main = () => Right(
            // Leftmost lines, or projects at any level.
            // (in whole document, or in visible focus)
            (
                showAll ? (
                    editor.outline.evaluateItemPath(
                        '/* union //project'
                    )
                ) : visibleLeftmostAndProjects()
            )
            // with any subtree of each,
            // (excluding sub-projects)
            .map(topLevel => foldTree(
                item => nest =>
                Node(item)(
                    nest.filter(
                        x => {
                            const dct = x.root;
                            return (
                                showAll || dct.visible
                            ) && ('project' !== dct.kvs[
                                'data-type'
                            ])
                        }
                    )
                )
            )(fmapPureTPTree(x => ({
                text: x.bodyContentString,
                kvs: x.attributes,
                depth: x.depth,
                visible: editor.isDisplayed(x),
                projectLevel: 'project' !== topLevel
                    .getAttribute('data-type') ? (
                        0
                    ) : topLevel.depth
            }))(topLevel)))
        );

        //  visibleLeftmostAndProjects :: () -> [TP Item]
        const visibleLeftmostAndProjects = () => {
            const
                xs = editor.displayedItems,
                outermost = minimum(xs.map(x => x.depth));
            return xs.filter(
                x => outermost === x.depth || (
                    'project' === x.attributes['data-type']
                )
            );
        };

        // --------GENERIC FUNCTIONS FOR TP 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
        });

        // Node :: a -> [Tree a] -> Tree a
        const Node = v => xs => ({
            type: 'Node',
            root: v,
            nest: xs || []
        });

        // fmapPureTPTree :: (TPItem -> a) -> TPItem -> Tree TPItem
        const fmapPureTPTree = f => item => {
            const go = x => Node(f(x))(
                x.hasChildren ? (
                    x.children.map(go)
                ) : []
            );
            return go(item);
        };

        // foldTree :: (a -> [b] -> b) -> Tree a -> b
        const foldTree = f =>
            // The catamorphism on trees. A summary
            // value obtained by a depth-first fold.
            tree => {
                const go = x => f(x.root)(
                    x.nest.map(go)
                );
                return go(tree);
            };

        // minimum :: Ord a => [a] -> a
        const minimum = xs =>
            0 < xs.length ? (
                xs.slice(1)
                .reduce((a, x) => x < a ? x : a, xs[0])
            ) : undefined;

        return main();
    };

    // FUNCTIONS FOR JXA

    // --------------BASIC MARKDOWN FUNCTIONS---------------

    // mdFromTPForest :: [Tree Dict] -> String
    const mdFromTPForest = xs => {
        const blnTags = options.includeTags;
        return concat(xs.map(foldMapTree(x =>
            '    '.repeat(
                x.depth - (
                    x.projectLevel + (
                        isProject(x) ? 0 : 1
                    )
                )
            ) + mdPrefix(x) + x.text + (
                blnTags ? mdTags(x) : ''
            ) + '\n'
        )));
    };

    // isProject :: Dict -> Bool
    const isProject = x =>
        'project' === x.kvs['data-type'];

    // mdPrefix :: Dict -> String
    const mdPrefix = x => {
        const type = x.kvs['data-type'];
        return 'note' !== type ? (
            'task' !== type ? (
                'project' !== type ? (
                    ''
                ) : ('\n' + '#'.repeat(x.depth) + ' ')
            ) : '- '
        ) : '';
    };

    // mdTags :: Dict -> String
    const mdTags = x => {
        const
            dct = x.kvs,
            ks = Object.keys(dct).filter(
                k => k.startsWith(
                    'data'
                ) && k !== 'data-type'
            );
        return 0 < ks.length ? (
            ' @' + ks.map(
                k => 0 < dct[k].length ? (
                    `${k.slice(5)}(${dct[k]})`
                ) : k.slice(5)
            ).join(' @')
        ) : '';
    };

    // ------------------JXA FUNCTIONS------------------

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

    // copyText :: String -> IO String
    const copyText = s => {
        // String copied to general pasteboard.
        const pb = $.NSPasteboard.generalPasteboard;
        return (
            pb.clearContents,
            pb.setStringForType(
                $(s),
                $.NSPasteboardTypeString
            ),
            s
        );
    };

    // GENERIC FUNCTIONS ----------------------------
    // https://github.com/RobTrew/prelude-jxa

    // Just :: a -> Maybe a
    const Just = x => ({
        type: 'Maybe',
        Nothing: false,
        Just: x
    });

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

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

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

    // append (++) :: [a] -> [a] -> [a]
    // append (++) :: String -> String -> String
    const mappend = xs =>
        // A list or string composed by
        // the concatenation of two others.
        ys => xs.concat(ys);

    // bindLR (>>=) :: Either a ->
    // (a -> Either b) -> Either b
    const bindLR = m =>
        mf => undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
        x => fs.reduceRight((a, f) => f(a), x);

    // concat :: [[a]] -> [a]
    // concat :: [String] -> String
    const concat = xs =>
        0 < xs.length ? (
            xs.every(x => 'string' === typeof x) ? (
                ''
            ) : []
        ).concat(...xs) : xs;

    // either :: (a -> c) -> (b -> c) -> Either a b -> c
    const either = fl =>
        fr => e => 'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

    // foldMapTree :: Monoid m => (a -> m) -> Tree a -> m
    const foldMapTree = f =>
        // Result of mapping each element of the tree to
        // a monoid, and combining with mappend.
        node => {
            const go = x =>
                0 < x.nest.length ? mappend(f(x.root))(
                    foldl1(mappend)(x.nest.map(go))
                ) : f(x.root);
            return go(node);
        };

    // foldl1 :: (a -> a -> a) -> [a] -> a
    const foldl1 = f =>
        // Left to right reduction of the non-empty list xs,
        // using the binary operator f, with the head of xs
        // as the initial acccumulator value.
        xs => 1 < xs.length ? xs.slice(1)
        .reduce(uncurry(f), xs[0]) : xs[0];

    // uncurry :: (a -> b -> c) -> ((a, b) -> c)
    const uncurry = f =>
        // A function over a pair, derived
        // from a curried function.
        (x, y) => f(x)(y);

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