Script: Sorting top level projects alphabetically

UPDATED

This example was originally written in a pre-release version of the scripting API, and has now been updated to run the release version of TaskPaper 3

To run this JavaScript for Automation Script in Script Editor (Yosemite onwards):

  • Set the language selector at top left to JavaScript
  • Paste the code below (either version)
  • Run
// SORT TOP LEVEL PROJECTS
(function () {
    'use strict';

    function sortTopLevelProjects(editor) {

        var outline = editor.outline;

        // PREPARE TO UPDATE UNDOABLY

        outline.groupUndoAndChanges(function () {

            outline.evaluateItemPath(
                '/@type=project'
            )

            .sort(function (a, b) {
                var strA = a.bodyString.toLowerCase(),
                    strB = b.bodyString.toLowerCase();

                return (strA !== strB) ? (strA < strB ? -1 :
                    1) : 0;
            })

            .reduceRight(
                function (nextSibling, oProj) {
                    outline.insertItemsBefore(oProj,
                        nextSibling);

                    return oProj; // binds it to the name 'nextSibling' for next item
                },
                outline.root.firstchild // initialize nextSibling for the rightmost item
            );

        });

        return true;
    }

    var ds = Application("TaskPaper")
        .documents;

    return ds.length ? ds[0].evaluate({
        script: sortTopLevelProjects.toString()
    }) : 'no document found in TaskPaper ...';

})();

3 Likes

If you prefer to launch scripts from AppleScript, you can set the language selector at top left of Script Editor to AppleScript, and use a version like this:

-- SORT TOP LEVEL PROJECTS 
-- (VERSION FOR LAUNCHING FROM APPLESCRIPT

-- Ver 0.2 updated for release version of TaskPaper 3

set taskPaperContextScript to "
function sortTopLevelProjects(editor) {

        var outline = editor.outline;

        // PREPARE TO UPDATE UNDOABLY

        outline.groupUndoAndChanges(function () {

            outline.evaluateItemPath(
                '/@type=project'
            )

            .sort(function (a, b) {
                var strA = a.bodyString.toLowerCase(),
                    strB = b.bodyString.toLowerCase();

                return (strA !== strB) ? (strA < strB ? -1 :
                    1) : 0;
            })

            .reduceRight(
                function (nextSibling, oProj) {
                    outline.insertItemsBefore(oProj,
                        nextSibling);

                    return oProj; // binds it to the name 'nextSibling' for next item
                },
                outline.root.firstchild // initialize nextSibling for the rightmost item
            );

        });

        return true;
    }
"

tell application "TaskPaper"
    
    set lstDocs to documents
    if lstDocs is not {} then
        tell item 1 of lstDocs
            evaluate script taskPaperContextScript
        end tell
    end if
    
end tell
1 Like

Seriously, thank you very much. I have been using your code to learn how to program JavaScript and TaskPaper. After commenting things like crazy and figuring out your script that moves tags and changes @now to @done; I was wondering about how to do several other things. This is answers a lot of those questions (although I still don’t understand what is going on). Maybe that will happen with time.

Again, Thanks.

Good – I’m glad that seemed useful – and it would be very helpful to know which bits are the least clear, at first.

Any particular lines that could do with some additional comments, or any issues that could do with a bit more clarification ?

i appreciate any and all of them you post! trying to to learn by osmosis here :wink:

@complexpoint, since you are offering. Here are my questions from the move now → done script. :smile:

  1. I noticed that throughout the program the variable blnDown is assigned several different values. I was wondering why, or what was going on.

  2. I noticed that your program picked the first task tagged with now by doing the following,

lstNow = oOutline.evaluateItemPath(‘//@’ + strTag + ‘[0]’);

When you could have gone with

lstNow = oOutline.evaluateItemPath(‘//@’ + strTag);

and have you program work (at least mine works right in every test I have run.) Can you explain why did you decided to include only the first task with the now tag in your code?

  1. Around the same part of the code, I also noticed that you obtain the next task by doing the following,

oNext = lstNext.length ? lstNext[0] : null;

I have no clue how does that work, but it accomplishes the trick. It seems as if there should be easier or clearer ways to accomplish this, but I am thinking you had very specific reasons to do it this way. Can you explain to me why you went this route?

  1. I really do not understand how does this tags the parent project with done when all the tasks inside are done. I mean, I get that when all the task remaining inside the project reach 0, something happens, but I don’t get why you need a forEach(function(x){... to set the attribute.
 itemProjects(oNow).filter(function(p) {
    return tasksRemaining(p).length === 0;
  }).forEach(function(x) {
    x.setAttribute('data-done', strTime);
  });	
  1. All of this is voodoo magic. Could you explain or comment that code.
     var oTaggable = (oNext && !oNext.isRoot) ? oNext : (
        function() {
          var lstDoable = oOutline.evaluateItemPath(
            '//* except @done[' + (blnDown ? '0' : '-1') + ']'
          );
          return lstDoable.length ? lstDoable[0] : null;
        }()
      );

      return oTaggable ? (
        oTaggable.setAttribute(strAttrib, ''),
        editor.isVisible(
          oTaggable
        ) || editor.makeVisible(oTaggable),
        true
      ) : false;
  1. I do not understand why this is necessary, or when does it run.
     // monadic bind/chain for lists
      // [a] -> (a -> [b]) -> [b]
      function chain(xs, f) {
        return [].concat.apply([], xs.map(f));
      }
  1. I also modified your code so that when the parent project is tagged with done, it does not tag the task with done. I did it like this,
         if (oNow.getAttribute('data-type') === 'task') {
            if (oNow.parent.hasAttribute('data-done')) {} else {
              var strTime = moment().format('YYYY-MM-DD HH:mm');

              oNow.setAttribute('data-done', strTime);

            }

The problem with this approach is that the script still has to go through all the tasks inside that project instead of jumping to the next task outside that project that doesn’t have a parent with a done tag. Can you give me some clues or examples on how to accomplish exactly that? I was thinking on doing it on the first evaluation with something like the following, but I don’t know if that is possible, so I am using language to describe the query,

lstNow = oOutline.evaluateItemPath(‘//@’ + strTag except when parent is @done);

Good shopping list : - )

JavaScript has a useful conditional operator

The best quick reference on JavaScript is the Mozilla documentation, and light will be shed on several of your questions by the page which describes JavaScript’s very useful conditional operator

condition ? expressionA : expressionB

which is similar to

if (condition) {
      statementA
} else {
      statementB
}

but has the advantage that it is itself an expression (i.e. it returns a useable value), rather than a statement which may do something useful, but doesn’t itself return a value that you can do anything useful with.

For example, the impression that blnDown is getting several different values arises, I think, from some expressions in that script which include the three-part Test ? ValueA : ValueB conditional operator. What is really happening there is that the value of blnDown is repeatedly being tested, in order to decide what the value of something else should be.

(blnDown doesn’t change after it is assigned at the very start – it just holds the user’s choice for the overall behaviour of the script – are we using this as a ‘move down’ script (blnDown = true) ? or as a ‘move up’ script (blnDown = false) ?

Experiment with .forEach(), and then with .map(), filter(), sort(), and finally with .reduce() and .reduceRight()

These are amongst the best things in JavaScript (built-in functions available for any array), they will save you from fussing with setting up loop variables, testing them and incrementing them – and will protect you from a lot of little bugs along the way. They may also give you new ideas for structuring your scripts a bit more solidly.

What they have in common is that they allow you to define a small function, which is then applied one by one to each item in a list/array, without your having to set up a loop and mess with iterator variables and their changing contents.

I would starting by experimenting in Script Editor with the examples in the relevant Mozilla pages:

When you only need the first match for a query

You asked about the lines:

	lstNow = oOutline.evaluateItemPath('//@' + strTag + '[0]');

	if (lstNow.length) {
		var oNow = lstNow[0],
        ...

Why restrict the harvest to the first item with + '[0]' when it would still work if we collected every match in lstNow ?

Well, we are only looking for one line (we want to know which line is tagged @now – if there are several we only want the first. We don’t need to know too much about what is going on ‘under the hood’ in the query system, but it seems reasonably prudent to guess that asking for a large harvest when we only need one item is possibly inefficient …

But as you suggest, the stakes are not high, and if there are unlikely to be many @now tags, you can drop it without having much to worry about : - )

I will pause there, because

  • making friends with the ‘ternary’ conditional operator, and
  • experimenting with applying functions to each array item with .forEach(), .map() etc gives you plenty to start with, and will probably change the focus of your questions :smile:

Though thinking about your question about this function:

      function chain(xs, f) {
        return [].concat.apply([], xs.map(f));
      }

perhaps worth adding this page to the list of things to experiment with:

( The short answer on the chain() function is that simply combining concat with map turns out to have all kinds of semi-magical applications and usefulnesses, but probably best to start by feeling completely at home with what each of them does on its own ).

Good luck !

A couple of other things:

You asked about excluding all descendants of a @done project.

One way of doing that is to begin by specifying projects which are not marked @done

//(@type=project and not @done)

Once that seems to be working, you can extend the path from that base to ask for task descendants which are also not done:

//(@type=project and not @done)/descendant::(@type=task and not @done)

Also, as a kind of explanation in response to some of your particular questions, I have edited a slightly more verbose version of the script, which breaks a few sections down into more stages, with more named variables. I’ll post it in a minute, in the original thread.

1 Like

@complexpoint. Thank you very much for taking all that time to answer so many of my questions. I truly appreciate that more than anything else. I will get to experiment and work through the code again during the week.

1 Like

Thought I’d chime in with my 2 cents. A great way to experiment with a few examples that @complexpoint has mentioned is to use an app such as CodeRunner and choose the syntax node.js. That will allow you to log things to console with console.log(something). I’ve found that helps to get a grasp of what is happening along the way.

If you don’t want to spend the cash, then there are some decent online options such as Node.js Online Compiler & Interpreter - Replit

Obviously Script Editor is also an option, but I feel that is more geared toward JXA than simply pure javascript (could be wrong though)

1 Like

CodeRunner.app is a very useful thing to have and certainly a good environment for experimentation with general (non TaskPaper) JavaScript examples. (To run TaskPaper and other app-automation scripts you need the Application object which is available in the Script Editor JavaScript environment).

If you use the built-in Node.js language settings, you will, of course be experimenting with V8 (Chrome) JavaScript rather than with JSC (Safari) but as long as you keep to the ES5 standards you probably won’t notice the difference.

If you want to use exactly the same JavaScript dialect as JXA (just without the Application automation references), you can very simply set up an additional language (JavaScript JSC) in CodeRunner. Note that for simple output, (equivalent to the Results panel in Script Editor), JSC expects you to wrap the output expression with the print() function. As in print(2+2) or print('concatentation' + ' test').

(Instructions for adding JavaScript JSC to CodeRunner.app below)

Once you are starting to develop actual scripts, the Script Editor does have the advantage that you can use Safari’s JavaScript debugger from it, and actually automate apps like TaskPaper.

Adding JSC JavaScript (JavaScript Core) to CodeRunner

  • CodeRunner > Preferences > Languages,
  • click + at lower left to add a language.

  • Give the new language settings a name like JavaScript JSC
  • and use the other settings shown below:

  • Then create a fresh document using the newly added language,
  • and run a simple one-line test.

You should see the result of the OS X JavaScript Core’s evaluation of your line:

(186ms is mainly the CodeRunner.app launching overhead. Script Editor is a bit more light-weight)

1 Like

PS you can also use console.log in Script Editor:

To see the console.log output stream:

  • choose the Show or Hide the log icon at the bottom edge of the Script Editor window, then
  • click the Message panel.

You will still get the final expression evaluation results,
but they will be preceded by the console.log stream

It seems as if I use console.log every other line in the code using Script Editor and the debug window in Taskpaper. Does CodeRunner offer something else that I don’t know about?

Not sure what @pslobo’s view would be but I think my own view is that CodeRunner may be most useful if you are already using it for other things, and would like to experiment with core JavaScript examples in a familiar environment.

The main advantages which come to mind are some editing facilities like toggling the comment status of a line on and off, block indent and outdent etc. etc.

(another option for editing functions like that within Script Editor is to add them in the form of keyboard assignments to a few JXA scripts for Script Editor itself).

@complexpoint hit the nail on the head. CodeRunner is a great app for experimentation IF you already have and use it for other things (as is my case). You don’t need to go out and buy it just for this since it is possible to use ScripEditor and debugger as has already been mentioned.

A few things I appreciate about CodeRunner are:

  1. Syntax highlighting;
  2. More easily manipulate text (in/outdent), comment, etc.;
  3. Code completion helps a great deal, especially when learning (take a look at the image below)
  4. Tabs. Makes it easy to have multiple documents open and quickly experiment with various snippets;

@pslobo Thank you.

1 Like

@complexpoint This script seems to crash my TP every second or third time I use it nowadays. This started happening around 3.0. Is that just me?

Well caught – the final API did go through some changes during the Preview stages.

I’ll sketch a more up to date version of that.

Updated draft, using the release API’s outline.groupUndoAndChanges() and outline.insertItemsBefore()

// SORT TOP LEVEL PROJECTS
(function () {
    'use strict';

    function sortTopLevelProjects(editor) {

        var outline = editor.outline;

        // PREPARE TO UPDATE UNDOABLY

        outline.groupUndoAndChanges(function () {

            outline.evaluateItemPath(
                '/@type=project'
            )

            .sort(function (a, b) {
                var strA = a.bodyString.toLowerCase(),
                    strB = b.bodyString.toLowerCase();

                return (strA !== strB) ? (strA < strB ? -1 :
                    1) : 0;
            })

            .reduceRight(
                function (nextSibling, oProj) {
                    outline.insertItemsBefore(oProj,
                        nextSibling);

                    return oProj; // binds it to the name 'nextSibling' for next item
                },
                outline.root.firstchild // initialize nextSibling for the rightmost item
            );

        });

        return true;
    }

    var ds = Application("TaskPaper")
        .documents;

    return ds.length ? ds[0].evaluate({
        script: sortTopLevelProjects.toString()
    }) : 'no document found in TaskPaper ...';

})();
1 Like

@complexpoint Thanks so much! Super useful.

2 Likes