TP Scripting - a generic accumulating tree-walker (foldlTP)


#1

Here is a generic recursion scheme (a function named foldlTP) for:

  • starting with some initial value (a number, string, dictionary etc)
  • walking systematically over the whole TaskPaper outline, while
  • accumulating incremental updates to the initial value.

This simplest examples might be:

  • Counting how many childless leaves an outline contains,
  • finding the maximum indent level in an outline, or just
  • counting the total number of items in an outline,
  • or counting the number of items that have a particular tag.

foldlTP may look familiar if you have already acquainted yourself with the standard JavaScript Array methods Array.reduce and Array.reduceRight.

If you haven’t, I recommend them – they are wonderfully powerful and convenient.

Array.reduce walks from left to right through a flat JavaScript array, accumulating changes to an initial value.

foldlTP walks top-down left-left through the whole tree structure of a TaskPaper outline, accumulating changes to an initial value.

From the TP3 outline which I am looking at now, I could obtain the result:

{
  "leafCount": 34,
  "maxDepth": 5,
  "nodeCount": 54,
  "doneCount": 6
}

using this generic recursion-scheme function four times,

// foldlTP :: (a -> TPItem -> a) -> a -> TPItem -> a
const foldlTP = (f, acc, item) => {
    const go = (a, x) =>
        x.hasChildren ? x.children.reduce(
            go,
            f(a, x)
        ) : f(a, x);
    return go(acc, item);
};

by writing something like the following, and testing it in Script Editor etc

(See https://guide.taskpaper.com/using-taskpaper/using-scripts.html )

(() => {
    'use strict';

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

    const tpJSContext = (editor, options) => {

        // showJSON :: a -> String
        const showJSON = x => JSON.stringify(x, null, 2);

        // foldlTP :: (a -> TPItem -> a) -> a -> TPItem -> a
        const foldlTP = (f, acc, item) => {
            const go = (a, x) =>
                x.hasChildren ? x.children.reduce(
                    go,
                    f(a, x)
                ) : f(a, x);
            return go(acc, item);
        };

        // max :: Ord a => a -> a -> a
        const max = (a, b) => b > a ? b : a;


        // MAIN --------------------------------------------------------------

        // root :: TP Item
        const root = editor.outline.root;

        // measure :: (Int -> Int -> Int) -> Int
        const measure = f => foldlTP(f, 0, root);

        return showJSON({

            // (+1) if the item is childless
            leafCount: measure((a, x) => a + (x.hasChildren ? 0 : 1)),

            // Greater of (deepest so far) and (this item's depth)
            maxDepth: measure((a, x) => max(a, x.depth)),

            // (+1) for any item
            nodeCount: measure(a => a + 1),

            // (+1) for any @done item
            doneCount: measure(
                (a, x) => a + (x.hasAttribute('data-done') ? 1 : 0)
            )
        });
    };

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

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

    const
        ds = Application('TaskPaper')
        .documents,
        lrResult = bindLR(
            ds.length > 0 ? Right(ds.at(0)) : Left('No documents open'),
            d => Right(d.evaluate({
                script: tpJSContext.toString(),
                withOptions: {}
            }))
        );
    return lrResult.Right || lrResult.Left;
})();


#2

And of course, using Array.reduce alone, you could do the same over

  • the outline.items array, or, for a particular sub-tree, the
  • item.descendants array