A first experiment with JavaScript (JXA) in Tinderbox 8

About the same speed as the AppleScript version - both are much faster now, and use the .notes() method which I had at first failed to spot in the dictionary, as a single apple-event route to the full collection of children for a given note.

Only one (very minor) request emerges from this:

  • Perhaps a more easily read value type when nextSibling has no real referent ? Something like missing value in AppleScript, or null / or undefined in JS.

Note that if you want to run the JavaScript for Automation code below in Script Editor, you will need to change the language selection tab at top left from AppleScript to JavaScript.

Now takes less than a second here for several traversals of a document with c 14o notes.

(() => {
    'use strict';

    // TESTING THE JAVASCRIPT FOR AUTOMATION API
    const main = () => {
        const ds = Application('Tinderbox 8').documents;
        return bindLR(
            0 < ds.length ? (
                Right(ds.at(0))
            ) : Left('No documents open in Tinderbox'),
            d => {
                const tree = pureTreeTBX(d);
                return 'Nodes: ' + foldTree(nodeCount, tree) +
                    '\nLeaf nodes: ' + foldTree(treeWidth, tree) +
                    '\nLayers of nesting: ' + foldTree(treeDepth, tree) +
                    '\nTitles:' + unlines(foldTree(preorder, tree));
            }
        );
    };

    // TINDERBOX OSA API ----------------------------------

    // noteID :: Note -> String
    const noteID = note =>
        note.attributes.byName('ID').value();

    // pureTreeTBX :: Note  -> Tree Note
    const pureTreeTBX = note => {
        const go = x =>
            Node(x, x.notes().map(go));
        return go(note);
    };

    // GENERIC TREE TRAVERSALS FOR USE WITH FOLDTREE ------

    // nodeCount :: Tree a -> Int
    const nodeCount = (_, xs) =>
        // One more than the total number of descendants.
        // (With foldTree, returns the total number of nodes in tree,
        // including the document/root node)
        1 + sum(xs);

    // treeDepth :: Tree a -> Int
    const treeDepth = (_, xs) =>
        // One more than that of the deepest child.
        // (With foldTree, returns the total number of levels in the tree)
        0 < xs.length ? 1 + maximum(xs) : 0;

    const treeWidth = (_, xs) =>
        // Sum of widths of any children, or a minimum of 1.
        // (With foldTree, returns the total count of
        //  childless leaf notes in the tree)
        0 < xs.length ? sum(xs) : 1;

    // preorder :: a -> [[a]] -> [a]
    const preorder = (x, xs) =>
        // Name of this node followed by the rest.
        // (With foldTree, returns an ordered list of note names)
        cons(x.name(), concat(xs))


    // 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, xs) => ({
        type: 'Node',
        root: v, // any type of value (consistent across tree)
        nest: xs || []
    });

    // Nothing :: Maybe a
    const Nothing = () => ({
        type: 'Maybe',
        Nothing: true,
    });

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

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = (a, b) => ({
        type: 'Tuple',
        '0': a,
        '1': b,
        length: 2
    });

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

    // concat :: [[a]] -> [a]
    // concat :: [String] -> String
    const concat = xs =>
        0 < xs.length ? (() => {
            const unit = 'string' !== typeof xs[0] ? (
                []
            ) : '';
            return unit.concat.apply(unit, xs);
        })() : [];

    // cons :: a -> [a] -> [a]
    const cons = (x, xs) =>
        Array.isArray(xs) ? (
            [x].concat(xs)
        ) : 'GeneratorFunction' !== xs.constructor.constructor.name ? (
            x + xs
        ) : ( // Existing generator wrapped with one additional element
            function*() {
                yield x;
                let nxt = xs.next()
                while (!nxt.done) {
                    yield nxt.value;
                    nxt = xs.next();
                }
            }
        )();

    // foldTree :: (a -> [b] -> b) -> Tree a -> b
    const foldTree = (f, tree) => {
        const go = node => f(node.root, node.nest.map(go));
        return go(tree);
    };

    // foldl1 :: (a -> a -> a) -> [a] -> a
    const foldl1 = (f, xs) =>
        1 < xs.length ? xs.slice(1)
        .reduce(f, xs[0]) : xs[0];

    // maximum :: Ord a => [a] -> a
    const maximum = xs =>
        0 < xs.length ? (
            foldl1((a, x) => x > a ? x : a, xs)
        ) : undefined;

    // sum :: [Num] -> Num
    const sum = xs => xs.reduce((a, x) => a + x, 0);

    // unfoldl(x => 0 !== x ? Just([x - 1, x]) : Nothing(), 10);
    // --> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

    // unfoldl :: (b -> Maybe (b, a)) -> b -> [a]
    const unfoldl = (f, v) => {
        let
            xr = [v, v],
            xs = [];
        while (true) {
            const mb = f(xr[0]);
            if (mb.Nothing) {
                return xs
            } else {
                xr = mb.Just;
                xs = [xr[1]].concat(xs);
            }
        }
    };

    // unlines :: [String] -> String
    const unlines = xs => xs.join('\n');

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

1 Like

PS, it’s a delight to be able to script Tinderbox like this, and makes the whole package much more useful to me.

I very much appreciate the huge amount of work that this has involved.

Thank you !

2 Likes