TaskPaper ⇄ Tinderbox import export

Hello,

I’m also an academic and use Tinderbox to structure my notes and projects, it just sometimes feels as if TBX too much going on and this distracts me from the task at hand. But it is also a remarkable tool that I see myself using for some time to come.
It’s just, like you, I’d rather think/compose initially in a sparer environment. Taskpaper is ideal for this.

I was intrigued by the idea of mapping the @key(value) tags of Taskpaper onto TBX’s attributes. I learned that this is something that the program automatically does for some of TPs @key(value) pairs upon import related mainly to tasks. Could you elaborate a bit on the basic scripting necessary to manage other attributes?

All best,
Maurice

I will certainly look out my old Tinderbox scripts, perhaps nearer the end of the week.

(I’ve personally moved away from using Tinderbox – I realised in the end that part of what I’m used to in plain text is not just the lack of modal widgets, it’s also the solidity and the rarity of crashes, perhaps inevitably more common with a more ambitious and more experimental GUI).

1 Like

Thanks so much for this. I really appreciate it.

I know what you mean. I was an emacs/org-mode user until last year. In the end, I found that I spent too much time messing with the init file and not enough time writing.

For me, the visual aspect of seeing my notes in Tinderbox quite appealing, but I will surely evaluate the cost of spending time adjusting the program’s robust options and avoiding its many pitfalls. It would be nice to know that I have some easy plaintext backups which is where Taskpaper fits the bill nicely. Thanks again. I’ve been relying on many of your other scripts too. You are inspiring me to learn Javascript.

1 Like

TaskPaper is good middle ground – very uncluttered outlining,
and scripting when you need it : - )

I’ll catch up later in the week.

Haven’t forgotten – Sat eve or Sunday now, I think.

Thanks. No rush. I appreciate the note.

Perhaps the place for this is really a Github collection of notes and examples on moving attributes between TaskPaper and Tinderbox, but lets start here, and then move elsewhere if it grows too much.

Let’s assume:

  • we are drafting things in TaskPaper, and then adding them to some part of a growing Tinderbox document.
  • Taskpaper @tags and @key(value) pairs will map onto Tinderbox Attributes.
  • and that we are going to place things in Tinderbox via JavaScript.
    (an alternative is to create an XML-defined clipboard from which we can paste items with attributes into Tinderbox. Let me know if you would prefer a copy as Tinderbox script for TaskPaper)

Let’s also assume that it may be more helpful to step through the basics, rather than just hand over a clump of slightly over-specialised scripts and script-fragments.


The first thing to get right is the set of data types at each end.

A few basics:

  • Case: While TaskPaper @tags and @key(value) pairs tend to have lower-case names, the Tinderbox convention is for attributes whose names start with a Capital.
  • Available attributes: TaskPaper gives us the freedom to create any new @tag or @key(value) combination on the fly, but Tinderbox needs an Attribute to be named, and given a data type, and a default value, before we can create a note that has such an attribute.

That means that to use a TaskPaper tag (renamed to start with a capital) as an attribute in a Tinderbox document, we will need to:

  • see if such an Attribute already exists in the target Tinderbox document,
  • and if it doesn’t, create it, and give it a specific type (and default value).

A function to find or create a Tinderbox Attribute of a given name and type:

click to expand code
// tbxAttribFoundOrCreated ::
//      TBX App -> TBX Attributes ->
//       String -> String -> TBX Attribute
const tbxAttribFoundOrCreated = tbx =>
    // Either a reference to an existing attribute, if found,
    // or to a new attribute of the given name and type,
    // where type is a string drawn from:
    // {boolean,color,date,file,interval,
    //  list,number,set,string,url}
    attribs => strTypeName => attribName => {
        const maybeAttrib = attribs.byName(attribName);

        return maybeAttrib.exists() ? (
            maybeAttrib
        ) : (() => {
            const newAttrib = tbx.Attribute({
                name: attribName,
                type: strTypeName
            });
            return (
                attribs.push(newAttrib),
                newAttrib
            );
        })();
    };

TaskPaper and Tinderbox value types

The Tinderbox attribute type names are:

  • boolean
  • color
  • date
  • file
  • interval
  • list
  • number
  • set
  • string
  • url

And the obvious TaskPaper mappings to a sub-set of these might be:

boolean:

The presence or absence of simple TaskPaper tag

date:

@somedate(2020-06-07)
@somedate(2020-06-07 14:00)

list:

@participants(london, paris, tokyo)

number:

@priority(3)

string:

@any(other)

Tinderbox default values:

Tinderbox attribute values are stored as strings, but interpreted in the light of their data type name. Each of these data types is associated with a special string which defines their default value:

  • boolean – 'false'
  • date – 'never'
  • list – ''
  • number – '0'
  • string – ''

Creating a Tinderbox note.

Once all the attributes needed have been found or created (see above), we can start to create notes in Tinderbox, using our TaskPaper data.

Note creation via JavaScript for Automation is a three-step process:

  • The new note object is defined,
  • it is ‘pushed’ into the notes collection of its parent (either another note, or the document itself),
  • and its various attribute-value pairs are specified.

Defining the new note object:

const newNote = tbx.Note({
    name: "a task, note or project string from TaskPaper"
});

Pushing it into the notes collection of its parent object:

parent.notes.push(newNote)

and specifying any attribute values assuming that we have obtaining a key-value dictionary object of Attribute names, and their values for this note from TaskPaper:

Object.keys(dict).reduce(
    (a, k) => 'Name' !== k ? (
        a.byName(k).value = dict[k],
        a
    ) : a,
    newNote.attributes
)

I’m not sure how much JS scripting you have done so far. So perhaps I can pause there and let you direct what we clarify or show code for next ?

Or if you prefer, we can restart with an XML approach ?

Rob

1 Like

Dear Rob,
Thanks so much for this!

Your assumptions are spot on-- this is far more useful than an arcane and specialized script that I would likely have to parse for meaning and use without thorough understanding.

As for my JS knowledge, I’ve been reading Eloquent Javascript, and can follow the outlines of what you are doing. But I think I’m still a relative beginner.

You’ve given a very clear explanation of the problem and the code you’ve shared thus far makes sense. If you don’t mind, though, could we back up to the basic point where we obtain the Taskpaper @tags and @key(value) pairs and establish their types. Then proceed onward?

I’m mainly curious about the JXA examples and hadn’t even thought of an XML approach. Although that sounds intriguing too.

Thanks!
Maurice

could we back up to the basic point where we obtain the Taskpaper @tags and @key(value) pairs and establish their types.

Sure. Are you happy with this kind of ‘Hello world’ in which we:

  1. Define a function (with editor and options arguments),
  2. obtain a source-code (stringified) representation of it with .toString(), and then
  3. pass that source code (and any key-value options) to TaskPaper’s internal JS interpreter
Click to reveal JS code
(() => {
    'use strict';

    const main = () => {

        // Function to evaluate in TaskPaper's JS CONTEXT
        const tp3ContextFunction = (editor, options) => {
            return 'hello world';
        };

        // CODE TO EVALUATE IN JS FOR AUTOMATION CONTEXT
        const
            tp3 = Application('TaskPaper'),
            docs = tp3.documents;

        return 0 < docs.length ? (
            docs.at(0).evaluate({
                // Function converted to source code
                // and passed to TaskPaper's JS interpreter.
                script: tp3ContextFunction.toString(),
                withOptions: {
                    someKey: 'someValue',
                    someKey2: 'someValue2'
                }
            })
        ) : 'No documents open in TaskPaper';
    };

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

?

And if so, which parts of this slightly elaborated version might seem worth clarifying ?

Click for JS code
(() => {
    'use strict';

    const main = () => {

        const tp3Context = (editor, options) => {
            const tp3Main = () => {
                return editor.outline.root.children.map(
                    fmapPureTP(
                        // Just a simple reading of item text,
                        // for the moment.
                        x => x.bodyContentString
                    )
                );
            };

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

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

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

            return tp3Main();
        }

        const inner = () => {
            const
                ds = Application('TaskPaper')
                .documents;
            return either(alert('Problem'))(
                // x => JSON.stringify(x, null, 2)
                x => x
            )(
                bindLR(
                    ds.length > 0 ? (
                        Right(ds.at(0))
                    ) : Left('No TaskPaper documents open')
                )(
                    d => Right(d.evaluate({
                        script: tp3Context.toString(),
                        withOptions: {
                            optionName: 'someValue'
                        }
                    }))
                )
            );
        };

        // 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',
                    withIcon: sa.pathToResource('TaskPaper.icns', {
                        inBundle: 'Applications/TaskPaper.app'
                    })
                }),
                s
            );
        };

        return inner()
    };

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

    // 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 => undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // 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 => 'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

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

And then this:

(Reading attribute names and any attribute values, together with the item string, copying a JSON representation to the clipboard)

Click to expand code
(() => {
    'use strict';

    ObjC.import('AppKit');

    const main = () => {

        // Ver 3: Reading attribute names and strings, with body text.

        const tp3Context = (editor, options) => {
            const tp3Main = () => {
                return editor.outline.root.children.map(
                    fmapPureTP(
                        // Reading text and also named attribute key/values.
                        x => {
                            const dct = x.attributes;
                            return Object.keys(dct).reduce(
                                (a, k) => k.startsWith(
                                    'data-'
                                ) && k !== 'data-type' ? (
                                    Object.assign(a, {
                                        [k.slice(5)]: dct[k]
                                    })
                                ) : a, {
                                    text: x.bodyContentString
                                }
                            );
                        }
                    )
                );
            };

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

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

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

            return tp3Main();
        }

        const
            ds = Application('TaskPaper')
            .documents;
        return either(alert('Problem'))(
            // JSON copied to the clipboard.
            x => copyText(JSON.stringify(x, null, 2))
        )(
            bindLR(
                ds.length > 0 ? (
                    Right(ds.at(0))
                ) : Left('No TaskPaper documents open')
            )(
                d => Right(d.evaluate({
                    script: tp3Context.toString(),
                    withOptions: {
                        optionName: 'someValue'
                    }
                }))
            )
        );
    };

    // 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',
                withIcon: sa.pathToResource('TaskPaper.icns', {
                    inBundle: 'Applications/TaskPaper.app'
                })
            }),
            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

    // 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 => undefined !== m.Left ? (
            m
        ) : mf(m.Right);

    // 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 => 'Either' === e.type ? (
            undefined !== e.Left ? (
                fl(e.Left)
            ) : fr(e.Right)
        ) : undefined;

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

Dear Rob,
Apologies for the delay. I’m several timezones to the east.

Thanks so much. I see how you are slowly building things up, processing the taskpaper file and eventually identifying the attribute names and values in a JSON. Thanks for laying this all out so elegantly in terms that even a beginner such as myself can understand.

1.) I think I understand your first version.

2.) Your second version likewise made sense, once I adjusted for the fact that I own the Setapp version :slight_smile: of TaskPaper

3.) However, I’m still trying to think through the way you are using generic functions in version 2 and 3. I think I understand the modular nature of these primitive functions (Left and Right are used in bindLR which is employed above), but the ways I might reuse these in the future may require some further work on my part to assimilate. This looks extremely promising though as a path forward in my self-education.

Given the caveats concerning my ignorance of those features of the code-- I think I see what the scripts do.

Now I’m imagining that you would check these TaskPaper attributes against a list of standard TBX attributes, and then create whatever new attributes before creating notes in TBX (making sure to establish their types properly beforehand)? Is this right?

Thanks,
Maurice

1 Like

Good – I’ll unpack those things a bit when I can (on Wednesday evening I hope).

Once the mechanics are feeling a bit clearer and more practiced, the mapping of TaskPaper strings to typed Tinderbox values involves making a few choices on a kind of spectrum between the extremes of automation and precise control. For example:

  • Try to automatically detect the intended value type of previously unseen TaskPaper @key(value) tags ? Or manually list them and specify the type ?
  • Export all tag types to Tinderbox ? Or specify particular lists of tags to include or ignore ?

but I’ll get back to you mid-week.

Thanks. These are interesting considerations. My sense is that manually specifying types will work better for me. But I’m curious to see how one might do this automatically.

Manually specifying the types will certainly lighten the scripting task.

I’ll post a few snippets though, later this evening, to sketch out:

  1. Collecting the values associated with a particular TaskPaper tag, and making a guess at the intended type from them.
  2. Finding existing attributes by name in a Tinderbox document, and creating new attributes (with a given type) if there seems to be a gap.
  3. Checking that the existing Tinderbox attribute seems to have the same type as the matching TaskPaper tag.

Yes, those are a way of putting error messages in one kind of envelope, and successful results in another.

At the end of a chain of computations, you either:

  • get a Left envelope containing a message which you might want to show to the user in an alert (perhaps the contents of a TaskPaper tag didn’t seem to match the type of a target TBX attribute for example),
  • or get a Right envelope containing a value which can be used by the script.

leftRight

At each stage of the chain of processes / computations there is a repeated pattern:

  • Is the incoming envelope of the Left or the Right flavour ?
  • If Left, we just pass it straight on, without reading the contents.
  • If Right, we extract the contents, and apply a new function to it.

The new function will itself either put an error message in to a Left, or a derived new value into a Right envelope before posting it on to the next stage.

That pattern (pass Left envelopes straight on, process the contents of Right envelopes, and pass on the result, in a Left or Right envelope of its own), is used so often that it helps to give it a name and prepackage it:

// bindLR (>>=) :: Either a -> 
// (a -> Either b) -> Either b
const bindLR = m =>
    mf => undefined !== m.Left ? (
        m // This is a Left value. We pass it on unchanged.
    ) : mf(m.Right);  // mf applied to the contents of a Right value.

where m is the envelope, and mf is the function to apply to any Right contents that it may have.

(mf should itself be function which wraps any successful output that it produces as a Right values, and wrap any error string as a Left value)

Thanks very much. This makes a lot of sense and is very clearly explained. I can now see how this functions. Thanks for the very nice graphic also!
Maurice

(By writing a script which traverses a generic forest of trees built from nested Node objects, each of which connects some value (number, string, dictionary of key-value pairs) to a forest of sub-trees, each also built of Node objects).


Background

macOS lets us create our own copies of JavaScript interpreters (JS Contexts), and specialise them a bit by:

  1. Building libraries of useful functions into them, and
  2. given them interfaces to our own programs.

TaskPaper has its own internal JS Context, and there is also the general (‘JavaScript for Automation’ or ‘JXA’) JS Context used by Script Editor etc, which is equipped with an Automation library, and the same interface to applications as AppleScript.

For interactions between TaskPaper and Tinderbox, you have a choice of how much you do in the TP3 JS Context, and how much you do in the JXA JS Context, through which we can interface to Tinderbox.

Not much turns on it, but because I work with various kinds of nested or outline data:

I tend to:

  1. Translate the TaskPaper data using the TP3 JS context into a more generic format (a tree of nodes, each with a value and some sub-nodes), and then
  2. Work on that generic tree in the general JavaScript for Automation context.

The efficiency for me is just that I like to use prepackaged ways of processing trees (traversing and changing them, deriving summary values from them), and using some kind of generic hub format protects me from having to write separate tree-traversals and tree-folds etc for each different type of outline or nested data.

The generic forests of trees which I use are built by a Node constructor function, which returns an object linking some given root value to a nest of sub-Nodes:

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

I’m personally using the labels root and nest just because they are brief, both other traditions prefer names like label and subforest.

The root or label can be a value of any kind (string number, date, dictionary of keys and mixed values etc), and the nest or subForest is always a list (Array) of other Node objects.

At the outer fringes of a tree of Nodes, we have leaf Nodes – that is Nodes without descendants, or to put it another way, Nodes which have empty lists for their nests or subForests.

A simple Tree of strings, with three leaf nodes:

Node('Capitals')([
    Node('Paris')([]), 
    Node('London')([]), 
    Node('Rome')([])
])
          ┌─ Paris 
 Capitals ┼ London 
          └── Rome 

Once our outline data is in a generic Tree format, we can derive related trees and values by:

  • using simple functions,
  • recursively applied across the whole tree by very general helper functions.

For example the ‘higher-order’ helper function fmapTree

// fmapTree :: (a -> b) -> Tree a -> Tree b
const fmapTree = f => {
    // A new tree. The result of a structure-preserving
    // application of f to each root in the existing tree.
    const go = tree => Node(f(tree.root))(
        tree.nest.map(go)
    );
    return go;
};

can apply a simple function like toUpper, to every node in a given tree.

This, for example, returns an upper-cased transform of the original tree:

(() => {
    'use strict';

    const main = () => {

        const tree = Node('Countries')([
            Node('France')([
                Node('Lyon')([]),
                Node('Paris')([])
            ]),
            Node('Britain')([
                Node('Cardiff')([]),
                Node('Edinburgh')([]),
                Node('London')([])
            ]),
            Node('Italy')([
                Node('Milano')([]),
                Node('Roma')([])
            ])
        ]);

        const
            // The original tree systematically mapped to
            // one with upper case versions of the `root` strings.
            newTree = fmapTree(toUpper)(
                tree
            );

        // String representation,
        // each levels indented by 2 spaces.
        return JSON.stringify(
            newTree,
            null,
            2
        );
    };

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

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

    // fmapTree :: (a -> b) -> Tree a -> Tree b
    const fmapTree = f => {
        // A new tree. The result of a structure-preserving
        // application of f to each root in the existing tree.
        const go = tree => Node(f(tree.root))(
            tree.nest.map(go)
        );
        return go;
    };

    // toUpper :: String -> String
    const toUpper = s =>
        s.toLocaleUpperCase();

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

Returns:

{
  "type": "Node",
  "root": "COUNTRIES",
  "nest": [
    {
      "type": "Node",
      "root": "FRANCE",
      "nest": [
        {
          "type": "Node",
          "root": "LYON",
          "nest": []
        },
        {
          "type": "Node",
          "root": "PARIS",
          "nest": []
        }
      ]
    },
    {
      "type": "Node",
      "root": "BRITAIN",
      "nest": [
        {
          "type": "Node",
          "root": "CARDIFF",
          "nest": []
        },
        {
          "type": "Node",
          "root": "EDINBURGH",
          "nest": []
        },
        {
          "type": "Node",
          "root": "LONDON",
          "nest": []
        }
      ]
    },
    {
      "type": "Node",
      "root": "ITALY",
      "nest": [
        {
          "type": "Node",
          "root": "MILANO",
          "nest": []
        },
        {
          "type": "Node",
          "root": "ROMA",
          "nest": []
        }
      ]
    }
  ]
}

and foldTree, another staple helper function for tree-shaped data, can reduce a whole tree to some kind of summary value based on the contents of each root value in the tree.

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

So, if, for example we have a tree of dictionaries, in which each dictionary has a string value and a numeric value, we could add up all of the numeric values in that tree by writing:

(() => {
    'use strict';

    // main :: IO ()
    const main = () => {

        const dictTree = Node({
            name: 'alpha',
            n: 34
        })([
            Node({
                name: 'child 1',
                n: 26
            })([
                Node({
                    name: 'grandchild A',
                    n: 89
                })([]),
                Node({
                    name: 'grandchild B',
                    n: 42
                })([]),
                Node({
                    name: 'grandchild B',
                    n: 73
                })([]),
            ]),
            Node({
                name: 'child 2',
                n: 16
            })([]),
            Node({
                name: 'child 3',
                n: 12
            })([]),
        ]);

        const sumTotal = foldTree(
            x => xs => x.n + sum(xs)
        )(dictTree);

        return sumTotal;
    };

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

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

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

    // sum :: [Num] -> Num
    const sum = xs =>
        // The numeric sum of all values in xs.
        xs.reduce((a, x) => a + x, 0);

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

which reduces the whole tree, by summation, to the number 292.